diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 16070be..fe6b929 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,10 @@ on: push: branches: [main] pull_request: + # External links rot on their own schedule, not on ours — catch it weekly + # rather than only when someone happens to open a PR. + schedule: + - cron: "0 6 * * 1" workflow_dispatch: permissions: @@ -16,7 +20,10 @@ concurrency: cancel-in-progress: true jobs: + # The weekly run exists only to catch external link rot, so everything that + # depends solely on the repo's own contents sits it out. build: + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -37,6 +44,7 @@ jobs: path: _build/html internal-links: + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -57,10 +65,17 @@ jobs: with: nix_path: nixpkgs=channel:nixos-unstable + # No -W: conf.py warns (rather than errors) when GITHUB_TOKEN is absent + # and github.com links get skipped. - name: Check external links + env: + # Lifts github.com link checks onto the authenticated api.github.com + # rate limit — see conf.py. Available on every event, fork PRs included. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: nix develop -c sphinx-build -b linkcheck docs-source _build/linkcheck spellcheck: + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -73,6 +88,7 @@ jobs: run: nix develop -c codespell docs-source README.md format: + if: github.event_name != 'schedule' runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 diff --git a/docs-source/_static/site.css b/docs-source/_static/site.css index 4736d8e..ef20ba1 100644 --- a/docs-source/_static/site.css +++ b/docs-source/_static/site.css @@ -36,3 +36,52 @@ .two-col-diagram-grid .mermaid-container { width: 100%; } + +/* For diagrams rendered with useMaxWidth: false (native size instead of + shrink-to-fit-column) — scroll instead of overflowing the page if it's + wider than the column. */ +.wide-mermaid pre.mermaid, +.wide-mermaid .mermaid-container { + overflow-x: auto; +} + +/* Scale the whole rendered diagram up, text included. Mermaid's own + actorFontSize/messageFontSize/noteFontSize sequence config keys don't + reach the rendered elements in the pinned mermaid version — they + stay hardcoded at 16px regardless. `zoom` (not `transform: scale`) + correctly reflows the layout box, so it doesn't overlap the content + below it. + + sphinxcontrib-mermaid's own CSS (pre.mermaid > svg, specificity 0,1,2) + forces every diagram's SVG to a fixed height (mermaid_height, default + 500px). For most diagrams that's load-bearing — it's what keeps a large + flowchart's raw viewBox from rendering at full intrinsic height, via + letterboxing (preserveAspectRatio: xMidYMid meet). It only becomes a + problem here because useMaxWidth:false + zoom already size these + diagrams from their own content, so the fixed-height letterbox just adds + blank bars on top. Scoped to wide-mermaid only — applying this globally + regressed other diagrams that rely on the fixed-height cap. */ +.wide-mermaid pre.mermaid > svg { + height: auto; + zoom: 2; +} + +/* Same letterbox fix as above (see comment), without the zoom bump — for + flowcharts whose aspect ratio is wide/flat enough that the fixed-height + box wastes a lot of it as blank top/bottom bars, but don't need bigger + text. Only apply to diagrams confirmed wide via their rendered viewBox + (mermaid-cli) — a portrait diagram would instead grow uncontrolled + without the fixed-height cap, since width:100% + auto height scales + height up from the full container width. */ +.no-letterbox-mermaid pre.mermaid > svg { + height: auto; +} + +/* furo's generic `.container` class (applied to every MyST {container} block, + including this one) adds fixed margin/padding that doesn't scale with the + diagram — a bigger fraction of a short diagram's height than a tall one, so + it reads as excess whitespace above/below on shorter sequence diagrams. */ +.wide-mermaid { + margin: 0; + padding: 0; +} diff --git a/docs-source/conf.py b/docs-source/conf.py index 8fe8096..5927370 100644 --- a/docs-source/conf.py +++ b/docs-source/conf.py @@ -1,9 +1,14 @@ import os +import re import shutil import subprocess import time from pathlib import Path +from sphinx.util import logging as sphinx_logging + +logger = sphinx_logging.getLogger(__name__) + project = "RoboCup Small Size League" copyright = "2026, RoboCup Small Size League" author = "RoboCup Small Size League" @@ -71,6 +76,8 @@ DUAL_THEME_DIAGRAMS = [ "field/network/diagrams/ssl_field_network_fanout_truss.drawio", "field/network/diagrams/ssl_field_network_fanout_direct.drawio", + "protocol/diagrams/ssl_field_network_fanout_truss_vision_highlight.drawio", + "protocol/diagrams/ssl_field_network_fanout_truss_gc_highlight.drawio", ] @@ -155,6 +162,11 @@ def setup(app): app.set_html_assets_policy("always") app.connect("builder-inited", render_dual_theme_diagrams) + if _GITHUB_TOKEN: + app.connect("linkcheck-process-uri", rewrite_github_url_to_api) + else: + app.connect("builder-inited", warn_github_links_skipped) + linkcheck_user_agent = ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) " @@ -182,3 +194,68 @@ def setup(app): # 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$", ] + +# On a 429 without a Retry-After header, Sphinx's limit_rate() starts at a fixed +# 60s backoff and reports the link as *broken* once a delay exceeds this timeout, +# so anything under 60 makes the first 429 fatal with no retry. +linkcheck_workers = 2 +linkcheck_retries = 2 +linkcheck_rate_limit_timeout = 120.0 + +# The docs link to ~19 unique github.com URLs (one per proto, mostly). Anonymous +# requests share a per-IP bucket CI runners are already deep into, so they 429 +# and no amount of backoff tuning makes that reliable. An authenticated +# api.github.com run has 1000 requests/hour per repo and never comes close, so +# GitHub links are checked via the API when a token is present and skipped +# outright when it isn't — anonymous api.github.com is 60/hour, worse than the +# web bucket, so there's no fallback worth having. CI always has a token +# (issued for every event, read-only on fork PRs, which is all this needs); +# locally, export any PAT with public-repo read or accept the skip. +# +# The rewrite only affects what linkcheck fetches — the URLs in the built HTML +# are untouched — but report lines show the rewritten api.github.com URL. +_GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") + +_GITHUB_BLOB_URL = re.compile(r"^https://github\.com/([^/]+)/([^/]+)/blob/([^/]+)/(.+)$") +_GITHUB_REPO_URL = re.compile(r"^https://github\.com/([^/]+)/([^/?#]+?)/?$") + + +def rewrite_github_url_to_api(app, uri): + if match := _GITHUB_BLOB_URL.match(uri): + owner, repo, ref, path = match.groups() + return f"https://api.github.com/repos/{owner}/{repo}/contents/{path}?ref={ref}" + + if match := _GITHUB_REPO_URL.match(uri): + owner, repo = match.groups() + return f"https://api.github.com/repos/{owner}/{repo}" + + # Releases, issues, wikis and the like have no one-to-one API equivalent + # worth maintaining — leave them on the web bucket. + return None + + +def warn_github_links_skipped(app): + # A warning, not a note, because a silent skip reads as a pass — so the + # linkcheck job must not use -W, or a tokenless local run becomes an error. + if app.builder.name != "linkcheck": + return + + logger.warning( + "GITHUB_TOKEN is not set — skipping all github.com links. Anonymous " + "requests are rate-limited to the point of being unusable; export a " + "token (any PAT with public-repo read access) to check them." + ) + + +if _GITHUB_TOKEN: + # Matched by scheme+netloc. Keep the token scoped to api.github.com — a "*" + # key here would leak it to every third-party host in the docs. + linkcheck_request_headers = { + "https://api.github.com/": { + "Authorization": f"Bearer {_GITHUB_TOKEN}", + "Accept": "application/vnd.github+json", + }, + } +else: + # Reported as "ignored" rather than checked — never as a pass. + linkcheck_ignore.append(r"^https://github\.com/") diff --git a/docs-source/protocol.md b/docs-source/protocol.md index ba27bee..d534104 100644 --- a/docs-source/protocol.md +++ b/docs-source/protocol.md @@ -1,12 +1,94 @@ # League Protocols and Communication +[Just looking for the connection summary table?](#connection-summary-table) + +"How do I connect to an SSL field?" is one of the first questions a software team member will ask. This page and its +sub-pages document the process of connecting up: interfacing to the field, transport protocols, and data formats. If you +aren't already roughly familiar with the SSL field, checkout the [field documentation](field.md) to learn about the +layout and network structure. + +## What does a SSL field provide? + +RoboCup SSL fields provide two primary services _to teams_. + +1. Robot and Ball Localization (where are the robots and balls?) +1. Game State (game half, timeouts, penalties, etc.) + +This article first covers vision (field geometry, robot and ball localization), then game controller (game state). +Conceptually, teams first want to receive robot positions. It does not matter what the game state is if you can't see +anything. Ensuring rules and game state compliance comes next. + +## How do I connect to an SSL Field? + +1. **A team is never required to reply to the field.** Teams can and do play full matches only _receiving_ field vision + and game controller data. The only input required from a team is it's keeper number which is entered via the Game + Controller Operator or the Remote Control, neither of which is provided by the team. +1. **Field data delivery is not guaranteed.** It's sent at a high rate so **teams are expected to tolerate occasional + dropped packets without re-transmission.** Typically a re-transmission would take a few ms to execute, by which time + the next data frame is probably ready anyway. +1. **All required field data is sent via [multicast](https://en.wikipedia.org/wiki/Multicast) or + [broadcast]().** This allows the networking equipment to + duplicate the data stream as many times as needed without the league software managing dozens of connections. It also + means the team's software does not need to know the addresses or hostnames of any specific field computers in order + to connect. The team software simply needs to know the standard multicast addresses and ports of vision and game + controller, and you'll be able to connect to any standard field around the world. Specifically, these data streams + are [UDP](https://en.wikipedia.org/wiki/User_Datagram_Protocol) /[IPv4](https://en.wikipedia.org/wiki/IPv4) + +[IGMP](https://en.wikipedia.org/wiki/Internet_Group_Management_Protocol). +1. **All league packet data is defined by protocol buffers.** You can find the definitions in the + [ssl-protocol-defs repository](https://github.com/RoboCup-SSL/ssl-protocol-defs). + +The actual protocols and data formats are documented in the sections below. + +## Vision + +A detailed description of the vision protocol is provided in a dedicated article. + +[Vision Protocol](protocol/vision.md) + +## Game Controller + +A detailed description of the game controller protocol is provided in a dedicated article. + +[Game Controller Protocol](protocol/gamecontroller.md) + +## Connection Summary Table + +Below is the summary table of protocol addressing and ports. The GC address can be found by listening to the GC +multicast group and reading the [source address](https://en.wikipedia.org/wiki/IPv4#Source_address) field of the IP +layer, rather than knowing it statically. + +| Protocol | Protobuf | Type | Address | Port | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------- | ----- | +| SSL-Game-Controller (GC) | [Referee](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_referee_message.proto) | UDP Multicast | 224.5.23.1 | 10003 | +| SSL-Vision Detections | [SSL_WrapperPacket](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_wrapper.proto) | UDP Multicast | 224.5.23.2 | 10006 | +| AutoRef -> GC | [AutoRef](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_autoref.proto) | TCP | GC | 10007 | +| AutoRef -> GC | [AutoRef](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_autoref.proto) | TCP + SSL | GC | 10107 | +| Team -> GC | [Team](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_team.proto) | TCP | GC | 10008 | +| Team -> GC | [Team](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_team.proto) | TCP + SSL | GC | 10108 | +| Remote Control -> GC | [Remote Control](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_remotecontrol.proto) | TCP | GC | 10011 | +| Remote Control -> GC | [Remote Control](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_remotecontrol.proto) | TCP + SSL | GC | 10111 | +| SSL-Vision-Tracker | [TrackerWrapperPacket](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_wrapper_tracked.proto) | UDP Multicast | 224.5.23.2 | 10010 | +| Simulation Control | [SimulationControl](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/simulation/ssl_simulation_control.proto) | UDP | Simulator | 10300 | +| Robot Control Blue | [RobotControl](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/simulation/ssl_simulation_robot_control.proto) | UDP | Simulator | 10301 | +| Robot Control Yellow | [RobotControl](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/simulation/ssl_simulation_robot_control.proto) | UDP | Simulator | 10302 | + +## Additional Resources + +- Unicast +- Multicast +- Broadcast +- TCP +- UDP +- IPv4 +- IGMPv4 + ```{toctree} --- maxdepth: 1 caption: Contents --- protocol/vision.md +protocol/gamecontroller.md protocol/referee.md -protocol/teamclient.md protocol/simulator.md ``` diff --git a/docs-source/protocol/diagrams/coordinate-system-dark.svg b/docs-source/protocol/diagrams/coordinate-system-dark.svg new file mode 100644 index 0000000..496f6e4 --- /dev/null +++ b/docs-source/protocol/diagrams/coordinate-system-dark.svg @@ -0,0 +1,37 @@ + + + + + + + + SSL Vision Coordinate System (schematic, not to scale) + + + + + + + + +X + +Y + + + + origin (0, 0) + position units: mm + + + + + + + + + + + θ + + robot + orientation θ: radians, measured counter-clockwise from +X + diff --git a/docs-source/protocol/diagrams/coordinate-system-light.svg b/docs-source/protocol/diagrams/coordinate-system-light.svg new file mode 100644 index 0000000..5b3418d --- /dev/null +++ b/docs-source/protocol/diagrams/coordinate-system-light.svg @@ -0,0 +1,37 @@ + + + + + + + + SSL Vision Coordinate System (schematic, not to scale) + + + + + + + + +X + +Y + + + + origin (0, 0) + position units: mm + + + + + + + + + + + θ + + robot + orientation θ: radians, measured counter-clockwise from +X + diff --git a/docs-source/protocol/diagrams/message-hierarchy.mmd b/docs-source/protocol/diagrams/message-hierarchy.mmd new file mode 100644 index 0000000..5f28b0c --- /dev/null +++ b/docs-source/protocol/diagrams/message-hierarchy.mmd @@ -0,0 +1,26 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart TD + Wrapper["SSL_WrapperPacket
one of detection / geometry present"] + Wrapper --> Detection["SSL_DetectionFrame
camera_id, frame_number,
t_capture, t_sent"] + Wrapper --> Geometry["SSL_GeometryData"] + + Detection --> Balls["balls[]
SSL_DetectionBall"] + Detection --> RY["robots_yellow[]
SSL_DetectionRobot"] + Detection --> RB["robots_blue[]
SSL_DetectionRobot"] + + Geometry --> Field["field
SSL_GeometryFieldSize"] + Geometry --> Calib["calib[]
SSL_GeometryCameraCalibration"] + + classDef slate fill:#f8fafc,stroke:#475569,color:#0f172a; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef purple fill:#faf5ff,stroke:#9333ea,color:#581c87; + classDef amber fill:#fffbeb,stroke:#d97706,color:#78350f; + class Wrapper slate; + class Detection,Geometry cyan; + class Balls,RY,RB purple; + class Field,Calib amber; diff --git a/docs-source/protocol/diagrams/multi-camera-merge.mmd b/docs-source/protocol/diagrams/multi-camera-merge.mmd new file mode 100644 index 0000000..61654e6 --- /dev/null +++ b/docs-source/protocol/diagrams/multi-camera-merge.mmd @@ -0,0 +1,21 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart LR + subgraph Cams["Cameras"] + C0["Camera 0"] + C1["Camera 1"] + CN["Camera N"] + end + + C0 -- "SSL_DetectionFrame
camera_id=0" --> Client["Team Client
merge by camera_id,
reconcile t_capture"] + C1 -- "camera_id=1" --> Client + CN -- "camera_id=N" --> Client + + classDef amber fill:#fffbeb,stroke:#d97706,color:#78350f; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + class C0,C1,CN amber; + class Client cyan; diff --git a/docs-source/protocol/diagrams/multicast-join-sequence.mmd b/docs-source/protocol/diagrams/multicast-join-sequence.mmd new file mode 100644 index 0000000..205dd47 --- /dev/null +++ b/docs-source/protocol/diagrams/multicast-join-sequence.mmd @@ -0,0 +1,29 @@ +%%{init: {"sequence": {"useMaxWidth": false, "actorMargin": 220, "messageMargin": 40}}}%% +sequenceDiagram + participant Team as Team Client + participant Net as Switch / Router
(multicast group 224.5.23.2:10006) + participant Vision as Vision Processor + + Team->>Net: IGMP Join 224.5.23.2:10006 + Note over Team,Vision: No handshake — Team is now another multicast receiver + + Vision->>Net: SSL_WrapperPacket
detection (frame 1) + Net->>Team: SSL_WrapperPacket
detection (frame 1) + + Vision->>Net: SSL_WrapperPacket
detection (frame 2) + Net->>Team: SSL_WrapperPacket
detection (frame 2) + + Note over Vision,Team: Geometry publisher re-sends field geometry every 1s + + Vision->>Net: SSL_WrapperPacket
geometry + Net->>Team: SSL_WrapperPacket
geometry + + Vision->>Net: SSL_WrapperPacket
detection (frame 3) + Net->>Team: SSL_WrapperPacket
detection (frame 3) + + Vision->>Net: SSL_WrapperPacket
detection (frame 4) + Net--xTeam: SSL_WrapperPacket
detection (frame 4) — dropped + Note over Team: No retransmission — team just waits for the next frame + + Vision->>Net: SSL_WrapperPacket
detection (frame 5) + Net->>Team: SSL_WrapperPacket
detection (frame 5) diff --git a/docs-source/protocol/diagrams/multicast-join.mmd b/docs-source/protocol/diagrams/multicast-join.mmd new file mode 100644 index 0000000..b6b2a22 --- /dev/null +++ b/docs-source/protocol/diagrams/multicast-join.mmd @@ -0,0 +1,17 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart TD + Team["Team Client
IGMP join only"] -- "IGMP Join
224.5.23.2:10006" --> Net["Switch / Router
duplicates stream"] + Vision["Vision Processor"] -- "SSL_WrapperPacket
UDP multicast, continuous, no ack" --> Net + Net -- "SSL_WrapperPacket" --> Team + + classDef purple fill:#faf5ff,stroke:#9333ea,color:#581c87; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef gray fill:#f1f5f9,stroke:#64748b,color:#1e293b; + class Team purple; + class Vision cyan; + class Net gray; diff --git a/docs-source/protocol/diagrams/rcon-handshake-sequence.mmd b/docs-source/protocol/diagrams/rcon-handshake-sequence.mmd new file mode 100644 index 0000000..69170cb --- /dev/null +++ b/docs-source/protocol/diagrams/rcon-handshake-sequence.mmd @@ -0,0 +1,15 @@ +%%{init: {"sequence": {"useMaxWidth": false, "actorMargin": 220, "messageMargin": 40}}}%% +sequenceDiagram + participant Team as Team Client + participant GC as Game Controller + + Team->>GC: TCP connect :10008 (or :10108 for TLS) + Team->>GC: TeamRegistration (team_name[, signature]) + GC->>Team: ControllerReply (status_code=OK, next_token) + Note over Team,GC: Connection stays open — one registration, then repeated exchanges.
next_token must be echoed back in the next Signature if using secure comms. + + Team->>GC: TeamToController (ping) + GC->>Team: ControllerToTeam (controller_reply) + + Team->>GC: TeamToController (desired_keeper) + GC->>Team: ControllerToTeam (controller_reply) diff --git a/docs-source/protocol/diagrams/referee-broadcast-sequence.mmd b/docs-source/protocol/diagrams/referee-broadcast-sequence.mmd new file mode 100644 index 0000000..58cd2ee --- /dev/null +++ b/docs-source/protocol/diagrams/referee-broadcast-sequence.mmd @@ -0,0 +1,18 @@ +%%{init: {"sequence": {"useMaxWidth": false, "actorMargin": 220, "messageMargin": 40}}}%% +sequenceDiagram + participant Team as Team Client + participant Net as Switch / Router
(multicast group 224.5.23.1:10003) + participant GC as Game Controller + + Team->>Net: IGMP Join 224.5.23.1:10003 + Note over Team,GC: No handshake — Team is now another multicast receiver + + GC->>Net: Referee (stage, command, scores) + Net->>Team: Referee (stage, command, scores) + + GC->>Net: Referee + Net--xTeam: Referee — dropped + Note over Team: No retransmission — team just waits for the next packet + + GC->>Net: Referee + Net->>Team: Referee diff --git a/docs-source/protocol/diagrams/referee-broadcast.mmd b/docs-source/protocol/diagrams/referee-broadcast.mmd new file mode 100644 index 0000000..8d01a00 --- /dev/null +++ b/docs-source/protocol/diagrams/referee-broadcast.mmd @@ -0,0 +1,17 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart TD + Team["Team Client
IGMP join only"] -- "IGMP Join
224.5.23.1:10003" --> Net["Switch / Router
duplicates stream"] + GC["Game Controller"] -- "Referee
UDP multicast, continuous, no ack" --> Net + Net -- "Referee" --> Team + + classDef purple fill:#faf5ff,stroke:#9333ea,color:#581c87; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef gray fill:#f1f5f9,stroke:#64748b,color:#1e293b; + class Team purple; + class GC cyan; + class Net gray; diff --git a/docs-source/protocol/diagrams/referee-hierarchy.mmd b/docs-source/protocol/diagrams/referee-hierarchy.mmd new file mode 100644 index 0000000..6f28b1a --- /dev/null +++ b/docs-source/protocol/diagrams/referee-hierarchy.mmd @@ -0,0 +1,23 @@ +--- +config: + layout: elk + elk: + nodePlacementStrategy: NETWORK_SIMPLEX +--- +flowchart TD + Referee["Referee
one packet per broadcast"] + Referee --> Stage["stage, command,
packet_timestamp"] + Referee --> Yellow["yellow
Referee.TeamInfo"] + Referee --> Blue["blue
Referee.TeamInfo"] + Referee --> Pos["designated_position
Referee.Point"] + Referee --> Events["game_events[]
GameEvent"] + Referee --> Proposals["game_event_proposals[]
GameEventProposalGroup"] + + classDef slate fill:#f8fafc,stroke:#475569,color:#0f172a; + classDef cyan fill:#ecfeff,stroke:#0891b2,color:#164e63; + classDef purple fill:#faf5ff,stroke:#9333ea,color:#581c87; + classDef amber fill:#fffbeb,stroke:#d97706,color:#78350f; + class Referee slate; + class Stage cyan; + class Yellow,Blue purple; + class Pos,Events,Proposals amber; diff --git a/docs-source/protocol/diagrams/ssl_field_network_fanout_truss_gc_highlight.drawio b/docs-source/protocol/diagrams/ssl_field_network_fanout_truss_gc_highlight.drawio new file mode 100644 index 0000000..8ada836 --- /dev/null +++ b/docs-source/protocol/diagrams/ssl_field_network_fanout_truss_gc_highlight.drawio @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-source/protocol/diagrams/ssl_field_network_fanout_truss_vision_highlight.drawio b/docs-source/protocol/diagrams/ssl_field_network_fanout_truss_vision_highlight.drawio new file mode 100644 index 0000000..a04ad25 --- /dev/null +++ b/docs-source/protocol/diagrams/ssl_field_network_fanout_truss_vision_highlight.drawio @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs-source/protocol/gamecontroller.md b/docs-source/protocol/gamecontroller.md new file mode 100644 index 0000000..bdf1b97 --- /dev/null +++ b/docs-source/protocol/gamecontroller.md @@ -0,0 +1,155 @@ +# Game Controller Protocol + +This page describes the layers that deliver and decode the game state protocol to and from the field. This protocol is +partly bidirectional — teams may optionally talk back to the +[Game Controller](https://github.com/RoboCup-SSL/ssl-game-controller) to request a keeper change, a timeout, or a +substitution. + +## League Software + +There is one piece of league maintained software that manages game state and produces this protocol: + +- [Game Controller](https://github.com/RoboCup-SSL/ssl-game-controller) + +If you're actively trying to connect to a field, you'll need to make sure it's running. Automatic referee +implementations are separate, optional software that talk to the Game Controller over the same protocol described below +— see the [referee protocol](./referee.md) page for how their filtered game state gets used. + +## Transport + +**A team is never required to reply to the Game Controller.** Teams can and do play full matches only _receiving_ game +state. The Game Controller talks to teams two ways: a one-way broadcast every team should listen to, and an optional +two-way TCP connection only needed if a team wants to actively request something. + +### Referee Broadcast + +The Game Controller publishes overall game state (stage, command, scores, cards) as a one-way UDP multicast stream. The +league defines this multicast group as having an address of `224.5.23.1` at port `10003`. You join a multicast group +just like you connect to any IP address and port combo, though you may need to pass some additional options when binding +the socket. + +```{mermaid} diagrams/referee-broadcast.mmd +``` + +Once you've joined, you should expect to receive a `Referee` packet on a regular interval, independent of any camera +framerate. **Do not assume the game is running the current command until you've received a `Referee` packet saying so.** +Field data delivery is not guaranteed here either — a dropped packet is not re-transmitted, and packet loss should be +_well below_ 1%. + +````{container} wide-mermaid +```{mermaid} diagrams/referee-broadcast-sequence.mmd +``` +```` + +### Remote Control "rcon" Channels (Optional) + +If a team's software/AI wants to request a keeper change, respond to an advantage choice, or request a substitution, it +opens a TCP connection to the Game Controller and registers. Again, many of these functions can be done by the human +robot handler and physical remote control provided by the event organizer/field. There are three separate rcon channels, +each on its own port: one for teams, one for autoRefs, and one for physical remote controls. Registration and signing +are described in +[ssl_gc_rcon.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon.proto); +signatures are optional and only needed for secure/verified communication. + +Unlike the broadcast channel, opening a TCP connection requires knowing the Game Controller's actual IP. **You don't +need to guess this or hardcode a hostname** — read it off the +[source address](https://en.wikipedia.org/wiki/IPv4#Source_address) field of the IP packets carrying the `Referee` +broadcast you're already receiving. Whatever machine that traffic comes from is the Game Controller. + +````{container} wide-mermaid +```{mermaid} diagrams/rcon-handshake-sequence.mmd +``` +```` + +The connection stays open — registration happens once, then the team sends further requests and receives replies over +the same socket for as long as it stays connected. If a team never connects, it simply never receives a +`ControllerReply` and never gets to make requests; the game continues based on the referee's broadcast state alone. + +### Connections Table + +The following table contains connection information for the broadcast channel and the team/remote-control rcon channels. +AutoRef connections use the same rcon flow but are documented on the [referee protocol](./referee.md#connections-table) +page instead, alongside the rest of the autoRef-specific content. + +| Protocol | Protobuf | Type | Address | Port | +| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- | ------------- | ---------- | ----- | +| SSL-Game-Controller (GC) | [Referee](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_referee_message.proto) | UDP Multicast | 224.5.23.1 | 10003 | +| Team -> GC | [Team](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_team.proto) | TCP | GC | 10008 | +| Team -> GC | [Team](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_team.proto) | TCP + SSL | GC | 10108 | +| Remote Control -> GC | [Remote Control](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_remotecontrol.proto) | TCP | GC | 10011 | +| Remote Control -> GC | [Remote Control](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_remotecontrol.proto) | TCP + SSL | GC | 10111 | + +## Message Format + +The league uses [google protobuf](https://protobuf.dev/) to define the data format for this protocol too. The interface +definitions are in the same [league protobuf repository](https://github.com/RoboCup-SSL/ssl-protocol-defs) used for +vision. The image below shows the hierarchy of the broadcast `Referee` message — the one every team decodes, whether or +not it ever opens an rcon connection. + +````{container} no-letterbox-mermaid +```{mermaid} diagrams/referee-hierarchy.mmd +``` +```` + +`Referee` is the single top level structure sent on the broadcast channel. It carries the coarse game `stage` and +fine-grained `command` teams must obey, a `Referee.TeamInfo` for each of `yellow` and `blue` (score, cards, timeouts, +keeper id), an optional `designated_position` for ball placement, and any `game_events`/`game_event_proposals` the +active autoRef has reported. + +The table below contains links to the specific proto files needed to decode the broadcast message and the three rcon +channels. It's recommended you include the entire +[ssl-protocol-defs repository](https://github.com/RoboCup-SSL/ssl-protocol-defs) as a submodule in your code, rather +than copying the files directly. + +| Message | Proto File | +| ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| Referee, Referee.TeamInfo, Referee.Point | [ssl_gc_referee_message.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_referee_message.proto) | +| GameEvent, GameEventProposalGroup | [ssl_gc_game_event.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_game_event.proto) | +| ControllerReply, Signature | [ssl_gc_rcon.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon.proto) | +| TeamRegistration, TeamToController, ControllerToTeam | [ssl_gc_rcon_team.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_team.proto) | +| AutoRefRegistration, AutoRefToController, ControllerToAutoRef | [ssl_gc_rcon_autoref.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_autoref.proto) | +| RemoteControlRegistration, RemoteControlToController, ControllerToRemoteControl, RemoteControlTeamState | [ssl_gc_rcon_remotecontrol.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_remotecontrol.proto) | +| Team, RobotId, Division | [ssl_gc_common.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_common.proto) | + +## Team Sides + +This protocol shares its coordinate frame and units with the vision protocol — see +[vision's coordinate system](./vision.md#coordinate-system) for the full breakdown (mm, radians, CCW from +X). The only +game-controller-specific piece is `blue_team_on_positive_half`, which tells you which side of the field's +X axis the +blue team currently defends. **This flips between halves — do not hardcode a side.** `designated_position` (used for +ball placement) is a `Referee.Point` in the same field frame. + +## Registration Channels + +The Game Controller's rcon interface is split into three independent TCP channels — team, autoRef, and remote control — +each on its own port, each with its own registration message and message types. This means if your software wants to act +as more than one role (for example, a team client that is also a remote control), it needs a separate TCP connection and +registration per role; they are not multiplexed over one socket. + +**The [referee protocol](./referee.md) page documents how teams can use the `game_events` an automatic referee reports +over this protocol.** + +## Network Flow + +The diagram below is the same competition network fan-out shown in the +[field network documentation](../field/network/compnetwork.md), with the machines involved in the game controller +protocol highlighted and the vision-only machines (cameras, Truss NUC, Vision Computer) muted. The Game Controller +publishes the `224.5.23.1:10003` broadcast and serves the three rcon channels; team PCs, remotes, the status board, and +the officiating/broadcast machines that read referee state all consume it. The router and switch are highlighted too — +they maintain the multicast group and carry the rcon TCP connections. + +```{image} diagrams/ssl_field_network_fanout_truss_gc_highlight.light.svg +--- +class: only-light +alt: SSL field network fan-out diagram with game-controller-producing and + game-controller-consuming machines highlighted, vision-only machines muted +--- +``` + +```{image} diagrams/ssl_field_network_fanout_truss_gc_highlight.dark.svg +--- +class: only-dark +alt: SSL field network fan-out diagram with game-controller-producing and + game-controller-consuming machines highlighted, vision-only machines muted +--- +``` diff --git a/docs-source/protocol/referee.md b/docs-source/protocol/referee.md index 8793b19..c2a0698 100644 --- a/docs-source/protocol/referee.md +++ b/docs-source/protocol/referee.md @@ -1,10 +1,22 @@ # Referee Protocol +Probably mostly document the vision filter data stream? + ## Transport +### Connections Table + +The following table contains connection information for automatic referee (autoRef) implementations talking to the +[Game Controller](https://github.com/RoboCup-SSL/ssl-game-controller). See the +[game controller protocol](./gamecontroller.md#remote-control-rcon-channels-optional) page for the shared rcon +registration/signing flow these channels use, including +[how to find the Game Controller's IP](./gamecontroller.md#remote-control-rcon-channels-optional) without guessing it. + +| Protocol | Protobuf | Type | Address | Port | +| ------------- | -------------------------------------------------------------------------------------------------------------------- | --------- | ------- | ----- | +| AutoRef -> GC | [AutoRef](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_autoref.proto) | TCP | GC | 10007 | +| AutoRef -> GC | [AutoRef](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/gamecontroller/ssl_gc_rcon_autoref.proto) | TCP + SSL | GC | 10107 | + ## 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/teamclient.md b/docs-source/protocol/teamclient.md deleted file mode 100644 index cc13712..0000000 --- a/docs-source/protocol/teamclient.md +++ /dev/null @@ -1,7 +0,0 @@ -# Team Client Protocol - -## Transport - -## Message Format - -## League Software diff --git a/docs-source/protocol/vision.md b/docs-source/protocol/vision.md index b03d690..9fca5bc 100644 --- a/docs-source/protocol/vision.md +++ b/docs-source/protocol/vision.md @@ -1,12 +1,172 @@ # Vision Protocol +This page describes the layers that deliver and decode the vision data stream from the field. + +## League Software + +There are two pieces of league maintained software that interpret camera data to produce a vision data stream: + +- (Recommended) [Vision Processor](https://github.com/RoboCup-SSL/ssl-vision-processor) +- (Legacy) [SSL Vision](https://github.com/RoboCup-SSL/ssl-vision) + +If you're actively trying to connect to a field, you'll need to make sure at least one is running and has a valid field +geometry calibration. More info about running each tool can be found in the repo linked above. + ## Transport +The [transport layer](https://en.wikipedia.org/wiki/Transport_layer) is responsible for delivering raw data to the team +computers from the field. It doesn't say anything about the data it's delivering, but this is the first step of field +connectivity. As shown in the diagram below, a team computer connects to the field's vision data stream by joining the +vision multicast group. The league defines this multicast group as having an address of `224.5.23.2` at port `10006`. +You join a multicast group just like you connect to any IP address and port combo, though you may need to pass some +additional options when binding the socket. Consult the documentation for the language of your choosing. + +```{mermaid} diagrams/multicast-join.mmd +``` + +Once you've joined, you should expect to receive messages on a regular interval. Robot and Ball detections will arrive +at the framerate of the field camera(s), usually 60-75Hz. You will also receive field geometry data every few seconds. +**Do not assume the dimensions of the field in your software.** No field is perfect and your software/AI should always +operate on the real physically constructed field dimensions. These will be loaded into the league vision software, +calibration will occur, and final field geometry will be sent on the vision data channel. Always use this data as your +source of truth for the field. Competition fields are often off by up to a few cm and aspect ratio is not preserved. + +Shown in the join sequence diagram below is also a dropped detection frame. Note that re-transmission does not occur. +Packet loss should be _well below_ 1%. If it's not, network quality investigation is warranted. + +````{container} wide-mermaid +```{mermaid} diagrams/multicast-join-sequence.mmd +``` +```` + +### Connections Table + +The following table contains all relevant connection information related to vision data streams. + +| Protocol | Protobuf | Type | Address | Port | +| --------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------- | ---------- | ----- | +| SSL-Vision Detections | [SSL_WrapperPacket](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_wrapper.proto) | UDP Multicast | 224.5.23.2 | 10006 | + ## Message Format -## League Software +Now that a data stream is received, the raw data needs to be decoded. The league uses +[google protobuf](https://protobuf.dev/) to define the data format actually sent via multicast. The interface +definitions are available in the [league protobuf repository](https://github.com/RoboCup-SSL/ssl-protocol-defs). The +definition file there are needed to decode the raw data. The image below shows the hierarchy of the data structure send +in the data stream. -There are two pieces of league maintained software that interpret camera data: +````{container} no-letterbox-mermaid +```{mermaid} diagrams/message-hierarchy.mmd +``` +```` -- (Recommended) [Vision Processor](https://github.com/TIGERs-Mannheim/vision-processor) -- (Legacy) [SSL Vision](https://github.com/RoboCup-SSL/ssl-vision) +There are two top level data structures, SSL_DetectionFrame and SSL_GeometryData. These correspond to the packet types +seen in the join sequence diagram above. SSL_DetectionFrame is the meat of the data stream, and as seen in the diagram, +contains only the positions and orientations of friendly robots, opponent robots, and balls. It also includes timing +information so you can tell if a packet has been lost or has arrived out of order. SSL_GeometryData is sent on a regular +1s interval. Most teams will only care about the SSL_GeometryFieldSize which contains information about the real +physical field dimensions. These will differ from the idealized values and teams should use the real ones. +SSL_GeometryCameraCalibration contains information about the camera calibration result. Many teams do not use the camera +calibration data, only the field size data. + +The table below contains links to the specific proto files needed to decode the mentioned data types. It's recommended +you include the entire [ssl-protocol-defs repository](https://github.com/RoboCup-SSL/ssl-protocol-defs) as a submodule +in your code, rather than copying the files directly. + +| Message | Proto File | +| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| SSL_WrapperPacket | [ssl_vision_wrapper.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_wrapper.proto) | +| SSL_DetectionFrame | [ssl_vision_detection.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_detection.proto) | +| SSL_DetectionBall | [ssl_vision_detection.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_detection.proto) | +| SSL_DetectionRobot | [ssl_vision_detection.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_detection.proto) | +| SSL_GeometryData | [ssl_vision_geometry.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_geometry.proto) | +| SSL_GeometryFieldSize | [ssl_vision_geometry.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_geometry.proto) | +| SSL_GeometryCameraCalibration | [ssl_vision_geometry.proto](https://github.com/RoboCup-SSL/ssl-protocol-defs/blob/main/proto/vision/ssl_vision_geometry.proto) | + +## Coordinate System + +At this point, a team should have valid decoded field geometry, robot positions, and ball positions. This section +discusses the actual units and coordinate frame of the decoded. Position units are in millimeters (mm) and angles are in +radians measured counter-clockwise from +X. Likewise, field geometry is in mm. + +Timestamps are of type `double` and are unix timestamps in seconds. The "frame_number" field is a monotonic counter. + +```{image} diagrams/coordinate-system-light.svg +--- +class: only-light +alt: SSL vision coordinate system, origin at field center, +X/+Y axes, and + orientation angle convention +--- +``` + +```{image} diagrams/coordinate-system-dark.svg +--- +class: only-dark +alt: SSL vision coordinate system, origin at field center, +X/+Y axes, and + orientation angle convention +--- +``` + +The coordinate frame is centered in the middle of the field, with +Y axis going up and the +X axis going right. When +facing the field from the side table, nominally the negative most corner will be at the viewers bottom left and the +positive most corner in the upper right. + +| Field | Unit | +| ----------------------------------------- | ---------------------------- | +| `x`, `y` (position) | mm | +| `orientation` | rad, CCW from +X | +| `t_capture`, `t_sent`, `t_capture_camera` | s (unix timestamp, `double`) | +| `frame_number` | monotonic counter, unitless | +| `confidence` | unitless, `[0-1]` | + +## Multi-camera Handling and Filtering + +SSL_DetectionsFrames carry only positions as seen by **one camera**. This means if you play on any field with more than +one camera, **you will receive a detection per camera**. When robots/balls overlap cameras, the data stream will contain +multiple detections of the object and the timestamps and positions may disagree slightly. This is intentional behavior, +as teams may disagree on the approach to handle detections at boundaries. For example, when kicking off on a 4 camera +field, all four cameras are likely to see the ball and robots at the center and you will receive 4 detections for each +entity. This is visualized in the graphic below. + +````{container} no-letterbox-mermaid +```{mermaid} diagrams/multi-camera-merge.mmd +``` +```` + +There is no single agreed upon way to resolve these overlaps. An additional challenge is filtering position in velocity. +Teams will see noise in this derivative without filtering. Again, there are many approaches to solve this. The committee +notes that is a challenging problem in the SSL and robotics in general. Since 2023, the automatic referee software +packages in use by the game controller publish their filtered game state. This is so teams can inspect the data +interpretation behind issued penalties and infractions. It's also intended to be a "decent" or better implementation of +a shared data filter. Teams are allowed, and even encouraged, to use this filtered data in their AI. Eventually a team +may need to write a custom filter, but its expected that years would pass for a new team before the autoref filter is a +limiting factor. Many teams have written about +[vision filters in the SSL](https://tdpsearch.com/search?q=vision+filter&league=soccer_smallsize). + +**The [referee protocol](./referee.md) page documents how teams can use the filtered vision data from the automatic +referees.** + +## Network Flow + +The diagram below is the same competition network fan-out shown in the +[field network documentation](../field/network/compnetwork.md), with the machines involved in the vision stream +highlighted and everything else (game state / referee / broadcast only) muted. Cameras and the Truss NUC produce it, the +Vision Computer publishes the merged `224.5.23.2:10006` stream, and team PCs (plus, optionally, a streaming PC) consume +it. The router and switch are highlighted too — they maintain the multicast group (IGMP snooping/querier) the whole +stream depends on, even though they never decode the vision data stream. + +```{image} diagrams/ssl_field_network_fanout_truss_vision_highlight.light.svg +--- +class: only-light +alt: SSL field network fan-out diagram with vision-producing and + vision-consuming machines highlighted, everything else muted +--- +``` + +```{image} diagrams/ssl_field_network_fanout_truss_vision_highlight.dark.svg +--- +class: only-dark +alt: SSL field network fan-out diagram with vision-producing and + vision-consuming machines highlighted, everything else muted +--- +``` diff --git a/scripts/check.sh b/scripts/check.sh index 302fbac..e15ad32 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -12,6 +12,7 @@ rm -rf _build declare -a names=() declare -a results=() +declare -a notes=() run_check() { local name="$1" @@ -24,11 +25,38 @@ run_check() { tail -n 20 /tmp/check-"$name".log fi names+=("$name") + notes+=("") +} + +# linkcheck reports a skipped URL as "-ignored-" and still passes, so PASS on +# its own doesn't mean everything got checked. Two separate reasons a link is +# skipped: the WAF-blocked vendor sites in conf.py's linkcheck_ignore +# (permanent, re-verified by hand via reverify-ignored-links.sh), and every +# github.com link when GITHUB_TOKEN is unset (fixable, and most of the external +# links in these docs) — worth telling them apart. +skipped_link_note() { + local log="/tmp/check-external-links.log" + [[ -f "$log" ]] || return 0 + + local github other note="" + github=$(grep -c -- '-ignored-.*https://github\.com/' "$log" || true) + other=$(grep -- '-ignored-' "$log" | grep -vc 'https://github\.com/' || true) + + if [[ "$github" -gt 0 ]]; then + note="$github github links skipped, GITHUB_TOKEN unset" + fi + if [[ "$other" -gt 0 ]]; then + [[ -n "$note" ]] && note+="; " + note+="$other skipped via linkcheck_ignore" + fi + + echo "$note" } 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 +notes[$((${#notes[@]} - 1))]="$(skipped_link_note)" 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 @@ -36,7 +64,9 @@ echo echo "== Summary ==" failed=0 for i in "${!names[@]}"; do - echo "${results[$i]} ${names[$i]}" + line="${results[$i]} ${names[$i]}" + [[ -n "${notes[$i]}" ]] && line+=" (${notes[$i]})" + echo "$line" [[ "${results[$i]}" == "FAIL" ]] && failed=1 done