magellan
// ~/projects/magellan.rs
let magellan = Project {
status: Status::Active,
started: "2026-09-19",
tags: &["rust", "typescript", "cloudflare", "grafana", "opentofu", "ansible"],
};
An edge-to-cloud sensor pipeline. Small boards read local sources and keep the readings safe through outages; the cloud stores them and serves them back.
It runs end to end. The device reads a solar inverter over Modbus TCP, the ingest worker is live, and Grafana reads the series back through the API worker.
Pieces
ls ~/projects/magellan/
| active | cloudTwo Cloudflare Workers over D1 and R2. It stores what devices declare and serves it back. | #typescript #cloudflare #d1 #r2 |
| active | contractThe one seam between device and cloud, and the only thing the two repositories share. | #typescript #zod #openapi |
| active | deviceA Rust binary on a small board. It reads local sources and keeps every reading through outages. | #rust #tokio #modbus |
| active | grafanaThe client that reads Magellan back. One dashboard and its alerts, provisioned from files, over the cloud's read API. | #grafana #infinity #ansible |
| active | infraTofu and Ansible run the fleet. The boards read, and an Oracle VM serves Grafana behind Cloudflare. | #opentofu #ansible #grafana #oracle-cloud |
How it works
The path of a reading:
- Poll. The device reads a source on its cadence. One poll is one reading: a timestamp and the source’s values, each a whole number with a declared decimal scale.
- Buffer. Readings gather into a batch, and the batch is written to flash before anything leaves the board. A power cut or a dead link loses nothing.
- Upload. The device sends the oldest batch first. The cloud’s answer decides what happens: on a commit the batch is dropped; on an outage it waits, backs off and tries again.
- Store. The ingest worker archives the raw batch to R2, then commits its readings to D1. A batch that arrives twice collides with itself and vanishes.
- Read. The API worker serves series, latest values and health. Grafana reads it, draws the dashboards and raises the alerts.
Design
The cloud never learns a device-specific word. The device declares what it measures in a manifest, and the cloud stores whatever that manifest says, so a new kind of source is a device change, never a cloud deploy.
- New source, new driver. Each kind of source is a crate of its own behind one seam on the device. A current clamp beside the inverter is one more crate; the manifest carries it to the cloud.
- Two halves, one contract. Device and cloud are built, tested and deployed apart. Either can be rewritten as long as the contract holds.
- Any client. The cloud has no UI. Grafana is one client of the read API; a script or another dashboard reads the same routes without a cloud change.
- Hosts by role. What a host runs follows from the group it is in. A new board is an inventory entry.
Scope
I cut Magellan down to what matters to me and what I do best: the backend, from a register on the wire to a row in the store. Everything else is off the shelf.
- The device was not Rust at first. The prototype was tied to one inverter and written in Python against the existing library. It never worked reliably, so I mapped the registers myself and rebuilt the device in Rust.
- The dashboard went. An early single-page app drew the charts. After a first prototype I dropped it for Grafana: dashboards, alerts and any panel I want, for the price of a VM to run.
- Cut to finish. Whatever did not serve capture, resilience or diagnostics was cut so the project could ship.