Skip to content
Merged
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
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"crates/wp-link",
"crates/wp-drivers",
"crates/wp-client",
"crates/wp-fake",
"crates/wireless-programmer",
]

Expand Down
12 changes: 11 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,23 @@ CARGO ?= cargo
RUSTUP_TOOLCHAIN ?= stable
export RUSTUP_TOOLCHAIN

.PHONY: all build release release-musl check test test-release-assertions clean fmt clippy
# Optional wireless iface for `make dev` (e.g. INTERFACE=wlan0).
INTERFACE ?=

.PHONY: all build release release-musl check test test-release-assertions clean fmt clippy dev

all: build

build:
$(CARGO) build --workspace

# Build and run the daemon in the foreground (local development).
# Override iface: make dev INTERFACE=wlp2s0
# Override data root / socket: DATA_DIR=/tmp/wp-dev make dev
dev:
$(CARGO) run -p wireless-programmer -- daemon --verbose \
$(if $(INTERFACE),--interface $(INTERFACE),)

release:
$(CARGO) build --workspace --release

Expand Down
31 changes: 25 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,29 @@ crates/
wp-proto/ socket wire types + 4-byte-LE length+JSON framing
wp-core/ DeviceDriver trait, capabilities, typed errors
wp-link/ radio (nl80211/rtnetlink) + bounded HTTP client
wp-drivers/ wifred/ — NewHeiko WiFred driver
wp-drivers/ wifred/, longfred/ — Soft-AP programming drivers
wp-fake/ FakeRadio + Soft-AP HTTP mocks (dev / tests)
wp-client/ Rust client SDK (mirrors go/client)
wireless-programmer/ bin: socket server, job registry, dispatch + CLI
go/client/ Go client (vendored by bigfred)
docs/ api.md, cli.md, go-client.md, drivers/wifred.md
docs/ api.md, cli.md, go-client.md, drivers/
```

## Fake mode (no WiFi hardware)

```bash
# Full daemon with fake radio + Soft-AP HTTP mock (one candidate per driver)
wireless-programmer daemon --interface fake --verbose
# Optional: --fake-webserver-port 8070 (default) or 0 for ephemeral

# Standalone Soft-AP HTTP mock only (no IPC / radio)
wireless-programmer fake --driver wifred --bind 127.0.0.1:8070
wireless-programmer fake --driver longfred
```

With `--interface fake`, scan always returns one WiFred and one LongFred
candidate; programming talks to an in-process HTTP mock on `127.0.0.1`.

## Memory profile

Every crate is **allocation-conscious** (an administrative service, not a hot
Expand All @@ -46,10 +62,14 @@ max 64 scan results, max 8 socket connections, max 1 MiB socket frame, max

```bash
make build # debug
make dev # build + run daemon in foreground (`--verbose`)
make release # release (opt-level z, LTO, strip)
make release-musl TARGET_MUSL=aarch64-unknown-linux-musl # static arm64 → dist/
```

`make dev` accepts `INTERFACE=wlan0` and the usual env vars (`DATA_DIR`,
`WIRELESS_PROGRAMMER_ALLOW_USERS`, …).

Or the usual Cargo checks:

```bash
Expand All @@ -67,10 +87,9 @@ workflow (`dcc-bigfred/common` `rust-musl-ci`); tagged releases inject
## Socket API

Length-prefixed JSON on `$BIGFRED_DATA_DIR/run/wireless-programmer/wireless-programmer.sock`
(`DATA_DIR`, fallback `/data`), mode `0660`, peers verified with
`SO_PEERCRED`. The socket is chowned to the primary group of the first
allowlist entry, without which `0660` would refuse every non-root peer before
its credentials could be checked. See `docs/api.md`.
(`DATA_DIR`, fallback `/data`). Peer auth is **off by default** (socket
`0666`); enable with `--require-auth` / `WIRELESS_PROGRAMMER_REQUIRE_AUTH`
for `0660` + `SO_PEERCRED` allowlist. See `docs/api.md`.

## CLI

Expand Down
5 changes: 5 additions & 0 deletions crates/wireless-programmer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,17 @@ description = "Daemon that discovers and programs physical throttle hardware for
name = "wireless-programmer"
path = "src/main.rs"

[lib]
name = "wireless_programmer"
path = "src/lib.rs"

