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
48 changes: 48 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# wireless-programmer — build / release helpers

TARGET_MUSL ?= aarch64-unknown-linux-musl
CARGO ?= cargo
RUSTUP_TOOLCHAIN ?= stable
export RUSTUP_TOOLCHAIN

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

all: build

build:
$(CARGO) build --workspace

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

# Static musl binary (default: aarch64). Override: make release-musl TARGET_MUSL=x86_64-unknown-linux-musl
release-musl:
RUSTFLAGS='-C target-feature=+crt-static' \
$(CARGO) build --workspace --release --target $(TARGET_MUSL)
@mkdir -p dist
@case "$(TARGET_MUSL)" in \
aarch64-*) dist_name=wireless-programmer-linux-arm64 ;; \
x86_64-*) dist_name=wireless-programmer-linux-amd64 ;; \
*) dist_name=wireless-programmer-$(TARGET_MUSL) ;; \
esac; \
cp -f target/$(TARGET_MUSL)/release/wireless-programmer "dist/$${dist_name}"; \
echo "wrote dist/$${dist_name}"

check:
$(CARGO) check --workspace

test:
$(CARGO) test --workspace --locked

test-release-assertions:
$(CARGO) test --workspace --locked --profile release-assertions

fmt:
$(CARGO) fmt --all

clippy:
$(CARGO) clippy --workspace --all-targets --locked -- -D warnings

clean:
$(CARGO) clean
rm -rf dist
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,22 @@ max 64 scan results, max 8 socket connections, max 1 MiB socket frame, max

## Building

```bash
make build # debug
make release # release (opt-level z, LTO, strip)
make release-musl TARGET_MUSL=aarch64-unknown-linux-musl # static arm64 → dist/
```

Or the usual Cargo checks:

```bash
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
cargo test --workspace --locked --profile release-assertions
```

Static musl builds (arm64 / amd64) are produced by CI; see
Static musl builds (arm64 / amd64) are also produced by CI; see
`.github/workflows/ci.yml`.

## Socket API
Expand All @@ -71,6 +79,7 @@ subcommand it runs the daemon; the subcommands below are clients.
# daemon (default)
wireless-programmer --socket /data/run/wireless-programmer/wireless-programmer.sock
wireless-programmer daemon --verbose
wireless-programmer daemon --interface wlan0

# discovery + programming
wireless-programmer scan # list candidates on the radio
Expand Down
50 changes: 42 additions & 8 deletions crates/wireless-programmer/src/cli/daemon.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Daemon subcommand runner (the previous `main` behaviour).

use std::path::PathBuf;
use std::process::ExitCode;

use clap::Args;
use tracing_subscriber::EnvFilter;
Expand All @@ -15,30 +16,63 @@ pub struct DaemonArgs {
/// Verbose logging.
#[arg(short, long)]
pub verbose: bool,

/// Wireless interface to use (e.g. `wlan0`, `wlp2s0`).
///
/// When omitted, the first wireless interface under `/sys/class/net` is
/// selected. Overrides `WIRELESS_PROGRAMMER_INTERFACE` when set.
#[arg(short = 'i', long = "interface", value_name = "IFACE")]
pub interface: Option<String>,
}

