Skip to content
Open
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
20 changes: 18 additions & 2 deletions crates/rsk-rescue/src/phy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ const TAG_LED_DRIVER: u8 = 0xC;
// 0 = rgb (passthrough), 1 = grb (red/green swapped). PicoForge skips it as
// unknown and drops it on a read-modify-write; RS-Key's own tools preserve it.
const TAG_LED_ORDER: u8 = 0xD;
// RS-Key vendor tag: number of physically-connected addressable LEDs.
// 0 = unset (use the build's MAX_LEDS default).
const TAG_LED_NUM: u8 = 0xE;

/// `led_order` wire value: a standard WS2812B (GRB) part, red↔green swapped.
pub const LED_ORDER_GRB: u8 = 1;
Expand Down Expand Up @@ -62,7 +65,7 @@ pub fn effective_usb_itf(phy: &PhyData) -> u8 {
}

/// Largest serialized record (every TLV present, 32-byte product). The trailing
/// `(2 + 1)` is the RS-Key `led_order` tag.
/// `(2 + 1) × 2` covers the RS-Key `led_order` and `led_num` tags.
pub const PHY_MAX_SIZE: usize = (2 + 4)
+ (2 + 1)
+ (2 + 1)
Expand All @@ -72,7 +75,8 @@ pub const PHY_MAX_SIZE: usize = (2 + 4)
+ (2 + 4)
+ (2 + 1)
+ (2 + 1)
+ (2 + 1);
+ (2 + 1)
+ (2 + 1); // led_num

const PRODUCT_CAP: usize = 32;

Expand Down Expand Up @@ -120,6 +124,9 @@ pub struct PhyData {
pub led_driver: Option<u8>,
/// RS-Key WS2812 wire order (tag `0x0D`): `0` = rgb, `1` = grb.
pub led_order: Option<u8>,
/// Number of physically connected addressable LEDs (tag `0x0E`);
/// `None` / `0` = use the build's `MAX_LEDS` default.
pub led_num: Option<u8>,
}

impl PhyData {
Expand Down Expand Up @@ -159,6 +166,7 @@ impl PhyData {
(TAG_ENABLED_USB_ITF, 1) => phy.enabled_usb_itf = Some(v[0]),
(TAG_LED_DRIVER, 1) => phy.led_driver = Some(v[0]),
(TAG_LED_ORDER, 1) => phy.led_order = Some(v[0]),
(TAG_LED_NUM, 1) => phy.led_num = Some(v[0]),
_ => {}
}
p = &p[tlen..];
Expand Down Expand Up @@ -207,6 +215,9 @@ impl PhyData {
if let Some(o) = self.led_order {
w.tlv(TAG_LED_ORDER, &[o])?;
}
if let Some(n) = self.led_num {
w.tlv(TAG_LED_NUM, &[n])?;
}
Some(w.len)
}
}
Expand Down Expand Up @@ -326,6 +337,9 @@ mod proofs {
if kani::any() {
phy.led_order = Some(kani::any());
}
if kani::any() {
phy.led_num = Some(kani::any());
}

let mut buf = [0u8; PHY_MAX_SIZE];
let n = phy.serialize(&mut buf).unwrap();
Expand All @@ -348,6 +362,7 @@ mod proofs {
);
assert_eq!(got.led_driver, phy.led_driver);
assert_eq!(got.led_order, phy.led_order);
assert_eq!(got.led_num, phy.led_num);
}
}

