<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>F451 Labs</title><link>https://f451labs.com/</link><description>Recent content on F451 Labs</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 31 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://f451labs.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Preservation of Order — Alarm Dashboard</title><link>https://f451labs.com/dashboards/alerts/</link><pubDate>Sun, 31 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/dashboards/alerts/</guid><description>&lt;div class="alarm-display"
 role="status"
 aria-live="polite"
 data-signals='{"ambient_temperature_f":"--","whisper_alert":"--","seashell_interference_pct":"--","updated":"--"}'
 data-init="@get('/api/sensors/stream?device=iot-hound-01')"
 data-class='{"alarm": $ambient_temperature_f &gt;= 451 || $whisper_alert &gt;= 1 || ($seashell_interference_pct !== "--" &amp;&amp; $seashell_interference_pct &lt;= 0)}'&gt;
 &lt;div class="alarm-display-head"&gt;
 &lt;span class="alarm-display-title"&gt;PRESERVATION OF ORDER&lt;/span&gt;
 &lt;span class="alarm-display-device"&gt;iot-hound-01&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="alarm-display-sensors"&gt;
 &lt;div&gt;
 &lt;span class="alarm-display-sensor-label"&gt;TEMP °F&lt;/span&gt;
 &lt;span class="alarm-display-sensor-value" data-text="$ambient_temperature_f"&gt;--&lt;/span&gt;
 &lt;/div&gt;
 &lt;div&gt;
 &lt;span class="alarm-display-sensor-label"&gt;WHISPER&lt;/span&gt;
 &lt;span class="alarm-display-sensor-value" data-text="$whisper_alert"&gt;--&lt;/span&gt;
 &lt;/div&gt;
 &lt;div&gt;
 &lt;span class="alarm-display-sensor-label"&gt;SEASHELL %&lt;/span&gt;
 &lt;span class="alarm-display-sensor-value" data-text="$seashell_interference_pct"&gt;--&lt;/span&gt;
 &lt;/div&gt;
 &lt;/div&gt;
 &lt;div class="alarm-display-level" id="alarm-level-iot-hound-01"&gt;—&lt;/div&gt;
 &lt;div class="alarm-display-message" id="alarm-message-iot-hound-01"&gt;no alerts on record&lt;/div&gt;
 &lt;div class="alarm-display-meta"&gt;LAST ALERT &lt;span id="alarm-ts-iot-hound-01"&gt;—&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(() =&gt; {
 const lvl = document.getElementById("alarm-level-iot-hound-01");
 const msg = document.getElementById("alarm-message-iot-hound-01");
 const ts = document.getElementById("alarm-ts-iot-hound-01");
 const es = new EventSource("/api/alerts/stream?backlog=5");
 es.addEventListener("alert", (e) =&gt; {
 const a = JSON.parse(e.data);
 lvl.textContent = a.level.toUpperCase();
 msg.textContent = a.message;
 ts.textContent = String(a.ts).replace("T", " ").slice(0, 19);
 });
 window.addEventListener("beforeunload", () =&gt; es.close(), { once: true });
})();
&lt;/script&gt;</description></item><item><title>Relative humidity is a temperature measurement</title><link>https://f451labs.com/filed/relative-humidity-is-a-temperature-measurement/</link><pubDate>Sun, 31 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/filed/relative-humidity-is-a-temperature-measurement/</guid><description>&lt;p&gt;Filed under best practices, because it is the kind of thing that is easy to get wrong
and expensive to notice later.&lt;/p&gt;
&lt;p&gt;Relative humidity is not an independent quantity. It is the ratio of the water vapor
actually present to the most the air could hold at that temperature, expressed as a
percentage. The denominator — saturation vapor pressure — climbs steeply with
temperature: roughly &lt;code&gt;7%&lt;/code&gt; per &lt;code&gt;°C&lt;/code&gt; near room temperature. So the same absolute amount
of water in the air reads as a different RH at &lt;code&gt;20°C&lt;/code&gt; than at &lt;code&gt;25°C&lt;/code&gt;. A relative
humidity figure without the temperature it was measured at is half a reading.&lt;/p&gt;</description></item><item><title>Sector Field Map</title><link>https://f451labs.com/dashboards/street-map/</link><pubDate>Sun, 31 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/dashboards/street-map/</guid><description>&lt;div class="street-map"&gt;
 &lt;svg class="street-map-svg" viewBox="0 0 600 400" xmlns="http://www.w3.org/2000/svg"
 aria-hidden="true"&gt;
 &lt;rect width="600" height="400" fill="#EFE7D5"/&gt;
 &lt;line x1="0" y1="130" x2="600" y2="130" stroke="#1A1715" stroke-width="6"/&gt;
 &lt;line x1="0" y1="270" x2="600" y2="270" stroke="#1A1715" stroke-width="6"/&gt;
 &lt;line x1="150" y1="0" x2="150" y2="400" stroke="#1A1715" stroke-width="6"/&gt;
 &lt;line x1="420" y1="0" x2="420" y2="400" stroke="#1A1715" stroke-width="6"/&gt;
 &lt;text x="20" y="120" font-family="monospace" font-size="9" fill="#1A1715"
 letter-spacing="2"&gt;FIREMEN'S WAY&lt;/text&gt;
 &lt;text x="20" y="260" font-family="monospace" font-size="9" fill="#1A1715"
 letter-spacing="2"&gt;MONTAG TERRACE&lt;/text&gt;
 &lt;text x="155" y="20" font-family="monospace" font-size="9" fill="#1A1715"
 letter-spacing="2"&gt;SECTOR 4&lt;/text&gt;
 &lt;text x="425" y="20" font-family="monospace" font-size="9" fill="#1A1715"
 letter-spacing="2"&gt;SECTOR 9&lt;/text&gt;
 &lt;circle cx="240" cy="195" r="6" fill="#D43A2E"/&gt;
 &lt;circle cx="495" cy="300" r="6" fill="#D43A2E"/&gt;
 &lt;/svg&gt;
 &lt;div class="street-map-tiles"&gt;
 &lt;div class="map-tile" id="tile-iot-hound-01"
 style="left:33%;top:42%;transform:translate(-50%,-50%)"&gt;
 &lt;span class="map-tile-label"&gt;IOT-HOUND-01 / MONTAG&lt;/span&gt;
 &lt;div class="map-tile-temp" id="t01-temp"&gt;--°F&lt;/div&gt;
 &lt;div class="map-tile-meta" id="t01-meta"&gt;no signal&lt;/div&gt;
 &lt;/div&gt;
 &lt;div class="map-tile" id="tile-iot-hound-02"
 style="left:78%;top:69%;transform:translate(-50%,-50%)"&gt;
 &lt;span class="map-tile-label"&gt;IOT-HOUND-02 / MILDRED&lt;/span&gt;
 &lt;div class="map-tile-temp" id="t02-temp"&gt;--°F&lt;/div&gt;
 &lt;div class="map-tile-meta" id="t02-meta"&gt;no signal&lt;/div&gt;
 &lt;/div&gt;
 &lt;/div&gt;
