Each crate has a single, clear focus. Cross-crate concerns (e.g. networking
- firewall) live in the consuming application, not here.
Primary target hardware: Raspberry Pi CM4 on Ubuntu / Raspberry Pi OS. APIs aim to work on any Linux but are tuned for that deployment shape.
Generic filesystem toolkit: tar+zstd pack/unpack, file/dir CRUD, declarative directory scaffolding, POSIX permission helpers. No domain knowledge.
Linux host network introspection (interfaces, IPs, MAC, MTU, IO counters,
gateway, DNS, hostname, public IP) plus a Configurator trait with three
backends: NetworkManager (nmcli), netplan, ifupdown.
Async wrapper around zbus_systemd for managing systemd units —
start/stop/restart/reload/enable/disable/list/status. Pure-Rust D-Bus
client (no libsystemd-dev). System bus and user session bus.
Manage Ubuntu's UFW firewall via the ufw CLI. Typed Rule builder,
parsed Status snapshot, lifecycle (enable/disable/reload/reset),
defaults, logging. UFW has no library API — shell-out is the only path.
Purpose. Read-only Raspberry Pi hardware introspection — model, revision, serial, firmware, thermal/throttling state, SoC clocks and voltages.
API surface.
model(),revision(),serial(),cpuinfo_summary()soc_temp_c() -> f32throttling() -> Throttling { under_voltage_now, freq_capped_now, throttled_now, soft_temp_limit_now, *_since_boot }clocks() -> Clocks { arm_hz, core_hz, h264_hz, ... }voltages() -> Voltages { core_v, sdram_c_v, sdram_i_v, sdram_p_v }firmware_version() -> String
Underlying sources.
/proc/cpuinfo,/proc/device-tree/model,/proc/device-tree/serial-number/sys/class/thermal/thermal_zone0/tempvcgencmdshell-out for throttling, clocks, voltages, firmware
External crates. None — all data is in /proc, /sys, or vcgencmd
output. Wrapping them adds dependency weight without payoff.
Out of scope. Hardware control (GPIO → gpio crate), generic CPU/mem
stats (sysinfo), kernel logs (journald).
Purpose. Talk to the Linux watchdog driver (/dev/watchdog,
/dev/watchdog0, ...) — set timeout, kick (keepalive), graceful close.
On Pi this drives the BCM2835 hardware watchdog.
API surface.
Watchdog::open(path)/Watchdog::default()(opens/dev/watchdog)set_timeout(secs: u32) -> Result<u32>— returns the value the kernel actually accepted (drivers clamp)get_timeout() -> Result<u32>keepalive() -> Result<()>— kick the dogtime_left() -> Result<u32>info() -> Result<WatchdogInfo>— driver name, firmware version, optionsmagic_close()consumes the handle and writes'V'so the kernel disables the watchdog instead of rebooting on close
Underlying sources. linux/watchdog.h ioctls (WDIOC_KEEPALIVE,
WDIOC_SETTIMEOUT, WDIOC_GETTIMEOUT, WDIOC_GETTIMELEFT,
WDIOC_GETSUPPORT).
External crates. nix for ergonomic, type-safe ioctl wrappers
instead of raw libc::ioctl.
Out of scope. Software watchdog (systemd has WatchdogSec=), pet-it-
from-a-thread daemonisation (the consumer wires that up).
Purpose. Disk and mount introspection — what's mounted where, how much space, what kind of device backs it (eMMC vs SD vs USB vs NVMe).
API surface.
mounts() -> Vec<Mount>— parsed/proc/mountsdisk_usage(path: &Path) -> DiskUsage { total, used, free, available }viastatvfsblock_devices() -> Vec<BlockDevice>— name, size, rotational, removable, model, kind (Emmc/Sd/Usb/Nvme/Scsi/Other)device_for_mount(mount: &Mount) -> Option<BlockDevice>
Underlying sources. /proc/mounts, /sys/block/<dev>/{size, removable, queue/rotational, device/type, device/model}, statvfs(2).
External crates. nix for statvfs. Avoiding sysinfo —
overkill, drags in everything.
Out of scope. Mount/unmount (mount(2) requires root and is rarely
useful from a service), partitioning, formatting, smartctl health.
Purpose. GPIO, PWM, I²C, SPI, and UART access on the Pi. Thin
re-export of rppal with a few ergonomic shortcuts and Pi-pin helpers.
API surface.
- Re-export
rppal::gpio,rppal::pwm,rppal::i2c,rppal::spi,rppal::uartunder stable module names pi_pin(bcm: u8) -> Result<rppal::gpio::Pin>— convenience wrapper- Re-export
rppal::system::DeviceInfoso callers can branch on Pi model
External crates. rppal (the standard pure-Rust Pi peripheral
crate). No system-lib dependency, builds anywhere.
Out of scope. High-level sensor drivers (DHT22, BME280, etc.) — those
belong in their own crates that depend on embedded-hal traits, which
rppal already implements. We don't reinvent that.
Purpose. Read systemd journal entries — filter by unit, priority, boot, time range; one-shot fetch or follow (tail).
API surface.
JournalReaderbuilder:.unit("ssh.service"),.priority(Priority::Warning),.since(SystemTime),.boot(BootRef::Current),.lines(N).fetch() -> Result<Vec<Entry>>— one-shot.follow() -> impl Stream<Item = Result<Entry>>— tail, asyncEntry { timestamp, hostname, unit, syslog_identifier, priority, pid, message, fields: HashMap<String, String> }
Underlying source. Shell-out to journalctl --output=json. Parses
each line as a JSON object. Same rationale as nmcli and ufw:
keeps the dep tree clean, avoids needing libsystemd-dev at build
time, and the output format is documented and stable.
External crates. serde, serde_json for line parsing. tokio for
the async follow stream.
Out of scope. Writing to the journal — use tracing-journald or
systemd-journal-logger directly from your app.
Purpose. Inspect and toggle system time-sync state — NTP enabled, synchronised, current timezone, RTC mode, system vs RTC time. Important on Pi because it has no built-in RTC and many CM4 carriers add one.
API surface.
TimeSync::system().await -> Result<TimeSync>status() -> Result<TimeStatus { ntp_enabled, ntp_synchronized, timezone, local_rtc, time, rtc_time }>set_ntp(enabled: bool) -> Result<()>set_timezone(tz: &str) -> Result<()>set_local_rtc(local: bool, adjust_system: bool) -> Result<()>
External crates. Reuses the zbus / zbus_systemd we already pulled
in for systemd-units, with the timedate1 feature added. Async, pure
D-Bus to systemd-timedated.
Out of scope. Choosing or configuring an NTP daemon (chrony / ntpsec / systemd-timesyncd) — that's deployment policy.
Purpose. Discover hosts on a network by TCP connect. Expand a target spec across a port spec, connect with bounded concurrency, report what answered. The motivating case is finding product devices on a LAN — but what counts as "yours" is a caller-supplied probe, not crate knowledge.
API surface.
parse_targets(&str) -> Result<Vec<Ipv4Addr>>—10.0.0.5,10.0.0.0/24,10.0.0.5-40,10.0.0.5-10.0.0.40, comma-mixed.parse_ports(&str) -> Result<Vec<u16>>—8099,80,443,8000-8100.Scanner::new(targets, ports)?+.timeout()/.concurrency()?/.probe(); then.run().await -> Vec<Host>or.stream()for results as they land.trait Probe— inspect an open socket, returnSome(banner)to keep it orNoneto drop it.NoProbe(keep everything) andHttpProbe(GET <path>, require substrings) ship with the crate.
External crates. tokio for connect/timeout, futures for the
bounded FuturesUnordered + Semaphore fan-out. No raw-socket or pcap
dependency, so no root needed.
Out of scope. SYN/stealth scanning (raw sockets, root), UDP probing,
service-version databases, OS fingerprinting, ARP sweeps — that is nmap's
job. IPv4 only; sweeping an IPv6 subnet is not meaningful. TLS is not
spoken, so HttpProbe is plaintext-only.
Purpose. Answer "which machine is this?" — and be honest about how
much the answer can be trusted. No identifier is simultaneously
universal, stable, and unique across Linux/Windows/macOS and x86/ARM, so
this crate gathers the candidates that exist and tags each with a
Confidence. The failure that motivates it is silent: an OS machine-id
is baked into a disk image, so a whole fleet flashed from one image
reports the same value.
API surface.
resolve(path) -> Result<MachineId>— best available identity, preferring one that survives fleet imaging. Never returns aProvisionedOsid.report(path) -> IdReport— every identifier this host can produce. Infallible; does not create a node id.os::os_machine_id(),hardware::pi_serial(),node::node_id(path),node::read(path)for the individual sources.Confidence::{Hardware, Assigned, ProvisionedOs}+Confidence::clonable().
External crates. machine-uid (zero deps on Linux; windows-sys +
windows-registry on Windows, libc on macOS/illumos) for the OS id,
uuid for the assigned node id. No root needed by any path.
Platform note. The one crate here that is not Linux-only —
machine-uid covers Windows and macOS, node is pure filesystem, and
hardware returns None off-Linux rather than failing to compile.
Out of scope. MAC addresses (randomised per-SSID, spoofable, several
per host — net-info exposes them if wanted), DMI/SMBIOS UUIDs
(root-only, absent on ARM, often zeroed or batch-duplicated), disk
serials (identify a disk, not a machine), and hashing several weak
signals into one fingerprint — any component changing silently changes
the identity.
- No domain knowledge. A crate here knows about Linux primitives, not about agents/extensions/products.
- Linux-only. No
cfg(windows)or macOS branches; this is a CM4 target. Cross-platform crates (e.g.sysinfo,netdev) are fine as dependencies because they handle Linux gracefully. - Sync where it can be, async where the upstream forces it. Read-only
filesystem and
/procaccess stays sync. D-Bus (systemd-units,time-sync) is async becausezbusis async. Journalfollow()is async because tailing is. - Shell-out is fine when the canonical interface is a CLI (
ufw,nmcli,vcgencmd,journalctl). It's clearer, more debuggable, and avoids dragging in heavy bindings. - Errors are typed per-crate (
UfwError,WatchdogError, ...). No shared error crate — each one's failure modes are different and callers usually handle one at a time.