Expand All @@ -368,6 +383,7 @@ mod tests {
enabled_usb_itf: Some(USB_ITF_CCID | USB_ITF_HID),
led_driver: Some(3),
led_order: Some(LED_ORDER_GRB),
led_num: Some(4),
};
let mut buf = [0u8; PHY_MAX_SIZE];
let n = phy.serialize(&mut buf).unwrap();
Expand Down
4 changes: 3 additions & 1 deletion docs/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,11 @@ flowchart TD
| `LED_PIN` | `16` | `0..=29` | The status-LED GPIO for the `ws2812` and `gpio` backends (RP2350A). Default GPIO16 is the Waveshare RP2350-One. Point it at a free GPIO on boards that use 16 for something else; the indicator simply drives whatever pin you pick. (Unused by `pimoroni`, which has fixed PWM pins, and by `none`.) |
| `LED_KIND` | `ws2812` | `ws2812` / `gpio` / `pimoroni` / `none` | The LED driver backend, and the **boot default** — a non-`none` build compiles all three so the driver/pin/order are runtime-switchable via `rsk hw` / PicoForge (see below). `ws2812` = a single addressable RGB on `LED_PIN` (the Waveshare default). `gpio` = a plain on/off LED on `LED_PIN` — hue/brightness collapse to lit/unlit, but the blink *pattern* still distinguishes statuses. `pimoroni` = a 3-pin PWM RGB (Pimoroni Tiny 2350: R=GPIO18, G=GPIO19, B=GPIO20, common-anode). `none` = no indicator (the status engine still runs; nothing renders it, and the phy LED fields are ignored). |
| `LED_ORDER` | `rgb` | `rgb` / `grb` | WS2812 wire byte order (`ws2812` backend only). The Waveshare RP2350-One is unusually `rgb` (the default); standard WS2812B parts (e.g. the TenStar RP2350-USB) are `grb`. If a `ws2812` board comes up with red and green swapped (blue fine), flip this to `grb`. |
| `MAX_LEDS` | `8` | `1`–`64` | PIO state-machine and frame-buffer ceiling for addressable LEDs. The **actual** connected count is set at runtime via `rsk hw --led-num` and must be ≤ `MAX_LEDS`. Default 8 covers the common 1–8 range; boards with more (up to 64) override this.|
| `FAKE_MKEK` / `FAKE_DEVK` | unset | 64 hex chars | **Test builds only.** Bakes a fake OTP master key / device key into the image instead of reading the OTP fuses, so the whole OTP migration path can be exercised with zero fuse writes. The build prints a loud warning and the key is greppable in the binary. Flashing a FAKE build onto a provisioned device migrates its data under the fake key — going back orphans that data (recovery = per-applet resets). Never flash one on a device you care about. |

The `LED_PIN` / `LED_KIND` / `LED_ORDER` values are **boot defaults** only. A
The `LED_PIN` / `LED_KIND` / `LED_ORDER` / `MAX_LEDS` values are **boot defaults**
only. A
non-`none` build compiles all three backends, so the LED pin, driver, and wire
order are runtime-configurable — no reflash — with `rsk hw` (or PicoForge),
which write them to the device's `phy` record. The build knobs decide the
Expand Down
216 changes: 113 additions & 103 deletions docs/guides/led.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,66 @@
The status LED is the device's only display. On the reference board — the
**Waveshare RP2350-One** — it's a WS2812 addressable RGB on GPIO16.

Three properties of the indicator are **runtime-configurable**, like the USB
identity: they live in the device's `phy` record and change with `rsk hw` (or
PicoForge) **without reflashing**, because a non-`none` build compiles all
three backends. The `LED_KIND` / `LED_PIN` / `LED_ORDER` build knobs set the
*boot defaults* used when the phy record doesn't override them
([build.md](../build.md), [hardware.md](../hardware.md)):

- **backend** — `LED_KIND` / `rsk hw --led-driver`: `ws2812` (addressable RGB,
the default), `gpio` (a plain on/off LED — can't show the colours below, but
the blink *pattern* still tells the four states apart), `pimoroni` (a 3-pin
PWM RGB, Pimoroni Tiny 2350), or the build-only `none`.
- **pin** — `LED_PIN` / `rsk hw --led-pin`: the `ws2812`/`gpio` data GPIO
(`0..=29`), for a board that wires its LED off GPIO16.
- **wire order** — `LED_ORDER` / `rsk hw --led-order`: a `ws2812` board whose
**red and green come out swapped** (blue unaffected) has the other byte
order — the Waveshare RP2350-One is `rgb` (the default), most other WS2812B
parts are `grb`.

A `none` build is the exception: it renders nothing and ignores the phy LED
fields (there is no backend compiled to switch to).

The LED runs on its own high-priority task, so it keeps animating even while
the firmware blocks waiting for a touch or grinds through a long RSA keygen —
a frozen LED means the firmware itself is wedged, not just busy.

## What the states mean

There are four states. Each has a **fixed blink timing** baked into the
firmware (`firmware/src/led.rs`); only the **color and brightness** are
configurable.