/// Run the IPC daemon until shutdown.
pub fn run_daemon(args: DaemonArgs, socket_override: Option<PathBuf>) -> std::process::ExitCode {
let mut cfg = Config::default();
if let Some(s) = socket_override {
cfg.socket = s;
}

pub fn run_daemon(args: DaemonArgs, socket_override: Option<PathBuf>) -> ExitCode {
let filter = if args.verbose {
EnvFilter::new("debug")
} else {
EnvFilter::new("info")
};
tracing_subscriber::fmt().with_env_filter(filter).init();

let mut cfg = Config::default();
if let Some(s) = socket_override {
cfg.socket = s;
}
// CLI wins over the environment default baked into Config::default.
if let Some(iface) = args.interface {
let iface = iface.trim().to_string();
if iface.is_empty() {
tracing::error!("--interface must not be empty");
return ExitCode::FAILURE;
}
cfg.interface = Some(iface);
}

// Validate the preferred interface early so a typo fails at start-up
// rather than on the first scan/program request.
if let Some(ref name) = cfg.interface {
match wp_link::resolve_wireless_interface(Some(name)) {
Ok(resolved) => cfg.interface = Some(resolved),
Err(e) => {
tracing::error!("wireless interface: {e}");
return ExitCode::FAILURE;
}
}
}

match &cfg.interface {
Some(name) => tracing::info!("wireless interface: {name}"),
None => tracing::info!("wireless interface: auto (first wireless)"),
}

let registry = DriverRegistry::new();
let runtime = Server::new(cfg, registry);

match runtime.run() {
Ok(()) => std::process::ExitCode::SUCCESS,
Ok(()) => ExitCode::SUCCESS,
Err(e) => {
tracing::error!("fatal: {e}");
std::process::ExitCode::FAILURE
ExitCode::FAILURE
}
}
}
5 changes: 5 additions & 0 deletions crates/wireless-programmer/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ pub struct Cli {
/// Verbose logging (daemon only).
#[arg(short, long)]
pub verbose: bool,

/// Wireless interface for the daemon (e.g. `wlan0`). Also accepted on
/// `daemon --interface`. Overrides `WIRELESS_PROGRAMMER_INTERFACE`.
#[arg(short = 'i', long = "interface", value_name = "IFACE")]
pub interface: Option<String>,
}

/// Top-level subcommands.
Expand Down
2 changes: 2 additions & 0 deletions crates/wireless-programmer/src/cli/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ fn build_request(args: &ProgramArgs) -> Result<ProgramRequestWire, CliError> {
wifi,
server,
roster,
bigfred: None,
roster_mode: None,
})
}

Expand Down
12 changes: 12 additions & 0 deletions crates/wireless-programmer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub struct Config {
pub commit: Option<String>,
/// Source address bound on the wireless interface during programming.
pub source_addr: SocketAddr,
/// Wireless interface to use (`wlan0`, `wlp2s0`, …). `None` means auto-
/// select the first wireless interface at radio open time.
pub interface: Option<String>,
}

impl Default for Config {
Expand All @@ -45,6 +48,7 @@ impl Default for Config {
version: env!("CARGO_PKG_VERSION").into(),
commit: option_env!("WIRELESS_PROGRAMMER_GIT_COMMIT").map(Into::into),
source_addr: "192.168.4.2:0".parse().expect("valid default source addr"),
interface: resolve_interface_env(),
}
}
}
Expand Down Expand Up @@ -75,6 +79,14 @@ fn resolve_allow_users() -> Vec<String> {
}
}

/// Optional wireless interface from `WIRELESS_PROGRAMMER_INTERFACE`.
fn resolve_interface_env() -> Option<String> {
std::env::var("WIRELESS_PROGRAMMER_INTERFACE")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}

