Dagger
Search

flash

usual pile of shell/Make recipes with composable `dagger call`s. v1 wraps
probe-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@bc5cee36080549722c6d3bf02152aa7d46d2dcf3

Entrypoint

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
NameTypeDefault ValueDescription
busidString !-No description provided
portString !"3240"No description provided
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
 bridge-command --busid string --port string
func (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
NameTypeDefault ValueDescription
chipString !-No description provided
registryString !"docker.io"No description provided
Example
dagger -m github.com/z5labs/devex/daggerverse/flash@bc5cee36080549722c6d3bf02152aa7d46d2dcf3 call \
 chip-info --chip string --registry string
func (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
NameTypeDefault ValueDescription
firmwareFile !-No description provided
chipString !-No description provided
formatEnum !"ELF"No description provided
baseAddressInteger !0No description provided
usbipString !""No description provided
busidString !""No description provided
remoteString !""No description provided
probeSelectorString !""No description provided
chipDescriptionFile -No description provided
registryString !"docker.io"No description provided
nameString !""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
func (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
NameTypeDefault ValueDescription
portInteger !1337No 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 integer
func (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 \
 plan
func (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 \
 reset
func (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
NameTypeDefault ValueDescription
timeoutSecondsInteger !120No 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 integer
func (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
NameTypeDefault ValueDescription
timeoutSecondsInteger !120No 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 integer
func (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
NameTypeDefault ValueDescription
hostString !-No description provided
svcService !-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:PORT
func (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 \
 output
func (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-code
func (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()
}