&lt;/div&gt;
&lt;script&gt;
(() =&gt; {
 const SIG_PREFIX = "signals ";
 const parseSig = (data) =&gt;
 JSON.parse(data.startsWith(SIG_PREFIX) ? data.slice(SIG_PREFIX.length) : data);

 const tiles = [
 { device: "iot-hound-01",
 tempEl: document.getElementById("t01-temp"),
 metaEl: document.getElementById("t01-meta"),
 tileEl: document.getElementById("tile-iot-hound-01") },
 { device: "iot-hound-02",
 tempEl: document.getElementById("t02-temp"),
 metaEl: document.getElementById("t02-meta"),
 tileEl: document.getElementById("tile-iot-hound-02") },
 ];
 const connections = [];
 for (const { device, tempEl, metaEl, tileEl } of tiles) {
 const es = new EventSource(
 `/api/sensors/stream?device=${encodeURIComponent(device)}`
 );
 connections.push(es);
 es.addEventListener("datastar-patch-signals", (e) =&gt; {
 const sig = parseSig(e.data);
 const temp = sig.ambient_temperature_f;
 const whisper = sig.whisper_alert;
 const seashell = sig.seashell_interference_pct;
 tileEl.classList.remove("disconnected");
 if (temp != null) tempEl.textContent = `${temp}°F`;
 if (sig.updated != null) metaEl.textContent = String(sig.updated).slice(0, 8);
 const isAlarm =
 (temp != null &amp;&amp; temp &gt;= 451) ||
 (whisper != null &amp;&amp; whisper &gt;= 1) ||
 (seashell != null &amp;&amp; seashell &lt;= 0);
 tileEl.classList.toggle("alarm", isAlarm);
 });
 es.onerror = () =&gt; {
 metaEl.textContent = "disconnected";
 tileEl.classList.add("disconnected");
 };
 }
 window.addEventListener("beforeunload", () =&gt; connections.forEach((c) =&gt; c.close()), { once: true });
})();
&lt;/script&gt;</description></item><item><title>sim-01</title><link>https://f451labs.com/dashboards/sim-01/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/dashboards/sim-01/</guid><description>&lt;p&gt;The synthetic first device — the simdev stand-in for hardware. Live values update over
SSE as readings arrive; the table below is the recent backlog from the logger.&lt;/p&gt;
&lt;div class="readout" role="status" aria-live="polite"
 data-signals="{temperature: '--', humidity: '--', updated: '--'}"
 data-init="@get('/api/sensors/stream?device=sim-01')"&gt;
 &lt;div class="readout-head"&gt;
 &lt;span class="readout-title"&gt;LIVE READOUT&lt;/span&gt;
 &lt;span class="readout-device"&gt;sim-01&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="readout-grid"&gt;
 &lt;div class="readout-cell"&gt;
 &lt;span class="readout-label"&gt;TEMP&lt;/span&gt;
 &lt;span class="readout-value" data-text="$temperature"&gt;--&lt;/span&gt;
 &lt;span class="readout-unit"&gt;°C&lt;/span&gt;
 &lt;/div&gt;
 &lt;div class="readout-cell"&gt;
 &lt;span class="readout-label"&gt;RH&lt;/span&gt;
 &lt;span class="readout-value" data-text="$humidity"&gt;--&lt;/span&gt;
 &lt;span class="readout-unit"&gt;%&lt;/span&gt;
 &lt;/div&gt;
 &lt;/div&gt;
 &lt;div class="readout-meta"&gt;LAST CONTACT &lt;span data-text="$updated"&gt;--&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class="history" data-device="sim-01" data-limit="15"&gt;
 &lt;div class="history-head"&gt;
 &lt;span class="history-title"&gt;RECENT READINGS&lt;/span&gt;
 &lt;span class="history-device"&gt;sim-01&lt;/span&gt;
 &lt;/div&gt;
 &lt;table class="history-table"&gt;
 &lt;thead&gt;&lt;tr&gt;&lt;th&gt;TIME (UTC)&lt;/th&gt;&lt;th&gt;METRIC&lt;/th&gt;&lt;th&gt;VALUE&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
 &lt;tbody class="history-rows"&gt;&lt;tr&gt;&lt;td colspan="3"&gt;loading…&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;
 &lt;/table&gt;