/// Resolve the BigFred data directory.
pub fn resolve_data_dir() -> PathBuf {
if let Ok(d) = std::env::var("BIGFRED_DATA_DIR") {
Expand Down
32 changes: 21 additions & 11 deletions crates/wireless-programmer/src/drivers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,31 @@
//! (guidelines §8.2) rather than `Box<dyn DeviceDriver>`.

use wp_core::{DeviceCandidate, DeviceDriver, DriverCapabilities, Observation};
use wp_drivers::WiFredDriver;
use wp_drivers::{LongFredDriver, WiFredDriver};

/// All registered drivers.
#[derive(Debug, Clone, Copy)]
pub enum Driver {
/// NewHeiko WiFred.
WiFred,
/// LongFred Soft-AP programming.
LongFred,
}

impl Driver {
/// The driver's stable id string.
pub fn id_str(self) -> &'static str {
match self {
Driver::WiFred => "wifred",
Driver::LongFred => "longfred",
}
}

/// Human-readable name.
pub fn name(self) -> &'static str {
match self {
Driver::WiFred => "NewHeiko WiFred",
Driver::LongFred => "LongFred",
}
}
}
Expand All @@ -33,19 +37,24 @@ impl Driver {
#[derive(Debug)]
pub struct DriverRegistry {
wifred: WiFredDriver,
longfred: LongFredDriver,
}

impl DriverRegistry {
/// Construct a registry with all built-in drivers.
pub fn new() -> Self {
Self {
wifred: WiFredDriver::new(),
longfred: LongFredDriver::new(),
}
}

/// Iterate over (driver tag, capabilities) for `hello`.
pub fn drivers(&self) -> Vec<(Driver, DriverCapabilities)> {
vec![(Driver::WiFred, self.wifred.capabilities())]
vec![
(Driver::WiFred, self.wifred.capabilities()),
(Driver::LongFred, self.longfred.capabilities()),
]
}

/// Build the `hello` result's driver list.
Expand All @@ -55,13 +64,7 @@ impl DriverRegistry {
.map(|(d, caps)| wp_proto::DriverInfoWire {
id: d.id_str().into(),
name: d.name().into(),
capabilities: wp_proto::CapabilitiesWire {
max_roster_slots: caps.max_roster_slots,
max_function_index: caps.max_function_index,
identity_format: caps.identity_format.into(),
supports_throttle_server: caps.supports_throttle_server,
commissioning: caps.commissioning.into(),
},
capabilities: caps.into(),
})
.collect()
}
Expand All @@ -70,20 +73,27 @@ impl DriverRegistry {
pub fn driver_for(&self, candidate: &wp_proto::CandidateRef) -> Option<Driver> {
match candidate.driver.as_str() {
"wifred" => Some(Driver::WiFred),
"longfred" => Some(Driver::LongFred),
_ => None,
}
}

/// Claim a raw observation against every driver.
pub fn identify(&self, obs: &Observation) -> Option<DeviceCandidate> {
// WiFred is the only driver today; its filter is the SSID prefix.
self.wifred.identify(obs)
self.longfred
.identify(obs)
.or_else(|| self.wifred.identify(obs))
}

/// Borrow the WiFred driver.
pub fn wifred(&self) -> &WiFredDriver {
&self.wifred
}

/// Borrow the LongFred driver.
pub fn longfred(&self) -> &LongFredDriver {
&self.longfred
}
}

impl Default for DriverRegistry {
Expand Down
6 changes: 5 additions & 1 deletion crates/wireless-programmer/src/ipc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ impl ServerInner {
kind: RequestKind::LinkStatus,
result: Some(ResultBody::LinkStatus(wp_proto::LinkStatusWire {
busy: self.jobs_is_busy(),
interface: None,
interface: self
.cfg
.interface
.clone()
.or_else(|| wp_link::first_wireless_interface().ok()),
rfkill_blocked: false,
})),
error: None,
Expand Down
13 changes: 12 additions & 1 deletion crates/wireless-programmer/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ use cli::{Cli, Command};
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Some(Command::Daemon(args)) => cli::run_daemon(args, cli.socket),
Some(Command::Daemon(mut args)) => {
// Top-level `--interface` / `--verbose` apply when the
// subcommand did not set them itself.
if args.interface.is_none() {
args.interface = cli.interface;
}
if !args.verbose {
args.verbose = cli.verbose;
}
cli::run_daemon(args, cli.socket)
}
Some(command) => cli::run_client(command, cli.socket),
None => cli::run_daemon(
cli::DaemonArgs {
verbose: cli.verbose,
interface: cli.interface,
},
cli.socket,
),
Expand Down
2 changes: 2 additions & 0 deletions crates/wp-client/tests/client_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ fn busy_maps_to_a_typed_error() {
automatic: None,
},
roster: Vec::new(),
bigfred: None,
roster_mode: None,
},
)
.expect_err("expected busy");
Expand Down
Loading
Loading