Holger Trahe · Rust engineering
Real code from my projects, next to the decisions behind it: the problem, what I chose, what I turned down, and what it bought. Each chapter links to the thing running live.
01 · Architecture · eulumdat-rs, gldf-rs
Lighting data — EULUMDAT (.ldt), IES and GLDF files — is needed in a browser editor, in Python scripts, on PHP servers, in iOS and Android apps, in the Windows Explorer preview pane and on HarmonyOS. Every platform speaks a different language.
One crate per format owns parsing, the calculations and the diagrams: eulumdat for LDT and IES, gldf-rs for GLDF. Each target is a thin adapter in its own idiom: PyO3 for Python, UniFFI for Swift and Kotlin, phper for PHP, a plain C ABI for HarmonyOS, a COM preview handler on Windows, wasm-bindgen and Leptos in the browser.
PolarDiagram::from_eulumdat(…).to_svg(…) appears in every eulumdat adapter on the right, and the PHP extension is the same few lines around GldfProduct::load_gldf. Adapters only convert types and errors./// Generate a polar diagram SVG.
#[pyo3(signature = (width=500.0, height=500.0, theme=SvgTheme::Light))]
fn polar_svg(&self, width: f64, height: f64, theme: SvgTheme) -> String {
let diagram = CorePolarDiagram::from_eulumdat(&self.inner);
diagram.to_svg(width, height, &theme.to_core())
}Keyword defaults come from #[pyo3(signature)] — from Python it reads like any other method.
/// Generate polar diagram as SVG string
#[uniffi::export]
pub fn generate_polar_svg(ldt: &Eulumdat, width: f64, height: f64, theme: SvgThemeType) -> String {
let core_ldt = to_core_eulumdat(ldt);
let polar = PolarDiagram::from_eulumdat(&core_ldt);
polar.to_svg(width, height, &theme.to_core())
}#[uniffi::export] generates the Swift and Kotlin side; the record is converted to the core type first.
fn gldf_to_xml(arguments: &mut [ZVal]) -> phper::Result<String> {
let path = arguments[0].expect_z_str()?.to_str()?;
let loaded: GldfProduct = GldfProduct::load_gldf(path).unwrap();
Ok(loaded.to_xml().unwrap())
}
⋯
#[php_get_module]
pub fn get_module() -> Module {
// New `Module` with extension info.
let mut module = Module::new(
env!("CARGO_CRATE_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"),
);
⋯
module
.add_function("gldf_to_xml", gldf_to_xml)
.argument(Argument::by_val("path"));
module
.add_function("gldf_to_json", gldf_to_json)
.argument(Argument::by_val("path"));
⋯
module
}#[php_get_module] generates the extension's C entry point; each PHP function is a few lines over the gldf-rs API.
#[no_mangle]
pub unsafe extern "C" fn eulumdat_polar_svg(
handle: *const EulumdatHandle,
width: f64,
height: f64,
theme: i32,
) -> *mut c_char {
if handle.is_null() {
return ptr::null_mut();
}
let ldt = &(*handle).inner;
let svg_theme = if theme == 1 {
SvgTheme::dark()
} else {
SvgTheme::light()
};
let polar = PolarDiagram::from_eulumdat(ldt);
let svg = polar.to_svg(width, height, &svg_theme);
string_to_c(&svg)
}Plain C types for the Cangjie side: a null check, an opaque handle, a string the caller frees.
/// Generate SVG from file content
fn generate_svg(content: &str, width: f64, height: f64) -> Result<String, String> {
// Try LDT first, then IES
let ldt = eulumdat::Eulumdat::parse(content)
.or_else(|_| eulumdat::IesParser::parse(content))
.map_err(|e| e.to_string())?;
let polar = eulumdat::diagram::PolarDiagram::from_eulumdat(&ldt);
let theme = eulumdat::diagram::SvgTheme::light();
Ok(polar.to_svg(width, height, &theme))
}The preview pane tries LDT first, then IES — and draws with the same two calls.
02 · Architecture · gldf-rs
GLDF — the Global Lighting Data Format — is a zip of XML, photometry and 3D files. Its editor at gldf.ch grew inside the web front end. Then a terminal UI, a CLI and Python scripting needed exactly the same editing operations.
I moved every edit into a gldf-rs-editor crate: a serde-tagged GldfAction enum and one reducer, apply(action). A front end deserializes an action and calls it. The only side effect — remembering the UI language — sits behind a trait.
#[serde(tag = "action", content = "data")] turns the action list into a JSON protocol for free — a Python script and the browser send the same payload.UiLanguageStore is implemented by the browser, by the TUI and by a no-op for tests.last_error — and each binding turns them into its own idiom: a ValueError in Python, a string for JavaScript./// Actions that can be performed on the GLDF state.
///
/// Serialized as `{"action": "<variant>", "data": <variant payload>}`
⋯
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "action", content = "data")]
#[allow(clippy::large_enum_variant, dead_code)]
pub enum GldfAction {
/// Load a new GLDF product (without embedded file bytes).
Load(GldfProduct),Adjacently tagged: every action serializes as {"action": "Load", "data": …}.
fn apply_json(&mut self, action_json: &str) -> PyResult<()> {
let action: gldf_rs_editor::GldfAction = serde_json::from_str(action_json)
.map_err(|e| PyValueError::new_err(format!("parse action: {}", e)))?;
self.inner.apply(action);
if let Some(err) = self.inner.last_error.as_deref() {
return Err(PyValueError::new_err(err.to_string()));
}
Ok(())
}Python sends JSON, Rust parses and applies — the reducer's error becomes a ValueError.
#[wasm_bindgen::prelude::wasm_bindgen(js_name = gldf_apply_action_json)]
pub fn apply_action_json(json: &str) -> String {
let action: GldfAction = match serde_json::from_str(json) {
Ok(a) => a,
Err(e) => return format!("parse: {}", e),
};
let mut result = String::new();
GLDF_CTX.with(|cell| {
match cell.get() {
Some(ctx) => {
ctx.update(|s| {
s.apply(action);
if let Some(err) = &s.last_error {
result = format!("apply: {}", err);
}
});
}
None => result = "GldfContext not yet initialised".to_string(),
}
});
result
}The same parse-and-apply, inside a Leptos signal update so the UI re-renders.
//! UI translation language — picked from the viewer's chrome, independent
//! of the document's locale. Persisted via a frontend-supplied store so
//! the state crate stays free of browser- and filesystem-specific calls.
⋯
/// Storage adapter for the user's UI language preference. The WASM
/// frontend persists to `localStorage`; the TUI persists to a config file
/// under `$XDG_CONFIG_HOME`; tests use [`NullLangStore`] which does
/// nothing.
pub trait UiLanguageStore: Send + Sync {
/// Read the last-chosen UI language, if any.
fn load(&self) -> Option<String>;
/// Persist the chosen UI language.
fn store(&self, lang: &str);
}localStorage, a config file or nothing: the front end decides, the state crate stays pure.
03 · Parsing · gldf-rs, tm25ray
GLDF rc.3 has typos in element names that rc.4 corrects — and files of both kinds are in circulation. TM-25 ray files (millions of measured light rays per LED) differ by vendor in how they write “unknown” and whether an optional block exists.
Accept every spelling that exists, keep one canonical model, write exactly what the target expects. For TM-25: match vendor quirks by exact bit pattern and settle ambiguities with arithmetic, not heuristics.
alias reads the rc.3 typo and the rc.4 spelling into one field. rc.3 export rewrites the names back, so saving a file never silently bumps its schema version.&[u8]: no per-ray allocation, and the whole block can be uploaded to a GPU buffer unchanged./// The rated chromaticity coordinate values of the light source.
///
/// On serialize we emit the rc.4-corrected element name
/// `RatedChromaticityCoordinateValues`; on deserialize we accept
/// either spelling via the `alias` (rc.3 files in the wild) and the
/// rc.4 corrected form. rc.3 export wraps `to_xml()` in a downconvert
/// pass that rewrites the element name back.
#[serde(
rename = "RatedChromaticityCoordinateValues",
alias = "RatedChromacityCoordinateValues",
skip_serializing_if = "Option::is_none"
)]
pub rated_chromaticity_coordinate_values: Option<RatedChromaticityCoordinateValues>,rename is what we write, alias is what we also accept.
/// Rewrite rc.4-canonical spellings back to their rc.3 typo'd forms.
///
/// The rc.4 schema kept both spellings valid via `<xs:choice>` wrappers
/// and deprecated-form enum aliases, so this rewrite is needed only for
/// consumers that haven't bumped to rc.4 yet. Pure string replacement —
/// the substrings we replace don't appear anywhere except as element
/// names (`<RatedChromaticityCoordinateValues>` open/close tags) and as
/// the literal enum values inside `<Cie97LampType>...</Cie97LampType>`
/// element bodies, so global replace is safe.
fn downconvert_rc4_to_rc3(xml: &str) -> String {
xml
// Element name (rc.4 only added this; rc.3 only knew the typo'd form)
.replace(
"RatedChromaticityCoordinateValues",
"RatedChromacityCoordinateValues",
)
// Cie97LampType enum values
.replace("Fluorescent Triphosphor", "Flourescent Triphosphor")
.replace("Fluorescent Halophosphate", "Flourescent Halophosphate")
}A plain string rewrite — and the doc comment says why it is safe.
/// True for the bit patterns vendors use to mean "not known".
///
/// Normally that is a NaN written little-endian like every other field
/// (ams OSRAM: `01 00 C0 7F`). Lumileds LUXEON files instead write the
/// sentinel **byte-swapped** (`7F 80 00 01`), which read little-endian is not
/// a NaN at all but the denormal 2.36e-38 — small enough to look like a real
/// measurement and poison anything derived from it. The ray data in those same
/// files is ordinary little-endian, so this is a quirk of the sentinel only.
///
/// Matched exactly, never by bit class: a value is "unknown" only if it is a
/// real NaN or precisely that one swapped pattern.
fn is_unknown(v: f32) -> bool {
// Exact patterns only. An earlier version asked "would the swapped bytes
// be a NaN?", which also matched ~0.4 % of ordinary positive values and
// would have silently zeroed real measurements.
const BYTE_SWAPPED_SENTINEL: u32 = 0x0100_807F;
v.is_nan() || v.to_bits() == BYTE_SWAPPED_SENTINEL
}One exact bit pattern, no clever classification — the comment records the version that was wrong.
/// Zero-copy view over a ray block.
#[derive(Clone, Debug)]
pub struct RayView<'a> {
layout: RayLayout,
data: &'a [u8],
len: usize,
}
⋯
/// Raw bytes of record `i`.
pub fn raw(&self, i: usize) -> Option<&'a [u8]> {
if i >= self.len {
return None;
}
let s = i * self.layout.record_size;
Some(&self.data[s..s + self.layout.record_size])
}
pub fn get(&self, i: usize) -> Option<Ray> {
self.raw(i).map(|r| self.layout.decode(r))
}
⋯
/// The whole block as bytes (for uploading to a GPU buffer as-is).
pub fn bytes(&self) -> &'a [u8] {
&self.data[..self.len * self.layout.record_size]
}A view borrows the file's bytes; records are sliced on demand, and the GPU gets the block as-is.
04 · Embedded · geodb-rs
geodb-rs puts a world city database on a microcontroller: an STM32F769I-DISCO (Cortex-M7, 216 MHz) with an 800×480 touch screen and a software-rendered globe. I wanted people to try it without the board — and without a second version that slowly drifts away from the real one.
geodb-fw-core is no_std and does not allocate. It takes plain borrowed slices — the city image, the framebuffer — and closures for the clock. The board points it at flash; the browser build points it at a buffer the page fills with the very same geodb.fw file.
Vec. Same parser, same ui::draw, same screen.extern "C" exports, called from one HTML page. Errors cross as plain integers (-1 … -4).//! The core of the geodb firmware, `no_std` and without `alloc`:
//!
//! - [`image`]: a flash-resident, zero-copy image of the cities. Geoids are
//! 32-bit Morton codes on 16-bit axes (0.0027° of latitude, about 305 m),
//! sorted, so the sort order is the spatial index, as in `geodb-core`.
//! - [`geo`], [`query`]: the covering Z-order ranges of a circle (integer
//! arithmetic) and radius / nearest queries with an f32 haversine, the
//! method of the GPU kernels.
//! - [`render`]: a software globe into an RGB565 framebuffer, dots and text,
//! the same code on the host (PNG previews) and on the board.
//! - [`ui`]: the whole 800 x 480 screen, drawn from the image.
//!
//! With the `std` feature, the `build` module writes the image from plain slices.
#![cfg_attr(not(any(feature = "std", test)), no_std)]no_std unless a host tool or a test asks for std.
/// The city image (`make_image`) sits in the last flash sectors, outside both program slots, so a
/// program update leaves it alone: flash it once with
/// `probe-rs download --binary-format bin --base-address 0x080C0000 geodb.fw`.
const IMAGE_ADDR: usize = 0x080C_0000;
const IMAGE_MAX: usize = 0x14_0000;
⋯
/// The image in flash, when it parses (its header names the length); else the placeholder.
fn flash_image() -> &'static [u8] {
if !ota::UPDATING_IMAGE.load(core::sync::atomic::Ordering::Relaxed) {
let len = unsafe { ((IMAGE_ADDR + 60) as *const u32).read_volatile() } as usize;
let bytes =
unsafe { core::slice::from_raw_parts(IMAGE_ADDR as *const u8, len.min(IMAGE_MAX)) };
if FwImage::parse(bytes).is_ok() {
return bytes;
}
}
PLACEHOLDER
}The image is read in place from flash — and only used if it parses.
//! The board's screen in the browser: `geodb-fw-core` on the firmware image
//! (`geodb.fw`), the same `ui::draw` the STM32F769I-DISCO runs, and the
//! board's main loop (touch, flick, spin, quick and full redraws).
//!
//! No wasm-bindgen: a handful of plain exports, driven by `web/index.html`.
⋯
/// A buffer of `len` bytes for the image; the page writes `geodb.fw` into it.
#[no_mangle]
pub extern "C" fn image_alloc(len: usize) -> *mut u8 {
let buf: &'static mut [u8] = Box::leak(vec![0u8; len].into_boxed_slice());
let ptr = buf.as_mut_ptr();
IMAGE.with(|i| *i.borrow_mut() = Some(buf));
ptr
}
⋯
/// Parses the image and draws the first screen: the number of cities, or -1
/// (no image), -2 (not a geodb firmware image), -3 (other version), -4 (damaged).
#[no_mangle]
pub extern "C" fn start() -> i32 {
let Some(bytes) = IMAGE.with(|i| i.borrow_mut().take()) else {
return -1;
};
let bytes: &'static [u8] = bytes;
let img = match FwImage::parse(bytes) {
Ok(img) => img,
Err(geodb_fw_core::ImageError::Magic) => return -2,
Err(geodb_fw_core::ImageError::Version) => return -3,
Err(geodb_fw_core::ImageError::Layout) => return -4,
};The page writes geodb.fw into a leaked buffer; from there it is the same FwImage::parse.
05 · Data structures · geodb-rs
“Cities within 300 km of Munich” has to be fast on a 216 MHz chip with no heap — and on the GPU, across 153,000 cities in the browser.
Cities are stored sorted by geoid, a Morton code that interleaves the bits of latitude and longitude. A circle becomes a few contiguous ranges of that order; each range costs two binary searches, then an exact haversine test. The GPU takes the same ranges, cut into segments of 256 cities — one workgroup each.
u32 halves and compared as exact integer deltas. Running GPU and CPU side by side even exposed a CPU nearest-city bug — wrong on 501 of 2,000 queries.//! The flash image, read in place (no copy, no allocation).
//!
//! Little-endian; every section starts on a 4-byte boundary, offsets count
//! from the start of the image. The 84-byte header:
//!
//! | at | field |
//! |---|---|
//! | 0 | magic `GDFW` |
//! | 4 | version u16, flags u16 |
//! | 8 | cities u32, countries u32, named cities u32 |
//! | 20 | coastline length u32 |
//! | 24 | offsets: geoids, country ids, named index, name offsets, names |
//! | 44 | names length; offsets: countries, country names, coastline |
//! | 60 | total length |
//! | 64 | colour picture (the Blue Marble, 256 x 128 RGB565): offset u32, length u32 (0: none) |
//! | 72 | elevation picture (packed, see [`crate::relief`]): offset u32, packed length u32 (0: none), width u16, height u16 |
⋯
let names_len = u32_at(bytes, 44);
let fits = |off: usize, len: usize| {
off.is_multiple_of(4)
&& off >= HEADER_LEN
&& off.checked_add(len).is_some_and(|end| end <= bytes.len())
};
let ok = u32_at(bytes, 60) == bytes.len()
&& fits(img.off_geoids, img.cities * 4)
&& fits(img.off_country, img.cities)
&& fits(img.off_named, img.named * 4)
&& fits(img.off_name_off, (img.named + 1) * 4)
&& fits(img.off_names, names_len)
&& fits(img.off_countries, img.countries * 4)
&& fits(img.off_country_names, 0)
&& fits(img.off_coast, img.coast_len)
&& (img.marble_len == 0 || fits(img.off_marble, img.marble_len))
&& (img.elev_len == 0 || fits(img.off_elev, img.elev_len));A hand-written format, read in place; one fits() check per section before anything trusts an offset.
/// Calls `f(city, haversine)` for every city within `radius_km` of
/// (lat, lon) (degrees), in geoid order; the haversine sin²(d / 2R)
/// orders like the distance. Returns how many cities it tested.
pub fn radius_index(
&self,
lat: f32,
lon: f32,
radius_km: f32,
mut f: impl FnMut(u32, f32),
) -> usize {
let limit = hav_of_km(radius_km);
let cos_q = cos(lat * DEG_TO_RAD);
let half = 0.5 * DEG_TO_RAD;
let rg = crate::geo::ranges(lat, lon, radius_km);
let mut tested = 0;
for &(a, b) in rg.as_slice() {
let (lo, hi) = (self.lower_bound(a), self.upper_bound(b));
for i in lo..hi {
tested += 1;
let (la, lo) = to_deg(self.geoid(i));
let s_lat = sin((la - lat) * half);
let s_lon = sin((lo - lon) * half);
let h = s_lat * s_lat + cos_q * cos(la * DEG_TO_RAD) * s_lon * s_lon;
if h <= limit {
f(i as u32, h);
}
}
}
tested
}Ranges of the sort order, two binary searches each, then the haversine.
fn push_segments(&self, out: &mut Vec<[u32; 4]>, q: u32, center: u64, radius_km: f64) {
for (start, end) in self.index_ranges(center, radius_km) {
let mut at = start;
while at < end {
let len = (end - at).min(WORKGROUP);
out.push([q, at, len, 0]);
at += len;
}
}
}The same ranges, cut into segments that fit one workgroup.
// One query, indexed: workgroup = segment, thread = city of the segment.
@compute @workgroup_size(256)
fn radius_single_seg(
@builtin(workgroup_id) wid: vec3<u32>,
@builtin(local_invocation_index) li: u32,
) {
let s = wid.y * GRID + wid.x;
if (s >= p.nseg) {
return;
}
let seg = segments[s];
if (li >= seg.z) {
return;
}
let i = seg.y + li;
if (hav(split(cities[i]), split(p.center)) <= p.h) {
let k = atomicAdd(&counters[0], 1u);
if (k < p.cap) {
hits[k] = i;
}
}
}Workgroup = segment, thread = city. Hits are appended with one atomic counter.
06 · Embedded · geodb-rs
The Cortex-M7 on this board has a single-precision FPU. libm's sinf and cosf compute in software double precision there — about 20 µs per haversine — and the screen has 384,000 pixels to fill.
Short f32-only polynomials replace libm (Cephes-style, tested against f64). The globe projection is computed once per tilt and zoom into a fixed-point table; a spinning frame then only shifts the longitude and samples the texture with integer math into RGB565.
sqrt is a bit-pattern guess and two Newton steps: about 40 cycles, relative error ~2e-7, no library call.trig_matches_f64 holds sin and cos to 2e-6 of f64.//! f32 math without f64: `libm`'s sinf, cosf, atan2f and friends compute in
//! software double precision, which is very slow on the Cortex-M7 targets
//! with a single-precision FPU (about 20 us per haversine). These are short
//! polynomials in f32 only (Cephes-style), accurate to ~1e-6 for sin/cos and
//! ~2e-5 rad for atan, and `sqrt` is a bit guess and two Newton steps.
⋯
/// Square root: a bit-pattern guess (3.5% off) and two Newton steps
/// (relative error ~2e-7), in f32 only: no library call, about 40 cycles.
pub fn sqrt(x: f32) -> f32 {
if x <= 0.0 {
return 0.0;
}
let mut y = f32::from_bits(0x1fbd_1df5 + (x.to_bits() >> 1));
y = 0.5 * (y + x / y);
0.5 * (y + x / y)
}The magic constant halves the exponent; two Newton steps do the rest.
/// What one block of the globe needs from the view, computed once for a
/// tilt and zoom: the texture row and the longitude offset (fixed point),
/// and the shading. Turning the globe (spinning) changes none of it.
#[derive(Clone, Copy)]
pub struct LutCell {
/// Longitude offset from the view centre, in texels x 256.
du: i32,
/// Texture row x 32 ([`EMPTY`] = the block is outside the disc; 1024 rows still fit a u16).
v: u16,
/// Brightness x 128.
shade: u8,
/// Strength of the blue rim, 0..255.
rim: u8,
}
⋯
let u = (centre + c.du).rem_euclid(wrap);
let rgb = sample_fixed(tex, u, i32::from(c.v), rows);
let (shade, rim) = (u32::from(c.shade), u32::from(c.rim));
let mut out = [0u8; 3];
for k in 0..3 {
out[k] = (((rgb[k] * shade) >> 7) + ((GLOW[k] * rim) >> 8)).min(255) as u8;
}
let color = rgb565(out[0], out[1], out[2]);Eight bytes per block, computed once per view; the per-frame loop is shifts and multiplies.
07 · Size · geodb-rs
Search should find Москва when you type “Moskva”, and 東京 for “dongjing”. The general-purpose transliteration crate, deunicode, weighs hundreds of kilobytes — in a page that should start below 300 KB.
At build time, each dataset gets its own fold table: only the few thousand characters it actually contains, mapped to their transliteration. The tables ship as small data layers, split by script. The wasm folds names and queries with them and needs no deunicode.
/// What [`fold_key`] does to the non-ASCII characters of one dataset, as a
/// table: character -> its transliteration, lowercased. A dataset only ever
/// contains a few thousand distinct characters, so its table is a few KB
/// where the general transliteration (`deunicode`) is hundreds: the compact
/// globe files carry their own tables and the wasm needs no `deunicode`.
///
/// Entries that would be the plain lowercase of the character are left out
/// (a [`Folder`] falls back to that).
⋯
let entries = chars
.into_iter()
.filter(|c| skip.is_none_or(|s| s.get(*c).is_none()))
.filter_map(|c| {
// Between two letters, so the padding a transliteration adds
// ("Dong " for 東) is not trimmed off like at a string's end.
let with = fold_key(&format!("a{c}a"));
let folded = with
.strip_prefix('a')
.and_then(|w| w.strip_suffix('a'))
.map_or_else(|| fold_key(c.encode_utf8(&mut [0; 4])), str::to_string);
let plain: String = c.to_lowercase().collect();
(folded != plain).then_some((c, folded))
})
.collect();Each character is folded between two letters, so padding like “Dong ” survives.
// A name's folded form, and where a transliteration pads syllables
// with spaces also the form typed without them ("Dong Jing" and
// "dongjing" for 東京).
let add = |f: &mut Vec<String>, s: &str| {
f.push(folder.fold(s));
f.extend(folder.compact(s));
};Folded, and compacted without spaces: “Dong Jing” and “dongjing”.
08 · IoT · plantworks
The TRAHE SYSTEMS greenhouse console shows a DALI lighting bus and its MQTT traffic in the browser. The same gateway has to run on an ESP32-C6 over USB, on an STM32 with Ethernet, and next to a server. Four hosts, four ways to do concurrency.
The gateway is a set of pure functions in plantworks-core: a DALI frame, an input event or an MQTT message goes in together with the bus state; log rows and setpoint writes come out. No I/O, no clocks. Each host only wires its own transport around it.
Target enum becomes the exact address byte, and the standard's logarithmic dimming curve is one tested function — half light is level 229.select races a cancel-safe TCP read against Timer::at(next_tick): broker input and a steady 500 ms tick in one loop.//! The DALI/MQTT **gateway logic**, shared by the browser demo and the ESP32
//! firmware. Pure: no I/O, no clocks. Callers feed it a `Plant` (simulated or
//! bound to a real bus), it returns log rows + setpoint writes.
⋯
//! In the browser the "bus" is the simulated plant; on the device the same
//! calls will be wired to the RMT bus driver (M1).
⋯
/// Output of an executor call: log rows + setpoint writes for the plant.
#[derive(Default, Debug)]
pub struct Out {
pub rows: Vec<Row>,
pub writes: Vec<(String, f64)>,
}
⋯
pub fn exec_forward(ctx: &Ctx, st: &mut BusState, out: &mut Out, f: [u8; 2]) {
⋯
pub fn exec_mqtt_in(ctx: &Ctx, st: &mut BusState, out: &mut Out, topic: &str, payload: &str) {
⋯
pub fn step(p: &Plant, st: &mut BusState) -> Out {Every entry point takes state and returns output — nothing else.
let gs = gateway::gears(p, &st.ids);
let mut out = Out::default();
let dali_topic = format!("plantworks/{GATEWAY_ID}/dali");
{
let ctx = Ctx::new(p, &gs, st.ids);
if msg.topic == dali_topic {
// raw bus access from the console: {"fwd":[a,d]} or {"evt":[b0,b1,b2]}
let v: serde_json::Value = serde_json::from_str(&msg.payload).unwrap_or_default();
let bytes = |k: &str| -> Vec<u8> {
v.get(k).and_then(|a| a.as_array()).map(|a| a.iter().filter_map(|x| x.as_u64()).map(|x| x as u8).collect()).unwrap_or_default()
};
let fwd = bytes("fwd");
let evt = bytes("evt");
if fwd.len() == 2 {
gateway::exec_forward(&ctx, st, &mut out, [fwd[0], fwd[1]]);
} else if evt.len() == 3 {
gateway::exec_event_frame(&ctx, st, &mut out, [evt[0], evt[1], evt[2]]);
} else {
emit_ack(&msg.topic, false, "dali message needs {\"fwd\":[a,d]} or {\"evt\":[b0,b1,b2]}");
return;
}
} else {
gateway::exec_mqtt_in(&ctx, st, &mut out, &msg.topic, &msg.payload);
}
}Raw frames from the console or MQTT messages: both end in the shared executor.
run_local(&move |ctx, s, out| match c {
Composed::Fwd(f) => gateway::exec_forward(ctx, s, out, f),
Composed::Evt(f) => gateway::exec_event_frame(ctx, s, out, f),
});
⋯
run_local(&move |ctx, s, out| gateway::exec_mqtt_in(ctx, s, out, &t, &pl));The same three calls, from a Leptos view.
/// Address byte of a 16-bit forward frame (Part 102 §7.2):
/// short `0AAAAAAS`, group `100GGGGS`, broadcast `1111111S`; `S` = 1 for a
/// command, 0 for direct arc power (DAPC).
pub fn address_byte(t: Target, command: bool) -> u8 {
let s = command as u8;
match t {
Target::Short(a) => ((a & 0x3F) << 1) | s,
Target::Group(g) => 0x80 | ((g & 0x0F) << 1) | s,
Target::Broadcast => 0xFE | s,
}
}
/// Direct Arc Power Control: set `level` (0 = off, 1..=254, 255 = MASK).
pub fn dapc(t: Target, level: u8) -> [u8; 2] {
[address_byte(t, false), level]
}
/// Addressed command with an opcode from [`opcode`].
pub fn command(t: Target, op: u8) -> [u8; 2] {
[address_byte(t, true), op]
}
⋯
/// Logarithmic dimming curve (Part 102 §9.3): level 1 → 0.1 %, 254 → 100 %.
pub fn level_to_percent(level: u8) -> f64 {
match level {
0 => 0.0,
255 => f64::NAN,
l => 10f64.powf((l as f64 - 1.0) / (253.0 / 3.0) - 1.0),
}
}Bit layouts straight from the standard, with the section numbers in the docs.
let mut next_tick = Instant::now() + Duration::from_millis(TICK_MS);
let mut last_ping = Instant::now();
loop {
let sock = link.sock.as_mut().unwrap();
match select(sock.read(&mut buf), Timer::at(next_tick)).await {
Either::First(Ok(0)) => return Err("broker closed the connection"),
Either::First(Ok(got)) => {
inbox.extend_from_slice(&buf[..got]);
while let Some((first, body)) = mqtt::take_packet(&mut inbox) {
if let Some(p) = mqtt::parse_publish(first, &body) {
if let Some(pk) = p.pkid {
link.sock.as_mut().unwrap().write_all(&mqtt::puback_packet(pk)).await.map_err(|_| "tcp write")?;
}
let payload = String::from_utf8_lossy(&p.payload).into_owned();
handle(&mut link, gw, &p.topic, &payload).await?;
}
}
}
Either::First(Err(_)) => return Err("tcp read"),
Either::Second(()) => {
next_tick += Duration::from_millis(TICK_MS);
led_ok.toggle();
tick(&mut link, gw, net).await?;
if last_ping.elapsed() >= Duration::from_secs((KEEPALIVE_S / 2) as u64) {
link.sock.as_mut().unwrap().write_all(&mqtt::pingreq_packet()).await.map_err(|_| "tcp write")?;
last_ping = Instant::now();
}
}
}
}select() over a socket read and a deadline: no threads, no drift.
09 · Health IT · hl7kit
HL7 v2 messages carry hospital orders and results as delimited text. The dicomscope demo links a DICOM study to its HL7 order — on the user's own machine, because patient data must not be pasted into a hosted tool.
hl7kit takes one owned copy of the message and records only byte spans. Every segment, field and component is a Copy view that borrows that string for 'm. Values are slices; decoding escapes returns a Cow, which allocates only when there is an escape.
Message free of a lifetime parameter: easy to keep in a UI signal or a Python object.#![forbid(unsafe_code)].Option<bool> separates “absent” from “different”, and a disagreement becomes a named diagnosis — it is exactly the case integration engineers hunt for./// A parsed HL7 v2 message.
///
/// The message owns a copy of the input text; all views borrow from it.
#[derive(Debug, Clone)]
pub struct Message {
raw: String,
encoding: Encoding,
segments: Vec<SegmentNode>,
warnings: Vec<Warning>,
}
⋯
/// A field view.
#[derive(Clone, Copy)]
pub struct Field<'m> {
msg: &'m Message,
node: &'m FieldNode,
number: usize,
}
impl<'m> Field<'m> {
⋯
/// Raw field text including separators of nested levels.
pub fn value(&self) -> &'m str {
self.node.span.slice(&self.msg.raw)
}
/// Field text with escape sequences decoded.
pub fn decoded(&self) -> Cow<'m, str> {
unescape(self.value(), self.msg.encoding)
}One String owns the text; a Field is three words wide and Copy.
/// Returns the input unchanged (borrowed) when it contains no escape character.
pub fn unescape<'a>(text: &'a str, enc: Encoding) -> Cow<'a, str> {
let esc = enc.escape as char;
if !text.contains(esc) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
let mut rest = text;
while let Some(start) = rest.find(esc) {
out.push_str(&rest[..start]);
let after = &rest[start + esc.len_utf8()..];
match after.find(esc) {
None => {
out.push_str(&rest[start..]);
rest = "";
break;
}
Some(end) => {
let seq = &after[..end];
match decode_sequence(seq, enc) {
Some(s) => out.push_str(&s),
None => {
out.push(esc);
out.push_str(seq);
out.push(esc);
}
}
rest = &after[end + esc.len_utf8()..];
}
}
}
out.push_str(rest);
Cow::Owned(out)
}The common case returns the borrowed input; only real escapes allocate.
/// `None` when either side is absent.
pub fn matches(&self) -> Option<bool> {
Some(self.dicom.as_deref()? == self.hl7.as_deref()?)
}
⋯
/// The two keys disagree with each other: both sides carry both a
/// study UID and an accession number, one pair matches and the other
/// does not. `None` when they agree or when either key is absent on
/// either side.
pub fn key_conflict(&self) -> Option<KeyConflict> {
match (self.study_uid.matches(), self.accession.matches()) {
(Some(true), Some(false)) => Some(KeyConflict::AccessionDiffers),
(Some(false), Some(true)) => Some(KeyConflict::StudyUidDiffers),
_ => None,
}
}
⋯
/// DICOM string values are space-padded to even length and UIDs NUL-padded;
/// HL7 values may carry stray whitespace. Compare after trimming both.
fn clean(v: Option<&str>) -> Option<String> {
let s = v?.trim_end_matches('\0').trim();
(!s.is_empty()).then(|| s.to_string())
}Two Option<bool>s, one match — absent, equal and conflicting stay distinct.
10 · BIM · bimifc
bimifc.de opens IFC building models in the browser: a Leptos UI for the model tree and filters, Bevy with WebGPU for the 3D view. A stadium or a hospital means millions of vertices to hand from one side to the other.
Both compile into one wasm binary. With the unified feature, parsed geometry moves from Leptos to Bevy by ownership — vertex buffers in an Arc, no serialization. Discipline filters come from a three-step classifier shared by every viewer.
Vec<f32> is free; serializing it across a JavaScript bridge is not. Replacing JSON with a binary hand-over already cut RAM 3–5×; the unified build drops the bridge.#[cfg(not(feature = "unified"))] — one Cargo feature switches architectures.; — except inside a quoted string like 'SANITAIRE TOESTELLEN;'. The STEP parser builds on the open-source ifc-lite; that fix is mine.pub fn save_geometry(geometry: Vec<GeometryData>) {
let start = js_sys::Date::now();
let mesh_count = geometry.len();
#[cfg(feature = "unified")]
{
// UNIFIED MODE: Direct memory transfer - no serialization!
// Both Leptos and Bevy share the same WASM binary + memory space.
use bimifc_bevy::{IfcMesh, MeshGeometry};
use std::sync::Arc;
let meshes: Vec<IfcMesh> = geometry
.into_iter()
.map(|g| IfcMesh {
entity_id: g.entity_id,
geometry: Arc::new(MeshGeometry::new(g.positions, g.normals, g.indices)),
color: g.color,
transform: g.transform,
entity_type: g.entity_type,
name: g.name,
has_ifc_color: g.has_ifc_color,
⋯
source_id: 0,
discipline: g.discipline,
})
.collect();
// Store in global for Bevy to pick up
bimifc_bevy::set_pending_meshes(meshes);
⋯
}Leptos and Bevy share one memory: the buffers change owner, nothing is copied.
//! Three lookup paths, tried in order; first non-`Other` wins:
//!
//! 1. **Concrete IFC type name** — works for IFC4+ files that use
//! `IfcCableSegment` / `IfcPipeSegment` / `IfcSpaceHeater` etc.
//! 2. **Defining IFC TypeObject** — IFC2x3 exporters (MagiCAD,
//! Plancal) leave the instance Name null and attach the
//! discipline-revealing class on `IfcRelDefinesByType →
//! IfcDuctSegmentType` etc. The TUI scene module already does
//! this; the classifier here is the rule it consults.
//! 3. **Instance Name keyword** — Revit-style exports name things
//! helpfully ("Rectangular Duct", "Pipe Types: Waste", "Troffer
//! Light…"). Last-resort when neither type-class layer matches.
⋯
pub fn classify_by_type_name(type_class_upper: &str, type_name: Option<&str>) -> Discipline {
match type_class_upper {
"IFCDUCTSEGMENTTYPE"
| "IFCDUCTFITTINGTYPE"
⋯
| "IFCSPACEHEATERTYPE" => return Discipline::Hvac,
"IFCPIPESEGMENTTYPE"
⋯
| "IFCTANKTYPE" => return Discipline::Plumbing,
"IFCCABLESEGMENTTYPE"
⋯
| "IFCOUTLETTYPE" => return Discipline::Electrical,
"IFCLIGHTFIXTURETYPE" | "IFCLAMPTYPE" => return Discipline::Lighting,
_ => {}
}
// Type class wasn't conclusive — try the type object's Name.
if let Some(n) = type_name {
return classify_by_name(n);
}
Discipline::Other
}The module doc is the design: three lookups, first non-Other wins.
/// Find the end of an entity (semicolon), handling quoted strings
fn find_entity_end(&mut self) -> Option<usize> {
let bytes = self.content.as_bytes();
let mut in_string = false;
while self.pos < bytes.len() {
match bytes[self.pos] {
b'\'' => {
// Check for escaped quote ''
if in_string && self.pos + 1 < bytes.len() && bytes[self.pos + 1] == b'\'' {
self.pos += 2;
continue;
}
in_string = !in_string;
}
b';' if !in_string => {
self.pos += 1;
return Some(self.pos);
}
_ => {}
}
self.pos += 1;
}
None
}A byte loop that knows about quotes — and about '' inside them.
11 · ML · Burn
Removing a photo's background usually means uploading it. U²-Net does the job well — but it is a PyTorch model, and nobody should have to trust a server with their pictures.
The network is ported to Burn by hand, the weights converted to SafeTensors and compiled into the wasm. A Cargo feature picks the backend — WebGPU by default, a pure-CPU ndarray fallback — behind one type alias, so the model code doesn't know which one it runs on.
std store feature pulled in a chain that didn't compile for wasm32; the no_std core was enough. On the way I fixed two things upstream: staging-buffer flushing in CubeCL's wgpu runtime, and a dtype match in burn-candle.#[cfg(feature = "wgpu")]
pub use backend_wgpu::{Backend, init_backend};
#[cfg(all(feature = "ndarray", not(feature = "wgpu")))]
pub use backend_ndarray::{Backend, init_backend};
#[cfg(feature = "wgpu")]
mod backend_wgpu {
use burn::backend::wgpu::{Wgpu, WgpuDevice, graphics::AutoGraphicsApi, init_setup_async};
pub type Backend = Wgpu<f32, i32>;
/// Initialize the WebGPU device. Must be awaited before first inference.
pub async fn init_backend() -> WgpuDevice {
let device = WgpuDevice::default();
init_setup_async::<AutoGraphicsApi>(&device, Default::default()).await;
device
}
}
#[cfg(all(feature = "ndarray", not(feature = "wgpu")))]
mod backend_ndarray {
use burn::backend::ndarray::{NdArray, NdArrayDevice};
pub type Backend = NdArray<f32>;
/// NdArray needs no async setup; returns the default device.
pub async fn init_backend() -> NdArrayDevice {
NdArrayDevice::default()
}
}Same Backend name, same init_backend(): the model never sees the difference.
/// Pretrained u2netp weights, converted to SafeTensors and embedded in the
/// binary. Run `scripts/convert_weights.py` to (re)generate this file.
static WEIGHTS: &[u8] = include_bytes!("../weights/u2netp.safetensors");
⋯
/// Build the model and load the embedded pretrained weights.
pub fn load_model(device: &<Backend as burn::tensor::backend::Backend>::Device) -> U2NetP<Backend> {
let mut model = U2NetP::<Backend>::new(device);
let result = model.apply(
snapshots(),
None,
Some(Box::new(PyTorchToBurnAdapter)),
false,
);
⋯
// Every model parameter must have been filled; missing params would mean the
// remapping is broken and inference would be garbage.
assert!(
!result.applied.is_empty() && result.missing.is_empty(),
"weight load incomplete: {} params missing",
result.missing.len()
);
model
}include_bytes! puts the model in the binary; the assert keeps a bad mapping from shipping.
// Build a planar CHW f32 buffer with ImageNet normalization.
// Reference divides by max; for 8-bit input max is 255, so /255.
let mut chw = vec![0f32; 3 * n];
for i in 0..n {
for c in 0..3 {
let v = rgba[i * 4 + c] as f32 / 255.0;
chw[c * n + i] = (v - MEAN[c]) / STD[c];
}
}
let input = Tensor::<Backend, 1>::from_data(
TensorData::new(chw, [3 * n]),
&self.device,
)
.reshape([1, 3, size, size]);
// Forward pass → sigmoid saliency map in [0, 1], shape [1,1,H,W].
let mask = model.forward(input);
// Read once (async, GPU-safe) then min-max normalize on the CPU —
// matches the reference `normPRED`. Avoids blocking GPU scalar reads.
let data = mask.into_data_async().await.expect("read mask data");
let values: Vec<f32> = data.to_vec().expect("mask to vec");
⋯
let denom = (max - min).max(1e-8);
values
.into_iter()
.map(|v| (((v - min) / denom).clamp(0.0, 1.0) * 255.0 + 0.5) as u8)
.collect()RGBA to a normalized CHW tensor, one forward pass, one await.
12 · Web · Leptos
This site, the demo page and the five-minute reel need server rendering — for search engines and a fast first paint — and real interactivity in the browser: video autoplay, a clock, keyboard control, the tabs next to this text.
Leptos 0.7 on Axum: one crate compiled twice, ssr for the server binary and hydrate for the WebAssembly bundle. Browser-only code sits behind #[cfg(feature = "hydrate")]; browser APIs come typed from web-sys. The only hand-written JavaScript is a few lines that set the theme before first paint.
play() returns a Promise; awaiting it as a JsFuture and matching a NotAllowedError lets the reel switch to stills instead of freezing on a black frame.AnyView from cards and sections keeps that type shallow — the fully nested one overflowed rustc's query depth in release SSR builds. // Ask the video to play; a NotAllowedError means autoplay is blocked -> stills mode.
let try_play = move || {
#[cfg(feature = "hydrate")]
if let Some(v) = video.get_untracked() {
if let Ok(promise) = v.play() {
leptos::task::spawn_local(async move {
if let Err(err) = wasm_bindgen_futures::JsFuture::from(promise).await {
use wasm_bindgen::JsCast;
let not_allowed = err
.dyn_ref::<web_sys::DomException>()
.is_some_and(|e| e.name() == "NotAllowedError");
if not_allowed && !blocked.get_untracked() {
leptos::logging::log!("[reel] autoplay blocked, showing stills");
item_started.set(now_ms());
still_k.set(0);
blocked.set(true);
}
}
});
}
}
};A rejected play() promise is a typed DomException in Rust — the reel checks its name and falls back to stills.
// Cards and sections return AnyView: erasing the types keeps the page's view type shallow
// (the fully nested type overflows rustc's query depth in release SSR builds).
fn demo_card(d: &'static Demo) -> AnyView {
⋯
.into_any()
}AnyView trades a little static dispatch for a view type rustc can still handle in release builds.
// The reel answers at /landing-page/ (canonical) and /landing-page, both without a
// redirect: browsers cache 308s, and an old one pointing the other way would loop.
<Route path=(StaticSegment("landing-page"), StaticSegment("/")) view=ReelPage/>
<Route path=StaticSegment("landing-page") view=ReelPage/>Two routes, one page, no redirect — learned the hard way from a cached 308.
// Identifiers: keyword, macro, type, function, or plain
if is_ident_start(c) {
let n = b.iter().take_while(|&&ch| is_ident(ch)).count();
let word = &rest[..n];
if lang.keywords().contains(&word) {
return (Some("kw"), n);
}
if lang == Lang::Rust && b.get(n) == Some(&b'!') && b.get(n + 1) != Some(&b'=') {
return (Some("mc"), n + 1);
}
if lang.is_type(word) {
return (Some("ty"), n);
}
if rest[n..].trim_start_matches(' ').starts_with('(') {
return (Some("fn"), n);
}
return (None, n);
}One pass, no regex: an identifier becomes a keyword, macro, type or function name by looking one token ahead.