&lt;/div&gt;
&lt;script&gt;
(() =&gt; {
 const el = document.currentScript.previousElementSibling;
 const tbody = el.querySelector(".history-rows");
 const note = (msg) =&gt; {
 tbody.replaceChildren();
 const tr = document.createElement("tr"), td = document.createElement("td");
 td.colSpan = 3; td.textContent = msg; tr.appendChild(td); tbody.appendChild(tr);
 };
 const url = `/api/sensors/${encodeURIComponent(el.dataset.device)}/history?limit=${encodeURIComponent(el.dataset.limit)}`;
 fetch(url, { headers: { Accept: "application/json" } })
 .then((r) =&gt; (r.ok ? r.json() : Promise.reject(r.status)))
 .then((rows) =&gt; {
 if (!rows.length) return note("no readings on record");
 tbody.replaceChildren();
 for (const x of rows) {
 const tr = document.createElement("tr");
 const cells = [
 String(x.recorded_at).replace("T", " ").slice(0, 19),
 x.metric,
 `${x.value}${x.unit || ""}`,
 ];
 for (const v of cells) {
 const td = document.createElement("td");
 td.textContent = v;
 tr.appendChild(td);
 }
 tbody.appendChild(tr);
 }
 })
 .catch(() =&gt; note("telemetry unavailable"));
})();
&lt;/script&gt;</description></item><item><title>Synthetic Telemetry</title><link>https://f451labs.com/directives/02-synthetic-telemetry/</link><pubDate>Mon, 25 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/directives/02-synthetic-telemetry/</guid><description>&lt;p&gt;The first thing to report to the telemetry service is not a sensor in a room. It is a
program on the desk. Before any board is flashed, a synthetic device stands up, onboards
through the same provisioning flow as hardware, and posts readings on an interval — so the
service has a producer to receive, the readout has something to show, and the failure modes
are rehearsed where they are cheap to fix.&lt;/p&gt;</description></item><item><title>Applied Logic</title><link>https://f451labs.com/directives/01-applied-logic/</link><pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/directives/01-applied-logic/</guid><description>&lt;p&gt;This is the founding entry. It records the logic the lab applies to everything that
follows, so that later Directives can assume it rather than restate it.&lt;/p&gt;
&lt;p&gt;F451 Labs exists to build small, legible systems — sensors, microcontrollers, the
software that reads them — and to write down what happened. The premise is narrow on
purpose. A project is worth a Directive only if it ran, produced data, and taught
something that survives the writing-up.&lt;/p&gt;</description></item><item><title>About</title><link>https://f451labs.com/about/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/about/</guid><description>&lt;p&gt;F451 Labs is a personal field archive — one person&amp;rsquo;s record of what they built, what
they learned, and what they found worth keeping. The primary material is technology:
IoT hardware, microcontrollers, and the software that makes physical things talk to
networks. Not every book. This one.&lt;/p&gt;
&lt;p&gt;The record splits three ways. &lt;strong&gt;Directives&lt;/strong&gt; are projects: numbered, documented, built
to run. They read like field reports because that is what they are — what was
attempted, what held, what failed under load. &lt;strong&gt;Dispatches&lt;/strong&gt; are everything around the
work — notes, observations, the occasional argument with a datasheet. &lt;strong&gt;Filed&lt;/strong&gt; is the
record worth preserving on its own terms: climate data, educational material,
scientific reference, and best practices. The things kept precisely because they are
inconvenient to ignore.&lt;/p&gt;</description></item><item><title>Dispatch 01: Online</title><link>https://f451labs.com/dispatches/2026-05-16-dispatch-01-online/</link><pubDate>Sat, 16 May 2026 00:00:00 +0000</pubDate><guid>https://f451labs.com/dispatches/2026-05-16-dispatch-01-online/</guid><description>&lt;p&gt;F451 Labs is live. Hardware projects pending — &lt;em&gt;Directives&lt;/em&gt; will follow.&lt;/p&gt;</description></item></channel></rss>