Skip to content

Contributing to RaceLink

Conventions and PR rules that apply across the four RaceLink repositories. Component-specific deep-dives live in the per-component docs (see the See also links at the end).

Repositories and where to push code

Component Repository Open PRs against
Host runtime, WebUI, services RaceLink_Host the appropriate feature branch (most work today lives on wled-advanced-control)
Gateway firmware RaceLink_Gateway continous-rx-mode
WLED usermod + build profiles RaceLink_WLED auto-identify
RotorHazard adapter RaceLink_RH-plugin adapt-host-changes

If a change touches the wire format, it touches all three of host, gateway and WLED — see host/docs/DEVELOPER_GUIDE.md §"Adding a new wire opcode".

Smoke tests before opening a PR

# All host PRs:
py -m pytest -q                                           # full suite
py -m pytest tests/test_no_german_in_ui.py                # no DE strings in operator UI
py -m pytest tests/test_exception_hygiene.py              # every `except Exception` is logged or annotated

# Touched racelink_proto.h?
py -m pytest tests/test_proto_header_drift.py             # three-way header parity

# Touched JS?
node --check racelink/static/racelink.js
node --check racelink/static/scenes.js

For features the test suite cannot fully cover (frontend behaviour, RF interactions), include a manual smoke checklist in your PR description.

Coding conventions

Boolean send contract

Every send_* method on control_service returns bool:

  • True — the transport accepted the frame for queueing.
  • False — transport not ready / no target / nothing went out.
def send_my_new_opc(self, ...) -> bool:
    transport = self._require_transport("sendMyNewOpc")
    if transport is None:
        return False
    transport.send_my_new_opc(...)
    return True

The audit's B2 finding traced silent-success bugs back to methods that returned None instead. New send-style methods follow the contract above.

Exception hygiene

tests/test_exception_hygiene.py requires every except Exception block to either log, re-raise, or carry a # swallow-ok: <reason> comment. The reason should be substantive — "best-effort fallback; caller proceeds with safe default" is the bare minimum, but a one-line why is better.

If you are tempted to swallow at an RF / persistence boundary, prefer logger.warning(..., exc_info=True) over a silent pass.

Locking rule

Anything that mutates shared state uses an existing lock (state_repository.lock, _pending_config_lock, _pending_expect_lock, _tx_lock). If you add a shared field, add a matching lock and pin it with a regression test in tests/test_state_concurrency.py.

Never hold state_repository.lock across RF I/O. This deadlocks the gateway reader thread. Reference pattern: _apply_device_meta_updates in racelink/web/api.py. See host/ARCHITECTURE.md §"Locking Rule".

Thread naming

Always pass name="rl-<purpose>" when creating a thread; the audit's A8 finding established the convention. New threads without a name make threading.enumerate() and py-spy traces illegible.

No German in operator-facing UI

tests/test_no_german_in_ui.py is a CI gate. Use English everywhere in operator-visible text. Internal comments can be any language.

Effect vs. preset terminology

Use preset for "WLED preset slot" (a numeric ID on a node). Use effect for "WLED segment effect parameters" (mode + speed + intensity + sliders + palette + colours). The terminology cleanup plan renamed several legacy "effect_*" symbols that actually referred to presets; check GLOSSARY.md before introducing new symbols.

PR description template

A good PR description includes:

  • What changed and why. Two sentences are usually enough.
  • Wire-format impact. "No wire change" / "Adds opcode X (bumped PROTO_VER_MINOR)" / "Reshapes struct Y (bumped PROTO_VER_MAJOR)".
  • Test impact. New tests added; tests modified; manual smoke steps if needed.
  • Audit-trail note. If the change is significant, append a note to the active project audit plan in the source repo's .claude/plans/. (The plan archive is internal; the audit note is for the running ledger.)

Cross-repo coordination

A wire-format change is a coordinated PR across host + gateway + WLED. Sequence:

  1. Open a host PR with the new racelink_proto.h (and the matching transport / service additions).
  2. Open the matching gateway PR using the same byte-identical racelink_proto.h.
  3. Open the matching WLED PR ditto.
  4. Land them in the same release window. The proto-drift test will start failing as soon as the headers diverge.

See also