Files
project-yukart/docs/REPOS.md
T
2026-10-13 21:31:52 +09:00

270 lines
12 KiB
Markdown

---
type: reference
---
# Repositories
## Splitting rule
- **`project-yukart`** — overview, roadmap, cross-cutting design. No code, ever.
- **`orin-nano-env-setup`** — the host: kernel modules, drivers, OS packages,
device permissions, systemd plumbing. Anything that must be true *before* an
application starts.
- **One repo per application or application cluster.** A cluster is a set of
services that are developed, versioned and deployed together.
- **Helper/deployment repos** stand alone when they are reusable outside Yukart.
Anything cross-repo — topic names, payload schemas, the DBC — belongs in the
contracts repo, not copied into each consumer.
## Current
| Repository | Role |
|---|---|
| [project-yukart](gitea:yukart/project-yukart) | Hub: overview, roadmap, architecture |
| [orin-nano-env-setup](gitea:yukart/orin-nano-env-setup) | Jetson host bring-up |
| [app-flutter](gitea:yukart/app-flutter) | Flutter display client |
| [contracts](gitea:yukart/contracts) | Topic namespace, payload schemas, signal registry |
| [svc-core](gitea:yukart/svc-core) | Acquisition + processing cluster: `recorder`, `decoder`, `orin-status`, `obd-poller` now; `can-bridge`, `gnss` to come |
| [svc-gw](gitea:yukart/svc-gw) | Telemetry gateway — aggregation and the display boundary |
| [svc-nav](gitea:yukart/svc-nav) | Navigation service — routing and guidance |
| [svc-vision](gitea:yukart/svc-vision) | Camera: capture and the live view; recording and inference to come |
| [tool-mapmint](gitea:yukart/tool-mapmint) | Map artifact minting, workstation-side |
| [tool-mock](gitea:yukart/tool-mock) | Mocks and fixtures — a fake car on a workstation |
| [tool-crosscheck](gitea:yukart/tool-crosscheck) | Consistency checks across all yukart repos; a reusable workflow every repo runs on push |
## Planned
| Repository | Role | Why separate |
|---|---|---|
| `app-esp32` | ESP32 RLCD/e-ink display firmware (Class B) | Embedded toolchain, flashed not deployed |
| `tool-analysis` | Offline session analysis, reverse-engineering notebooks | Runs off-car, no runtime coupling |
Create these lazily — when there is something to put in them, not before.
## Naming
Repositories are named `<group>-<thing>`, with **no project prefix**. The `yukart`
Gitea org already namespaces everything, so a `yukart-` prefix would be a constant
occupying the most significant characters of every name — pure noise in a sorted
index, and the reason the earlier scheme scattered.
| Group | Meaning |
|---|---|
| `svc-` | A service running on the Orin |
| `app-` | A display client, running off the Orin |
| `tool-` | Off-car tooling; never deployed to the car |
The goal is that **lexical order equals semantic grouping**: opening the org
listing puts every service together, every client together, every tool together.
The group segment does that work because it is the first thing that varies.
Ungrouped singletons — `contracts` — are fine where a category has exactly one
member and always will.
`project-yukart` and `orin-nano-env-setup` predate this and keep their names
rather than paying a rename for two repos that are stable and easy to find.
### Reusability
Nothing in a name marks a repo as reusable. A component that turns out to be
genuinely reusable outside Yukart **leaves the org** rather than being renamed —
location says it, not spelling.
## Helper and deployment repos
Arduino and ESP32 work implies flashing, build and deployment scaffolding that
is likely to be reused. Keep those generic and outside the org — e.g. an ESP32
project skeleton with the toolchain, flashing scripts and CI already wired up.
### Gitea template repositories
Gitea supports these: repo **Settings → Repository → Template**, after which the
repo offers **"Use this template"** to create new repos from it.
Use a template repo for **scaffolding** — a new ESP32 or Arduino project that
should start with the toolchain, directory layout and flashing scripts in place.
Do **not** use one for code you intend to keep updating in one place. Template
instantiation is a fork-at-create: the copy has no link back, so improvements to
the template never reach repos already created from it. For genuinely shared
code, use a git submodule or publish a package instead.
Rule of thumb: *template for the starting shape, submodule/package for living
code.*
## Linking convention
Cross-repo links use the `gitea:owner/repo` scheme, as established in the hub
README — e.g. `[Environment setup](gitea:yukart/orin-nano-env-setup)`.
## Per-repo document layout
Following the convention already set by `orin-nano-env-setup`:
```
README.md minimal portal, points everywhere else
docs/
GUIDELINES.md implementation practice for this repo
BOM.md hardware, where the repo owns hardware
impl/
plans/ what is intended
reports/ what actually happened, and why
```
The `impl/plans` ÷ `impl/reports` split is worth keeping: plans go stale,
reports do not. A report like `PCAN-SETUP.md` stays valuable precisely because
it records the investigation and the dead ends, not just the final commands.
### Scaffolding conventions
Component repos are scaffolded by hand. A template repo is for carrying a real
toolchain, which is the ESP32 case above.
Every component repo carries:
```
README.md the shared blurb, "What this repo is about", then
links to guidelines and impl/
docs/GUIDELINES.md numbered sections, ends "## N. To be added"
docs/impl/plans/ at least INITIAL.md
docs/impl/reports/
docs/BOM.md only where the repo owns hardware
```
### Languages
| Where | Language | Why it is there |
|---|---|---|
| `svc-*` — on the Orin, except `svc-vision` | **Rust** | No GC, low resident memory on a shared 8 GB pool, and fast start for the tier-1 capture services |
| `svc-vision` — on the Orin | **Python** | Camera pipelines (GStreamer) and inference live in the Python ecosystem (decision 29). Standard library and host packages only, until a dependency earns its place |
| `tool-*` — off-car | **Python** | Batch work where development speed is the only constraint that binds |
| `app-flutter` | **Dart** | Flutter |
Python floors at **3.10**, the Orin's version, for anything that might run there.
The workstation has 3.14; do not rely on it.
`Cargo.lock` and `pubspec.lock` are committed — these are applications, not
libraries.
#### Building for the Orin
Services build **natively on the development Orin**. The sysroot is then the
target's own, so a binary cannot reference a symbol the car unit lacks — the
guarantee is structural rather than configured.
Two Orin Nanos, and they have different jobs:
| Unit | Role |
|---|---|
| Development Orin (`mikkeli-orin-nano-2`) | Build, CI runner, bench testing. Always powered, on the LAN |
| Car Orin (`mikkeli-orin-nano`) | Deployment target only |
The SIM7600 LTE/GNSS hat lives in the car unit only — its custom enclosure doesn't
move between units. Anything touching the modem or gpsd is tested on the car unit;
on the development unit, `gnss` runs against its own mock input (decision 19).
⚠ **The guarantee is version parity, not architecture.** An aarch64 binary built
on a newer userland still fails to load on an older one. A JetPack or Ubuntu
upgrade applied to one unit and not the other reintroduces the problem, silently
and only at runtime. **Upgrade both, or neither.**
The check that catches drift between them is `scripts/check-glibc.sh` in each
service repo:
```bash
scripts/check-glibc.sh target/release/svc-nav # ceiling defaults to 2.35
```
| Exit | Meaning |
|---|---|
| 0 | Highest referenced version is at or below the ceiling |
| 1 | Ceiling exceeded — the responsible symbols are printed |
| 2 | **Could not check** — no `objdump`, missing binary, no versions found |
It detects, it does not fix; the fix is building on a unit whose release matches
the car's. Exit 2 is distinct on purpose: a check that cannot check must not
report success.
##### The runner must build on the host, not in an arbitrary container
⚠ **A container-mode runner discards the entire guarantee.** Jobs then build in
whatever image the workflow names — `rust:latest` carries a much newer glibc — and
the Orin being the host is irrelevant. The binary compiles cleanly and fails to
load on the car unit.
So either run `act_runner` in **host mode**, or give it an image built from the
same Ubuntu release as the car Orin. `scripts/check-glibc.sh` catches the mistake
either way, which is the second reason it is worth having.
The runner needs a writable toolchain: rustup is installed per-user, so the
runner either runs as a user that owns one or gets its own. A root-owned shared
install cannot have targets added to it without `RUSTUP_ALLOW_SHARED_WRITE`.
##### Cross-compiling from the workstation
Available as a fallback. Each service repo's `.cargo/config.toml` points at
`aarch64-linux-gnu-gcc`, from the `gcc-aarch64-linux-gnu` package:
```bash
cargo build --release --target aarch64-unknown-linux-gnu
```
⚠ **The linker binds each symbol to the newest version present in the sysroot**,
so the workstation's glibc 2.43 sets the floor — not the toolchain, which targets
2.17. Currently the highest referenced version is 2.34, below the Orin's 2.35,
because everything above 2.30 comes from one event: glibc 2.34 merged
`libpthread` and `libdl` into `libc` and re-versioned the moved symbols. Stable,
but not guaranteed the way a native build is.
⚠ **`std::process::Command` breaks it.** Any binary that spawns a process links
`pidfd_spawnp` and `pidfd_getpid` (GLIBC_2.39) from the workstation's sysroot.
They are weak references, but the version requirement is not, so the car Orin
refuses to load the binary. `check-glibc.sh` catches it; such binaries build
natively (svc-core's `orin-status` runs `mmcli`, so its deploy script builds on
the development Orin).
### Writing style
Documents describe **what is and what will be**. They are not changelogs — git
holds the sequence.
| Instead of | Write |
|---|---|
| "X was dropped because Y" | what the design **is** |
| "this was a risk, now resolved" | what runs, and the measurements |
| "the earlier decision assumed Z" | the current constraint |
| "X was rejected in favour of Y" | Y, stated as the rule |
⚠ **A sentence justifying a current state by a current reason becomes false when
the reason changes, and nothing announces it** — it stays, confident and
well-written, and reads as fact.
Two things survive the cut, because they stay true when their subject is gone:
- **A mechanism that can bite again.** Not "we picked the wrong extract once", but
"driving side is a country-level property, so a clipped extract routes mirrored".
- **A condition that would change the decision**, stated generally — "a
permanently installed, car-powered panel" rather than a story about a panel that
was considered.
Reports under `impl/reports/` are the exception and the place for all of it: the
investigation, the dead ends, what was measured and found wrong.
### Front matter
Every markdown document carries front matter, with **no exceptions for the hub or
for any particular repo**. The `type` discriminates:
| `type` | Applies to | Extra fields |
|---|---|---|
| `reference` | READMEs, OVERVIEW, ARCHITECTURE, BOM — things that describe what is | — |
| `guidelines` | `GUIDELINES.md` | — |
| `plan` | anything under `impl/plans/`, plus `ROADMAP.md` | `status`, `updated` |
| `report` | anything under `impl/reports/` | `date` |
`status` is one of `active`, `superseded` or `done`.
`status` and `updated` make a plan's staleness visible — plans go stale and
reports do not, and without them a plan from months ago reads identically to one
written this morning. `date` on a report records the moment it describes.