fabbr@site:~/projects/magellan/device$

device

// ~/projects/magellan/device.rs
let device = Project {
    status: Status::Active,
    started: "2026-09-20",
    tags: &["rust", "tokio", "modbus"],
    repo: Some("https://github.com/fabbrito/magellan-device"),
};

A single Rust binary on a Raspberry Pi. It reads a solar inverter over Modbus today, holds the readings on flash, and uploads them when the cloud is reachable.

polluploadoutage: retryinverterbuffer onflashcloud

Why it carries the variety

Everything specific to a source, from register maps to scaling and units, stays on the device. It describes what it measures in a manifest, so adding a new kind of source is a device change and never a cloud deploy.

How it got here

The first version was a prototype for one inverter, in Python, on the existing library. The library never read the logger reliably, so I stopped working around it: I mapped the inverter’s registers from a community spreadsheet and checked them against captures from my own installation. With the map in hand, Rust was the language I wanted to build the real device in, and the inverter-specific code became the first driver behind a generic seam.

Decisions

  • Outages are normal. Wi-Fi drops and power cuts are expected, not errors. Readings go to a buffer on flash first, and that buffer survives a reboot.
  • A bounded buffer. The buffer has a fixed size. In a long outage it drops the oldest batch and leaves a visible gap, rather than filling the disk and dying.
  • Catch its own mistakes. The device checks its work against the contract’s limits before queueing it, so a bad reading never waits in the buffer for a rejection.
  • Read-only. It never writes to a source. No commands, no settings changed.
  • Layers the compiler enforces. Runtime, drivers and platform are separate crates, so a layer reaching where it should not fails to build.
  • Alive is apart from data. An hourly heartbeat says the device is running, even at night when the inverter has nothing to report.