| State | Default color | Blink (on/off) | Means |
## Build-time knobs

Three hardware properties of the indicator are **compile-time** knobs set by build
flags; a fourth, `MAX_LEDS`, sets the upper bound for the PIO buffer. The actual
number of connected LEDs is configured at **runtime** via `rsk hw --led-num` (or
PicoForge) and must be ≤ `MAX_LEDS`.

| Knob | Default | When to change it |
|---|---|---|
| `LED_KIND` | `ws2812` | `ws2812` (addressable RGB, default), `gpio` (plain on/off), `pimoroni` (3-pin PWM RGB), or `none` (no indicator). See [build.md](build.md). |
| `LED_PIN` | `16` | A board whose addressable LED is on a different GPIO (`0..=29`). |
| `LED_ORDER` | `rgb` | A WS2812 board with swapped red/green — set `grb` (the WS2812B standard). The Waveshare RP2350-One is `rgb`; most other parts are `grb`. |
| `MAX_LEDS` | `8` | A board with **more than 8** daisy-chained addressable LEDs (max `64`). The actual connected count is set at runtime with `rsk hw --led-num`. |

```sh
# example: build for a 4-LED board with standard GRB order
env MAX_LEDS=4 LED_ORDER=grb cargo build --release -p firmware
# then set the runtime count (persists across reboots):
rsk hw --led-num 4
```

Once built, a non-`none` build compiles all backends, so the pin, driver, wire
order, and LED count are **runtime-changeable** — no reflash — with `rsk hw`
([build.md](../build.md)). The build knobs set the boot defaults.

What the LED shows — colour, brightness, and the **visual effect** — is
runtime-configurable separately, covered next.

## Effects

Each of the four states can run one of several animated effects. The effect
determines *how* the LED(s) display the state's colour and brightness. All
effects work with any number of LEDs — `vapor` and `sparkle` shine on a single
LED too; `bounce` and `flow` naturally reduce to a static colour or a single
pixel when there is only one LED.

Effects only render on the **`ws2812`** backend (addressable RGB). The `gpio` and
`pimoroni` backends always use the classic on/off blink, regardless of the
effect setting — they lack per-LED control and pixel-level colour.

| Effect | ID | What you see | Suits |
|---|---|---|---|
| idle | green | 500 / 500 ms — slow, even | ready, nothing in flight |
| processing | green | 50 / 50 ms — fast flicker | handling an APDU / crypto op |
| **waiting for touch** | yellow | 1000 / 100 ms — long on, brief blink | press the button to confirm |
| boot | red | 500 / 500 ms | the brief power-up state |
| `legacy` | 0 | Classic on/off blink (TIMING table) | Original blink behaviour |
| `vapor` | 1 | All LEDs breathe together — smooth triangle-wave brightness | Idle (default) |
| `bounce` | 2 | A wide hump of light glides back and forth with half-step interpolation | Touch (default) |
| `flow` | 3 | Yellow→red gradient flowing left to right with a trailing wake | Processing (default) |
| `sparkle` | 4 | Each LED flashes an independent random colour | Boot (default) |

### Default mapping (multiple LEDs)

The **touch** state is the one to learn. WebAuthn dialogs, `ssh`, and `gpg`
look hung at exactly the moment the device is waiting for your press — a
near-solid yellow that ticks off once a second is the cue to tap the button.
| State | Default effect | Default colour | Means |
|---|---|---|---|
| idle | `vapor` — gentle breathing | green | ready, nothing in flight |
| processing | `flow` — warm-colour flow | yellow→red gradient | handling an APDU / crypto op |
| **waiting for touch** | `bounce` — smooth bounce | yellow | press the button to confirm |
| boot | `sparkle` — random sparkle | red | the brief power-up state |

A few honest details:

- **No dedicated error color.** The firmware does not light a distinct "error"
- **No dedicated error colour.** The firmware does not light a distinct "error"
state; a failed operation just drops back to idle. Read the host tool's exit
code, not the LED, for success or failure.
- **The touch state needs the touch build.** It is only ever shown on the
Expand All @@ -67,9 +82,10 @@ whatever the ROM does. That mode is for flashing firmware and OTP, covered in

## Customize

