fabbr@site:~/projects/magellan$

magellan

// ~/projects/magellan.rs
let magellan = Project {
    status: Status::Active,
    started: "2026-09-19",
    tags: &["rust", "typescript", "cloudflare", "grafana", "opentofu", "ansible"],
};

Magellan mission patch: an embroidered spiral galaxy, a sensor at its edge, ringed by “Edge-to-cloud sensor pipeline”

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

pollcontractinverterdevicebuffer onflashingestworkerR2 archiveD1 readingsAPI workerGrafana

ls ~/projects/magellan/

activecloudTwo Cloudflare Workers over D1 and R2. It stores what devices declare and serves it back.#typescript #cloudflare #d1 #r2
activecontractThe one seam between device and cloud, and the only thing the two repositories share.#typescript #zod #openapi
activedeviceA Rust binary on a small board. It reads local sources and keeps every reading through outages.#rust #tokio #modbus
activegrafanaThe client that reads Magellan back. One dashboard and its alerts, provisioned from files, over the cloud's read API.#grafana #infinity #ansible
activeinfraTofu 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.