# Build a new robot on bb_mcuhub

A linear, step-by-step recipe for going from nothing to a flashable robot on
`bb_mcuhub`. Each step states **what you produce**, gives the **minimal real code**
(copied from the worked example, `examples/segby_v1/`), and ends with a **check to
run before moving on**. By the end you have a robot that host-builds, whose firmware
envs compile, and that you can drive with zero hardware — then you hand off to
[`BRINGUP.md`](https://github.com/lostbean/bb_mcuhub/blob/main/examples/segby_v1/BRINGUP.md) for the on-hardware stages.

> This guide owns **software assembly**. The [`README`](../README.md) is reference;
> [`BRINGUP.md`](https://github.com/lostbean/bb_mcuhub/blob/main/examples/segby_v1/BRINGUP.md) starts _after_ you have a green
> build to flash.

---

## The whole thing at a glance

**What you're building.** An Elixir brain (a Raspberry-Pi-class host) talks over a
single UART to a tree of microcontrollers. Each microcontroller is a **hub**: it
reads sensors, drives actuators, and forwards messages for its children. The same
hub shape nests to any depth. The worked example is a two-wheel balancing bot:

```
 Pi (Elixir host)  --UART-->  Blaster ESP32 (root hub)  --UART-->  Wheels ESP32 (leaf hub)
 BeamBots app                 IMU + range + status LED              two FOC motors,
 + LinkOwner                  + bridges down to Wheels              each behind its own floor
```

**What you write — seven small files (the end state).** You never edit the library;
you fill in this shape in your own Mix app:

```
my_app/
  mix.exs                              Step 0  dep on bb_mcuhub + bb; a wire.gen alias
  lib/my_app/
    value_types/temp.ex                Step 1  ~10 lines: what bytes a value is on the wire
    hubs/sense.ex                      Step 2  the ports on one hub (and their safety knobs)
    robot.ex                           Step 3  place hubs on nodes + declare the topology
    host.ex                            Step 7  a thin wrapper over BBMCUHub.Host
  firmware/
    gen/my_app/                        Step 4  GENERATED by `mix wire.gen` — never hand-edit
    mcu/sense.cpp                      Step 5  the only C: one hook per port (fill a struct)
    platformio.ini                     Step 6  one env per hub
```

**Hardware to start.** One ESP32 plus a motor driver over UART is enough for first
motion. CAN is supported but optional — the worked example is UART-only. The root
hub speaks UART up to the host; child links can be UART (point-to-point, no
transceiver) or CAN (shared bus, needs a transceiver).

**You can run all of this with no hardware.** Point your host launcher at
`BBMCUHub.Host.Transport.Loopback` (shipped in the library) and the whole host stack
runs on your laptop over the real COBS+CRC framing — see Step 7. For safety and
freshness behaviour against the _real firmware C floor_, the test-only
`BBMCUHub.Test.VirtualHub` does the same with the actual C wire path behind it
(available when working inside the `bb_mcuhub` repo — it lives in the library's
`test/support/` and is not part of the shipped package).

**Do you need Nix?** No. Nix is a convenience for a reproducible, pinned toolchain
shared across worktrees (Elixir 1.19, PlatformIO 6.1, clang/make). Bring your own
Elixir 1.19+ and PlatformIO if you prefer; the devShell just guarantees matching
versions.

**The order is the backbone.** Each step produces what the next consumes: a
value-type is named by a port; ports live on a hub module; hub modules are placed by
the robot; the robot is the input to `mix wire.gen`, whose headers your device hooks
and `platformio.ini` consume; the host launcher runs the robot. Do them in order.

<details>
<summary>Status: what's stable, what's deferred</summary>

v1 is a small, hardware-validated core. Implemented and tested today (Elixir suite +
host-compiled C harnesses + a real-C-floor e2e seam): the wire path, the on-chip
floor, born-stale freshness, the generated contract + cross-language parity, and CAN
segmentation. It powers the segby_v1 balancing bot on real ESP32 boards.

Explicitly deferred (each defaults to the safe behaviour): a `mix
bb_mcuhub.gen.robot` scaffold to collapse Steps 1-3 and 6 (so v1 robot assembly is
the manual path this guide describes), arm-nonce, `fw_id`/board-identity (node
identity is trust-on-first-use), conflation/backpressure and wire-budget enforcement,
host coordination, CAN-FD, and stream-event observers (v1 observers are
sample-state/poll only). Two things are out of host-test scope by design and need the
bench/QEMU: the watchdog boot-loop and motor-sign calibration.

</details>

<details>
<summary>Glossary terms used below (all defined fully in <a href="design/CONTEXT.md">CONTEXT.md</a>)</summary>

- **hub** — one microcontroller node; the only node type. It reads sensors, drives
  actuators, and forwards for child hubs, so the same shape nests into a tree.
- **value-type** — a small reusable module defining what bytes a kind of value puts
  on the wire and how those bytes become a typed `BB.Message` (your wire vocabulary).
- **floor** — the dead-man code on an actuator's own chip: if commands stop arriving,
  it drives the actuator to its safe value and disarms, by itself.
- **born-disarmed / born-stale** — an actuator boots at its safe value and won't move
  until a fresh command arrives; a consumer trusts nothing until it sees a new value.
- **seq** — a per-write counter the producing chip bumps by one each value; trust and
  the dead-man key off "did this number advance", never a clock.
- **link / root hub** — a link is a hub's connection up to its parent (UART or CAN);
  the root hub is the one that declares `parent: :host` and owns the host UART.

The architecture, its rationale, and the `§` section numbers live in
[`docs/design/design.md`](design/design.md).

</details>

---

## Step 0 — Your app and its dependency on the library

**You produce:** a Mix app in your own namespace (e.g. `MyApp.*`) that depends on
`bb_mcuhub` and `bb`. Mirror the example's
[`mix.exs`](../examples/segby_v1/mix.exs):

```elixir
defp deps do
  [
    {:bb_mcuhub, path: "../.."},        # the library
    {:bb, "~> 0.25"},                   # the BeamBots framework — the robot sits on it
    {:bb_tui, github: "mcass19/bb_tui"} # optional dashboard
  ]
end
```

> `{:bb, "~> 0.25"}` is the published form; Elixir 1.19+ is required.
> The example's `mix.exs` wraps it in a
> `bb_dep/1` `BB_VERSION` switch so the beam-bots workspace can resolve `bb`
> from a sibling checkout for cross-package integration tests; normal builds
> use hex unchanged.

`bb` (BeamBots) is the Elixir robotics framework `bb_mcuhub` extends — it provides
the robot DSL, PubSub, and the Sensor/Actuator component model. `bb_mcuhub` is a
Spark DSL extension to it, not a standalone system, so the dep is required.

**Check it worked:** `mix deps.get && mix compile` succeeds.

<details>
<summary>Depending on it as a real external consumer (not the in-tree path dep)</summary>

The example uses `{:bb_mcuhub, path: "../.."}` because it lives in the repo. A
published consumer swaps that one line for a Hex dep `{:bb_mcuhub, "~> x.y"}` or
`{:bb_mcuhub, github: "..."}` — no other change. (The firmware side has the same
in-tree-vs-published split; see Step 6.)

</details>

---

## Step 1 — Value-types (your wire vocabulary)

**You produce:** one `use BBMCUHub.ValueType` module per kind of value, declaring an
ordered `layout` (the bytes on the wire) plus `lift/1`/`unlift/1` (raw `%{field =>
number}` map ↔ typed `BB.Message`). The library ships `:imu`, `:effort`, `:status`
as stock; you add your own with no library edit.

A sense-only value-type is about ten lines. `lift`/`unlift` are **identity** when the
raw field map _is_ the payload (the example's `Range`):

```elixir
defmodule SegbyV1.ValueTypes.Range do   # one f32 metres on the wire
  use BBMCUHub.ValueType

  layout(
    distance_m: :f32
  )

  @impl BBMCUHub.ValueType
  def lift(map) when is_map(map), do: map

  @impl BBMCUHub.ValueType
  def unlift(map) when is_map(map), do: map
end
```

A **command** value-type additionally names its own `BB.Message` command struct with
`command_message/0`, and `lift`/`unlift` do **real mapping** between that struct and
the wire field map (the example's `Led`, which also ships its own
[`LedColor`](../examples/segby_v1/lib/segby_v1/messages/led_color.ex) struct):

```elixir
defmodule SegbyV1.ValueTypes.Led do      # an RGB triple: three u8 on the wire
  use BBMCUHub.ValueType

  layout(r: :u8, g: :u8, b: :u8)

  @impl BBMCUHub.ValueType
  def lift(%{r: r, g: g, b: b}), do: %SegbyV1.Messages.LedColor{r: r, g: g, b: b}

  @impl BBMCUHub.ValueType
  def unlift(%SegbyV1.Messages.LedColor{r: r, g: g, b: b}), do: %{r: r, g: g, b: b}

  # The command struct this value-type accepts; an actuator view derives its PubSub
  # subscribe from this — the view never hard-codes a struct (the value-type names it).
  @impl BBMCUHub.ValueType
  def command_message, do: SegbyV1.Messages.LedColor
end
```

The verifier requires a non-`nil` `command_message` on every command (`dir: :in`)
port, so a sense-only value-type on a command port fails loud at compile time.

**Why first:** a value-type is named by a **port** (Step 2), and the same value-type
reuses across hubs and robots — so it's the unit you author first.

**Check it worked:** `mix compile` — the value-type compiles standalone.

---

## Step 2 — Hub modules (the ports on a node)

**You produce:** one `use BBMCUHub.Hub` module per hub, declaring its ports and their
intrinsic wire facts — everything true about the _device_, independent of where it's
deployed. Each `port(:name, ...)` names its `dir` (`:in` command / `:out` produced),
value `type` (a stock atom or a value-type module), and `rate` (Hz). A produced port
may carry `t_dev: true` (ship the producer's µs stamp) and a `sample:` MFA; a command
port may carry a `step:` MFA.

A command port (`dir: :in`) that drives hardware **must** state its floored role with
the required `has_safe_action` boolean:

- `has_safe_action: true` ⇒ floored — you MUST give a `safe_action` value (a literal
  value of the port's own value-type — the dead-man's safe state). The port gets an
  on-chip **floor**.
- `has_safe_action: false` ⇒ a non-floored actuator (e.g. a decorative LED): no
  floor, no `safe_action`.

Because the flag is required, a floored port is never silently floorless — a
forgotten safe action is a compile error.

The leaf **Wheels** hub — two `:effort` command ports, each floored to zero torque,
plus two `:status` ports ([`hubs/wheels.ex`](../examples/segby_v1/lib/segby_v1/hubs/wheels.ex);
the real file also declares two `vel_*` wheel-speed sensor ports, omitted here
for brevity):

```elixir
defmodule SegbyV1.Hubs.Wheels do
  use BBMCUHub.Hub

  ports do
    port(:motor_left,
      dir: :in,
      type: :effort,
      rate: 50,
      has_safe_action: true,
      safe_action: %{nm: 0.0},        # if commands stop, this motor floors to 0 N·m on its own chip
      step: {SegbyV1.Hubs.Wheels.Floor, :step}
    )

    port(:motor_right,
      dir: :in, type: :effort, rate: 50,
      has_safe_action: true, safe_action: %{nm: 0.0},
      step: {SegbyV1.Hubs.Wheels.Floor, :step}
    )

    port(:status_left, dir: :out, type: :status, rate: 50)
    port(:status_right, dir: :out, type: :status, rate: 50)
  end
end
```

The root **Blaster** hub — sense ports plus a non-floored LED actuator
([`hubs/blaster.ex`](../examples/segby_v1/lib/segby_v1/hubs/blaster.ex)):

```elixir
defmodule SegbyV1.Hubs.Blaster do
  use BBMCUHub.Hub

  ports do
    port(:pose, dir: :out, type: :imu, rate: 100, t_dev: true,
      sample: {SegbyV1.Hubs.Blaster.SamplePose, :sample})

    port(:range_front, dir: :out, type: SegbyV1.ValueTypes.Range, rate: 20,
      sample: {SegbyV1.Hubs.Blaster.SampleRange, :sample})

    port(:status_led, dir: :in, type: SegbyV1.ValueTypes.Led, rate: 20,
      has_safe_action: false)        # decorative — non-floored
  end
end
```

A port names a stock value-type by atom (`type: :imu`) or a consumer one by module
(`type: SegbyV1.ValueTypes.Led`). The `sample`/`step` MFA refs are **declared data
only** — the host never invokes them; they travel into the generated per-hub schedule
for the firmware.

**Why second:** a hub's ports reference the value-types from Step 1; the robot
(Step 3) places these hub modules on nodes.

**Check it worked:** `mix compile` — each hub module compiles, and a command port
missing its `has_safe_action`/`safe_action` fails the build right here.

---

## Step 3 — The robot (place hubs, declare topology)

**You produce:** one `use BB, extensions: [BBMCUHub.Dsl]` module. The hub-gateway DSL
adds two blocks alongside BeamBots' own. Mirror
[`lib/segby_v1/robot.ex`](../examples/segby_v1/lib/segby_v1/robot.ex).

### `hubs do ... end` — place each hub on a node, declare the tree

Each `hub(:name, MyApp.Hubs.Foo, node: 0xNN, parent:, uplink:)` places a hub module
on a NODE id and declares the **link** to its parent (topology is declared,
never inferred). The root hub declares `parent: :host` and **no** `uplink` (its uplink
is the fixed host UART). A non-root hub declares both `parent: :some_hub` and an
`uplink: :uart | :can`. **This is the whole topology — two lines define the tree:**

```elixir
use BB, extensions: [BBMCUHub.Dsl]

hubs do
  hub(:blaster, SegbyV1.Hubs.Blaster, node: 0x02, parent: :host)
  hub(:wheels, SegbyV1.Hubs.Wheels, node: 0x05, parent: :blaster, uplink: :uart)
end
```

The route table and any UART↔CAN bridging fall out of these parent pointers — there
is no separate gateway/leaf/router code to write. Do **not** use a `transport:` key
(a deprecated earlier form). The Topology verifier checks the tree at compile time (exactly one
`parent: :host` root, the root declares no `uplink`, every non-root declares one,
every parent resolves, no cycles, fully connected) — a malformed tree refuses to
compile.

### `topology do ... end` — the BeamBots links/joints and the views

This is BeamBots' own topology, where `sensor(...)` / `actuator(...)` views name the
**hub + port** they read. A sensor view names `{BBMCUHub.BBHub.Sensor, hub:, port:,
fresh_for:, beat_ms:}`; an actuator view names `{BBMCUHub.BBHub.Actuator, hub:, port:,
status_port:, fresh_for:}` (its `status_port:` is the hub's matching **Status slot** —
read for liveness, never inferred from "we sent a command"). `fresh_for` is the
consumer's freshness window as a multiple of the producer period.

```elixir
topology do
  link :base_link do
    sensor(:chassis_imu,
      {BBMCUHub.BBHub.Sensor, hub: :blaster, port: :pose, fresh_for: 3, beat_ms: 10})

    joint :left_wheel do
      type(:continuous)
      axis do end
      limit do
        effort(~u(10 newton_meter))
        velocity(~u(20 radian_per_second))
      end

      actuator(:left_drive,
        {BBMCUHub.BBHub.Actuator,
         hub: :wheels, port: :motor_left, status_port: :status_left, fresh_for: 5})

      link :left_wheel_link do end
    end
    # ... right_wheel mirrors left
  end
end
```

The full file also carries `controllers do` (the host balance loop) and `commands do`
(operator teleop) — read it for the complete shape. The verifier reconciles every
`(hub, port)` a view names against exactly one producer in the IR.

**Why third:** the robot consumes the hub modules (Step 2) and is the input to the
generator (Step 4) — the one authored model the wire artifacts derive from.

**Check it worked:** `mix compile` — the robot compiles, meaning the topology is
valid and every view resolves to a real `(hub, port)`.

---

## Step 4 — Generate the wire artifacts (`mix wire.gen`)

**You produce:** the C-side headers, generated from the one robot model so the C and
Elixir codecs **cannot drift**. Add a `wire.gen` alias to `mix.exs` wrapping the
library task `wire.gen.run` with your robot, slug, and output base
([`mix.exs`](../examples/segby_v1/mix.exs)):

```elixir
defp aliases do
  [
    "wire.gen": [
      "wire.gen.run --robot SegbyV1.Robot --slug segby_v1 " <>
        "--gen-dir firmware/gen --fixtures-dir test/fixtures"
    ]
  ]
end
```

`--robot` is your robot module (generation is always explicit-robot); `--slug` names
the per-robot artifact dir (headers land under `firmware/gen/<slug>/`); `--gen-dir` /
`--fixtures-dir` resolve relative to your app root, so artifacts land in **your**
tree. It needs only Elixir compiling your robot — no device, no toolchain.

Run `mix wire.gen`. Into `firmware/gen/<slug>/` it writes `wire_contract.h` (the
packed structs + facts, including the generated `ROOT_NODE` and per-actuator floor
window), `<hub>.glue.h` per hub (the generated main-loop glue), `<hub>.device.h`
per hub (the hook prototypes you implement in Step 5), and `parity_vectors.h`;
under `--fixtures-dir` it writes the matching `parity_vectors.exs` (see the
details box below).

**The everyday inner loop is three commands, not a footnote:** edit a port in a hub
module → `mix wire.gen` → `git commit`. A **drift test** fails the build if committed
artifacts don't match fresh generation, so regen-and-commit is the routine.

**Why fourth:** the generator needs the complete robot (Step 3); its outputs are what
Steps 5 and 6 consume.

**Check it worked:** `mix wire.gen` writes the headers; `mix test` passes the drift
test (committed artifacts match a fresh run).

<details>
<summary>What lands in firmware/gen/&lt;slug&gt;/ (never hand-edit)</summary>

- `wire_contract.h` — the packed value structs + facts: the generated `ROOT_NODE`
  (from the hub that declared `parent: :host`), the per-link transport facts
  (`LINK<k>_TRANSPORT_UART`), the port ids, and each actuator's floor window.
- `<hub>.glue.h` (per hub) — the generated main-loop glue: route table, `hub_on_body`
  relay, command dispatch, the floor plumbing, the schedule.
- `<hub>.device.h` (per hub) — the device-hook prototypes you implement (Step 5).
- `parity_vectors.h` — the cross-language witness the host-compiled C harness
  asserts byte-identical to the Elixir codec. (Its Elixir twin,
  `parity_vectors.exs`, lands under `--fixtures-dir` for the Elixir suite.)

</details>

---

## Step 5 — Device hooks (the only firmware you hand-write)

**You produce:** one thin C hook per port. Everything mechanical — the router table,
`hub_on_body`, command dispatch, the floor's `init`/`on_command`/`control_tick`/status
plumbing, the schedule — is **generated** into `<hub>.glue.h`, so the safety-critical
seq/floor wiring is never hand-written. You implement only the prototypes the
generator emitted into `gen/<slug>/<hub>.device.h`, in `mcu/<hub>.{c,cpp}`.

> **The gen/ vs mcu/ split is the rule.** Everything under any `gen/` is generated and
> drift-tested — never hand-edited. Everything under `mcu/` is hand-authored.

The hook _signature_ is owned by the port's value-type: a single numeric field passes
by value; a multi-field value passes as a `const <Struct> *`. The job is the same
either way — **a sense hook fills a struct and returns a bool; an act hook applies a
value.** That is the entire firmware seam per port.

A sense hook, bare ([`mcu/blaster.cpp`](../examples/segby_v1/firmware/mcu/blaster.cpp)):

```c
/* fill the struct, return true. Return false (here: a failed I²C burst) and
   pose's seq stalls, so the host reader goes stale — no garbage is trusted. */
extern "C" bool blaster_pose_read(Imu *out) {
  uint8_t b[14];
  if (!mpu_read(MPU9250_REG_ACCEL_XOUT_H, b, 14)) return false;  /* bounded read failed */
  /* ... scale the raw int16 into the :imu layout's engineering units ... */
  return true;
}
```

The matching act side, with the floor story visible in one place
([`mcu/wheels.cpp`](../examples/segby_v1/firmware/mcu/wheels.cpp)). The generated glue
calls `_drive` with the live command, or — when the command seq stops advancing — with
the port's `safe_action` (0.0):

```c
/* port(:motor_left, has_safe_action: true, safe_action: %{nm: 0.0}) →
   if commands go silent, the floor calls this with 0.0, on this chip, no host needed. */
extern "C" void wheels_motor_left_drive(float effort) {
  if (!m0_ready) return;
  m0_motor.target = torque_to_uq(effort);
  m0_motor.loopFOC();
  m0_motor.move();
}
```

Two chassis rules: `<hub>_device_setup()` may run as long as it needs (the task
watchdog is armed _after_ setup, so a slow `initFOC`/I²C settle never boot-loops the
chip), and every `_read`/`_drive` tick must be bounded (no spin/blocking; a read that
can't complete returns `false` and goes stale, legibly).

**Why fifth:** the prototypes you implement come from Step 4's generation; the hooks
are compiled by Step 6's envs.

**Check it worked:** `cd firmware/test && make` builds the host-compiled C harnesses
(the floor + wire path, no device); the per-env compile in Step 6 then proves the
hooks build for the target.

<details>
<summary>The generated prototypes you implement (from wheels.device.h)</summary>

```c
/* GENERATED into wheels.device.h — the contract you implement in mcu/wheels.cpp */
void wheels_device_setup(void);              /* one-time bring-up; the WDT is armed AFTER this */
bool wheels_vel_left_read(WheelSpeed *out);  /* bounded read; false on timeout */
bool wheels_vel_right_read(WheelSpeed *out); /* bounded read; false on timeout */
void wheels_motor_left_drive(float);         /* apply to the plant */
void wheels_motor_right_drive(float);        /* apply to the plant */
void wheels_post_control(void);              /* OPTIONAL per-loop telemetry; #define
                                                WHEELS_POST_CONTROL_OVERRIDE to provide one */
```

`<hub>_device_setup()` plus, per port, a `<hub>_<port>_read` (sense) or
`<hub>_<port>_drive` (act). These are well-known C names the glue calls.

</details>

---

## Step 6 — `platformio.ini` envs (one env per hub)

**You produce:** one PlatformIO env per hub. Each env carries `-Igen/<slug>` (find the
generated headers), `-DMY_NODE=0xNN` (this hub's node id — **required**),
`build_src_filter +<mcu/<hub>.cpp>` (compile only this hub's device file), and
`lib_deps` pulling the C chassis. The **root** env additionally carries
`-DHOST_UART_BAUD`.

There is **no `-DROOT_HUB`** — root-ness is generated. The chassis computes `IS_ROOT =
(MY_NODE == ROOT_NODE)`, where `ROOT_NODE` is generated from the DSL's `parent: :host`,
so the root env's `MY_NODE` must equal the declared root node. The root
env, distilled
([`firmware/platformio.ini`](../examples/segby_v1/firmware/platformio.ini)):

```ini
[env:blaster_root]
build_flags =
  ${env.build_flags}
  -Igen/segby_v1            ; the generated-headers include path
  -DMY_NODE=0x02            ; this hub's node id; == ROOT_NODE, so this is the root
  -DHOST_UART_BAUD=115200   ; ROOT ENV ONLY; must match the host transport baud
build_src_filter =
  +<mcu/blaster.cpp>        ; compile ONLY this hub's device file
```

A fully-commented, ready-to-adapt template lives at
[`docs/templates/platformio.ini.example`](templates/platformio.ini.example) — copy it
to `<your-app>/firmware/platformio.ini` and replace the marked placeholders
(`<slug>`, `<hub>`, `<path>`, `0xNN`, `<baud>`).

**Why sixth:** an env needs both the generated headers (Step 4) and the device file
(Step 5) to compile.

**Check it worked:** `pio run -e <hub>_root` (and `-e <hub>_leaf` for each leaf)
compiles clean.

<details>
<summary>The chassis dep, and how an external consumer points lib_deps</summary>

The `[env]` base carries `platform = <pioarduino fork>` and `lib_deps`. The
**pioarduino fork** of `platform-espressif32` is required for arduino-esp32 3.x /
ESP-IDF 5.x (where the TWAI/CAN driver the chassis uses lives); stock
`platform-espressif32` will not build the chassis.

`lib_deps` pulls the chassis (codec/scheduler/router/transport/floor/segment + the
esp32 link layer + the generic main loop) as a PlatformIO library via its
`library.json`, not by globbing the library's sources. In-tree the example uses a
relative symlink, `lib_deps = symlink://../../../firmware`. A real external consumer
swaps that one line for the published firmware-library package (a PlatformIO registry
entry or a git URL) — the build shape is identical. The `symlink://` form is the
in-repo convenience, not the only way.

</details>

---

## Step 7 — Host launcher (a thin wrapper over `BBMCUHub.Host`)

**You produce:** a thin launcher. The generic `BBMCUHub.Host` derives the command
slots from the robot's IR and wires the standard `BB.Supervisor` + **LinkOwner** (the
process that owns the host↔root-hub UART) tree. Your wrapper just forwards `robot:`
plus the operator's transport opts
([`lib/segby_v1/host.ex`](../examples/segby_v1/lib/segby_v1/host.ex)):

```elixir
defmodule SegbyV1.Host do
  alias BBMCUHub.Host

  @robot SegbyV1.Robot

  @spec start_link(keyword()) :: Supervisor.on_start()
  def start_link(opts \\ []) do
    Host.start_link([robot: @robot] ++ opts)
  end
end
```

`start_link/1` accepts `:transport` (a `BBMCUHub.Host.Transport` module, default
`BBMCUHub.Host.Transport.UART`), `:transport_opts` (e.g. `[port: "ttyAMA0", baud:
115_200]`), `:bb_opts`, and `:name`. The transport baud here must match the root
firmware's `-DHOST_UART_BAUD` (Step 6).

Each hub port now surfaces as an ordinary BeamBots component: a sensor port is a
`BB.Sensor` view that publishes a typed `BB.Message` on its PubSub topic once
born-stale freshness passes; an actuator port is a `BB.Actuator` view that is the sole
writer of the command slot and subscribes to the command struct its value-type names.
You publish/subscribe through normal BeamBots PubSub topics; the views translate
to/from the wire.

**Why last on the software side:** the launcher runs the robot's IR (Step 3) over the
transport that talks to the firmware you flashed (Steps 4-6).

**Check it worked — run the whole stack with no hardware.** Point the launcher at the
loopback transport and the entire host pipeline runs on your laptop (views →
command-slot writes → the LinkOwner drain → real COBS+CRC framing):

```elixir
# iex -S mix
{:ok, _sup} =
  SegbyV1.Host.start_link(transport: BBMCUHub.Host.Transport.Loopback)
# the BeamBots tree + LinkOwner are up; publish a command on the actuator's PubSub
# topic and watch it become a framed body, or read a sensor view's published message.
```

For safety/freshness behaviour against the **real firmware C floor** (the actual C
wire path on explicit simulated time), use the test-only `BBMCUHub.Test.VirtualHub` —
see [`test/host/soft_fault_e2e_test.exs`](../test/host/soft_fault_e2e_test.exs).
(It lives in the library's `test/support/` and is compiled only there — available
when working inside the `bb_mcuhub` repo, not from your own app.)

---

## What's next — hand off to BRINGUP.md

At this point your robot host-builds and your firmware envs compile. The full software
gate is: `mix test`, `cd firmware/test && make`, and `pio run -e <hub>_root` /
`pio run -e <hub>_leaf` per env (see the [README's Build & test](../README.md#build--test)).
Software assembly is done.

Time-to-first-motion, honestly: assembling these seven files and watching the loop run
hardware-free over the loopback transport is an afternoon for someone comfortable with
Elixir. First _real_ motion adds the on-hardware bring-up — flashing, wiring links,
motor-sign calibration, IMU/closed-loop — which is realistically a day or more of
bench work dominated by hardware calibration, not the library.

Take it onto hardware with the example's
[`BRINGUP.md`](https://github.com/lostbean/bb_mcuhub/blob/main/examples/segby_v1/BRINGUP.md), which owns the on-hardware stages
(flashing, wiring the links, confirming frames cross, motor-sign calibration, IMU +
closed-loop bring-up, and the dashboard) in independently verifiable Stages −1 → 5.
Two things only hardware/QEMU can prove are deliberately out of host-test scope: the
WDT boot-loop and motor-sign calibration.

### Cross-references

- [`README.md`](../README.md) — reference: the consumer-vs-library table, the seams,
  the port-flow diagram.
- [`docs/design/CONTEXT.md`](design/CONTEXT.md) — the domain glossary (Hub,
  Value-type, Link, Floor, Safe action, Firmware hook, …).
- [`docs/design/design.md`](design/design.md) — the full architecture, its
  rationale, and the `§` sections.