Color and per-channel brightness are configurable **per state**; the values
persist in flash (`EF_LED_CONF`) and apply live — no reboot. The host command
is `rsk led`:
### Colour & brightness

Per-state colour and per-channel brightness are configurable; the values
persist in flash (`EF_LED_CONF`) and apply live — no reboot:

```sh
rsk led --get # print the current config
Expand All @@ -78,97 +94,86 @@ rsk led --status idle --brightness 64 # 0–255; 0 = that state goes da
rsk led --status idle --color blue --brightness 64
```

Selectors and values:
### Effect & speed

| Flag | Values |
|---|---|
| `--status` | `idle`, `processing`, `touch`, `boot` (default `idle`) |
| `--color` | `off`, `red`, `green`, `blue`, `yellow`, `magenta`, `cyan`, `white` |
| `--brightness` | `0`–`255` per channel (`0` = off) |
| `--steady` | solid color, no blinking — **global**, affects every state |
| `--blink` | the opposite: restore blinking |
Each state's effect and animation speed are configurable the same way:

```sh
rsk led --status idle --effect vapor # change the effect
rsk led --status touch --effect bounce --speed 15 # custom speed (ticks per step)
rsk led --status processing --effect legacy # revert to classic on/off blink
```

`--speed 0` (or omitting `--speed`) uses the effect's built-in default.

### Steady / blink

`--steady` and `--blink` are global, not per-state: the firmware keeps each
state's timing internally, but a single flag decides whether *any* of them
blink. So `--steady` makes the whole indicator a solid lamp whose color tracks
blink. So `--steady` makes the whole indicator a solid lamp whose colour tracks
the current state, and `--blink` brings the blink patterns back.

```sh
rsk led --status idle --color cyan --steady # solid cyan at idle, no pulse
rsk led --blink # back to the blink patterns
```

`rsk-tui` has a "Cycle idle color" action that steps the idle state through
the palette, plus "Read LED state" — for per-state color, brightness, or the
`rsk-tui` has a "cycle idle color" action that steps the idle state through
the palette, plus "Read LED state" — for per-state colour, brightness, or the
steady toggle, use `rsk led`.

### Selectors and values