[dependencies]
wp-proto = { path = "../wp-proto" }
wp-core = { path = "../wp-core" }
wp-link = { path = "../wp-link" }
wp-drivers = { path = "../wp-drivers" }
wp-client = { path = "../wp-client" }
wp-fake = { path = "../wp-fake" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
clap = { version = "4", features = ["derive"] }
Expand Down
94 changes: 91 additions & 3 deletions crates/wireless-programmer/src/cli/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use wp_client::{CandidateRef, JobFrame, JobStateWire};

use super::{
build_client, resolve_socket, CliError, Command, CommonArgs, IdentifyArgs, JobAction, JobArgs,
ProbeArgs,
ProbeArgs, ScanArgs, UpdateFirmwareArgs,
};

type HandlerResult = Result<(), CliError>;
Expand All @@ -19,11 +19,13 @@ pub fn run(command: Command, socket_override: Option<PathBuf>) -> ExitCode {
Command::Scan(a) => scan(&socket, a),
Command::Probe(a) => probe(&socket, a),
Command::Program(a) => super::program::run(&socket, a),
Command::UpdateFirmware(a) => update_firmware(&socket, a),
Command::Identify(a) => identify(&socket, a),
Command::LinkStatus(a) => link_status(&socket, a),
Command::Hello(a) => hello(&socket, a),
Command::Job(a) => job(&socket, a),
Command::Daemon(_) => unreachable!("daemon is not a client command"),
Command::Fake(_) => unreachable!("fake is not a client command"),
};
match result {
Ok(()) => ExitCode::SUCCESS,
Expand Down Expand Up @@ -97,13 +99,91 @@ fn print_scan(candidates: &[wp_client::CandidateWire], json: bool) {
}
}

fn scan(socket: &Path, args: CommonArgs) -> HandlerResult {
fn scan(socket: &Path, args: ScanArgs) -> HandlerResult {
let c = build_client(socket, args.common.timeout);
let candidates = c.scan()?;
let mode = parse_reach_mode(&args.mode);
let candidates = c.scan_mode(mode)?;
print_scan(&candidates, args.common.json);
Ok(())
}

fn parse_reach_mode(mode: &str) -> wp_client::ReachMode {
match mode {
"lan" => wp_client::ReachMode::Lan,
"usb" => wp_client::ReachMode::Usb,
_ => wp_client::ReachMode::Ap,
}
}

/// `update-firmware` watch idle: USB matches `espflash` (180 s), HTTP matches
/// the LongFred POST (120 s). Explicit `--timeout` still wins. The daemon also
/// heartbeats every 3 s so a 10 s client (Go) still works.
fn firmware_watch_timeout(
explicit: Option<humantime::Duration>,
mode: wp_client::ReachMode,
) -> Option<humantime::Duration> {
if explicit.is_some() {
return explicit;
}
let d = match mode {
wp_client::ReachMode::Usb => wp_link::USB_FLASH_DEADLINE,
_ => crate::jobs::FIRMWARE_DEADLINE,
};
Some(humantime::Duration::from(d))
}

fn update_firmware(socket: &Path, args: UpdateFirmwareArgs) -> HandlerResult {
if !args.file.is_file() {
return Err(CliError::File {
path: args.file.display().to_string(),
message: "not a file".into(),
});
}
let mode = if args.port.is_some() || args.mode == "usb" {
wp_client::ReachMode::Usb
} else if args.mode == "lan" || args.host.is_some() {
wp_client::ReachMode::Lan
} else {
wp_client::ReachMode::Ap
};
let key = args
.key
.clone()
.or_else(|| args.port.clone())
.or_else(|| args.host.clone());
if key.is_none() && mode != wp_client::ReachMode::Usb {
return Err(CliError::Usage("provide --key and/or --host".into()));
}
let c = build_client(socket, firmware_watch_timeout(args.common.timeout, mode));
let candidate = key.map(|key| wp_client::CandidateRef {
driver: args.driver.clone(),
key,
});
let started = c.update_firmware(
mode,
candidate,
args.file.display().to_string(),
args.host.clone(),
args.port.clone(),
args.partition_table
.as_ref()
.map(|p| p.display().to_string()),
)?;
if args.no_watch {
if args.common.json {
print_json(&started);
} else {
println!("job {}", started.job_id);
}
return Ok(());
}
let json = args.common.json;
let last = c
.job_watch(&started.job_id)?
.drain_with(|frame| print_frame(frame, json))?;
outcome(last)
}

fn probe(socket: &Path, args: ProbeArgs) -> HandlerResult {
let c = build_client(socket, args.common.timeout);
let info = c.probe(candidate(&args.driver, &args.key))?;
Expand Down Expand Up @@ -166,6 +246,14 @@ fn hello(socket: &Path, args: CommonArgs) -> HandlerResult {
println!("drivers:");
for d in &h.drivers {
println!(" {} — {}", d.id, d.name);
println!(
" firmware update: {}",
if d.capabilities.supports_firmware_update {
"yes"
} else {
"no"
}
);
}
Ok(())
}
Expand Down
Loading
Loading