diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3f99aa8..16070be 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -72,6 +72,18 @@ jobs: - name: Spellcheck run: nix develop -c codespell docs-source README.md + format: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: cachix/install-nix-action@v27 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Check formatting + run: nix develop -c mdformat --check --wrap 120 docs-source README.md + deploy: if: github.event_name == 'push' needs: build diff --git a/.gitignore b/.gitignore index d0d0921..d0fee01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,12 @@ # Sphinx build output _build/ +_built/ .doctrees/ +# Generated light/dark drawio SVG exports (rendered at build time, see conf.py) +docs-source/**/*.light.svg +docs-source/**/*.dark.svg + # Python __pycache__/ *.pyc @@ -18,6 +23,10 @@ result-* Thumbs.db # Editors -.vscode/ +.vscode/* +!.vscode/settings.json .idea/ *.swp + +# Claude Code local overrides +.claude/settings.local.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d520ecb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.wordWrap": "wordWrapColumn", + "editor.wordWrapColumn": 120, + "editor.rulers": [120] +} diff --git a/README.md b/README.md index ae8bb33..91e15ab 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@ See the [Contributing Guide](CONTRIBUTING.md) and the [Code of Conduct](CODE_OF_CONDUCT.md)! -RoboCup Small Size League docs. [Sphinx](https://www.sphinx-doc.org/) + [MyST](https://myst-parser.readthedocs.io/), deployed to GitHub Pages via Actions. +RoboCup Small Size League docs. [Sphinx](https://www.sphinx-doc.org/) + [MyST](https://myst-parser.readthedocs.io/), +deployed to GitHub Pages via Actions. -This repository documents top level items, such as field architecture, new team guides, etc. Highly technical documentation regarding the usage of a -league software tool should be in the repository for that tool. Any top level integration/usages guides would be welcome here. +This repository documents top level items, such as field architecture, new team guides, etc. Highly technical +documentation regarding the usage of a league software tool should be in the repository for that tool. Any top level +integration/usages guides would be welcome here. ## Build @@ -60,11 +62,17 @@ nix develop -c sphinx-build -b linkcheck docs-source _build/linkcheck # spelling (codespell) nix develop -c codespell docs-source README.md + +# formatting (mdformat, 120-col wrap) +nix develop -c mdformat --check --wrap 120 docs-source README.md ``` +Auto-fix formatting: drop `--check` from the last command. + ## Deploy -`main` push → [`.github/workflows/docs.yml`](.github/workflows/docs.yml) builds + deploys via GitHub Pages Actions. Not RTD-hosted. +`main` push → [`.github/workflows/docs.yml`](.github/workflows/docs.yml) builds + deploys via GitHub Pages Actions. Not +RTD-hosted. ## Diff versions @@ -73,6 +81,13 @@ scripts/diff-versions.sh v1.0.0 v1.1.0 scripts/diff-versions.sh v1.0.0 main rules.md ``` +## External Links Manual Checks + +If a website blocks bots, then the external links Ci check will fail. You can add these links to `linkcheck_ignore` in +`conf.py`. Periodically, anyone can run `./scripts/reverify-ignored-links.sh` which will find a browser and open all the +ignored links to be checked manually. This usually passes the cloudflare bot check. Currently no clear way to do this in +CI. + ## License [CC BY 4.0](LICENSE). diff --git a/docs-source/_static/camera-calc.css b/docs-source/_static/camera-calc.css new file mode 100644 index 0000000..2c31405 --- /dev/null +++ b/docs-source/_static/camera-calc.css @@ -0,0 +1,39 @@ +.camera-calc { border: 1px solid var(--color-background-border, #ccc); border-radius: 6px; padding: 1em; margin: 1em 0; } +.camera-calc-group { border: 1px solid var(--color-background-border, #ddd); border-radius: 6px; margin: 0 0 1em 0; padding: 0.75em 1em 1em; } +.camera-calc-group legend { font-weight: bold; padding: 0 0.4em; } +.camera-calc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 0.75em; } +.camera-calc-grid label { display: flex; flex-direction: column; font-size: 0.9em; gap: 0.25em; } +.camera-calc-grid label > span:first-child { display: flex; align-items: center; height: 1.4em; gap: 0.3em; } +.camera-calc-grid mjx-container { + vertical-align: middle !important; + min-height: 0 !important; + max-height: 1.4em; + overflow: hidden; +} +.camera-calc-grid input { padding: 0.3em; } +.camera-calc-grid input:disabled { + background: var(--color-background-secondary, #e9e9e9); + color: var(--color-foreground-muted, #767676); + border-color: var(--color-background-border, #ccc); + cursor: not-allowed; + opacity: 0.85; +} +.camera-calc-radio { display: flex; gap: 1.5em; margin: 0.75em 0; font-size: 0.9em; flex-wrap: wrap; } +.camera-calc-radio label { display: flex; align-items: center; gap: 0.4em; font-weight: normal; } +.camera-calc-note { font-size: 0.85em; opacity: 0.75; margin: 0.25em 0 0; } +.camera-calc button { margin-top: 0.25em; padding: 0.5em 1.2em; cursor: pointer; } +.camera-calc-preset { + margin-top: 0 !important; + padding: 0.3em 0.7em !important; + font-size: 0.85em; + background: var(--color-background-secondary, #f0f0f0); + color: var(--color-foreground-primary, #000) !important; + border: 1px solid var(--color-background-border, #ccc); + border-radius: 4px; +} +.camera-calc-output { margin-top: 1em; } +.camera-calc-output dt { font-weight: bold; } +.camera-calc-output dd { margin: 0 0 0.6em 0; } +.camera-calc-output .cc-pass { color: #1a7f37; } +.camera-calc-output .cc-fail { color: #cf222e; } +.camera-calc-output .cc-subhead { margin-top: 1em; padding-top: 0.5em; border-top: 1px solid var(--color-background-border, #ddd); } diff --git a/docs-source/_static/camera-calc.js b/docs-source/_static/camera-calc.js new file mode 100644 index 0000000..3d5a502 --- /dev/null +++ b/docs-source/_static/camera-calc.js @@ -0,0 +1,285 @@ +// Loaded site-wide via html_js_files; no-op on pages without the calculator. +if (document.getElementById('cc-calc')) { + +function ccSensorMode() { + return document.querySelector('input[name="cc-sensor-mode"]:checked').value; +} + +function ccCameraCount() { + return parseInt(document.querySelector('input[name="cc-n"]:checked').value, 10); +} + +function ccLensMode() { + return document.querySelector('input[name="cc-lens-mode"]:checked').value; +} + +function ccMarkerMode() { + return document.querySelector('input[name="cc-marker-mode"]:checked').value; +} + +function ccLoadFieldPreset(w, h) { + document.getElementById('cc-tw').value = w; + document.getElementById('cc-th').value = h; +} + +document.getElementById('cc-preset-a').addEventListener('click', function () { + ccLoadFieldPreset(13400, 10400); +}); + +document.getElementById('cc-preset-b').addEventListener('click', function () { + ccLoadFieldPreset(10400, 7400); +}); + +function ccUpdateDerivedSensorSize() { + if (ccSensorMode() !== 'pixel') return; + const rh = parseFloat(document.getElementById('cc-rh').value); + const rv = parseFloat(document.getElementById('cc-rv').value); + const px = parseFloat(document.getElementById('cc-px').value) / 1000; // µm -> mm + if (isNaN(rh) || isNaN(rv) || isNaN(px)) return; + document.getElementById('cc-sh').value = (rh * px).toFixed(3); + document.getElementById('cc-sv').value = (rv * px).toFixed(3); +} + +function ccUpdateDerivedPixelSize() { + if (ccSensorMode() !== 'size') return; + const rh = parseFloat(document.getElementById('cc-rh').value); + const rv = parseFloat(document.getElementById('cc-rv').value); + const sh = parseFloat(document.getElementById('cc-sh').value); + const sv = parseFloat(document.getElementById('cc-sv').value); + if (isNaN(rh) || isNaN(rv) || isNaN(sh) || isNaN(sv) || rh === 0 || rv === 0) return; + // average of the two axes' implied pitch, since pixel size here assumes square pixels + const pxMm = ((sh / rh) + (sv / rv)) / 2; + document.getElementById('cc-px').value = (pxMm * 1000).toFixed(3); // mm -> µm +} + +function ccUpdateDerivedFields() { + ccUpdateDerivedSensorSize(); + ccUpdateDerivedPixelSize(); +} + +function ccSyncSensorModeDisabled() { + const isPixel = ccSensorMode() === 'pixel'; + document.getElementById('cc-sh').disabled = isPixel; + document.getElementById('cc-sv').disabled = isPixel; + document.getElementById('cc-px').disabled = !isPixel; +} + +document.querySelectorAll('input[name="cc-sensor-mode"]').forEach(function (radio) { + radio.addEventListener('change', function () { + ccSyncSensorModeDisabled(); + ccUpdateDerivedFields(); + }); +}); + +['cc-px', 'cc-sh', 'cc-sv', 'cc-rh', 'cc-rv'].forEach(function (id) { + document.getElementById(id).addEventListener('input', ccUpdateDerivedFields); +}); + +// Browsers restore previously-checked radios on a plain page refresh, which +// doesn't fire 'change' — resync disabled state to whatever's actually +// checked now, not just the HTML's hardcoded default. +ccSyncSensorModeDisabled(); +ccUpdateDerivedFields(); + +function ccUpdateDerivedVarifocalRange() { + if (ccLensMode() !== 'focal') return; + const f = parseFloat(document.getElementById('cc-f').value); + if (isNaN(f)) return; + document.getElementById('cc-f-min').value = f; + document.getElementById('cc-f-max').value = f; +} + +function ccUpdateDerivedFixedFocal() { + if (ccLensMode() !== 'varifocal') return; + const fMin = parseFloat(document.getElementById('cc-f-min').value); + const fMax = parseFloat(document.getElementById('cc-f-max').value); + if (isNaN(fMin) || isNaN(fMax)) return; + document.getElementById('cc-f').value = ((fMin + fMax) / 2).toFixed(2); +} + +function ccUpdateDerivedLensFields() { + ccUpdateDerivedVarifocalRange(); + ccUpdateDerivedFixedFocal(); +} + +function ccSyncLensModeDisabled() { + const mode = ccLensMode(); + document.getElementById('cc-f').disabled = mode !== 'focal'; + document.getElementById('cc-f-min').disabled = mode !== 'varifocal'; + document.getElementById('cc-f-max').disabled = mode !== 'varifocal'; + document.getElementById('cc-fov-d').disabled = mode !== 'fov'; + document.getElementById('cc-valid-range-note').hidden = mode !== 'fov'; +} + +document.querySelectorAll('input[name="cc-lens-mode"]').forEach(function (radio) { + radio.addEventListener('change', function () { + ccSyncLensModeDisabled(); + ccUpdateDerivedLensFields(); + }); +}); + +['cc-f', 'cc-f-min', 'cc-f-max'].forEach(function (id) { + document.getElementById(id).addEventListener('input', ccUpdateDerivedLensFields); +}); + +// Same refresh-restores-radios issue as sensor mode above. +ccSyncLensModeDisabled(); +ccUpdateDerivedLensFields(); + +function ccSyncMarkerModeDisabled() { + const isLeague = ccMarkerMode() === 'league'; + document.getElementById('cc-d').disabled = isLeague; + document.getElementById('cc-nmin').disabled = isLeague; +} + +document.querySelectorAll('input[name="cc-marker-mode"]').forEach(function (radio) { + radio.addEventListener('change', function () { + ccSyncMarkerModeDisabled(); + if (ccMarkerMode() === 'league') { + document.getElementById('cc-d').value = 40; + document.getElementById('cc-nmin').value = 6; + } + }); +}); + +// Resync disabled state on load too (see comments above) — deliberately +// doesn't reset values here, only on an actual user click, so a +// browser-restored "custom" selection keeps its restored values. +ccSyncMarkerModeDisabled(); + +function ccWHFromFocal(h, f, sh, sv) { + return [(h * sh) / f, (h * sv) / f]; +} + +function ccWHFromDiagonalFov(h, fovD, aspect) { + // aspect = W/H, taken from the resolution's aspect ratio (Rh/Rv). + const toRad = deg => (deg * Math.PI) / 180; + const diag = 2 * h * Math.tan(toRad(fovD) / 2); + const H = diag / Math.sqrt(aspect * aspect + 1); + const W = aspect * H; + return [W, H]; +} + +function ccComputeFromWH(h, W, H, rh, rv, d, nMin, tw, th, n) { + const toDeg = r => (r * 180) / Math.PI; + const fovH = 2 * toDeg(Math.atan(W / (2 * h))); + const fovV = 2 * toDeg(Math.atan(H / (2 * h))); + const rho = Math.min(rh / W, rv / H); + const pxAcross = rho * d; + const passes = pxAcross >= nMin; + + const result = { fovH, fovV, W, H, rho, pxAcross, passes }; + if (!isNaN(tw) && !isNaN(th) && tw > 0 && th > 0) { + result.margin = (n * W * H) / (tw * th) * 100; + result.marginOk = result.margin > 100; + } + return result; +} + +function ccCompute(h, f, sh, sv, rh, rv, d, nMin, tw, th, n) { + const [W, H] = ccWHFromFocal(h, f, sh, sv); + return ccComputeFromWH(h, W, H, rh, rv, d, nMin, tw, th, n); +} + +function ccComputeDiagonalFov(h, fovD, rh, rv, d, nMin, tw, th, n) { + const [W, H] = ccWHFromDiagonalFov(h, fovD, rh / rv); + return ccComputeFromWH(h, W, H, rh, rv, d, nMin, tw, th, n); +} + +function ccPerCameraTarget(n, tw, th) { + // 1 camera: full field. 2: split the wider axis in half (half field). + // 4: split both axes in half (quadrants). Matches the convention used + // throughout the Cameras and Lenses worked examples. + if (n === 2) return [tw / 2, th]; + if (n === 4) return [tw / 2, th / 2]; + return [tw, th]; +} + +function ccValidFocalRange(h, sh, sv, rh, rv, d, nMin, targetW, targetH) { + // Coverage needs W(f) >= targetW and H(f) >= targetH; since W,H shrink as + // f grows, this caps f from above. + const fMaxCoverage = Math.min((h * sh) / targetW, (h * sv) / targetH); + // Density needs rho(f) >= nMin/d; since rho grows as f grows, this bounds + // f from below. + const fMinDensity = Math.max((nMin * h * sh) / (d * rh), (nMin * h * sv) / (d * rv)); + return { fMinDensity, fMaxCoverage, feasible: fMinDensity <= fMaxCoverage }; +} + +function ccRenderValidRange(range, n) { + let html = '
Valid Focal Length Range
'; + if (range.feasible) { + html += `
${range.fMinDensity.toFixed(2)}–${range.fMaxCoverage.toFixed(2)} mm ` + + `— any lens in this range covers its assigned tile (of ${n}) and meets pixel density
`; + } else { + html += `
None — coverage needs f ≤ ${range.fMaxCoverage.toFixed(2)} mm, but density ` + + `needs f ≥ ${range.fMinDensity.toFixed(2)} mm. No single focal length satisfies both; you need a ` + + `wider sensor, higher resolution, lower mount, or more cameras.
`; + } + return html; +} + +function ccRenderResult(r, nMin) { + let html = ''; + html += `
Field of View
${r.fovH.toFixed(1)}° × ${r.fovV.toFixed(1)}°
`; + html += `
Ground Coverage
${r.W.toFixed(0)} × ${r.H.toFixed(0)} mm
`; + html += `
Pixel Density
${r.rho.toFixed(4)} px/mm
`; + html += `
Pixels Across Marker
${r.pxAcross.toFixed(1)} px ` + + `(need ≥ ${nMin}) — ${r.passes ? 'PASS' : 'FAIL'}
`; + if (r.margin !== undefined) { + html += `
Coverage Margin
${r.margin.toFixed(0)}% ` + + `— ${r.marginOk ? 'PASS' : 'FAIL (at or below 100%)'}
`; + } + return html; +} + +document.getElementById('cc-calc').addEventListener('click', function () { + const num = id => parseFloat(document.getElementById(id).value); + const h = num('cc-h'); + const rh = num('cc-rh'), rv = num('cc-rv'), d = num('cc-d'), nMin = num('cc-nmin'); + const tw = num('cc-tw'), th = num('cc-th'), n = ccCameraCount(); + const sh = num('cc-sh'), sv = num('cc-sv'); + const lensMode = ccLensMode(); + + let html = ''; + + const haveTarget = !isNaN(tw) && !isNaN(th) && tw > 0 && th > 0; + let range = null; + if (haveTarget && lensMode !== 'fov') { + const [targetW, targetH] = ccPerCameraTarget(n, tw, th); + range = ccValidFocalRange(h, sh, sv, rh, rv, d, nMin, targetW, targetH); + html += ccRenderValidRange(range, n); + } + + if (lensMode === 'fov') { + const r = ccComputeDiagonalFov(h, num('cc-fov-d'), rh, rv, d, nMin, tw, th, n); + html += ccRenderResult(r, nMin); + } else if (lensMode === 'focal') { + const r = ccCompute(h, num('cc-f'), sh, sv, rh, rv, d, nMin, tw, th, n); + html += ccRenderResult(r, nMin); + } else { + const fMin = num('cc-f-min'), fMax = num('cc-f-max'); + const rWide = ccCompute(h, fMin, sh, sv, rh, rv, d, nMin, tw, th, n); + const rNarrow = ccCompute(h, fMax, sh, sv, rh, rv, d, nMin, tw, th, n); + html += `
At widest (${fMin} mm)
`; + html += ccRenderResult(rWide, nMin); + html += `
At narrowest (${fMax} mm)
`; + html += ccRenderResult(rNarrow, nMin); + + if (range) { + const usableMin = Math.max(range.fMinDensity, fMin); + const usableMax = Math.min(range.fMaxCoverage, fMax); + html += '
Usable dial range on this lens
'; + if (usableMin <= usableMax) { + html += `
Dial to
${usableMin.toFixed(2)}–${usableMax.toFixed(2)} mm on this ` + + `lens works
`; + } else { + html += `
Dial to
No setting on this lens's ${fMin}–${fMax} mm range works ` + + `— see Valid Focal Length Range above for what would
`; + } + } + } + + document.getElementById('cc-output').innerHTML = html; +}); + +} diff --git a/docs-source/_static/site.css b/docs-source/_static/site.css new file mode 100644 index 0000000..4736d8e --- /dev/null +++ b/docs-source/_static/site.css @@ -0,0 +1,38 @@ +.full-width-table table { + width: 100%; +} + +.full-width-table img { + width: 100%; + height: auto; +} + +/* Two-column diagram layout: label paragraphs on row 1, diagrams on row 2, + auto-flowing into a 2-col grid. Reusable across pages that need to compare + two diagrams side by side. Defaults to even columns; add the modifier + class below when one diagram is meaningfully more complex. */ +.two-col-diagram-grid { + display: grid; + grid-template-columns: 1fr 1fr; + column-gap: 1.5em; + align-items: start; +} + +/* Modifier: widen the right column when it holds a more complex diagram + (e.g. the VLAN core diagram, which has 3 branches vs the physical core's 1). */ +.two-col-diagram-grid.wide-right { + grid-template-columns: 3fr 5fr; +} + +.two-col-diagram-grid > p { + margin: 0 0 0.5em 0; + padding-bottom: 0.4em; + font-weight: bold; + text-align: center; + border-bottom: 1px solid var(--color-background-border, #ddd); +} + +.two-col-diagram-grid pre.mermaid, +.two-col-diagram-grid .mermaid-container { + width: 100%; +} diff --git a/docs-source/conf.py b/docs-source/conf.py index 6500ed8..8fe8096 100644 --- a/docs-source/conf.py +++ b/docs-source/conf.py @@ -1,9 +1,19 @@ -project = "SSL Docs" +import os +import shutil +import subprocess +import time +from pathlib import Path + +project = "RoboCup Small Size League" copyright = "2026, RoboCup Small Size League" author = "RoboCup Small Size League" extensions = [ "myst_parser", + "sphinxcontrib.youtube", + "sphinxcontrib.mermaid", + "sphinxcontrib.drawio", + "sphinx.ext.mathjax", ] source_suffix = { @@ -14,9 +24,161 @@ myst_enable_extensions = [ "colon_fence", "deflist", + "dollarmath", ] +myst_heading_anchors = 4 + exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] html_theme = "furo" -html_static_path = [] +html_static_path = ["_static"] +html_css_files = ["site.css", "camera-calc.css"] +html_js_files = ["camera-calc.js"] + +# Diagrams using `layout: elk` in their frontmatter need the ELK layout +# plugin explicitly opted into — sphinxcontrib-mermaid doesn't load it by +# default even when a diagram requests it, silently falling back to dagre. +mermaid_include_elk = True +mermaid_version = "latest" + +# Without this, mermaid assigns each diagram's SVG id via Date.now() with no +# counter or dedup. Pages with several diagrams reliably render two of them +# in the same millisecond, so they collide on the exact same id and the +# second diagram's output gets written into the first's DOM location instead +# of its own. deterministicIds switches id generation to a simple incrementing +# counter, which can't collide. +mermaid_init_config = {"startOnLoad": False, "deterministicIds": True} + +# drawio's export binary is Electron, and nixpkgs doesn't set up the +# setuid chrome-sandbox helper Electron's sandbox needs outside of NixOS +# itself (local dev shells and the GitHub Actions runner alike), so it +# segfaults on startup without --no-sandbox. It also segfaults hunting for +# a GPU/DRI driver that doesn't exist in these headless environments, +# hence disabling GPU use as well. +drawio_no_sandbox = True +drawio_disable_gpu = True + +# Diagrams that need to track the site's light/dark toggle correctly. An +# -embedded SVG's light-dark() CSS resolves against the browser's OS/UA +# color-scheme preference, not the host page — and Furo's manual toggle never +# sets the CSS color-scheme property, only a data-theme attribute + CSS +# classes. So a live light-dark() SVG can go dark while the page is +# explicitly toggled to light. Each of these gets pre-rendered as two static +# SVGs (one per theme, via $DRAWIO_SVG_THEME on the nix-wrapped drawio +# binary) and picked between with Furo's own only-light/only-dark image +# classes — see field/network/compnetwork.md. +DUAL_THEME_DIAGRAMS = [ + "field/network/diagrams/ssl_field_network_fanout_truss.drawio", + "field/network/diagrams/ssl_field_network_fanout_direct.drawio", +] + + +def render_dual_theme_diagrams(app): + # Unlike sphinxcontrib-drawio's own directive, these are plain {image} + # references (see below) — Sphinx's image collector needs the file to + # exist on disk for every builder, not just ones that emit images, so + # this can't be skipped for the dummy/linkcheck builders. + drawio_bin = shutil.which("drawio") + if drawio_bin is None: + raise RuntimeError("drawio binary not found on PATH — run inside `nix develop`") + + xvfb_run_bin = shutil.which("xvfb-run") + if xvfb_run_bin is None: + raise RuntimeError("xvfb-run binary not found on PATH — run inside `nix develop`") + + for rel_source in DUAL_THEME_DIAGRAMS: + source = Path(app.srcdir) / rel_source + for theme in ("light", "dark"): + # Written alongside the .drawio source (not html_static_path) so + # the normal {image} directive can find and copy it like any + # other source-tree image. + out_path = source.with_suffix(f".{theme}.svg") + if out_path.exists() and out_path.stat().st_mtime > source.stat().st_mtime: + continue + + env = os.environ.copy() + env["DRAWIO_SVG_THEME"] = theme + # ELECTRON_RUN_AS_NODE (set by some terminal/editor hosts, e.g. + # VS Code) makes Electron run as plain Node instead of launching + # drawio's UI, so the export silently produces nothing. Also + # drop WAYLAND_DISPLAY: Electron's ozone "auto" platform picks + # Wayland whenever a live compositor socket exists, which + # collides with Xvfb's X11 display and crashes the export. + env.pop("ELECTRON_RUN_AS_NODE", None) + env.pop("ELECTRON_NO_ATTACH_CONSOLE", None) + env.pop("WAYLAND_DISPLAY", None) + + args = [ + xvfb_run_bin, + "-a", + drawio_bin, + "--export", + "--crop", + "--page-index", + "0", + "--format", + "svg", + "--output", + str(out_path), + str(source), + "--disable-gpu", + "--disable-software-rasterizer", + "--disable-features=DefaultPassthroughCommandDecoder", + "--no-sandbox", + ] + + # Running several of these back-to-back within one process + # occasionally hits a transient Xvfb display race between + # consecutive xvfb-run invocations (each one succeeds fine in + # isolation) — retry a couple of times before giving up, and + # surface real stderr on the final failure instead of Sphinx's + # generic "exited with error" message. + attempts = 3 + for attempt in range(1, attempts + 1): + result = subprocess.run(args, env=env, capture_output=True, text=True) + if result.returncode == 0: + break + if attempt == attempts: + raise RuntimeError( + f"drawio export failed after {attempts} attempts for {source} ({theme}):\n" + f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}" + ) + time.sleep(2) + + +def setup(app): + # The calculator page's math lives inside raw HTML labels, which Sphinx's + # math domain never sees, so its per-page MathJax inclusion heuristic + # misses it. Load MathJax (and other extension assets) unconditionally on + # every page instead. + app.set_html_assets_policy("always") + app.connect("builder-inited", render_dual_theme_diagrams) + + +linkcheck_user_agent = ( + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/124.0.0.0 Safari/537.36" +) + +# Some vendor sites sit behind bot-mitigation (WAF/CDN) that rejects every +# automated request with an instant 403 regardless of user-agent or headers, +# even though the pages load fine in a real browser (both curl with a real +# browser UA and the WebFetch tool get the same instant 403 — this isn't a +# UA-sniffing check we can spoof our way past). Each URL below was manually +# opened in a browser and confirmed good on the noted date; re-verify and +# bump the date if one starts 404ing for real. +linkcheck_ignore = [ + # rolling vs global shutter write-up — verified 2026-08-09 + r"^https://www\.teledynevisionsolutions\.com/learn/learning-center/imaging-fundamentals/rolling-vs-global-shutter/$", + # Spinnaker SDK product page — verified 2026-08-09 + r"^https://www\.teledynevisionsolutions\.com/products/spinnaker-sdk/\?model=Spinnaker%20SDK&vertical=machine%20vision&segment=iis$", + # dual-channel PoE GigE host adapter (ACC-01-1105) — verified 2026-08-09 + r"^https://www\.teledynevisionsolutions\.com/products/ethernet-network-interface-card/\?model=ACC-01-1105&vertical=machine%20vision&segment=iis$", + # Computar E3Z4518CS-MPIR lens product page — verified 2026-08-09 + r"^https://www\.bhphotovideo\.com/c/product/1091754-REG/computar_e3z4518cs_mpir_1_2_4_5_13_2mm_f1_8_dn\.html$", + # Computar E3Z3915CS-MPWIR 4K lens product page — verified 2026-08-09 + r"^https://www\.bhphotovideo\.com/c/product/1524951-REG/computar_e3z3915cs_mpwir_4k_1_1_8_3_9_10mm_f1_5\.html$", + # Intel Core i7-7567U spec sheet — verified 2026-08-09 + r"^https://www\.intel\.com/content/www/us/en/products/sku/97541/intel-core-i77567u-processor-4m-cache-up-to-4-00-ghz/specifications\.html$", +] diff --git a/docs-source/field.md b/docs-source/field.md new file mode 100644 index 0000000..7d9c2c4 --- /dev/null +++ b/docs-source/field.md @@ -0,0 +1,56 @@ +# Field Technical Documentation + +This page serves as a directory for common resources related to field mechanics, networks, and overall setup. As always, +the [SSL Rules](https://ssl.robocup.org/rules/) are the authoritative source, but many items are left formally +unconstrained. + +## Finding a Space + +The following page provide a brief list of considerations for finding appropriate lab space to host an SSL team or an +SSL event. + +- Lab Space (coming soon!) +- Competition Space (coming soon!) + +## Mechanics and Materials + +The following pages assist with materials and camera selection. + +- [Subflooring and Carpet](field/carpet.md) +- [Camera and Lens Selection](field/cameras.md) +- [Camera Field of View Calculator](field/camera-calculator.md) + +## Layout + +Two reference layouts are provided. The competition layout reflects what's expected to be found at a global competition, +though smaller events may omit some functionality. The lab layout reflects a more realistic and modest setup seen at +most university or community labs for weekly practice. + +- [Competition Layout](field/complayout.md) +- [Lab Layout](field/lablayout.md) + +## Compute and Physical Network + +See the compute page for recommended machine specifications and brands. + +- [Compute](field/network/compute.md) + +See the network pages below for detailed descriptions of competition networks and home lab networks. + +- [Competition Network](field/network/compnetwork.md) +- [Lab Network](field/network/labnetwork.md) + +```{toctree} +--- +maxdepth: 1 +caption: Contents +--- +field/carpet.md +field/cameras.md +field/camera-calculator.md +field/complayout.md +field/lablayout.md +field/network/compute.md +field/network/compnetwork.md +field/network/labnetwork.md +``` diff --git a/docs-source/field/camera-calculator.md b/docs-source/field/camera-calculator.md new file mode 100644 index 0000000..e7b8a30 --- /dev/null +++ b/docs-source/field/camera-calculator.md @@ -0,0 +1,100 @@ +# Camera Field of View Calculator + +Helps sanity check a given camera configuration for a desired field size. + +Enter numbers instead of doing the arithmetic in +[Steps 1-5 of Cameras and Lenses](cameras.md#calculating-major-factors-for-your-lab) by hand. + +
+
+ Physical Space +
+ + + +
+
+ + +
+

Use the full area the cameras need to see, not just the field lines — include the + surrounding margin (see the SSL + Rules). The preset buttons above load the field of play size including that margin, per the SSL Rules.

+
+ + + +
+

Other camera counts aren't supported by the league vision software.

+
+ +
+ Camera Parameters +
+ + +
+
+ + +
+
+ + +
+
+ +
+

One of sensor size or pixel size is computed from the other and resolution, based on + the mode selected above — the greyed-out field isn't editable while its value is derived.

+
+ +
+ Lens Parameters +
+ + + +
+

Webcam spec sheets often publish a single diagonal field of view instead of focal + length. Horizontal/vertical split is derived from the resolution's aspect ratio above. Field of view mode only + supports a fixed lens; the greyed-out fields below aren't editable until you pick the mode that uses them.

+ +
+ +
+
+ + +
+
+ +
+

Fixed mode mirrors the single focal length into both ends of the varifocal range. + Varifocal mode derives the greyed-out focal length as the range's midpoint — a display convenience, not a real + setting; results are computed at both actual ends of the range. An unmarked dial has no fixed setting in + between; see Peachtree Open for how to + reason about the range when the dial can't be read directly.

+ +
+ +
+ Marker Detection +
+ + +
+
+ + +
+

League defaults: 40 mm ID dot (the smaller, limiting marker per the + SSL Rules vision pattern), 6 px minimum (see + Step 4).

+
+ + + +
+
diff --git a/docs-source/field/cameras.md b/docs-source/field/cameras.md new file mode 100644 index 0000000..f1b7a37 --- /dev/null +++ b/docs-source/field/cameras.md @@ -0,0 +1,405 @@ +# Cameras and Lenses + +Picking cameras (and lenses) is one of the most asked questions by teams new and old alike. Often by the time a camera +upgrade is needed the team member who specd and built the original field is long gone. This page is intended to cover +the factors in choosing the number of cameras, parameters, vendors, and other complex and commonly asked about items. + +If you already know what you're doing and just want to check on configuration, see the +[camera calculator](camera-calculator.md). + +## Prerequisites + +Before choosing a camera you must have committed to a lab or event space. Ceiling height will determining factor. + +You should also have an idea of the field size. Are you doing a small patch in your lab for testing or are you hosting +an event with a regulation field. Either way, you need to know the size. This combined with ceiling height will +determine the number of cameras needed. + +## Major Factors + +First, we'll cover the major factor before doing an example calculation. This section covers the "why", not the "how". +Jump ahead if you'd like. + +### Pixel Density + +[Pixel density](https://en.wikipedia.org/wiki/Pixel_density) describes the number of pixels in a unit area, typically +pixels per inch (PPI) or pixels per cm (PPCM). This matters because interpreting the robot jersey requires enough pixels +covering its features; without enough, the robot's position cannot be recovered. Cameras have a +[field of view](https://en.wikipedia.org/wiki/Field_of_view), a cone that expands outward from the camera sensor. The +lens determines the properties of this cone, the same way your eyes resolve close objects in more detail than distant +ones. Because the camera's height above the field is fixed, knowing the height and the cone's properties is enough to +compute pixel density, or to solve for any other unknown in that relationship. + +| Field of View | Robot Jersey | +| ----------------------------------------------- | ----------------------------------------------------------- | +| ![Field of View](images/Angle_of_view.svg.webp) | ![Green Tea 2021 Robot](images/greentea-2021bot-jersey.jpg) | + +The left image shows a camera's field of view; the right shows an SSL robot's jersey (GreenTea 2021). If camera +resolution is too low to distinguish the yellow, pink, and green dots, the robot's position cannot be recovered. In the +left image, the green box grows larger the farther it sits from the camera. Since the sensor has a fixed number of +pixels, and that same pixel count spreads across a larger area farther from the camera, pixel density decreases with +distance. Eventually density drops below the size of a single dot, and position can no longer be recovered. The final +camera configuration must provide enough pixels to resolve each dot and the ball. + +### Field of View + +Field of view (FoV) refers to the properties of the cone or pyramid expanding from the image sensor, described above. It +is almost always given as either a single angle for a cone, or a horizontal and vertical angle for a pyramid. Because +FoV determines how much pixel density is lost per unit distance from the sensor, it is a key camera parameter: a wider +FoV loses more pixel density per unit distance but sees more of its surroundings. This is a geometry tradeoff — a low +ceiling calls for a wider FoV, since a narrow cone or pyramid under a low ceiling will not reach the outer parts of the +field. No single FoV is inherently good or bad; it is a tradeoff against mounting height and cost. Cheaper webcams +typically have an integrated lens with a fixed, wide FoV, which may not suit every space. High-end cameras separate the +camera body and lens, giving full control over FoV, at greater expense. + +### Shutter Type + +In this context, "shutter type" refers to a virtual shutter; physical shutters are not present on the cameras we buy. +The two major types are rolling and global. A rolling shutter converts one row of pixels at a time, whereas a global +shutter converts the whole image at once. A rolling shutter is cheaper, but when objects move quickly, different parts +of the object are converted at different times, an artifact sometimes called the jello effect. A global shutter avoids +this, at substantial cost and engineering complexity in the image sensor. **Global shutters are universally preferred +for robotics and computer vision tasks, but many teams do not buy them for their lab due to cost (sometimes 5-10x +cost).** The league's competition cameras are global shutter. **Anyone hosting an event should try to acquire global +shutter cameras.** + +The tradeoff is robot speed. The jello effect worsens as an object moves faster. On a small lab test patch, where robot +speed is limited by field size, global shutters offer no real benefit. On a regulation field with more open space, +robots reach much higher speeds, and global shutter cameras become necessary. The exact magnitude of the jello effect +depends on several environmentally dependent settings, including exposure, so it is not possible to state a reliable +threshold like "global shutter is needed above robot speed X". + +Teledyne-FLIR, our competition camera provider, has a +[detailed write-up of this effect](https://www.teledynevisionsolutions.com/learn/learning-center/imaging-fundamentals/rolling-vs-global-shutter/), +including the animation below. + +![Rolling vs Global Shutter](images/teledyne-rolling-vs-global-shutter_images_1.gif) + +Sony makes the global shutter image sensors that Teledyne-FLIR integrates. Their real-world example below shows the +difference between the shutter types in the curving of the helicopter rotor blades. + +```{youtube} YmEH8z1JWgc +``` + +### Frame Rate + +**Recommend >=60fps.** The competitions typically run 72fps, but this isn't guaranteed. + +### Software Support by League Vision Software + +The last key factor in purchasing a camera is software support. Most USB webcams use a standard protocol, supported via +[Video4Linux](https://en.wikipedia.org/wiki/Video4Linux) (v4l); the league vision tools integrate with v4l drivers +natively, so no development is needed for support. Ensure your camera's controls, such as white balance and exposure, +can be set from Linux and commanded not to auto-calibrate. Auto-calibration at the camera level is likely to break the +league software's color calibration, which expects those settings to remain constant after initial setup. + +Global shutter and other high-end machine vision cameras typically do not support standard USB drivers, since they do +not operate as standard USB devices; the vendor instead supplies a custom API that must be used. For the league cameras, +Teledyne-FLIR provides the +[Spinnaker SDK](https://www.teledynevisionsolutions.com/products/spinnaker-sdk/?model=Spinnaker%20SDK&vertical=machine%20vision&segment=iis) +for their USB3 and GigE cameras. The league software already supports this API, though sometimes small tweaks are needed +for a given camera model. Picking a high-end machine vision camera not supported by the Spinnaker SDK will probably +require writing your own driver interface code for the league software. This is possible, but not a trivial task. + +## Calculating Major Factors for Your Lab + +This section turns [Pixel Density](#pixel-density) and [Field of View](#field-of-view) above into a repeatable +calculation. Given a camera, lens, and mounting height, it tells you whether that combination resolves the robot +markers, and if not, what to change. + +### Inputs + +You need six numbers: + +- (h) — mounting height above the field surface (m) +- (f) — lens focal length (mm) +- $s_h, s_v$ — sensor width and height (mm), from the sensor datasheet +- $R_h, R_v$ — sensor resolution (pixels), horizontal and vertical +- (d) — diameter of the smallest feature you need to resolve (the robot ID/team marker dots) + +If your lens/camera is specified directly as a field of view angle instead of focal length + sensor size, skip to step +2\. + +### Step 1: Focal Length and Sensor Size to Field of View + +A rectilinear lens's field of view is: + +$$ +\mathrm{FoV}_h = 2 \arctan\left(\frac{s_h}{2f}\right) \qquad +\mathrm{FoV}_v = 2 \arctan\left(\frac{s_v}{2f}\right) +$$ + +### Step 2: Field of View to Ground Coverage + +For a camera mounted at height (h), looking straight down (nadir) at a flat field, the width and height of the ground +area it covers are: + +$$ +W = 2 h \tan\left(\frac{\mathrm{FoV}_h}{2}\right) \qquad +H = 2 h \tan\left(\frac{\mathrm{FoV}_v}{2}\right) +$$ + +This is an approximation: it assumes the camera points straight down and ignores lens distortion. It's accurate enough +to plan a purchase; the league software's calibration step corrects the remaining error at setup time. + +### Step 3: Ground Coverage to Pixel Density + +Pixel density is resolution divided by the ground distance it's spread across: + +$$ +\rho_h = \frac{R_h}{W} \qquad \rho_v = \frac{R_v}{H} +$$ + +Use $\rho = \min(\rho_h, \rho_v)$ as your working density — whichever dimension is tighter is what limits you. + +### Step 4: Check Against the Marker Size + +You need enough pixels across a marker dot to reliably find its center and classify its color, not just detect that +something is there. This page uses a minimum of 6 pixels across the diameter (d) — enough margin for motion blur and +rolling shutter that you don't also need to budget for those separately. The required density is: + +$$ +\rho_{\text{required}} = \frac{N_{\text{min}}}{d} +$$ + +The configuration works if $\rho \geq \rho_{\text{required}}$. If it doesn't, you have four options, in order of +usually-cheapest-first: + +- Lower the mounting height (h) (shrinks coverage, raises density; may not be possible if the ceiling is fixed) +- Use a longer focal length (f) (narrows FoV, raises density; also shrinks coverage — you may need more cameras to cover + the same field) +- Use a higher-resolution sensor (raises $R_h, R_v$ directly; no coverage tradeoff, but costs more) +- Split coverage across more cameras, each covering a smaller tile of the field at higher density (see + [Known Good Configurations](#known-good-configurations) for how the league does this at competition height) + +### Step 5: Field Coverage + +If a configuration needs $N$ cameras to cover a field of area $A_{\text{field}}$, a quick sanity check is the total +footprint budget relative to the field: + +$$ +\text{Coverage Margin} = \frac{N \times W \times H}{A_{\text{field}}} +$$ + +A margin at or below 100% means the cameras' combined footprint can't even cover the field once, let alone overlap — it +will not work regardless of how they're arranged. A margin comfortably above 100% (the excess is overlap budget, spent +on stitching adjacent frames together and tolerating calibration/mounting error) is necessary, but not sufficient by +itself — it doesn't check that the footprints can actually be arranged to eliminate gaps, only that there's enough total +area to make that arrangement possible. + +### Interactive Calculator + +Plug in numbers instead of doing the arithmetic in [Steps 1-5](#calculating-major-factors-for-your-lab) by hand, using +the [Camera Field of View Calculator](camera-calculator.md). + +## Known Good Configurations + +Below are some examples of camera configurations that meet the pixel density and software compatibility requirements. +Each configuration below is actively used by at least one team or event. Additional known good configurations are +welcome. Please include any known limitations if you make a pull request. + +### League Cameras and Lenses (Division A and Division B) + +Worked example using a common league camera and one of two compatible 5mm lenses. + +**Camera:** +[FLIR Blackfly S BFS-U3-51S5C-C](https://www.edmundoptics.com/p/bfs-u3-51s5c-c-usb3-blackfly-reg-s-color-camera/37240/) +— 2/3" sensor, $s_h = 8.45\ \text{mm}$, $s_v = 7.07\ \text{mm}$, $R_h = 2448\ \text{px}$, $R_v = 2048\ \text{px}$ (3.45 +µm pixels). + +**Lens:** either of Navitar's two 5 mm, 2/3"-format C-mount lenses (`1-19552` or `NMV-5M23`). They differ in aperture +range, not focal length or target sensor format, so $\mathrm{FoV}$ is the same either way for planning purposes: +$f = 5\ \text{mm}$. + +**Mounting height:** $h = 6000\ \text{mm}$, within the [6.0-6.25 m official range](complayout.md#cameras). + +**Marker size:** per the [SSL Rules vision pattern](https://ssl.robocup.org/rules/), the center (team) dot is Ø50 mm and +the four ID dots are Ø40 mm. The ID dots are the smaller, limiting feature: $d = 40\ \text{mm}$. + +Using the [general method](#calculating-major-factors-for-your-lab): + +$$ +\mathrm{FoV}_h = 2 \arctan\left(\frac{8.45}{2 \times 5}\right) \approx 80.4° +\qquad +\mathrm{FoV}_v = 2 \arctan\left(\frac{7.07}{2 \times 5}\right) \approx 70.5° +$$ + +$$ +W = 2(6000) \tan(40.2°) \approx 10{,}140\ \text{mm} +\qquad +H = 2(6000) \tan(35.3°) \approx 8{,}484\ \text{mm} +$$ + +$$ +\rho_h = \frac{2448}{10{,}140} \approx 0.241\ \text{px/mm} +\qquad +\rho_v = \frac{2048}{8{,}484} \approx 0.241\ \text{px/mm} +$$ + +At $\rho \approx 0.241\ \text{px/mm}$, the 40 mm ID dots resolve to about $0.241 \times 40 \approx 9.7$ pixels across, +and the 50 mm center dot to about 12 pixels across — both above the 6 px minimum from +[Step 4](#step-4-check-against-the-marker-size). **This configuration passes**, with roughly 1.6-2x margin over the +minimum threshold. + +**Field coverage** ([Step 5](#step-5-field-coverage)): this configuration's single-camera footprint is +$W \times H \approx 10.14 \times 8.48\ \text{m} \approx 86.0\ \text{m}^2$. Per the +[SSL Rules field dimensions](https://ssl.robocup.org/rules/), Division A's field of play is +$13.4 \times 10.4\ \text{m} = 139.4\ \text{m}^2$ and Division B's is $10.4 \times 7.4\ \text{m} = 77.0\ \text{m}^2$: + +| Division | Cameras ($N$) | Total Footprint | Field Area | Coverage Margin | +| -------- | ------------- | --------------- | ---------- | --------------- | +| A | 2 | 172.1 m² | 139.4 m² | 123% | +| B | 1 | 86.0 m² | 77.0 m² | 112% | + +Both divisions clear 100% with a positive overlap budget, consistent with these being real, working configurations — +Division B's margin is noticeably tighter, leaving less room for stitching/calibration error with only one camera. + +### Japan Open Cameras and Lenses (Division A) + +Same camera as [League Cameras and Lenses](#league-cameras-and-lenses-division-a-and-division-b) +([FLIR Blackfly S BFS-U3-51S5C-C](https://www.edmundoptics.com/p/bfs-u3-51s5c-c-usb3-blackfly-reg-s-color-camera/37240/)), +paired with a different 5 mm lens vendor: [Kowa LM5JC1M / LM5JCM](https://www.kowavision.com/products/lm5jc1m-lm5jcm) — +2/3" max sensor format, F2.8-16, 0.5% TV distortion. + +Since the focal length ($f = 5\ \text{mm}$) and sensor are identical to the League configuration, the +[FoV, ground coverage, and pixel density](#league-cameras-and-lenses-division-a-and-division-b) work out the same: +$\rho \approx +0.241\ \text{px/mm}$, about 9.7 px across the 40 mm ID dots. The lenses differ in aperture range and mechanical build, +not focal length or target format, so this doesn't change the optics. + +**Field coverage:** Division A uses 2 cameras, same as League — coverage margin 123% (see +[Step 5](#step-5-field-coverage)). Division B count for this configuration is not yet documented here. + +### Peachtree Open Cameras and Lenses (Division B) + +Four cameras cover Division B at Peachtree Open, split evenly into quadrants: two brought by RoboJackets, two by The +A-Team. Both teams use the same camera but different lenses, since each team's home lab ceiling differs. + +**Camera (both teams):** +[FLIR Blackfly BFLY-PGE-13E4C-CS](https://www.edmundoptics.com/p/bfly-pge-13e4c-cs-118-blackflyreg-poe-gige-color-camera-d7aa6dab/30322/) +— 1/1.8" sensor, $s_h = 6.78\ \text{mm}$, $s_v = 5.43\ \text{mm}$, $R_h = 1280\ \text{px}$, $R_v = 1024\ \text{px}$ (5.3 +µm pixels). + +Both lenses are **varifocal** (adjustable focal length, unmarked dial) rather than fixed, so there's no single focal +length to plug into the method — instead, this solves the [general method](#calculating-major-factors-for-your-lab) in +reverse: given the lens's widest available zoom setting, what's the minimum mounting height needed to cover a full +quadrant with no gap? + +#### At Peachtree Open + +Cameras mount on arms with adjustable height, 10-12 ft ($3048$-$3658\ \text{mm}$) — this is the actual competition +mounting range, independent of either team's home lab ceiling. + +**Target coverage:** a quadrant of Division B's $10.4 \times 7.4\ \text{m}$ field of play, split evenly: +$\text{target}_W = 5200\ \text{mm}$, $\text{target}_H = 3700\ \text{mm}$. + +Rearranging [Step 2](#step-2-field-of-view-to-ground-coverage) ($W = h s_h / f$) for the minimum height that satisfies +both axes at a given focal length (f): + +$$ +h_{\text{required}} = \max\left(\frac{\text{target}_W \cdot f}{s_h},\ \frac{\text{target}_H \cdot f}{s_v}\right) +$$ + +**RoboJackets** — +[Computar E3Z4518CS-MPIR](https://www.bhphotovideo.com/c/product/1091754-REG/computar_e3z4518cs_mpir_1_2_4_5_13_2mm_f1_8_dn.html), +4.5-13.2mm, F1.8. At the widest end ($f = 4.5\ \text{mm}$): + +$$ +h_{\text{required}} = \max\left(\frac{5200 \times 4.5}{6.78},\ \frac{3700 \times 4.5}{5.43}\right) += \max(3451,\ 3066)\ \text{mm} \approx 3.45\ \text{m} +$$ + +This sits near the top of the 3.05-3.66 m arm range — consistent with RoboJackets' cameras running high on the arm +(reachable by ladder, so not an installation problem). + +**The A-Team** — +[Computar E3Z3915CS-MPWIR](https://www.bhphotovideo.com/c/product/1524951-REG/computar_e3z3915cs_mpwir_4k_1_1_8_3_9_10mm_f1_5.html), +3.9-10mm, F1.5. At the widest end ($f = 3.9\ \text{mm}$): + +$$ +h_{\text{required}} = \max\left(\frac{5200 \times 3.9}{6.78},\ \frac{3700 \times 3.9}{5.43}\right) += \max(2991,\ 2657)\ \text{mm} \approx 2.99\ \text{m} +$$ + +This clears even the arm's minimum height (3.05 m), so The A-Team's cameras don't need to run high — any arm position +works. + +Using RoboJackets near the top of the range (12 ft) and The A-Team near the bottom (10 ft) as representative positions, +the aggregate [coverage margin](#step-5-field-coverage) is: + +$$ +\frac{2(5512 \times 4414) + 2(5300 \times 4245)}{10{,}400 \times 7400}\ \text{mm}^2 \approx \frac{93.6\ \text{m}^2}{77.0\ \text{m}^2} \approx 122\% +$$ + +This is comfortably above 100%, consistent with this being the actual working competition configuration. + +This can be seen practically in the below photo of the Peachtree Open Field, where the cameras are at different heights. + +![Peachtree Open Field](images/peachtree-open-field.jpg) + +#### In Each Team's Home Lab + +Both teams practice on a much smaller field than Division B — approximately $4.5 \times 3.5\ \text{m}$ each, still +covered by the same two cameras, split along the longer axis: $\text{target}_W = 2250\ \text{mm}$, +$\text{target}_H = 3500\ \text{mm}$. + +At home, RoboJackets mounts around $h = 3000\ \text{mm}$ (close to competition height) and The A-Team around +$h = 2438\ \text{mm}$ (8 ft, a much lower home ceiling) — using the same lenses as at competition, at their widest +setting. + +| Team | (h) | (f) | $\rho$ (px/mm) | Px across 40 mm dot | Coverage vs. target | +| ----------- | ------- | ------ | -------------- | ------------------- | ------------------------------------------ | +| RoboJackets | 3000 mm | 4.5 mm | 0.283 | ≈11.3 | passes, ≈3% margin (H-axis limited) | +| The A-Team | 2438 mm | 3.9 mm | 0.302 | ≈12.1 | short ≈3% (H-axis; needs $h \geq 2515$ mm) | + +Pixel density is never the constraint at either team's home setup — it's roughly 1.9-2x the +[Step 4](#step-4-check-against-the-marker-size) minimum in both cases. Coverage geometry is the real limiter, and it's +tight: RoboJackets clears their smaller home target with only a few percent to spare, and The A-Team's low ceiling falls +just short even on this much smaller field. This matches what you'd expect if a wider lens simply isn't available to buy +— the ceiling height is fixed, so the only lever left is focal length, and both teams are already at their lens's widest +setting. + +### A-Team Seattle Field Office (Living Room Testing Patch) + +**Camera:** +[MOKOSE UC70](https://www.mokose.com/products/mokose-4k-30fps-usb-camera-webcam-uvc-free-drive-compatible-windows-mac-os-x-linux) +with the 4.2mm wide-angle fixed lens option — a consumer USB webcam rather than a machine vision camera. 1/1.8" sensor, +$R_h = 3840\ \text{px}$, $R_v = 2160\ \text{px}$ (4K), 2 µm pixels. Sensor size isn't published directly, but is exact +from resolution × pixel pitch: $s_h = 3840 \times 0.002 = 7.68\ \text{mm}$, $s_v = 2160 \times 0.002 = 4.32\ \text{mm}$. + +**Mounting height:** not precisely known — estimated at $h = 2438\ \text{mm}$ (8 ft, a typical US apartment ceiling). +Treat the result below as provisional until the real height is confirmed. + +**Target coverage:** the full testing patch, one camera, no split: $\text{target}_W = 2500\ \text{mm}$, +$\text{target}_H = 1500\ \text{mm}$ (the stated $1.5 \times 2.5\ \text{m}$ upper bound). + +$$ +\mathrm{FoV}_h = 2 \arctan\left(\frac{7.68}{2 \times 4.2}\right) \approx 84.9° +\qquad +\mathrm{FoV}_v = 2 \arctan\left(\frac{4.32}{2 \times 4.2}\right) \approx 54.4° +$$ + +$$ +W = \frac{2438 \times 7.68}{4.2} \approx 4458\ \text{mm} +\qquad +H = \frac{2438 \times 4.32}{4.2} \approx 2508\ \text{mm} +$$ + +Both axes clear the target with room to spare: $4458 / 2500 \approx 178\%$ and $2508 / 1500 \approx 167\%$. Pixel +density is not close to a limit either — $\rho \approx 0.861\ \text{px/mm}$ puts about 34 pixels across the 40 mm ID +dot, roughly 5.7x the [Step 4](#step-4-check-against-the-marker-size) minimum. + +Solving [Step 2](#step-2-field-of-view-to-ground-coverage) in reverse, this lens only needs +$h_{\text{required}} \approx 1.46\ \text{m}$ to cover the patch at all — well under the assumed 2.44 m mount. + +**This doesn't come out geometry-limited**, unlike every other configuration on this page — the 4.2mm lens is wide +enough that, at the assumed height, both coverage and density have large margin. That's the opposite of what was +expected here, which most likely means either the actual mounting height is lower than 8 ft (e.g. shelf- or arm-mounted +well below the ceiling, not ceiling-mounted), or the real testing patch is smaller than the stated upper bound in a way +that matters less than height does. The real height is worth confirming if this configuration is meant to illustrate a +geometry-limited case. + +## Additional Resources + +- [ThorLabs Guide on Sensors and Lens Parameters](https://www.thorlabs.com/camera-lens-tutorial) +- [Wiki on Lens and Mounts](https://en.wikipedia.org/wiki/Camera_lens) diff --git a/docs-source/field/carpet.md b/docs-source/field/carpet.md new file mode 100644 index 0000000..5de6d98 --- /dev/null +++ b/docs-source/field/carpet.md @@ -0,0 +1 @@ +# Flooring and Carpet diff --git a/docs-source/field/complayout.md b/docs-source/field/complayout.md new file mode 100644 index 0000000..24908d9 --- /dev/null +++ b/docs-source/field/complayout.md @@ -0,0 +1,199 @@ +# Competition Field + +This page describes a full feature competition field setup and layout. +[Field dimensions](https://robocup-ssl.github.io/ssl-rules/sslrules.html#_dimensions) are described in the SSL Rules. + +Aspects of the described field are optional, particularly audience facing items. When something is optional, it is so +noted. + +## Top Level Diagram + +A full diagram is provided below. Scale is approximately relative to the field, but omitted to allow for local variance. + +![Full Competition Field Diagram](images/comp-field-arch-icons.svg) + +There's a lot to dissect here, but lets define some abbreviations: + +- GC - Game Controller +- VIS - Vision Computer +- NE - Networking Equipment +- SC - Streaming Computer + +The colors hold meaning as well: + +- Yellow - Yellow Team resource +- Blue - Blue Team resource +- Cyan - Event/league resource +- Orange - Role provided by neutral party +- Pink - Role provided by neurtal party + +## Placement and Layout Guidance + +The above layout is intentionally crafted to provide a smooth experience for officials, teams, and spectators. + +### Field Placement + +The field should placed in the venue such that a clear separation between spectators and teams is achieved. There's +should be a team area away from the field, often called "the pits", where mechanical and electrical work is done. Teams +spend most of their time in the pits when not competing in a match or testing during an official testing time slot. +Spectators should be granted a clear view of the competition field, but distanced from the pits. Occasionally robots +malfunction so this separation is key. A physical barrier is recommended between spectators and the field and pits. + +### Tables + +Enough tables should be provided such that the vision and game controller each have at least half a table. Each team +should have at least 1 full table at the field during their match or registered practice slot. A permanent dedicated +field table for the full duration of the event is not guaranteed; teams may need to share between matches. Tables should +be no smaller than ~1m x 2m (3 x 6ft). + +A gap should be present at the midfield line so game officials and robot handles have easy access to enter and leave the +field. Care should be taken to secure netwokring cables on the ground at this entry point in particular. This is also +the area most likely to be used to put in and remove robots (compared to the far side). + +Organizers should avoid placing tables between the field and spectators. Typically with specified field size and number +of teams in attendance it is not necessary to place tables all the way around the field. This means it is possible to +keep clear line of sight between spectators and the field. This also has the advantage of placing the robot handlers and +remote controls in closer proximity to the referee and game controller. If issues arise via challenge flags, or field +network faults all of the relevant parties are in closer proximity to each other to resolve any issues. + +Each table should be provided with one power outlet and one networking cable. It is the team's responsibility to bring a +power strip or network switch if they need more than one connection when arriving at their table for the match. + +### Event Equipment + +The vision and game controller computers should be placed close to each other, the midfield line, and the referee. This +has several advantages: + +- The Referee and GCO can communicate easily +- The VE and GCO can communicate as necessary +- Networking is centrally located and can be patched more easily +- A physical space buffer is provided between the teams and the midfield, lowering congestion + +It's shown in the diagram as being split across the midfield line, but combining them into one table on a single side is +also common. If this is done, it's preferable to have one empty table on the opposing side, or to omit it to provide the +referee a buffer. + +Ideally, at least one status board is positioned such that the audience, referees, and teams have good visibility into +the game state. While most events provide at least one status board, a first time event or casual meetup for scrimamge +may not set up any status boards. + +If possible, the Audio Referee (at least the speaker) should be placed on the far side of the field so it speaks only to +the audience, not the field *and* audience. It can speak fairly often and this can be overwhelming to the referee and +assistant referee who may be attempting to communicate both across the field and a language barrier. + +## Function of Components + +Only the function of components, not people are described here. For the purpose of specific human roles, consult the +rules. + +### Networking Equipment + +The networking equipment is the small cyan box labeled "NE" on the diagram. It is typically centrally located near the +ground. The networking equipment is responsible for connecting all of the computers at the field and providing essential +network services. See the [Network and Compute](network/compute.md) for physical network details. + +### Cameras + +One or more overhead cameras are needed to observe the field and pass video frames to league processing software. These +cameras are typically mounted between 2.5m and 6.5m above the field. At official tournaments, they are typically mounted +around 6.0-6.25m. When cameras are mounted near the top of the range, a small computer is mounted with the camera on the +truss or building features. This is usually to overcome difficulties in transmitting raw camera data long distances. +More information is present in the [Cameras and Lenses](cameras.md) page. + +### Vision Computer + +The vision computer is the device labeled "VIS" in the diagram. The vision computer is responsible for providing robot +and ball position data on the network. League software processes camera data and field geometry to produce accurate +positions. This computer needs a connection to one or more cameras directly, or their associated mounted computer. The +specifics of this connection can be very field dependent and are described in more detail under the possible +configurations section below. See the [Vision Protocol](/protocol/vision.md) page for the league software that fills +this role. + +### Game Controller Computer + +The game controller computer is the device labeled "GC" in the diagram. The game controller computer manages the game +state and publishes this information to the network. It also accepts requests from the teams via the remote control or +optionally the team AI software. See the [Referee Protocol](/protocol/referee.md) page for the league software that +fills this role. + +### Remote Control + +Each team is provided a remote control at the field to interact with the controller/referee software. These are labeled +"RC" in the diagram. Teams use these to request timeouts and robot exchanges. If possible, the 3D printed cases should +be color matched to the team so it's clear which remote is which. Additionally, cables should be color matched as well. +Often times networking contractors at large events are unable to accommodate the remote color matching. + +![Remote Control](images/peachtree-open-remote-controls.png) + +### Team Computer + +Teams are responsible for bringing their own computer to run their software and AI. Teams should expect only one network +and one power connection at their table. + +### Status Board (Recommended, Optional) + +The status board displays the score, game time, and events such as infractions and goals. At least one display should be +available to the referees and teams. Additional displays for the audience are optional. + +### Streaming Computer (Optional) + +The streaming computer is labeled "SC" on the diagram. It is an optional system to record and stream video in realtime +to one or more livestreams. It is capable of switching between camera feeds and can receive the game state on the field +network to produce a scoring overlay for viewiers. This is the only computer to which an internet connection is +guaranteed to be provided. Because it is optional, some competitions completely omit this system. + +### Audio Referee (Optional) + +The [audio referee](https://github.com/TIGERs-Mannheim/AudioRef) receives the game state and announces key events to the +audience automatically. Because it is optional, some competitions completely omit this system. + +## Possible Configurations + +A key challenge with setting up a RoboCup SSL events or lab space is the camera arrangement. Mounting cameras high up +carries strong advantages in producing a low-distoration image with a fewer number of cameras. This is a massive benefit +when setting up many fields at the international event. However, for many lab spaces and lower-cost events mounting +cameras at 6m simply isn't possible (ceiling is too low) or carries significant expense (renting overhead mounts and +certified personnel). As such, camera configuration may vary from event to event, though the main international event is +fiarly consistent. Some common configurations are listed below. + +### International Event - Division B + +The simplest configuration is the international event Division B field. In this configuration, a single camera is +mounted at ~6.25m in position "A" in the layout diagram. A computer is mounted with it on the overhead truss. The vision +computer remotely accesses the camera computer mounted overhead. In this configuration, the vision computer is only used +for remote access. + +### International Event - Division A + +The second simplest configuration is the international event Division A field. In this configuration, a two cameras are +mounted at ~6.25m in positions "B" in the layout diagram. Each camera covers half a field. A single camera is not used +because it would need to be nearly quadruple the resolution and mounted even higher, which often practically impossible. +A computer is mounted with each camera on the overhead truss in positions "B". The vision computer remotely accesses the +camera computers mounted overhead, one at a time. In this configuration, the vision computer is only used for remote +access. + +### Regional Event + +Regional events may cut costs by mounting a higher number of cameras lower to the ground. These cameras can also be +lower resolution because they are covering a smaller area of the field. This configuration is used at the North American +Peachtree Open Event. In this configuration, 4 cameras are mounted at positions "C". Because the data is lower and +mounting is lower, computers are not mounted overhead with the cameras, but instead the raw camera data is sent directly +to the vision computer. In this case, the vision computer is not used for remote access, but directly hosts the software +that processes the vision data. + +![Peachtree Open Field](images/peachtree-open-field.jpg) + +This picture is taken from the 1st Annual Peachtree Open. Notice there are four overhead cameras, none with associated +computers overhead. The vision and game controller computers are on the back field side, though they are obstructing the +midfield line which is not ideal. Note team tables to the left (and right, not pictured) do not block the spectator +view. Blue pipe and drape separates the spectators from the field. Team pits are located along the back wall. + +Not all regional events use this modified configuration. For example the Japan Open and German Open do not, as they have +established infrastructure and event space to do the higher mounting. + +### University Lab Space + +University lab space also has ceiling height and cost constraints most of the time. A typical lab configuration looks +similar to the regional event configuration, but with only a partial field and even fewer cameras. Often labs have a +half or quarter Division B field, with one or two cameras. Sometimes the cameras are mounted to the side and not +overhead. More details on this configuration can be seen in the [lab layout](lablayout.md) page. diff --git a/docs-source/field/images/Angle_of_view.svg.webp b/docs-source/field/images/Angle_of_view.svg.webp new file mode 100644 index 0000000..6dbd6da Binary files /dev/null and b/docs-source/field/images/Angle_of_view.svg.webp differ diff --git a/docs-source/field/images/comp-field-arch-icons.svg b/docs-source/field/images/comp-field-arch-icons.svg new file mode 100644 index 0000000..208be23 --- /dev/null +++ b/docs-source/field/images/comp-field-arch-icons.svg @@ -0,0 +1,1456 @@ + + + +image/svg+xmlRepresentative Competition Field ArchitectureSpectator / Audience AreaStatusBoardStatusBoardAudioRefereeNEBlueTeamYellowTeamVISSCGCRCRCRefereeGame Controller OperatorStreaming OperatorVision ExpertAssistant RefereeRobot HandlerRobot HandlerCCCCBBA diff --git a/docs-source/field/images/comp-field-full.png b/docs-source/field/images/comp-field-full.png new file mode 100644 index 0000000..d5b98fb Binary files /dev/null and b/docs-source/field/images/comp-field-full.png differ diff --git a/docs-source/field/images/comp-field-simpl.svg b/docs-source/field/images/comp-field-simpl.svg new file mode 100644 index 0000000..7b1d493 --- /dev/null +++ b/docs-source/field/images/comp-field-simpl.svg @@ -0,0 +1,1418 @@ + + + +image/svg+xmlRepresentative Competition Field ArchitectureSpectator / Audience AreaStatusBoardStatusBoardNEBlueTeamYellowTeamVISGCRCRCAssisted RefereeRefereeVision ExpertGame Controller OperatorRobot HandlerRobot Handler diff --git a/docs-source/field/images/greentea-2021bot-jersey.jpg b/docs-source/field/images/greentea-2021bot-jersey.jpg new file mode 100644 index 0000000..0dd8135 Binary files /dev/null and b/docs-source/field/images/greentea-2021bot-jersey.jpg differ diff --git a/docs-source/field/images/peachtree-open-field.jpg b/docs-source/field/images/peachtree-open-field.jpg new file mode 100644 index 0000000..4d6457b Binary files /dev/null and b/docs-source/field/images/peachtree-open-field.jpg differ diff --git a/docs-source/field/images/peachtree-open-remote-controls.png b/docs-source/field/images/peachtree-open-remote-controls.png new file mode 100644 index 0000000..699425c Binary files /dev/null and b/docs-source/field/images/peachtree-open-remote-controls.png differ diff --git a/docs-source/field/images/teledyne-rolling-vs-global-shutter_images_1.gif b/docs-source/field/images/teledyne-rolling-vs-global-shutter_images_1.gif new file mode 100644 index 0000000..9600cbb Binary files /dev/null and b/docs-source/field/images/teledyne-rolling-vs-global-shutter_images_1.gif differ diff --git a/docs-source/field/lablayout.md b/docs-source/field/lablayout.md new file mode 100644 index 0000000..6a29ec2 --- /dev/null +++ b/docs-source/field/lablayout.md @@ -0,0 +1,64 @@ +# Lab/Home Field + +This page describes a minimal functional field for use in the lab and at home. It emphasized the minimum required to get +started and to minimize cost. Generally speaking, everything on this page and diagram is required. + +If you haven't already read the [competition layout](complayout.md) page, you should read that one first to understand +all of the pieces at play. This minimal setup condenses that functionality. + +## Top Level Diagram + +A diagram is provided below. Scale is approximately relative to the field. + +**Picture coming soon!** + +![Full Competition Field Diagram](images/comp-field-full.png) + +There's a lot to dissect here, some abbreviations: + +- FC - Field Computer +- TC - Team Computer +- NE - Networking Equipment + +## Placement and Layout Guidance + +This is mostly a factor of what works for your team and space. You should checkout the section on +[Carpet and Materials](carpet.md) and [Cameras](cameras.md). There's some advice there if you floor is not level or you +have an unusual ceiling. **Remember, a practice field does not need to be standard dimensions or aspect ratio!** You +seek to maximize the field size given your space and cost constraints. The vision software can be configured for +arbitrary dimensions. + +## Function of Components + +In a typical lab setup, lots of equipment does double duty. + +### Networking Equipment + +The networking equipment is the small cyan box labeled "NE" on the diagram. This will connect the team computers +together, provide networking services, and also provide an internet uplink. This is often preferred over wireless. + +### Field Computer + +The lab field computer combines multiple services into one compared to a competition config. Typically the field +computer hosts at least vision and the game controller, the two services essential to test the team AI and ensure it +responds to events. The GC is also used to trigger/force out of sequence events, like kickoffs or penalties to test +specific plays or responses to match state. + +### Team Computer + +The team computer may be a member's laptop or dedicated machine. This will run the team AI. It's worth noting that if +possible the team and field computers should be separate. Hopefully you have at least two team members. Often times +vision and the AI are each computationally demanding enough they may effect the behavior of the other. Game controller +can be hosted on either, though most teams keep it with vision. + +### Cameras + +Most teams have one or two lower end cameras, like a webcam, mounted above the field. These are connected directly to +the field computer. If the team's lab is an office environment, they are typically mounted as close to the ceiling as +possible. The [Cameras](cameras.md) page has advice on camera and lens selection and calculating a mounting height. + +### Optional Items + +Lab setups almost always omit the status board, remote controls, audio ref, and stream equipment. + +## Recommended Equipment diff --git a/docs-source/field/network/compnetwork.md b/docs-source/field/network/compnetwork.md new file mode 100644 index 0000000..4e7c62b --- /dev/null +++ b/docs-source/field/network/compnetwork.md @@ -0,0 +1,213 @@ +# Competition Network + +This page describes the logical and physical network that runs the fields at the international competition. At the +international event, the league typically supports: + +- 1 Division A match field +- 1-2 Division B match fields +- 1 Division B practice field + +This document will first cover a top level connection diagram, then a physical connection diagram. League protocols that +transmit on the network are covered in a separate page. + +If you have not already familiarized yourself with the [competition layout](../complayout.md), please do so first. Also +checkout the [lab network](labnetwork.md) page for a simpler version of the field network that aligns with lab or +regional event single-field deployments. + +## High Level Overview + +The competition layout is re-posted here for convenience. + +![Competition Layout](../images/comp-field-arch-icons.svg) + +### Networking Equipment + +Every field needs networking equipment to provide key services: + +- Physical connectivity across all field components +- Addresses for computers +- Multicast and broadcast support for distributing league traffic +- Isolation from other networks and fields + +A switch at each field provides physical connectivity to all computers at the field via Gigabit Ethernet. These +connections are almost always copper Cat 5e or Cat 6. Switches should only service one field, no cross over. + +A router provides the rest of the functionality. The router hosts a +[DHCP](https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol) server to dynamically allocate addresses to +all machines at the field. It may provide management services for broadcast and multicast as well (e.g. an +[IGMP](https://en.wikipedia.org/wiki/Internet_Group_Management_Protocol) quierier). **Teams at the field should expect +to be able to walk up to an unused cable and receive an IP address.** Teams should expect the addresses of field +computers and their own computer to change overnight or between matches, as DHCP leases may expire. + +The router's firewall also typically isolates traffic. It will restrict broadcast and multicast to a specific field. It +may also be used to prohibit connection to the internet if affects the reliability of a field during a match. Any event +that runs multiple fields needs to provide strong partitioning between each field network. Failure to do so results in +conflicting vision and referee data and makes isolating offending services very difficult. There are two ways to do this +with a router, physical isolation and virtual isolation. + +The diagrams below show the options. + +````{container} two-col-diagram-grid wide-right +**Physical Network Core** + +**VLAN Network Core** + +```{mermaid} diagrams/net-core.mmd +``` + +```{mermaid} diagrams/net-core-vlan.mmd +``` +```` + +Left, providing a physical router/firewall per field solves the isolation problem by default. Nearly all routers will +have default rules in place to disallow broadcast and multicast traffic between the WAN (upstream) and LAN ports. This +is the configuration most teams use in their lab, and many regional events use as well. Again, if multiple field +switches are connected via the LAN ports, the isolation is broken. + +Right, big event competitions will use a more complex configuration. This is mostly the result of a logistics +complication; big event spaces will have a dedicated networking contractor/company and this company provides the field +networking equipment. It is generally easier for them to provide *virtual isolation* between fields as a part of the +bigger picture that provides networking at the venue to multiple leagues and may utilitze existing infrastructure in the +building. The mechanism that provides this isolation is called a [VLAN](https://en.wikipedia.org/wiki/VLAN). When this +mode of isolation is used, the SSL committees typically have no information or control regarding the backing +configuration. Compatible switches combined with a router can tag traffic to specific VLANs and networking equipment is +configured to prohibit cross-VLAN communication. A reasonable VLAN configuration would be one VLAN per field, though a +networking contractor has never confirmed this for sure at an international event. The router and switches must both +support VLAN features for this system to work. When receiving a quote for services from a big contractor, this option is +expected to be cheaper than having them provide dedicated equipment. + +### Cameras + +There are two paradigms for camera mounting. In both cases the cameras are mounted on a ceiling or truss. The main +difference is the location of the computer ingesting raw video data. In most competition configs, small computers are +suspended on the truss with the camera. In most lab configs the cameras stand alone on the ceiling (or truss) and the +computer is field side. Putting distributed compute on the truss sounds unnecessairly complex and expensive. Why pay for +multiple computers? Why force remote access to them? The reason is uncompressed video bandwidth. Machine vision cameras +typically don't compress their video. This means means the data link between the camera and vision computer must +reliably transport the full bitrate. Take for example the league camera that have a resolution of 2448 x 2024. This +means an image is 5MP. At 12bit (1.5 byte) color depth, and 75 fps. The data rate is 4.45 gbit/s excluding medium +[line code](https://en.wikipedia.org/wiki/Line_code) and +[error correction code](https://en.wikipedia.org/wiki/Error_correction_code) which add additional overhead. As of 2026, +carrying multiple 5gbit/s signals more than a few meters is still an expensive proposition. It was even more so when the +cameras were purchased in ~2019. 10GbE networking equipment is hundreds to thousands of dollars, and 5 and 10GbE machine +vision cameras are relatively new. USB 3.1+ cable extenders are also many hundreds of dollars per extender. This means +the cost to buy a small truss computer is about the same as methods to extend high-bitrate signals to the ground +reliably. As such, at the time of purchasing the league equipment, the committees concluded the on truss computer was a +more reliable option. The league purchased [NUCs](https://en.wikipedia.org/wiki/Next_Unit_of_Computing). + +For most lab setups, the length of USB cable needed is short enough, that a USB 3.1+ extender is not needed, just a nice +cable. Alternatively, the small field size means 1Gbit PoE is enough to carry the raw video frames, and expensive +networking equipment is not needed. As such, competitions run distributed vision systems, with primary processing being +on the truss and the field vision computer just providing a human interface for remote access. Most lab configurations +have the field side computer run everything, but the cable length and bitrates involved support this simpler cheaper +configuration. + +Strictly talking about competition configs, the on truss mounting is used for Division A and Division B fields at the +international event. Those configurations run 2-camera, 2-NUC and 1-camera, 1-NUC config respectively. Regional events +may use the 4-camera direct config for a Division B field if a sufficiently high truss cannot be secured. Limits on +optics and distortion force more cameras at lower resolution when truss height is low. + +These options are shows in the table below. + +````{container} two-col-diagram-grid +**Truss NUC — 1–2 Cameras** + +**Direct Wiring — 4 Cameras** + +```{mermaid} diagrams/cam-nuc.mmd +``` + +```{mermaid} diagrams/cam-direct.mmd +``` +```` + +### Field Side + +The networking of the remaining computers field side is straightforward. At this point, the complex requirements of +isolation and high bitrates are handled. The rest of the system is latency sensitive, but the data rate is so low this +is virtually never a problem when using wired connectivity. All equipment is linked via GbE copper to the field-side +switch. The remote controls need [Power over Ethernet (PoE)](https://en.wikipedia.org/wiki/Power_over_Ethernet) for +power. + +```{mermaid} diagrams/field-side-equipment.mmd +``` + +## Detailed Diagram + +The below diagrams show near full instantiations of above general architecture. As noted in the legend of the diagrams, +some items like IP addresses and VLANs are examples. In reality IP addresses will be dynamically allocated and will +change throughout the competition. Other such information is noted in the legend box. + +### Truss NUC Config — Division A / Division B + +```{image} diagrams/ssl_field_network_fanout_truss.light.svg +--- +class: only-light +alt: SSL field network fan-out diagram, truss-mounted camera config +--- +``` + +```{image} diagrams/ssl_field_network_fanout_truss.dark.svg +--- +class: only-dark +alt: SSL field network fan-out diagram, truss-mounted camera config +--- +``` + +### Direct Wiring Config — Regional Division B + +```{image} diagrams/ssl_field_network_fanout_direct.light.svg +--- +class: only-light +alt: SSL field network fan-out diagram, direct-wired camera config +--- +``` + +```{image} diagrams/ssl_field_network_fanout_direct.dark.svg +--- +class: only-dark +alt: SSL field network fan-out diagram, direct-wired camera config +--- +``` + +## Technical Configurations + +This section contains technical configurations for the network and computers that aren't captured by the physical and +logical layouts above. + +### Hostnames + +The hostname convention for machines is `--`. Examples are included below. + +- truss NUCs: `ssl-vision-a`, `ssl-vision-b` etc. +- field vision computer: `ssl-vision-b0` where "b0" is the field name +- field game controller computer: `ssl-gc-b0` where "b0" is the field name + +Host names may also reflect the machine owner or event name `pto-vision` where "pto" refers to the Peachtree Open. + +### LAN Config + +Field LAN should always be [IPv4](https://en.wikipedia.org/wiki/IPv4) and never +[IPv6](https://en.wikipedia.org/wiki/IPv6), though the upstream WAN/venue network may be IPv6. Field LAN should supply +at least a /24 subnet, with /16 preferred. For an example network, 10.10.0.0/16: + +- Subnet address CIDR notation: `10.10.0.0/16` +- Gateway: 10.10.0.1 +- Reserved for management addressing: `10.10.0.2`-`10.10.0.5` +- DHCP range: `10.10.0.6`-`10.10.255.249` +- Broadcast address: `10.10.255.255` + +[CIDR standardized notation and allocations](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) + +### IGMP Querier and IGMP Snooping Settings + +Somebody knowledgeable needs to root cause, data cap, and show mitigation settings. Look at Incheon stuff... + +## Additional Resources + +- [Network Router]() +- [Network Switch](https://en.wikipedia.org/wiki/Network_switch) +- [IPv4](https://en.wikipedia.org/wiki/IPv4) +- [Classless Inter-Domain Routing](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) +- [List of Reserved IP Addresses](https://en.wikipedia.org/wiki/List_of_reserved_IP_addresses) +- [Power over Ethernet](https://en.wikipedia.org/wiki/Power_over_Ethernet) diff --git a/docs-source/field/network/compute.md b/docs-source/field/network/compute.md new file mode 100644 index 0000000..18f8617 --- /dev/null +++ b/docs-source/field/network/compute.md @@ -0,0 +1,69 @@ +# Compute Resources + +This brief article lists vendors and approximiate specification of league computing resources. + +Below are the minimum specifications of league equipment. In for everything but the streaming computer, the minimum +specification is often driven by supporting a stable and smooth desktop environment. + +## Truss Vision Computer (Next Unit of Computing - NUC) + +The Intel NUCs are the truss computers. The league owns +[7th generation NUCs](https://www.intel.com/content/www/us/en/products/sku/97541/intel-core-i77567u-processor-4m-cache-up-to-4-00-ghz/specifications.html), +fist releasing in 2017. As such, their specification is quite modest. + +- CPU: + [i7-7567U](https://www.intel.com/content/www/us/en/products/sku/97541/intel-core-i77567u-processor-4m-cache-up-to-4-00-ghz/specifications.html) + @3.5GHz [Benchmark](https://www.cpubenchmark.net/cpu.php?cpu=Intel+Core+i7-7567U+%40+3.50GHz&id=2990) +- GPU: Intel® Iris® Plus Graphics 650 +- OpenCL Version 3.0 +- RAM: 16GB DDR4 +- OS: Xubuntu 22.04 LTS + +## Field Side Vision Computer + +The league doesn't own field side computers. They are typically provided by the venue contract. Approximate minimum +specification is provided below: + +- GPU: 4-core/8-thread Processor from within the last 5 years +- GPU: Integrated GPU +- OpenCL: Version 3.0+ +- RAM: 16GB DDR5 +- OS: Ubuntu LTS + +When deployed as a vision processor host: + +- 1-2x + [Teledyne FLIR Dual Channel PoE GigE Host Adapter](https://www.teledynevisionsolutions.com/products/ethernet-network-interface-card/?model=ACC-01-1105&vertical=machine%20vision&segment=iis) + +## Game Controller Computer + +The league doesn't own field side computers. They are typically provided by the venue contract. Approximate minimum +specification is provided below: + +- GPU: 4-core/8-thread Processor from within the last 5 years +- GPU: Integrated GPU +- OpenCL: Version 3.0+ +- RAM: 16GB DDR5 +- OS: Ubuntu LTS + +## Remote Controls + +Remote controls are Raspberry Pi 4, 2GB or 4GB model. Accessory hardware (screens, PoE) is described on the +[ssl-remote-control repository](https://github.com/RoboCup-SSL/ssl-remote-control). These device need at least PoE+ to +deliver enough power. + +## Status Board + +Status board computers are Raspberry Pi 4, 2GB or 4GB model. Any computer capable of hosting a small webpage can fill +this role. + +## Streaming Computer + +The league doesn't own field side computers. They are typically provided by the venue contract. Approximate minimum +specification is provided below: + +- GPU: 8-core/16-thread Processor >4.0GHz +- GPU: Nvidia 2060 or better, capable of 4k video transcoding +- OpenCL: Version 3.0+ +- RAM: 32GB DDR5 +- OS: Ubuntu LTS diff --git a/docs-source/field/network/diagrams/cam-direct.mmd b/docs-source/field/network/diagrams/cam-direct.mmd new file mode 100644 index 0000000..12c2b27 --- /dev/null +++ b/docs-source/field/network/diagrams/cam-direct.mmd @@ -0,0 +1,12 @@ +flowchart TD + subgraph TrussDirect["Truss — Elevated (4-Camera Config)"] + Cameras["Cameras ×4
Direct wiring"] + end + + Cameras -- "Direct — bypasses switch
PoE or USB w/ extenders" --> Vision["Vision Computer
Dedicated interfaces ×4"] + Vision -- "GbE" --> Switch["Field-Side Switch
(see network core diagram)"] + + classDef purple fill:#faf5ff,stroke:#9333ea,color:#581c87; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + class Cameras purple; + class Vision,Switch cyan; \ No newline at end of file diff --git a/docs-source/field/network/diagrams/cam-nuc.mmd b/docs-source/field/network/diagrams/cam-nuc.mmd new file mode 100644 index 0000000..be96392 --- /dev/null +++ b/docs-source/field/network/diagrams/cam-nuc.mmd @@ -0,0 +1,13 @@ +flowchart TD + subgraph TrussNuc["Truss — Elevated (1–2 Camera Config)"] + Camera["Camera ×1–2
1 NUC per camera"] + NUC["Truss NUC
Dedicated, 1:1"] + Camera -- "USB 3.1" --> NUC + end + + NUC -- "GbE
(via switch, remote access)" --> Switch["Field-Side Switch
(see network core diagram)"] + + classDef purple fill:#faf5ff,stroke:#9333ea,color:#581c87; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + class Camera purple; + class NUC,Switch cyan; \ No newline at end of file diff --git a/docs-source/field/network/diagrams/field-side-equipment.mmd b/docs-source/field/network/diagrams/field-side-equipment.mmd new file mode 100644 index 0000000..6f01258 --- /dev/null +++ b/docs-source/field/network/diagrams/field-side-equipment.mmd @@ -0,0 +1,52 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart TD + subgraph Switch["Field-Side Switch (see network core diagram) — IGMP Snooping"] + Note["All Links: GbE · PoE: Remotes Only"] + + subgraph Officiating["Officiating Core"] + Vision["Vision Computer"] + GC["Game Controller"] + BR["Blue Remote
GbE · PoE"] + YR["Yellow Remote
GbE · PoE"] + SB["Status Board"] + Vision ~~~ GC ~~~ BR ~~~ YR ~~~ SB + end + + subgraph TeamFacing["Team-Provided"] + BT["Blue Team PC"] + YT["Yellow Team PC"] + BT ~~~ YT + end + + subgraph Broadcast["Broadcast / AV — Optional"] + Stream["Streaming PC"] + Audio["Audio Referee"] + Boards["Boards ×(N+1)"] + Stream ~~~ Audio ~~~ Boards + end + + Note ~~~ Officiating + Note ~~~ TeamFacing + Note ~~~ Broadcast + end + + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef blue fill:#eff6ff,stroke:#2563eb,color:#1e3a8a; + classDef yellow fill:#fefce8,stroke:#ca8a04,color:#713f12; + classDef orange fill:#fff7ed,stroke:#ea580c,color:#7c2d12; + + class Vision,GC,SB,Stream,Boards cyan; + class BR,BT blue; + class YR,YT yellow; + class Audio orange; + + style Note fill:none,stroke:none,color:#475569,font-style:italic + style Switch fill:none,stroke:#0891b2,stroke-dasharray:4 3,stroke-width:1.5px + style Officiating fill:none,stroke:#0891b2,stroke-dasharray:3 3,stroke-width:1.5px + style TeamFacing fill:none,stroke:#64748b,stroke-dasharray:3 3,stroke-width:1.5px + style Broadcast fill:none,stroke:#ea580c,stroke-dasharray:3 3,stroke-width:1.5px \ No newline at end of file diff --git a/docs-source/field/network/diagrams/net-core-vlan.mmd b/docs-source/field/network/diagrams/net-core-vlan.mmd new file mode 100644 index 0000000..6a6b275 --- /dev/null +++ b/docs-source/field/network/diagrams/net-core-vlan.mmd @@ -0,0 +1,26 @@ +--- +config: + layout: elk + elk: + mergeEdges: false + nodePlacementStrategy: BRANDES_KOEPF +--- +flowchart TB + Upstream["Upstream / Venue Network"] --> Router["Router / Firewall
Shared — Per-Venue"] + Router -- "Trunk — 802.1Q Tagged" --> S1["Field 1 Switch
VLAN 10 · Tagged"] + Router -- "Trunk — 802.1Q Tagged" --> S2["Field 2 Switch
VLAN 20 · Tagged"] + Router -- "Trunk — 802.1Q Tagged" --> SN["Field N Switch
VLAN N0+"] + + S1 --> Truss1["Truss Cameras"] + S1 --> FS1["Field-Side Devices"] + S2 -.-> Note2["(Same downstream pattern as Field 1)"] + SN -.-> NoteN["(Same downstream pattern as Field 1)"] + + classDef amber fill:#fffbeb,stroke:#d97706,color:#78350f; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef gray fill:#f1f5f9,stroke:#64748b,color:#1e293b; + classDef slate fill:#f8fafc,stroke:#475569,color:#0f172a; + class Upstream slate; + class Router amber; + class S1,S2,SN cyan; + class Truss1,FS1,Note2,NoteN gray; \ No newline at end of file diff --git a/docs-source/field/network/diagrams/net-core.mmd b/docs-source/field/network/diagrams/net-core.mmd new file mode 100644 index 0000000..5b23cd8 --- /dev/null +++ b/docs-source/field/network/diagrams/net-core.mmd @@ -0,0 +1,20 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart TB + Upstream["Upstream / Venue Network"] --> Router["Router / Firewall
DHCP · Isolates Multicast"] + Router --> Switch["Field-Side Switch
PoE · IGMP Snooping"] + Switch --> TrussLink["To Truss Cameras
(previous diagram)"] + Switch --> Endpoints["To Field-Side Devices
(next diagram)"] + + classDef amber fill:#fffbeb,stroke:#d97706,color:#78350f; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef gray fill:#f1f5f9,stroke:#64748b,color:#1e293b; + classDef slate fill:#f8fafc,stroke:#475569,color:#0f172a; + class Upstream slate; + class Router amber; + class Switch cyan; + class TrussLink,Endpoints gray; \ No newline at end of file diff --git a/docs-source/field/network/diagrams/ssl_field_network_fanout_direct.drawio b/docs-source/field/network/diagrams/ssl_field_network_fanout_direct.drawio new file mode 100644 index 0000000..fa2a11e --- /dev/null +++ b/docs-source/field/network/diagrams/ssl_field_network_fanout_direct.drawio @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-source/field/network/diagrams/ssl_field_network_fanout_truss.drawio b/docs-source/field/network/diagrams/ssl_field_network_fanout_truss.drawio new file mode 100644 index 0000000..3e016d5 --- /dev/null +++ b/docs-source/field/network/diagrams/ssl_field_network_fanout_truss.drawio @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-source/field/network/images/FLIR_logo.png b/docs-source/field/network/images/FLIR_logo.png new file mode 100644 index 0000000..6081af5 Binary files /dev/null and b/docs-source/field/network/images/FLIR_logo.png differ diff --git a/docs-source/field/network/images/FLIR_logo.svg.webp b/docs-source/field/network/images/FLIR_logo.svg.webp new file mode 100644 index 0000000..061b97a Binary files /dev/null and b/docs-source/field/network/images/FLIR_logo.svg.webp differ diff --git a/docs-source/field/network/images/Xubuntu.png b/docs-source/field/network/images/Xubuntu.png new file mode 100644 index 0000000..5ddaefa Binary files /dev/null and b/docs-source/field/network/images/Xubuntu.png differ diff --git a/docs-source/field/network/images/Xubuntu.svg b/docs-source/field/network/images/Xubuntu.svg new file mode 100644 index 0000000..aef12bb --- /dev/null +++ b/docs-source/field/network/images/Xubuntu.svg @@ -0,0 +1,24 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-source/field/network/images/raspberry-pi.png b/docs-source/field/network/images/raspberry-pi.png new file mode 100644 index 0000000..f5a6f66 Binary files /dev/null and b/docs-source/field/network/images/raspberry-pi.png differ diff --git a/docs-source/field/network/images/raspberry-pi.svg b/docs-source/field/network/images/raspberry-pi.svg new file mode 100644 index 0000000..99331e8 --- /dev/null +++ b/docs-source/field/network/images/raspberry-pi.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-source/field/network/images/ssl_physical_truss_direct_config.png b/docs-source/field/network/images/ssl_physical_truss_direct_config.png new file mode 100644 index 0000000..5db9ebf Binary files /dev/null and b/docs-source/field/network/images/ssl_physical_truss_direct_config.png differ diff --git a/docs-source/field/network/images/ssl_physical_truss_direct_config.svg b/docs-source/field/network/images/ssl_physical_truss_direct_config.svg new file mode 100644 index 0000000..518750d --- /dev/null +++ b/docs-source/field/network/images/ssl_physical_truss_direct_config.svg @@ -0,0 +1,28 @@ + +RoboCup SSL truss zone, four-camera direct-wired configuration +In the four-camera regional configuration, each camera connects directly to a dedicated interface on the vision computer, bypassing the field-side switch entirely. This is commonly done over PoE, though USB with extenders is also used. Routing PoE camera traffic through a switch can introduce jumbo-frame and Linux CMA allocation issues, which the direct connection avoids. + + + + + + +Truss — Elevated (4-Camera Config) + + + + +Cameras ×4 +Direct Wiring + + + +Direct — Bypasses Switch +PoE Or USB W/ Extenders + + + +Vision Computer +Dedicated Interfaces ×4 + + \ No newline at end of file diff --git a/docs-source/field/network/images/ssl_physical_truss_nuc_config_v2.png b/docs-source/field/network/images/ssl_physical_truss_nuc_config_v2.png new file mode 100644 index 0000000..588566a Binary files /dev/null and b/docs-source/field/network/images/ssl_physical_truss_nuc_config_v2.png differ diff --git a/docs-source/field/network/images/ssl_physical_truss_nuc_config_v2.svg b/docs-source/field/network/images/ssl_physical_truss_nuc_config_v2.svg new file mode 100644 index 0000000..3b3db57 --- /dev/null +++ b/docs-source/field/network/images/ssl_physical_truss_nuc_config_v2.svg @@ -0,0 +1,37 @@ + +RoboCup SSL truss zone, NUC configuration +In the one or two camera configuration, each camera has its own dedicated truss-mounted NUC, connected via USB 3.1. Each NUC is remotely accessed over GbE via the field-side switch. A two-camera config duplicates this camera-and-NUC pair. + + + + + + +Truss — Elevated (1–2 Camera Config) + + + + +Camera +1 Of 1–2 Total + + + + +Truss NUC +Dedicated, 1:1 + + + +USB 3.1 + + +GbE +(Via Switch, Remote Access) + + + +Field-Side Switch +(See Network Core) + + \ No newline at end of file diff --git a/docs-source/field/network/images/ubuntu.png b/docs-source/field/network/images/ubuntu.png new file mode 100644 index 0000000..90d3578 Binary files /dev/null and b/docs-source/field/network/images/ubuntu.png differ diff --git a/docs-source/field/network/images/ubuntu.svg b/docs-source/field/network/images/ubuntu.svg new file mode 100644 index 0000000..02e8546 --- /dev/null +++ b/docs-source/field/network/images/ubuntu.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/docs-source/field/network/images/win11.png b/docs-source/field/network/images/win11.png new file mode 100644 index 0000000..11ff368 Binary files /dev/null and b/docs-source/field/network/images/win11.png differ diff --git a/docs-source/field/network/images/win11.webp b/docs-source/field/network/images/win11.webp new file mode 100644 index 0000000..3c1d393 Binary files /dev/null and b/docs-source/field/network/images/win11.webp differ diff --git a/docs-source/field/network/labnetwork.md b/docs-source/field/network/labnetwork.md new file mode 100644 index 0000000..59f2bfe --- /dev/null +++ b/docs-source/field/network/labnetwork.md @@ -0,0 +1,17 @@ +# Lab Network + +## Detailed Diagram + +```{image} diagrams/ssl_field_network_fanout_direct.light.svg +--- +class: only-light +alt: SSL field network fan-out diagram, direct-wired camera config +--- +``` + +```{image} diagrams/ssl_field_network_fanout_direct.dark.svg +--- +class: only-dark +alt: SSL field network fan-out diagram, direct-wired camera config +--- +``` diff --git a/docs-source/index.md b/docs-source/index.md index d4ee2ce..6cd1316 100644 --- a/docs-source/index.md +++ b/docs-source/index.md @@ -1,8 +1,34 @@ -# SSL Docs +# Home Page -Welcome to the RoboCup Small Size League documentation. +This is the documentation for [RoboCup](https://www.robocup.org/) [Small Size League](https://ssl.robocup.org/). It +contains motivation, extended documentation, and detailed first time setup and usage guidance for a variety of topics +common to the league as a whole. + +## Common Pages of Interest + +- [Field Technical Documentation](field.md) +- [Field Camera Calculator](field/camera-calculator.md) +- [Protocol/Coms Technical Documentation](protocol.md) + +## Motivation + +All SSL teams are required to interact with shared field software in order to play a match. This includes +[vision](https://github.com/TIGERs-Mannheim/vision-processor) (robot and ball positions) and +[game controller](https://github.com/robocup-ssl/ssl-game-controller) (game state). There are also a variety of other +league tools that need to establish communication links, some of which are helpful for teams like autoref. This raises a +key engineering challenge: standardizing the communication so that dozens of teams and tools can all show up and expect +to communicate correctly. + +The SSL uses [Protobuf](https://protobuf.dev/) to standardize the tool data formats. Protobuf defines an interface +description language, and the league creates packet definitions for each tool using this interface language. These +packet definitions are defined here. A single source of truth for packet definitions provides clarity to the standard, +and helps avoid diverging versions and licensing incompatibility. ```{toctree} -:maxdepth: 2 -:caption: Contents +--- +maxdepth: 0 +caption: Contents +--- +field +protocol ``` diff --git a/docs-source/protocol.md b/docs-source/protocol.md new file mode 100644 index 0000000..ba27bee --- /dev/null +++ b/docs-source/protocol.md @@ -0,0 +1,12 @@ +# League Protocols and Communication + +```{toctree} +--- +maxdepth: 1 +caption: Contents +--- +protocol/vision.md +protocol/referee.md +protocol/teamclient.md +protocol/simulator.md +``` diff --git a/docs-source/protocol/referee.md b/docs-source/protocol/referee.md new file mode 100644 index 0000000..8793b19 --- /dev/null +++ b/docs-source/protocol/referee.md @@ -0,0 +1,10 @@ +# Referee Protocol + +## Transport + +## Message Format + +## League Software + +The league's game management software, [Game Controller](https://github.com/RoboCup-SSL/ssl-game-controller), manages +game state and publishes it as this protocol. diff --git a/docs-source/protocol/simulator.md b/docs-source/protocol/simulator.md new file mode 100644 index 0000000..fef8b28 --- /dev/null +++ b/docs-source/protocol/simulator.md @@ -0,0 +1,7 @@ +# Simulator Protocol + +## Transport + +## Message Format + +## League Software diff --git a/docs-source/protocol/teamclient.md b/docs-source/protocol/teamclient.md new file mode 100644 index 0000000..cc13712 --- /dev/null +++ b/docs-source/protocol/teamclient.md @@ -0,0 +1,7 @@ +# Team Client Protocol + +## Transport + +## Message Format + +## League Software diff --git a/docs-source/protocol/vision.md b/docs-source/protocol/vision.md new file mode 100644 index 0000000..b03d690 --- /dev/null +++ b/docs-source/protocol/vision.md @@ -0,0 +1,12 @@ +# Vision Protocol + +## Transport + +## Message Format + +## League Software + +There are two pieces of league maintained software that interpret camera data: + +- (Recommended) [Vision Processor](https://github.com/TIGERs-Mannheim/vision-processor) +- (Legacy) [SSL Vision](https://github.com/RoboCup-SSL/ssl-vision) diff --git a/flake.nix b/flake.nix index 6a36503..07fa2ef 100644 --- a/flake.nix +++ b/flake.nix @@ -43,6 +43,64 @@ ]); venv = pythonSet.mkVirtualEnv "ssl-docs-env" workspace.deps.default; + + # drawio is Electron, and Electron's "auto" ozone platform detection + # picks Wayland whenever it finds a live compositor socket under + # $XDG_RUNTIME_DIR — which sphinxcontrib-drawio's own Xvfb (X11) then + # collides with, crashing the export. Force X11 unconditionally so + # this works headless in CI and on any contributor's Wayland desktop. + # + # drawio's SVG export always wraps every fill/stroke/font color in a + # light-dark(light, dark) CSS function, even for colors the diagram + # sets explicitly — --svg-theme light only changes a couple of + # built-in defaults, not this. sphinxcontrib-drawio has no config + # knob to reach it either, so the exported SVG is post-processed + # here to collapse light-dark(a, b) down to a single static color. + # + # Which side of the pair survives is controlled by $DRAWIO_SVG_THEME + # (light [default] or dark) rather than always picking the light + # value: light-dark() inside an -embedded SVG resolves against + # the *browser's* OS/UA color-scheme preference, not the host page's + # — and Furo's manual light/dark toggle never sets the CSS + # color-scheme property, only a data-theme attribute + CSS classes. + # So a live light-dark() SVG can show dark colors while the page is + # explicitly toggled to light. conf.py renders one static SVG per + # theme via this switch and picks between them with Furo's own + # only-light/only-dark image classes instead. + drawio = pkgs.writeShellScriptBin "drawio" '' + ${pkgs.drawio}/bin/drawio --ozone-platform=x11 "$@" + status=$? + if [ $status -ne 0 ]; then + exit $status + fi + + out="" + prev="" + for arg in "$@"; do + if [ "$prev" = "--output" ]; then + out="$arg" + fi + prev="$arg" + done + + case "$out" in + *.svg) + if [ -f "$out" ]; then + if [ "''${DRAWIO_SVG_THEME:-light}" = "dark" ]; then + ${pkgs.gnused}/bin/sed -E -i \ + -e 's/light-dark\(((#[0-9a-fA-F]+)|(rgb\([0-9, ]+\))), *((#[0-9a-fA-F]+)|(rgb\([0-9, ]+\)))\)/\4/g' \ + -e 's/--ge-adaptive-bg: light-dark\([^,]+, var\(--ge-dark-color, *([^)]+)\)\)/--ge-adaptive-bg: \1/g' \ + "$out" + else + ${pkgs.gnused}/bin/sed -E -i \ + -e 's/light-dark\(((#[0-9a-fA-F]+)|(rgb\([0-9, ]+\))), *((#[0-9a-fA-F]+)|(rgb\([0-9, ]+\)))\)/\1/g' \ + -e 's/--ge-adaptive-bg: light-dark\(([^,]+), var\([^)]*\)\)/--ge-adaptive-bg: \1/g' \ + "$out" + fi + fi + ;; + esac + ''; in { devShells.default = pkgs.mkShell { @@ -50,6 +108,9 @@ venv pkgs.uv pkgs.codespell + drawio + pkgs.xvfb + pkgs.xvfb-run ]; shellHook = '' diff --git a/pyproject.toml b/pyproject.toml index 0e04fb5..0f9cdaa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,18 @@ dependencies = [ "myst-parser>=2.0", "furo>=2024.1.29", "sphinx-autobuild>=2024.10.3", + "mdformat>=0.7", + "mdformat-myst>=0.2", + "mdformat-gfm>=0.4", + "sphinxcontrib-youtube>=1.4", + "sphinxcontrib-mermaid>=0.9", + "sphinxcontrib-drawio>=0.0.17", ] [tool.uv] package = false + +[tool.codespell] +# Generated per-theme drawio SVG exports (see conf.py) embed base64 font +# data whose glyph byte runs randomly collide with codespell's dictionary. +skip = "*.light.svg,*.dark.svg" diff --git a/scripts/check.sh b/scripts/check.sh index 85ca7ca..302fbac 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -30,6 +30,7 @@ run_check build nix develop -c sphinx-build -W -b html docs-source _build/html run_check internal-links nix develop -c sphinx-build -W -n -b dummy docs-source _build/dummy run_check external-links nix develop -c sphinx-build -b linkcheck docs-source _build/linkcheck run_check spellcheck nix develop -c codespell docs-source README.md +run_check format nix develop -c mdformat --check --wrap 120 docs-source README.md echo echo "== Summary ==" diff --git a/scripts/reverify-ignored-links.sh b/scripts/reverify-ignored-links.sh new file mode 100755 index 0000000..00e547b --- /dev/null +++ b/scripts/reverify-ignored-links.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# Open the linkcheck-ignored URLs (docs-source/conf.py's linkcheck_ignore) in +# a browser for periodic human re-verification. These are WAF-blocked sites +# that reject every automated request, so linkcheck can't check them itself +# — see the comment above linkcheck_ignore for details. +# +# After checking, bump the "verified YYYY-MM-DD" comment on each entry you +# confirmed still resolves correctly. +# +# Usage: +# scripts/reverify-ignored-links.sh [-y|--yes] +# +# -y, --yes Skip the confirmation prompt (for automation/CI use). + +set -euo pipefail + +assume_yes=0 +for arg in "$@"; do + case "$arg" in + -y|--yes) + assume_yes=1 + ;; + *) + echo "Usage: $0 [-y|--yes]" >&2 + exit 1 + ;; + esac +done + +repo_root="$(git rev-parse --show-toplevel)" +conf_py="$repo_root/docs-source/conf.py" + +if [[ ! -f "$conf_py" ]]; then + echo "Can't find $conf_py" >&2 + exit 1 +fi + +# Pull the quoted regex strings out of the linkcheck_ignore = [ ... ] block, +# then de-regex them back into plain URLs. Each entry is an anchored, +# backslash-escaped literal URL (^...$ with \. \? etc.), so stripping the +# anchors and any backslash before a single char recovers the original URL. +mapfile -t urls < <( + awk '/^linkcheck_ignore = \[/{flag=1; next} /^\]/{flag=0} flag' "$conf_py" \ + | grep -oP '(?<=r")[^"]+(?=")' \ + | sed -E 's/^\^//; s/\$$//; s/\\(.)/\1/g' +) + +if [[ ${#urls[@]} -eq 0 ]]; then + echo "No entries found in linkcheck_ignore ($conf_py)." >&2 + exit 0 +fi + +browsers=(google-chrome google-chrome-stable chrome chromium chromium-browser firefox) +browser="" +for candidate in "${browsers[@]}"; do + if command -v "$candidate" >/dev/null 2>&1; then + browser="$candidate" + break + fi +done + +if [[ -z "$browser" ]]; then + echo "No browser found on PATH (looked for: ${browsers[*]})." >&2 + echo "Check these manually instead:" >&2 + printf ' %s\n' "${urls[@]}" >&2 + exit 1 +fi + +echo "About to open ${#urls[@]} ignored link(s) in $browser for manual re-verification:" +printf ' %s\n' "${urls[@]}" + +if [[ "$assume_yes" -ne 1 ]]; then + if [[ ! -t 0 ]]; then + echo "Not running interactively — pass -y/--yes to skip this prompt." >&2 + exit 1 + fi + read -r -p "Open these in $browser? [y/N] " reply + case "$reply" in + y|Y|yes|YES) ;; + *) + echo "Aborted." >&2 + exit 1 + ;; + esac +fi + +"$browser" "${urls[@]}" & +disown diff --git a/uv.lock b/uv.lock index 598880b..47e538c 100644 --- a/uv.lock +++ b/uv.lock @@ -288,6 +288,78 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mdformat" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/05/32b5e14b192b0a8a309f32232c580aefedd9d06017cb8fe8fce34bec654c/mdformat-1.0.0.tar.gz", hash = "sha256:4954045fcae797c29f86d4ad879e43bb151fa55dbaf74ac6eaeacf1d45bb3928", size = 56953, upload-time = "2025-10-16T12:05:03.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/9a/8fe71b95985ca7a4001effbcc58e5a07a1f2a2884203f74dcf48a3b08315/mdformat-1.0.0-py3-none-any.whl", hash = "sha256:bca015d65a1d063a02e885a91daee303057bc7829c2cd37b2075a50dbb65944b", size = 53288, upload-time = "2025-10-16T12:05:02.607Z" }, +] + +[[package]] +name = "mdformat-footnote" +version = "0.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/c0/0fe461e3c53eb72b35b642add6c3eaf1f90e43a574a18633955f9e89791d/mdformat_footnote-0.1.3.tar.gz", hash = "sha256:70617e61af87f59d7dea93a392c5093089a6d4551126fa18025c6fddb18bf742", size = 6430, upload-time = "2026-01-30T14:13:05.983Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/ac/60d47237a0e37fe991cbf311f10f096a28cfdbbe25d1932dadf8e9abdac8/mdformat_footnote-0.1.3-py3-none-any.whl", hash = "sha256:3033184237cbaf2a71cc02a4c37f043a97c1f16d3ed6939bc104964e77aad0b3", size = 7767, upload-time = "2026-01-30T14:13:04.944Z" }, +] + +[[package]] +name = "mdformat-front-matters" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "ruamel-yaml" }, + { name = "toml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2c/53/008805510d3006d9aec6c602fa40305750cb96daba8ea9abae477149909b/mdformat_front_matters-2.0.0.tar.gz", hash = "sha256:46efabe93707699120c67d4b33b51fdb4cfe0e040acfb0f8212eb33c9a3e8a9a", size = 12576, upload-time = "2025-12-04T11:44:52.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/05/4c197c000dab41f1f58290221a4c02bd57804ff42e9374d3dd0f5408aa40/mdformat_front_matters-2.0.0-py3-none-any.whl", hash = "sha256:8ea92d23d1e9427fe6548b9044e5fbf7bcf6c217941f1a9442e0efd8b0a76e61", size = 13411, upload-time = "2025-12-04T11:44:51.209Z" }, +] + +[[package]] +name = "mdformat-gfm" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "mdformat" }, + { name = "mdit-py-plugins" }, + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/6f/a626ebb142a290474401b67e2d61e73ce096bf7798ee22dfe6270f924b3f/mdformat_gfm-1.0.0.tar.gz", hash = "sha256:d1d49a409a6acb774ce7635c72d69178df7dce1dc8cdd10e19f78e8e57b72623", size = 10112, upload-time = "2025-10-16T09:12:22.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/18/6bc2189b744dd383cad03764f41f30352b1278d2205096f77a29c0b327ad/mdformat_gfm-1.0.0-py3-none-any.whl", hash = "sha256:7305a50efd2a140d7c83505b58e3ac5df2b09e293f9bbe72f6c7bee8c678b005", size = 10970, upload-time = "2025-10-16T09:12:21.276Z" }, +] + +[[package]] +name = "mdformat-myst" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdformat" }, + { name = "mdformat-footnote" }, + { name = "mdformat-front-matters" }, + { name = "mdformat-gfm" }, + { name = "mdit-py-plugins" }, + { name = "ruamel-yaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/e1/e41d0a8acb2485c9da0fb194545c9827ce1399534110a5e45c465221318f/mdformat_myst-0.3.0.tar.gz", hash = "sha256:8195dec075683cac689c5586a5e4c6700ec8d6df0f9bc34a1e2f3514d5e745b0", size = 30032, upload-time = "2025-11-27T18:52:26.466Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/7b/2e8b6fa15fc47f5a512ebbe4bba4e2d5dfc988092269fc1cd3d22c9dad45/mdformat_myst-0.3.0-py3-none-any.whl", hash = "sha256:45e305cb1e742e712c0a8410315c351f3ab43e702d979d4f0eea9d9831245adc", size = 7088, upload-time = "2025-11-27T18:52:25.618Z" }, +] + [[package]] name = "mdit-py-plugins" version = "0.6.1" @@ -414,6 +486,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, ] +[[package]] +name = "ruamel-yaml" +version = "0.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/3b/ebda527b56beb90cb7652cb1c7e4f91f48649fbcd8d2eb2fb6e77cd3329b/ruamel_yaml-0.19.1.tar.gz", hash = "sha256:53eb66cd27849eff968ebf8f0bf61f46cdac2da1d1f3576dd4ccee9b25c31993", size = 142709, upload-time = "2026-01-02T16:50:31.84Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/0c/51f6841f1d84f404f92463fc2b1ba0da357ca1e3db6b7fbda26956c3b82a/ruamel_yaml-0.19.1-py3-none-any.whl", hash = "sha256:27592957fedf6e0b62f281e96effd28043345e0e66001f97683aa9a40c667c93", size = 118102, upload-time = "2026-01-02T16:50:29.201Z" }, +] + [[package]] name = "snowballstemmer" version = "3.1.1" @@ -507,6 +588,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, ] +[[package]] +name = "sphinxcontrib-drawio" +version = "0.0.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/9b/45a4e9f7d5b3f3e8e3fa792f99b20d2f6d337c4b84f9e55044aecf0e60c8/sphinxcontrib-drawio-0.0.17.tar.gz", hash = "sha256:1a3f82efd1ab4b41d1ee8dd27c296bae0944a10faca3568c462dc6b9a77748f5", size = 25645, upload-time = "2023-09-12T16:48:52.488Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/d4/81b8c94f216ef2fb68475b2f5a2836495e5da4d4ce011b3e41e663b89d1f/sphinxcontrib_drawio-0.0.17-py3-none-any.whl", hash = "sha256:c42e3fee5398da52836e8ee5c61c2f19bce947abb2d17b857057fb32137c1096", size = 10727, upload-time = "2023-09-12T16:48:50.826Z" }, +] + [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" @@ -525,6 +618,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, ] +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "pyyaml" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/07/9d/bf3a48a657682c7e0445c71d916bca7ab8194454276cc22b16e7f974e502/sphinxcontrib_mermaid-2.1.0.tar.gz", hash = "sha256:13c5f9ac395cb6abf403eca34e228dc9fb3a30c9d960dbf3e40e9a8cef969549", size = 21695, upload-time = "2026-07-18T23:08:11.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/bd/d3348d62296e73a91420f0edf671450c9003ecd8704da40b1e3d9b868459/sphinxcontrib_mermaid-2.1.0-py3-none-any.whl", hash = "sha256:417cd144ec4b28852f46ba653f02ce8e538881c812111671a4c30344e87f2112", size = 16190, upload-time = "2026-07-18T23:08:10.098Z" }, +] + [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" @@ -543,23 +650,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "sphinxcontrib-youtube" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/5c/61d1f11ad543f3a4da3b463de69f743051164087b5b9615aff9a1d2e49b8/sphinxcontrib_youtube-1.5.0.tar.gz", hash = "sha256:541b5880096e0a8759f49711f45087c91d6992ff45f03c178e974c66c8f5e326", size = 6088, upload-time = "2026-04-01T13:57:38.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/98/5a7457f5c57da4d30185a63fca7beff54e35fca8328473a92e90a1d621de/sphinxcontrib_youtube-1.5.0-py2.py3-none-any.whl", hash = "sha256:58d92bec536fbf029b642de3c9bbd32c7de307e0e13686015a0c0e91b7dfe3b8", size = 7680, upload-time = "2026-04-01T13:57:37.201Z" }, +] + [[package]] name = "ssl-docs" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "furo" }, + { name = "mdformat" }, + { name = "mdformat-gfm" }, + { name = "mdformat-myst" }, { name = "myst-parser" }, { name = "sphinx" }, { name = "sphinx-autobuild" }, + { name = "sphinxcontrib-drawio" }, + { name = "sphinxcontrib-mermaid" }, + { name = "sphinxcontrib-youtube" }, ] [package.metadata] requires-dist = [ { name = "furo", specifier = ">=2024.1.29" }, + { name = "mdformat", specifier = ">=0.7" }, + { name = "mdformat-gfm", specifier = ">=0.4" }, + { name = "mdformat-myst", specifier = ">=0.2" }, { name = "myst-parser", specifier = ">=2.0" }, { name = "sphinx", specifier = ">=7.0" }, { name = "sphinx-autobuild", specifier = ">=2024.10.3" }, + { name = "sphinxcontrib-drawio", specifier = ">=0.0.17" }, + { name = "sphinxcontrib-mermaid", specifier = ">=0.9" }, + { name = "sphinxcontrib-youtube", specifier = ">=1.4" }, ] [[package]] @@ -575,6 +707,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, ] +[[package]] +name = "toml" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0" @@ -692,6 +833,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, ] +[[package]] +name = "wcwidth" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/74/c6428f875774288bec1396f5bfcbc2d925700a4dad61727fd5f2b12f249d/wcwidth-0.8.2.tar.gz", hash = "sha256:91fbef97204b96a3d4d421609b80340b760cf33e26da123ff243d76b1fda8dda", size = 1466253, upload-time = "2026-06-29T18:11:11.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/42/3e5985a0a7e57de470b320c6d6a1a67c844f6737a587f3d44dd13d1819e7/wcwidth-0.8.2-py3-none-any.whl", hash = "sha256:d63947694a0539a1d51e01eda7caf800c291020e6cdd7e28ad7b14dd33ad4f85", size = 323166, upload-time = "2026-06-29T18:11:09.888Z" }, +] + [[package]] name = "websockets" version = "17.0.1"