Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -37,6 +44,7 @@ jobs:
path: _build/html

internal-links:
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
Expand All @@ -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
Expand All @@ -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
Expand Down
49 changes: 49 additions & 0 deletions docs-source/_static/site.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 <text> 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;
}
77 changes: 77 additions & 0 deletions docs-source/conf.py
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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",
]


Expand Down Expand Up @@ -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) "
Expand Down Expand Up @@ -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/")
84 changes: 83 additions & 1 deletion docs-source/protocol.md
Original file line number Diff line number Diff line change
@@ -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](<https://en.wikipedia.org/wiki/Broadcasting_(networking)>).** 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
```
37 changes: 37 additions & 0 deletions docs-source/protocol/diagrams/coordinate-system-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions docs-source/protocol/diagrams/coordinate-system-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading