flash
usual pile of shell/Make recipes with composable `dagger call`s. v1 wrapsprobe-rs: the physical write is driven from inside Dagger over USB/IP (a probe
exported from the host with usbipd) or probe-rs's remote websocket endpoint,
so flashing composes with the zig (#108) build/objcopy and qemu (#107)
off-device test stages.
The one seam that cannot be a hermetic function is presenting the physical USB
probe to the engine — that is a one-time host-side usbipd bridge (USB/IP),
runner setup rather than a per-project script. Everything downstream of the
bus is Dagger; BridgeCommand codeifies even that setup as emitted output.
File map (all `package main`, surfaced as one Dagger module):
- enums.go — Backend / ImageFormat enums plus the format token table.
- probers.go — Flash.ProbeRs factory: input validation, the probe-rs base
container, chip-registry validation, and the argv builder.
- flasher.go — *Flasher + its drive methods (Plan / Run / Verify / Reset /
GdbServer), FlashResult, and the run-to-completion exec
helper that captures output + exit code.
Installation
dagger install github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3Entrypoint
Return Type
Flash Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
func (m *MyModule) Example() *dagger.Flash {
return dag.
Flash()
}@function
def example() -> dagger.Flash:
return (
dag.flash()
)@func()
example(): Flash {
return dag
.flash()
}Types
Flash 🔗
Flash is the root namespace for every exported function in this module. The
ProbeRs factory hangs off *Flash so the generated SDK surfaces it under
dag.Flash().ProbeRs(...).
bridgeCommand() 🔗
BridgeCommand emits — it does NOT run — the host-side USB/IP command that
exports a probe to the engine, codeifying the one-time runner setup as output.
This runs on the machine physically holding the probe, OUTSIDE Dagger: it
binds busid to the USB/IP host driver and starts usbipd listening on
port, after which a Flasher built with usbip=<host>:<port> and the same
busid can attach the probe.
127.0.0.1 inside a container is the container’s own loopback, not the host —
point the Flasher’s usbip at the host’s engine-routable address, and run
this command on the host.
The rendered form is the Linux usbip/usbipd toolchain. On Windows the
equivalent is usbipd bind --busid <busid> followed by usbipd attach
from the engine side (usbipd-win); see the module README.
port and busid are shell-quoted in the output so a value carrying spaces
or shell metacharacters (;, $()) can’t inject into the emitted command
when it’s run via command substitution.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| busid | String ! | - | No description provided |
| port | String ! | "3240" | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
bridge-command --busid string --port stringfunc (m *MyModule) Example(ctx context.Context, busid string, port string) string {
return dag.
Flash().
Bridgecommand(ctx, busid, port)
}@function
async def example(busid: str, port: str) -> str:
return await (
dag.flash()
.bridgecommand(busid, port)
)@func()
async example(busid: string, port: string): Promise<string> {
return dag
.flash()
.bridgeCommand(busid, port)
}chipInfo() 🔗
ChipInfo resolves a chip against probe-rs’s registry and returns the human-readable info block, erroring on an unknown chip. It builds the module-pinned probe-rs container; no probe or hardware is involved.
Return Type
String !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| chip | String ! | - | No description provided |
| registry | String ! | "docker.io" | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
chip-info --chip string --registry stringfunc (m *MyModule) Example(ctx context.Context, chip string, registry string) string {
return dag.
Flash().
Chipinfo(ctx, chip, registry)
}@function
async def example(chip: str, registry: str) -> str:
return await (
dag.flash()
.chipinfo(chip, registry)
)@func()
async example(chip: string, registry: string): Promise<string> {
return dag
.flash()
.chipInfo(chip, registry)
}probeRs() 🔗
ProbeRs prepares a verified probe-rs flash, reaching the probe over exactly
one of two transports: USB/IP (usbip host:port + busid) or a remote
probe-rs server (remote host:port). It validates the inputs, builds the
probe-rs container, and confirms chip against probe-rs’s built-in target
registry, returning a *Flasher whose Run/Verify/Reset/Plan/GdbServer methods
drive the hardware.
There is intentionally NO *dagger.Socket transport. probe-rs speaks raw USB
(CMSIS-DAP HID, ST-Link, J-Link), not a byte stream, so a physical probe
cannot be carried over a unix socket — USB/IP is the only in-container path,
and the probe is presented to the engine by a host-side usbipd bridge (see
BridgeCommand). A socket transport will arrive with the serial backends
(esptool, dfu-util); if you came here hunting for a --socket flag, that is
why there isn’t one.
Note on addresses: 127.0.0.1 inside the container is the container’s own
loopback, NOT the host. To reach a host-side usbipd, point usbip at the
host’s engine-routable address, never 127.0.0.1.
Session-cached on name so parallel callers get independent Flashers; every
*Flasher method is never-cached so each Run / Verify / Reset re-executes.
Pass a unique name per parallel test for isolation.
Return Type
Flasher !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| firmware | File ! | - | No description provided |
| chip | String ! | - | No description provided |
| format | Enum ! | "ELF" | No description provided |
| baseAddress | Integer ! | 0 | No description provided |
| usbip | String ! | "" | No description provided |
| busid | String ! | "" | No description provided |
| remote | String ! | "" | No description provided |
| probeSelector | String ! | "" | No description provided |
| chipDescription | File | - | No description provided |
| registry | String ! | "docker.io" | No description provided |
| name | String ! | "" | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name stringfunc (m *MyModule) Example(firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string) *dagger.FlashFlasher {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
}@function
def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str) -> dagger.FlashFlasher:
return (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
)@func()
example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string): FlashFlasher {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
}Flasher 🔗
Flasher is a prepared-but-not-yet-run probe-rs flash. It carries the probe-rs container (with the firmware mounted) and the argv split into the pieces each subcommand needs, so Plan can render deterministically without hardware while Run / Verify / Reset / GdbServer swap in their verb and exec.
gdbServer() 🔗
GdbServer exposes probe-rs’s GDB stub as a Service so a debugger (or the gdb bridge, #106) can attach over the network. Requires a reachable probe to serve a live target; HIL-only at runtime. For the USB/IP transport the attach runs first inside the service command.
Return Type
Service !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| port | Integer ! | 1337 | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
gdb-server --port integerfunc (m *MyModule) Example(firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string, port int) *dagger.Service {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Gdbserver(port)
}@function
def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str, port: int) -> dagger.Service:
return (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.gdbserver(port)
)@func()
example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string, port: number): Service {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.gdbServer(port)
}plan() 🔗
Plan renders the exact probe-rs command the flash (download) path will run —
deterministic, hardware-free. For the USB/IP transport it includes the
usbip attach prefix; for BIN it includes --base-address; for the remote
transport it has no attach and carries --host.
Return Type
String ! Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
planfunc (m *MyModule) Example(ctx context.Context, firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string) string {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Plan(ctx)
}@function
async def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str) -> str:
return await (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.plan()
)@func()
async example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string): Promise<string> {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.plan()
}reset() 🔗
Reset resets the target (probe-rs reset). Requires a reachable probe; a
non-zero probe-rs exit becomes an error here (Reset has no FlashResult).
Reset takes no timeout argument (its v1 surface is Reset(ctx) error) and
always runs under defaultTimeoutSeconds.
Return Type
Void ! Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
resetfunc (m *MyModule) Example(ctx context.Context, firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string) {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Reset(ctx)
}@function
async def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str) -> None:
return await (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.reset()
)@func()
async example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string): Promise<void> {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.reset()
}run() 🔗
Run flashes the firmware (probe-rs download) and returns the combined
output and exit code. Requires a reachable probe; in CI (no hardware) it
returns a non-zero ExitCode with the probe-rs error in Output.
Return Type
Result !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| timeoutSeconds | Integer ! | 120 | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
run --timeout-seconds integerfunc (m *MyModule) Example(firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string, timeoutSeconds int) *dagger.FlashResult {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Run(timeoutSeconds)
}@function
def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str, timeoutseconds: int) -> dagger.FlashResult:
return (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.run(timeoutseconds)
)@func()
example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string, timeoutSeconds: number): FlashResult {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.run(timeoutSeconds)
}verify() 🔗
Verify checks that the on-target flash matches the firmware
(probe-rs verify) without rewriting it. Requires a reachable probe.
Return Type
Result !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| timeoutSeconds | Integer ! | 120 | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
verify --timeout-seconds integerfunc (m *MyModule) Example(firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string, timeoutSeconds int) *dagger.FlashResult {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Verify(timeoutSeconds)
}@function
def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str, timeoutseconds: int) -> dagger.FlashResult:
return (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.verify(timeoutseconds)
)@func()
example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string, timeoutSeconds: number): FlashResult {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.verify(timeoutSeconds)
}withServiceBinding() 🔗
WithServiceBinding binds a service into the Flasher’s container under
host, so a usbip or remote transport pointed at that hostname resolves to
it. This is the seam for reaching an in-Dagger probe relay (e.g. a usbip
service) — and how the test suite points a Flasher at a fake-usbipd service
without real hardware. Returns a new Flasher; the original is unchanged.
Return Type
Flasher !Arguments
| Name | Type | Default Value | Description |
|---|---|---|---|
| host | String ! | - | No description provided |
| svc | Service ! | - | No description provided |
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
with-service-binding --host string --svc PROTOCOL://HOST:PORTfunc (m *MyModule) Example(firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string, host string, svc *dagger.Service) *dagger.FlashFlasher {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Withservicebinding(host, svc)
}@function
def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str, host: str, svc: dagger.Service) -> dagger.FlashFlasher:
return (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.withservicebinding(host, svc)
)@func()
example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string, host: string, svc: Service): FlashFlasher {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.withServiceBinding(host, svc)
}Result 🔗
FlashResult pairs probe-rs’s combined output with its exit code. A clean probe-rs failure (e.g. no probe attached) is reported as a non-zero ExitCode with a nil Go error, so the captured Output is never lost to the error path.
output() 🔗
Output is probe-rs’s combined stdout
Return Type
String ! Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
verify --timeout-seconds integer \
outputfunc (m *MyModule) Example(ctx context.Context, firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string, timeoutSeconds int) string {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Verify(timeoutSeconds).
Output(ctx)
}@function
async def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str, timeoutseconds: int) -> str:
return await (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.verify(timeoutseconds)
.output()
)@func()
async example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string, timeoutSeconds: number): Promise<string> {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.verify(timeoutSeconds)
.output()
}exitCode() 🔗
ExitCode is probe-rs’s process exit code (0 = success). A run that exceeds the deadline is killed and yields the timeout code (124).
Return Type
Integer ! Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
probe-rs --firmware file:path --chip string --base-address integer --usbip string --busid string --remote string --probe-selector string --registry string --name string \
verify --timeout-seconds integer \
exit-codefunc (m *MyModule) Example(ctx context.Context, firmware *dagger.File, chip string, format , baseAddress int, usbip string, busid string, remote string, probeSelector string, registry string, name string, timeoutSeconds int) int {
return dag.
Flash().
Probers(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name).
Verify(timeoutSeconds).
Exitcode(ctx)
}@function
async def example(firmware: dagger.File, chip: str, format: , baseaddress: int, usbip: str, busid: str, remote: str, probeselector: str, registry: str, name: str, timeoutseconds: int) -> int:
return await (
dag.flash()
.probers(firmware, chip, format, baseaddress, usbip, busid, remote, probeselector, registry, name)
.verify(timeoutseconds)
.exitcode()
)@func()
async example(firmware: File, chip: string, format: , baseAddress: number, usbip: string, busid: string, remote: string, probeSelector: string, registry: string, name: string, timeoutSeconds: number): Promise<number> {
return dag
.flash()
.probeRs(firmware, chip, format, baseAddress, usbip, busid, remote, probeSelector, registry, name)
.verify(timeoutSeconds)
.exitCode()
}