| Flag | Values |
|---|---|
| `--status` | `idle`, `processing`, `touch`, `boot` (default `idle`) |
| `--color` | `off`, `red`, `green`, `blue`, `yellow`, `magenta`, `cyan`, `white` |
| `--brightness` | `0`–`255` per channel (`0` = off) |
| `--effect` | `legacy`, `vapor`, `bounce`, `flow`, `sparkle` |
| `--speed` | `0`–`255` (`0` = effect's built-in default) |
| `--steady` | solid colour, no blinking — **global**, affects every state |
| `--blink` | the opposite: restore blinking |

## Hardware wiring (`rsk hw`)

The **look** (`rsk led`, above) is one layer; the **wiring** — which pin, which
driver, which wire order — is the other. The wiring lives in the `phy` record
(the same device-config blob PicoForge writes) and is applied at **boot**, so a
change needs a reboot — `rsk hw` issues a warm one for you unless `--no-reboot`:
See [hw.md](hw.md) for the full reference. The LED wiring — pin, driver, wire
order — lives in the `phy` record, shared with PicoForge:

```sh
rsk hw # show the current phy LED config
rsk hw --led-pin 22 # move the WS2812/gpio data pin to GPIO22
rsk hw --led-driver gpio # switch to a plain on/off LED
rsk hw --led-order grb # fix a red/green swap on a GRB part
rsk hw --led-pin 22 --led-order grb # e.g. the TenStar RP2350-USB
rsk hw --led-driver ws2812 --no-reboot # stage a change; reboot later
```

| Flag | Values |
|---|---|
| `--led-pin` | `0`–`29` (RP2350A GPIOs) — the `ws2812`/`gpio` data pin |
| `--led-driver` | `gpio`, `pimoroni`, `ws2812` |
| `--led-order` | `rgb`, `grb` (the `ws2812` backend only) |
| `--get` | print the current phy LED config and exit |
| `--no-reboot` | write but don't reboot (the change applies on the next boot) |

A field you never set stays at the firmware build default (`rsk hw` with no
setters, or `--get`, prints which are overridden). `rsk hw` does a
read-modify-write of *only* the LED fields, so a USB identity or other phy
option set elsewhere (PicoForge) is preserved. A `none` build ignores these —
there is no backend compiled to render the LED.

### Reset to defaults

There's no single "reset LED" verb; set the values back yourself. The factory
defaults are the table above at brightness 16, blinking:

```sh
rsk led --status idle --color green --brightness 16
rsk led --status processing --color green --brightness 16
rsk led --status touch --color yellow --brightness 16
rsk led --status boot --color red --brightness 16
rsk led --status idle --color green --brightness 16 --effect vapor
rsk led --status processing --color green --brightness 16 --effect flow
rsk led --status touch --color yellow --brightness 16 --effect bounce
rsk led --status boot --color red --brightness 16 --effect sparkle
rsk led --blink
```

## Under the hood

`rsk led` talks to the firmware's vendor applet over CCID
(`tools/rsk/led.py`, `firmware/src/vendor.rs`): **SET LED** (`INS 0x10`) packs
brightness into `P1` and color + the steady bit + the target state into `P2`;
**GET LED** (`INS 0x11`) returns the whole `[steady, (color, brightness) × 4]`
block that `--get` prints. The firmware writes it to `EF_LED_CONF` and reloads
it on every boot, so your colors survive a power cycle but not an OpenPGP/FIDO
factory reset of other applets (those don't touch this file).

`rsk hw` instead writes the **wiring** to the `phy` record (`EF_PHY`) via the
rescue applet (`tools/rsk/hw.py`, `crates/rsk-rescue/src/phy.rs`): **READ**
(`INS 0x1E`, P1=01) and **WRITE** (`INS 0x1C`, P1=01) the same TLV blob
PicoForge uses — `led_gpio` (tag `0x04`), `led_driver` (`0x0C`), plus the
RS-Key vendor `led_order` tag (`0x0D`, which PicoForge skips as unknown). At
boot `firmware/src/main.rs` reads those fields and selects the pin (a `match`
over GPIO `0..=29`) and the driver; the wire order is a runtime red/green swap
in the render task, so one binary serves both RGB- and GRB-wired parts.
`EF_PHY` survives every applet factory reset.

One board quirk worth knowing: the Waveshare RP2350-One's WS2812 takes bytes in
**RGB** wire order, not the WS2812B-standard GRB — the `rgb` default matches it,
and a `grb` board just flips `LED_ORDER` (build) or `rsk hw --led-order grb`
(runtime); the swap touches red/green only (blue is unaffected).
(`tools/rsk/led.py`, `firmware/src/vendor.rs`):

- **SET LED** (`INS 0x10`) packs brightness into `P1` and colour + the steady
bit + the target state into `P2`. When the caller sends 1–2 data bytes, they
set the effect and speed for that state.
- **GET LED** (`INS 0x11`) returns the whole config block:
`[steady:1, (effect:1, color:1, brightness:1, speed:1) × 4]` (17 bytes).

The firmware writes the block to `EF_LED_CONF` and reloads it on every boot,
so your settings survive a power cycle but not an OpenPGP/FIDO factory reset
(those don't touch this file). The `led.rs` module keeps per-status atomics
that the render task reads live — SET LED updates them immediately, then
persists the full block to flash.

For the wiring half (`rsk hw`), see [hw.md](hw.md); it writes to `EF_PHY` via
the rescue applet and applies at next boot.

## Troubleshooting

Expand All @@ -180,6 +185,11 @@ and a `grb` board just flips `LED_ORDER` (build) or `rsk hw --led-order grb`
- **Red and green look swapped.** Wrong wire order for your LED part — flip it
with `rsk hw --led-order grb` (or build with `LED_ORDER=grb`); see the
RGB-vs-GRB note above.
- **Only the first LED lights up; the rest stay dark.** The board has multiple
daisy-chained addressable LEDs, but the runtime LED count was never set.
Run `rsk hw --led-num <your count>` to configure it (persists across reboots;
the change applies after a warm reboot). If you need a higher buffer ceiling,
rebuild with `MAX_LEDS=<n>`.
- **`rsk led` can't reach the device.** It needs the CCID interface up
(`pcscd` on Linux); if `gpg --card-status` / `rsk status` also fail, fix that
first ([linux.md](../linux.md)).
Expand Down
Loading