before any paint,
+ // so visitors without the preview flag never see field-notes content.
+ // The companion clientModule (fieldNotesGate.js) handles the body
+ // class for the navbar link; this is just the redirect.
+ tagName: 'script',
+ attributes: {},
+ innerHTML: `
+ (function () {
+ if (typeof window === 'undefined') return;
+ var path = window.location.pathname;
+ if (path !== '/field-notes' && path.indexOf('/field-notes/') !== 0) return;
+ try {
+ var params = new URLSearchParams(window.location.search);
+ var flag = params.get('preview');
+ if (flag === '1') {
+ window.sessionStorage.setItem('peridio:fn-preview', '1');
+ return;
+ }
+ if (flag === '0') {
+ window.sessionStorage.removeItem('peridio:fn-preview');
+ } else if (window.sessionStorage.getItem('peridio:fn-preview') === '1') {
+ return;
+ }
+ } catch (e) { /* sessionStorage unavailable — fall through to redirect */ }
+ window.location.replace('/');
+ })();
+ `,
+ },
{
tagName: 'script',
attributes: {},
@@ -234,6 +293,13 @@ const config = {
href: 'https://fonts.googleapis.com/css2?family=Montserrat:wght@100..900&family=Space+Grotesk:wght@300..700&family=Spline+Sans:wght@300..700&display=swap',
},
},
+ {
+ tagName: 'link',
+ attributes: {
+ rel: 'stylesheet',
+ href: 'https://fonts.googleapis.com/css2?family=Geist:wght@300..700&family=Geist+Mono:wght@400;500;600&display=swap',
+ },
+ },
{
tagName: 'link',
attributes: {
diff --git a/src/field-notes/2026-06-15-power-pull-orin-nx.mdx b/src/field-notes/2026-06-15-power-pull-orin-nx.mdx
new file mode 100644
index 00000000..89a9fd02
--- /dev/null
+++ b/src/field-notes/2026-06-15-power-pull-orin-nx.mdx
@@ -0,0 +1,40 @@
+---
+title: 'Pulling the power 200 times mid-OTA on an Orin NX'
+description: "200 scripted power cuts at random points during an A/B OTA update. Zero bricks at steady state — and the two early failures weren't where we expected."
+date: 2026-06-15
+authors: [jschneck]
+tags: [ota, rollback, jetson, orin-nx]
+tested_against: 'Avocado 1.0.0, JetPack 7.2, Orin NX 16GB'
+hn_title: "We cut power 200 times mid-update on a Jetson. Here's what bricked."
+poster: jschneck
+lift_for_blog: 'Zero bricks across 200 forced failures — the 100-device-wall fear, quantified, for the budget holder.'
+promote_to_track: "Tutorial 4 candidate — the 'break it on purpose' step."
+---
+
+import TestedAgainst from '@site/src/components/TestedAgainst'
+
+
+
+**TL;DR.** We scripted 200 power cuts at random points during an A/B OTA update on an Orin NX. Zero bricks — every device booted the last-known-good slot. Two early runs did fail, and the cause wasn't the bootloader.
+
+{/* truncate */}
+
+## What this is
+
+Peridio builds Avocado OS, an immutable embedded Linux runtime for devices like NVIDIA Jetson. Its updates are A/B and atomic: a failed or interrupted update should always fall back to the previously working slot. We wanted to know how literally true "always" is, so we tried to break it on purpose.
+
+## How we did it
+
+[Relay-controlled PSU, the update payload, the randomized power-cut harness, commands + output.]
+
+## What we learned
+
+[200 forced interruptions, zero bricks at steady state; what the boot logs show on recovery.]
+
+## What didn't work
+
+[Two early failures traced to a data-partition sync gap, not the OS slots; the fix and why it mattered.]
+
+## Reproduce it
+
+[Link to the power-cycle harness repo + step-by-step.]
diff --git a/src/field-notes/2026-06-16-rpi5-from-macos.mdx b/src/field-notes/2026-06-16-rpi5-from-macos.mdx
new file mode 100644
index 00000000..dcff06b4
--- /dev/null
+++ b/src/field-notes/2026-06-16-rpi5-from-macos.mdx
@@ -0,0 +1,68 @@
+---
+title: 'Provisioning a Raspberry Pi from macOS natively, no Linux VM and no USB passthrough'
+description: 'Flashing embedded boards from a Mac usually means a Linux VM and fragile USB passthrough. The Avocado Desktop preview provisions a Raspberry Pi 5 natively from macOS, so there is no VM to babysit and no passthrough to drop.'
+date: 2026-06-16
+authors: [jschneck]
+tags: [rpi5, provisioning, macos, usb, preview]
+tested_against: 'Avocado [ENGINEER: preview version], Raspberry Pi 5, macOS host [ENGINEER: macOS version and chip, Apple Silicon or Intel]'
+hn_title: 'Provisioning a Raspberry Pi from macOS natively, no Linux VM (Avocado Desktop preview)'
+poster: jschneck
+lift_for_blog: 'The Linux-VM-and-USB-passthrough dance for flashing boards from a Mac, removed: native provisioning on the laptop the engineer already owns.'
+promote_to_track: 'Tutorial candidate: native macOS provisioning of a Pi 5 as the first device bring-up on the on-ramp.'
+---
+
+import TestedAgainst from '@site/src/components/TestedAgainst'
+
+
+
+
+
+
+
+**TL;DR.** Provisioning an embedded board from macOS normally means running a Linux VM and passing the board's USB device through to it, which is fragile, more so on Apple Silicon. The Avocado Desktop preview provisions a Raspberry Pi 5 natively from macOS, no VM and no passthrough. [ENGINEER: confirm it is truly native (no VM, no passthrough), on which Mac architecture, and the install-to-boot time.]
+
+{/* truncate */}
+
+## The problem: provisioning a board over USB from a Mac
+
+Most board bring-up happens over USB. The host puts the board into a recovery or device mode and pushes an image over the USB link. The vendor tooling that does this tends to assume Linux, so on a Mac the usual move is to run a Linux VM and pass the board's USB device through to it.
+
+That passthrough is where it falls apart. USB resets and mode switches during flashing drop the device out of the VM, enumeration races leave the tool waiting on a device that is sitting on the host side, and Apple Silicon's virtualization stack makes all of it less predictable. [ENGINEER: keep only the failure modes you actually hit, on the board(s) you actually used. Do not ship a list of problems you did not personally see.]
+
+Orientation for anyone new to this: Peridio makes Avocado OS, an immutable embedded Linux runtime shipped as a binary distribution, for boards like the Raspberry Pi 5 and NVIDIA Jetson. Avocado Desktop is the local tool that builds and provisions it. The developer preview is the macOS build rolling out now, ahead of the Avocado 1.0 release. [ENGINEER: confirm the rollout status and timing you want to state.]
+
+## How Avocado Desktop does it
+
+This is the technical core of the post, and it is the first thing the audience will ask about. [ENGINEER: the real mechanism. How does it drive the board's USB recovery or device mode from macOS without a VM and without passthrough? Native libusb on the Mac, a helper that owns the USB handshake, something else? How does it survive the USB reset or mode switch mid-flash that breaks the passthrough approach? Write the actual explanation here, not a summary. If there is any case where it still falls back to something Linux-side, say so in this section.]
+
+The provisioning flow itself, on the Mac:
+
+[ENGINEER: the provision command and its output, including the USB enumeration and the recovery or device-mode handshake. In the demo it prompts before overwriting the board, you confirm, and it streams the device output back to the host. Note the cable, the port, and any board prep on the Pi 5.]
+
+[ENGINEER: paste the first boot log from the Pi 5, so the provision is a verified result and not a description.]
+
+## What this gets you
+
+[ENGINEER: replace each line with a result from a real run. Nothing here is a claim until it is verified.]
+
+- The board provisioned from macOS with no VM and no USB passthrough. [ENGINEER: confirm, and on which Mac architecture.]
+- The reliability angle is the real claim, not speed. [ENGINEER: if you have a before and after, for example how many passthrough attempts dropped versus how many native runs completed cleanly, put the real counts here. A reliability claim needs repeated runs, not one success.]
+- This was a Raspberry Pi 5. Jetson uses a different and harder USB recovery flow and is supported but was not tested here. [ENGINEER: do not extend the USB win to Jetson unless you actually provisioned one.]
+
+## What didn't work
+
+This is a preview, and a note with no rough edges reads as a press release. [ENGINEER: required. Where did it break, stall, or surprise you? Likely spots: a specific macOS version or chip where it still misbehaves, a board or recovery mode it does not cover yet, permissions prompts on macOS, or a USB quirk it does not fully handle. If it genuinely did not break across your runs, say how many runs that was and what is still out of scope.]
+
+## Reproduce it
+
+Developer preview, macOS, Homebrew. [ENGINEER: the exact brew tap and install command, and whether early access is an open tap or gated by a signup or token. Include prerequisites such as Xcode command line tools and a minimum macOS version, then the step list to provision a Pi 5 from the Mac. Note the Mac architecture you verified on, since USB behavior differs between Apple Silicon and Intel.]
+
+Docs and the rest of the Peridio ecosystem are at [docs.peridio.com](https://docs.peridio.com). [ENGINEER: confirm the preview landing URL.]
diff --git a/src/field-notes/2026-06-17-pre-seeded-docker-cache.mdx b/src/field-notes/2026-06-17-pre-seeded-docker-cache.mdx
new file mode 100644
index 00000000..9072464d
--- /dev/null
+++ b/src/field-notes/2026-06-17-pre-seeded-docker-cache.mdx
@@ -0,0 +1,108 @@
+---
+title: 'Shipping a device that runs its containers on first boot, with no internet'
+description: "Avocado pre-pulls container images into the device's writable /var at build time, inside a temporary dockerd running inside the SDK container. The device boots offline and starts its stack on first power-on, no registry pull needed."
+date: 2026-06-17
+authors: [jschneck]
+tags: [var, docker, offline-first, build, provisioning]
+tested_against: 'Avocado [ENGINEER: version], SDK [ENGINEER: image:tag], [ENGINEER: board(s) verified — e.g. Raspberry Pi 5, Jetson Orin NX]'
+hn_title: "We pre-seed an embedded device's /var partition with its Docker cache at build time"
+poster: jschneck
+lift_for_blog: "The 'factory floor with no internet' problem: every Avocado device ships with its container images already in /var, so first power-on starts the stack without a registry pull."
+promote_to_track: 'Tutorial candidate after the Pi 5 bring-up: declaring docker_images on an extension and watching the cache come up populated on the device.'
+---
+
+import TestedAgainst from '@site/src/components/TestedAgainst'
+
+
+
+**TL;DR.** A long-standing pain in embedded is that a device booting on a factory floor, behind a customer firewall, or in the field can't reliably reach a container registry — so any stack that relies on `docker pull` to start is dead on arrival. We bake the image cache into the device's writable partition at build time, so on first power-on the containers are already there. [ENGINEER: confirm with a real first-boot log and the install-to-running-stack time on the board you used.]
+
+{/* truncate */}
+
+## Why this is hard
+
+If your device runs containers (and most embedded products do now), there's a window between first power-on and the moment your application starts. The conventional path is: device boots, network comes up, `docker pull`, containers start. Each step is a place the bring-up can stall — and "network comes up" is the one outside your control. Customers have firewalls. Factories don't have WiFi. Cellular modems take their time. And nothing makes a field engineer happier than a brand-new device that won't run its app because it can't reach `registry-1.docker.io`.
+
+Orientation for anyone seeing this for the first time: Avocado OS is the immutable embedded Linux runtime we ship at Peridio as a binary distribution, for boards like the Raspberry Pi 5 and NVIDIA Jetson. An Avocado image has two storage areas — a read-only root assembled from extension images, and a writable `/var` partition that survives OTA updates. The trick this note is about is **pre-populating `/var` at build time** so the device ships with everything it needs on first boot, including its container image cache.
+
+## How it works
+
+You declare the images you want on an extension:
+
+```yaml
+extensions:
+ my-app:
+ version: '1.0.0'
+ docker_images:
+ - image: docker.io/library/redis
+ tag: '7-alpine'
+ - image: docker.io/library/nginx
+ tag: '1.25'
+ var_files:
+ - var/lib/docker/** # keep this out of the read-only .raw
+```
+
+During `avocado build`, the CLI sees `docker_images` on an extension and:
+
+1. Adds `--privileged` to the SDK container args (required for Docker-in-Docker)
+2. Starts a temporary `dockerd` _inside_ the SDK container, with its data-root pointed at the staging area that will become the device's `/var`
+3. Pulls each declared image for the target architecture (`linux/arm64`, `linux/amd64`, …)
+4. Shuts down the temporary `dockerd`
+5. Packs the now-populated Docker data-root into the var partition image
+
+The `var_files: var/lib/docker/**` on the extension is the other half of the trick. Without it, `avocado ext image` would helpfully bake the Docker storage directory into the read-only erofs image, which is exactly what you do _not_ want — Docker needs to write to that directory on the device. The exclusion tells `ext image` to leave it out of the read-only `.raw` so the writable copy on `/var` is the one Docker sees.
+
+[ENGINEER: paste the relevant snippet of `avocado build` output that shows the dockerd-in-SDK pull happening, and the line where it finalizes the var partition image.]
+
+## On the device
+
+[ENGINEER: paste the first-boot log on the board you used. The thing that matters: `docker images` listing the pre-pulled images with no registry round-trip, and the application container starting cold. If you have an air-gap repro — pull the network cable, power on, watch it come up — include the timing.]
+
+## What this gets you
+
+- **Containers running on first power-on with no network.** [ENGINEER: confirm on a board you actually booted offline, and note install-to-running-stack time.]
+- **One build artifact, one provision step.** The image cache isn't a second deploy — it's part of the OS image and ships with it. Same `avocado provision` as everything else.
+- **Atomic OTAs that don't touch the cache.** Updates swap the read-only root; `/var` (including the Docker cache) persists across updates, so the cache is also a hedge against a partial OTA leaving you without container layers.
+
+## What's also worth knowing
+
+Same `var_files` mechanism on a runtime block lets you seed _any_ static file onto `/var` at build time — TLS certs, default app config, seed data for an embedded database, anything the device needs writable on first boot. Docker is just the loudest case because of the multi-gigabyte cache size and the cost of pulling it again.
+
+```yaml
+runtimes:
+ dev:
+ extensions: [my-app]
+ var_files:
+ - source: certs/device.pem
+ dest: lib/myapp/certs/device.pem
+ - source: config/app-defaults/
+ dest: lib/myapp/
+```
+
+The seeding-var guide in the docs has the full configuration reference: [Seeding the var partition](/developer-reference/seeding-var).
+
+## What didn't work
+
+[ENGINEER: required, and the most credible part of this note. Real failure modes I'd ask you to consider:
+
+- A glob in `var_files` that was too narrow and let Docker storage leak into the read-only `.raw`, which surfaces as a confusing erofs-write error at runtime rather than a build failure.
+- Multi-arch pulls picking the wrong platform when the SDK and target architecture don't match, especially on Apple Silicon hosts pulling `linux/arm64` into the SDK.
+- SDK images missing `dockerd` / `containerd` / `runc`, which only shows up the moment the privileged step kicks in.
+- The var partition staging size blowing past a sensible default and the build erroring out late.
+ Pick the one you actually hit and write the diagnosis + the fix. If nothing broke, say across how many runs and what you'd still expect to bite someone.]
+
+## Reproduce it
+
+```bash
+avocado install
+avocado build # var seeding (static + docker) happens here, no extra step
+avocado provision dev
+```
+
+The seeding runs as part of `avocado build` (specifically `avocado runtime build`) — there's no separate `avocado seed-var` command, and there shouldn't be. Full configuration reference: [Seeding the var partition](/developer-reference/seeding-var).
+
+Docs and the rest of the Peridio ecosystem are at [docs.peridio.com](https://docs.peridio.com).
diff --git a/src/field-notes/2026-06-18-jetson-deepstream-15s-boot.mdx b/src/field-notes/2026-06-18-jetson-deepstream-15s-boot.mdx
new file mode 100644
index 00000000..ff2d436b
--- /dev/null
+++ b/src/field-notes/2026-06-18-jetson-deepstream-15s-boot.mdx
@@ -0,0 +1,108 @@
+---
+title: 'Five-model DeepStream pipeline on a Jetson Orin Nano that boots to a live dashboard in 15 seconds'
+description: 'Five sequential AI models — PeopleNet, NvDCF tracker, MoveNet pose, YOLOX hand detection, MediaPipe hand landmarks — running natively on a Jetson Orin Nano. The trick to skipping the 12-minute first-boot TensorRT compile: ship the engines inside the OTA payload, not just the ONNX.'
+date: 2026-06-18
+authors: [jschneck]
+tags: [jetson, deepstream, tensorrt, ota, computer-vision]
+tested_against: 'Avocado [ENGINEER: version], DeepStream 7.1, JetPack [ENGINEER: version], [ENGINEER: Jetson Orin Nano DevKit and/or AGX Orin DevKit]'
+hn_title: 'We ship the TensorRT engines in the OTA so the Jetson DeepStream pipeline boots in 15s, not 12 min'
+poster: jschneck
+lift_for_blog: 'The 12-minute first-boot compile is the canonical DeepStream-on-Jetson pain. Pre-building engines per target and shipping them in the sysext (with size-compare invalidation across OTAs) is the part nobody else does, and the part that makes the demo a demo and not a wait.'
+promote_to_track: 'Tutorial candidate after the Pi 5 native-USB and the var-seeding notes: the multi-model Jetson reference, with the engine-shipping trick explained inline rather than left to the reader to discover.'
+---
+
+import TestedAgainst from '@site/src/components/TestedAgainst'
+
+
+
+**TL;DR.** A multi-model DeepStream pipeline on Jetson normally spends 6–12 minutes on first boot compiling TensorRT engines from ONNX before it can show anything. We build the engines once on the target hardware, commit them per-target into the reference repo, ship them inside the sysext, and have the device serve the live MJPEG dashboard ~10–15 seconds after power-on. [ENGINEER: confirm the boot-to-dashboard time you actually measured, and on which board.]
+
+{/* truncate */}
+
+## Why first boot is the bottleneck
+
+A TensorRT engine is a hardware-specific blob — pinned to the GPU's compute capability, SM count, memory hierarchy, and the exact CUDA + TRT version. The DeepStream-canonical pattern is to ship the ONNX model and let `nvinfer` build the engine on the device the first time the pipeline starts. Which is fine, except the build-time tactic search for a real model is _long_.
+
+For the reference's five-model stack, on an Orin Nano, with no pre-built engines:
+
+| Model | First-boot fallback build |
+| ---------------------------------------- | ------------------------- |
+| PeopleNet (ResNet34, FP16) | ~30–60 s |
+| MoveNet (single-pose Lightning) | ~2 min |
+| MediaPipe Hand Landmark (sparse 224×224) | ~60 s |
+| YOLOX-Body-Head-Hand (320×320, FP16) | ~6–7 min |
+
+That's 10–12 minutes of a brand-new device sitting there with no display, no logs the customer can read, no port 8080. "Did it boot? Did it brick?" Anyone who has demoed a Jetson model in front of an audience has felt that minute hand.
+
+Orientation for anyone new to this: Peridio makes Avocado OS, an immutable embedded Linux runtime shipped as a binary distribution. An Avocado image is composed of read-only **sysext** + **confext** extensions plus a writable `/var`. OTA updates atomically swap the read-only extensions; `/var` persists. This note is about putting the engines inside the sysext.
+
+## The shape of the fix
+
+Two-part trick. First, the reference repo carries a per-target directory of pre-built engines:
+
+```
+prebuilt-engines/
+├── jetson-orin-nano-devkit/{peoplenet,movenet,handdet,handlandmark}/*.engine
+└── jetson-agx-orin-devkit/{...}/*.engine
+```
+
+`app-compile.sh` selects the right subdirectory at build time based on the Avocado target and stages those engines into the sysext alongside the ONNX. The device boots, the preflight script copies each engine from the read-only sysext (`/usr/lib/nvidia-deepstream/models//`) to its writable `/var` neighbour, `nvinfer` mmaps the engine, and the pipeline hits PLAYING.
+
+Second part is OTA-bumping the engines without resetting state. The preflight script size-compares the sysext-shipped engine against the cached `/var` copy on every boot. If they differ — which is what happens after an OTA that includes new engines — the new engine overwrites the cached one and `nvinfer` loads the new engine on this boot. The sysext is authoritative; `/var` is a cache. This is the embedded-specific bit that matters: you can ship a new model via `avocado runtime deploy` without re-flashing and without making the device recompile.
+
+Regenerating engines after a JetPack bump is also straightforward: SSH into a device running the target hardware + matching JetPack/TRT, let the app run once so `nvinfer` builds engines into `/var/lib/nvidia-deepstream/models//`, `scp` them back to the host, drop them into the right `prebuilt-engines///` directory, commit, build, deploy. Next boot the new engines are live.
+
+## What's actually running
+
+The pipeline is denser than DeepStream's stock samples — five GIEs plus the tracker plus analytics, all natively on the device, no container indirection:
+
+```
+v4l2src → MJPEG decode → nvstreammux →
+nvinfer/primary (PeopleNet) → nvtracker (NvDCF) → nvdsanalytics (ROIs) →
+nvinfer/secondary (MoveNet, per-person) →
+nvinfer (YOLOX-Body-Head-Hand, full-frame) →
+nvinfer/tertiary (MediaPipe Hand Landmark, per-hand) →
+nvdsosd → nvjpegenc → appsink → MJPEG / Flask :8080
+```
+
+PeopleNet finds people. NvDCF assigns persistent IDs across frames. `nvdsanalytics` reads the tracker IDs against an ROI polygon and a line-crossing definition and writes operational counters back into the buffer at zero extra inference cost. MoveNet runs as a secondary GIE on each PeopleNet bbox and emits 17-point COCO skeletons via tensor metadata. A separate YOLOX-Body-Head-Hand runs on the full frame to find hands; MediaPipe Hand Landmark runs as a tertiary GIE on each hand crop and emits 21 finger keypoints. The bounding boxes, skeletons, hand keypoints, and zone overlays are all rasterised by `nvdsosd` into the same JPEG the dashboard serves — no client-side rendering.
+
+[ENGINEER: paste the engine load lines from `journalctl -u app -b` on first boot. They are the proof that this works.]
+
+## What this gets you
+
+[ENGINEER: replace the bracketed lines with your measurements, on the board you actually used.]
+
+- **Boot to dashboard in ~10–15 s** with engines in the sysext, vs ~10–12 min if `nvinfer` has to compile from ONNX. [ENGINEER: confirm timing on Orin Nano, and on AGX Orin if you ran both.]
+- **~15–25 fps end-to-end on Orin Nano** at 720p with 1–2 people in frame; AGX Orin pushes past 30. The pose and hand-landmark secondaries are the cost; `ENABLE_POSE=0` / `ENABLE_HANDS=0` via a systemd drop-in lets you measure detect-only vs detect-plus-pose head-to-head.
+- **OTA-able engine cache.** A new model lands as part of the next sysext; the preflight script picks it up on the next boot, no re-flash and no recompile.
+- **All five models running natively on the device, no DeepStream container.** DeepStream 7.1, TensorRT, CUDA, cuDNN, the NVIDIA GStreamer plugins, and Python all come from the Avocado package feed and compose into one root filesystem.
+
+## What didn't work
+
+[ENGINEER: required, and the most credible part of this note. Real failure modes a reader would benefit from hearing:
+
+- The MoveNet NHWC→NCHW rewrite. MoveNet's PINTO ONNX has an NHWC input layer; `nvinfer` in DS 7.1 reads NCHW. The reference does an `onnx.helper.Transpose` insertion in `app-compile.sh` to fix this. If you're swapping in a different pose model and you skip this step, the engine build fails with a shape error that's not obviously about layout.
+- Engine ABI invalidation across JetPack bumps. The cached `/var` engine is silently invalid after a JetPack upgrade unless the sysext also ships a fresh engine. The size-compare in the preflight catches this when sysext engines are refreshed; if you upgrade JetPack on the device without refreshing the engines, `nvinfer` will compile from ONNX on the next boot — which works, but bursts you back to the 6-minute YOLOX wait once.
+- USB cameras that only do YUYV (no MJPG). The default pipeline negotiates MJPEG; cameras that only do raw YUYV require swapping `image/jpeg + jpegdec` for `video/x-raw,format=YUY2 + videoconvert` in `_build_pipeline()`. Hits about half the cheap USB webcams.
+- ROI coordinates pinned to 1280×720. If you override the camera resolution via a systemd drop-in you also need to rescale the Center zone in `analytics_config.txt`, otherwise the ROI rectangle ends up mostly off-screen and the dwell counters stay at zero.
+
+Pick the failure mode you actually hit on your run and write the real diagnosis + the fix. If you didn't hit any of these, say across how many runs and on what hardware.]
+
+## Reproduce it
+
+```bash
+avocado init --reference nvidia-deepstream nvidia-deepstream
+cd nvidia-deepstream
+avocado install -f
+avocado build
+avocado provision -r dev --profile tegraflash
+```
+
+Plug a UVC USB webcam (MJPEG at 1280×720, 30 fps works out of the box — Logitech C920/C270 confirmed), wait for boot, hit `http://:8080` for the live dashboard with bounding boxes, tracker IDs, skeletons, hand keypoints, and ROI dwell counters. Full reference repo: [avocado-linux/references/nvidia-deepstream](https://github.com/avocado-linux/references/tree/main/nvidia-deepstream); full step-by-step in the docs: [NVIDIA DeepStream reference](/developer-reference/references/nvidia-deepstream).
+
+Docs and the rest of the Peridio ecosystem are at [docs.peridio.com](https://docs.peridio.com).
diff --git a/src/field-notes/CONTRIBUTING.md b/src/field-notes/CONTRIBUTING.md
new file mode 100644
index 00000000..500c39be
--- /dev/null
+++ b/src/field-notes/CONTRIBUTING.md
@@ -0,0 +1,93 @@
+# Contributing to Field Notes
+
+## Purpose
+
+Field Notes is the surface where a skeptical embedded engineer sees real, reproducible proof that Avocado OS does the hard things — and it's the raw source that feeds HN, Reddit, and everything we repurpose downstream.
+
+Field Notes is _true_. The blog is _pretty_. Don't mix them up.
+
+## Goals
+
+- **Build engineer trust and create internal champions** — be where an IC sees the proof and decides to bring us in.
+- **Demonstrate the product doing hard things, reproducibly** — demos and hands-on guides (flashing containers on a Jetson, OTA rollback, JetPack migration) an engineer can run themselves.
+- **Be the primary-source artifact for the engineer channels** — written to go raw to HN/Reddit and to rank for technical long-tail.
+- **Feed the rest of the system** — the source material marketing lifts into blog narratives and that gets promoted into the curated track.
+
+## In scope
+
+- Technical developer content.
+- Raw demos (clips and terminal casts).
+- Reproducible how-tos and standalone guides (e.g. "flashing containers on Jetson").
+- "What we tried, what broke" experiments.
+
+## How to write a note
+
+Start by copying `_template.mdx`:
+
+```bash
+cp _template.mdx 2026-MM-DD-short-slug.mdx
+```
+
+The filename must start with `YYYY-MM-DD-` so it sorts correctly and the date matches frontmatter.
+
+## Frontmatter fields
+
+| Field | Required | Rendered | Purpose |
+| ------------------ | --------------- | --------------------------- | ------------------------------------------------------------------------------- |
+| `title` | yes | yes | The note title |
+| `date` | yes | yes | The dated contract — "true as of this date" |
+| `authors` | yes | yes | Real engineer keys from `authors.yml` |
+| `tags` | yes | yes | Target (`orin-nx`, `imx`, `rpi5`…) + topic (`ota`, `yocto`, `security`, `mcp`…) |
+| `tested_against` | when applicable | yes (via ``) | Avocado + JetPack/board versions |
+| `hn_title` | no | **no** | Suggested HN submission title |
+| `poster` | no | **no** | Engineer who submits to HN and is on-call in comments for ~24h |
+| `lift_for_blog` | no | **no** | One-line business-case angle for the blog (champion → approver) |
+| `promote_to_track` | no | **no** | Flag + note if this is curated-track on-ramp material |
+
+`hn_title`, `poster`, `lift_for_blog`, and `promote_to_track` are editorial metadata. They never render to readers, but they live in frontmatter so the team can grep them. Run from `src/field-notes/`:
+
+```bash
+grep -l "^hn_title:" *.mdx
+grep -l "^promote_to_track: \"[^\"]" *.mdx
+```
+
+## HN-readiness rules
+
+- **TL;DR first.** Two sentences: what you did, what happened. Above everything else.
+- **Cold-reader intro.** Assume the reader has never heard of Peridio or Avocado. Two or three sentences orient a stranger, then go dense.
+- **Result-first title.** Concrete and specific. "We cut power 200 times mid-update on a Jetson. Here's what bricked." beats "Improving OTA resilience on embedded Linux."
+- **No hype.** No "revolutionary," no "blazing fast," no "10x."
+- **Include what didn't work.** Honesty about dead ends is the single biggest credibility signal on HN.
+
+## Posting to HN / Reddit / Lobsters
+
+- **Disclose.** When you submit, state plainly in the first comment: "I work on this." Non-negotiable. It prevents the astroturf backlash that buries an otherwise good post.
+- **Be on-call for ~24h.** The `poster` is the engineer who submits _and_ answers comments. Don't post and disappear.
+- **Select, don't spray.** HN throttles domains that self-submit too often and flags obvious marketing. Post only genuinely strong notes; let the rest live in the feed. A few great ones a quarter beats a steady drip.
+
+## Light review gate
+
+Before merging a note, one rotating engineering owner confirms:
+
+1. The reproducible core actually reproduces on a fresh setup.
+2. The versions and tags in `tested_against` are accurate.
+
+This is a correctness check, not editorial polish. Field Notes is true; the blog is pretty.
+
+## Drafts and the `[ENGINEER: ...]` convention
+
+The `/field-notes` section is gated behind a `?preview=1` URL flag (see `src/clientModules/fieldNotesGate.js` and `src/plugins/field-notes-preview-gate/`). Visitors without the flag are synchronously redirected to `/` before any content paints; the section also ships `` on every page and no RSS/Atom/JSON feeds in production. So a note can live on `main` with unfinished sections and only the reviewers who pass `?preview=1` will see it.
+
+We use this on purpose. A note checked in with `[ENGINEER: paste the boot log here]` placeholders is a structured draft — the framing and the angle are committed, the engineer who runs the demo fills in the verified content. That review is the whole point of the gate:
+
+- The author drafts the framing, TL;DR, mechanism, and the section structure.
+- Placeholders mark every claim that needs a real measurement, command output, or honest "what didn't work" sentence.
+- A reviewer opens `docs.peridio.com/field-notes?preview=1`, reads the draft top-to-bottom, and the placeholders are the conversation: "do we actually have this number?", "is that the failure mode you hit?", etc.
+- The engineer who ran the demo replaces placeholders with the real content and re-pushes.
+- The draft only becomes "ready" when no `[ENGINEER: ...]` placeholders remain. Grep before merging promotion (when we eventually drop the gate):
+
+ ```bash
+ grep -l "\[ENGINEER:" *.mdx
+ ```
+
+We deliberately don't use Docusaurus' `draft: true` frontmatter for these. That flag excludes posts from production builds entirely, which would also exclude them from the gated preview — defeating the whole "share `?preview=1` with the reviewer" flow. The gate is the draft mechanism.
diff --git a/src/field-notes/_template.mdx b/src/field-notes/_template.mdx
new file mode 100644
index 00000000..f7c39620
--- /dev/null
+++ b/src/field-notes/_template.mdx
@@ -0,0 +1,38 @@
+---
+title: ''
+description: '' # one-line dek; shown under the title on the index
+date: YYYY-MM-DD
+authors: [] # keys from authors.yml
+tags: [] # target + topic
+tested_against: '' # free text for greppability; also pass to the component below
+hn_title: '' # suggested HN submission title; concrete, result-first, no hype
+poster: '' # engineer who posts + answers comments for ~24h
+lift_for_blog: '' # one-line business-case angle (champion → approver)
+promote_to_track: '' # on-ramp candidate? note the target tutorial
+---
+
+import TestedAgainst from '@site/src/components/TestedAgainst'
+
+
+
+**TL;DR.** Two sentences: what you did, what happened.
+
+## What this is
+
+Assume the reader has never heard of Peridio or Avocado. Orient a stranger in 2–3 sentences, then go dense.
+
+## How it works / what we did
+
+The meat: commands, config, output.
+
+## What we learned
+
+The payoff. The result that matters.
+
+## What didn't work
+
+Required when there were dead ends. Honesty here is the single biggest credibility signal on HN.
+
+## Reproduce it
+
+Steps, or a link to a runnable repo/gist. Include whenever the result can be reproduced.
diff --git a/src/field-notes/authors.yml b/src/field-notes/authors.yml
new file mode 100644
index 00000000..2073f59a
--- /dev/null
+++ b/src/field-notes/authors.yml
@@ -0,0 +1,5 @@
+jschneck:
+ name: Justin Schneck
+ title: CTO & creator of Avocado OS
+ url: https://github.com/mobileoverlord
+ page: true
diff --git a/src/plugins/field-notes-preview-gate/index.js b/src/plugins/field-notes-preview-gate/index.js
new file mode 100644
index 00000000..9b05146c
--- /dev/null
+++ b/src/plugins/field-notes-preview-gate/index.js
@@ -0,0 +1,61 @@
+/**
+ * postBuild hook that injects
+ * into every /field-notes HTML page, so crawlers that don't execute the JS
+ * redirect still get the no-index signal.
+ *
+ * The RSS/Atom/JSON feeds are cleaned up by build.sh after docusaurus build
+ * completes — postBuild hooks run concurrently across plugins, so this
+ * plugin can't reliably see the feed files written by the blog plugin's
+ * own postBuild.
+ *
+ * Pair this with src/clientModules/fieldNotesGate.js (URL/session flag +
+ * body class) and the inline head-tag script in docusaurus.config.js
+ * (synchronous, flash-free redirect). Remove all four pieces when
+ * Field Notes goes public.
+ */
+
+const fs = require('fs')
+const path = require('path')
+
+const NOINDEX_META = ''
+
+function injectNoindex(filePath) {
+ let html = fs.readFileSync(filePath, 'utf8')
+ // Replace any existing robots meta (e.g. an index,follow one another plugin
+ // might emit) rather than skipping — we want noindex,nofollow to win.
+ const existing = /]+name=["']robots["'][^>]*>/i
+ if (existing.test(html)) {
+ html = html.replace(existing, NOINDEX_META)
+ } else {
+ html = html.replace('', `${NOINDEX_META}`)
+ }
+ fs.writeFileSync(filePath, html)
+}
+
+function walkAndInject(dir) {
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
+ const full = path.join(dir, entry.name)
+ if (entry.isDirectory()) {
+ walkAndInject(full)
+ } else if (entry.name.endsWith('.html')) {
+ injectNoindex(full)
+ }
+ }
+}
+
+module.exports = function fieldNotesPreviewGatePlugin() {
+ return {
+ name: 'field-notes-preview-gate',
+ async postBuild({ outDir }) {
+ const fieldNotesDir = path.join(outDir, 'field-notes')
+ if (fs.existsSync(fieldNotesDir) && fs.statSync(fieldNotesDir).isDirectory()) {
+ walkAndInject(fieldNotesDir)
+ }
+
+ const listPage = path.join(outDir, 'field-notes.html')
+ if (fs.existsSync(listPage)) {
+ injectNoindex(listPage)
+ }
+ },
+ }
+}
diff --git a/src/scripts/build.sh b/src/scripts/build.sh
index a2dec996..32561314 100755
--- a/src/scripts/build.sh
+++ b/src/scripts/build.sh
@@ -8,3 +8,9 @@ mkdir -p static/schemas
cp schemas/avocado-config.json static/schemas/
docusaurus build
+
+# Field Notes preview gate: drop the public feeds for the gated section.
+# Companion to plugins/field-notes-preview-gate (noindex meta) and the
+# clientModule + inline head script (URL/session flag redirect).
+# Remove this block when Field Notes goes public.
+rm -f build/field-notes/rss.xml build/field-notes/atom.xml build/field-notes/feed.json
diff --git a/src/src/clientModules/fieldNotesGate.js b/src/src/clientModules/fieldNotesGate.js
new file mode 100644
index 00000000..298b7002
--- /dev/null
+++ b/src/src/clientModules/fieldNotesGate.js
@@ -0,0 +1,52 @@
+/**
+ * Soft gate for /field-notes. Companion to the inline head-tag script in
+ * docusaurus.config.js — that script handles the synchronous flash-free
+ * redirect; this module handles the body class that toggles the navbar
+ * link's visibility and persists the flag across client-side navigation.
+ *
+ * Activation: visit any URL with ?preview=1 (or visit a path under
+ * /field-notes?preview=1). Stored in sessionStorage.
+ * Deactivation: ?preview=0, or close the tab.
+ */
+
+const FLAG_KEY = 'peridio:fn-preview'
+const ATTR = 'data-show-field-notes'
+
+function readAndPersist() {
+ if (typeof window === 'undefined') return false
+ try {
+ const params = new URLSearchParams(window.location.search)
+ const param = params.get('preview')
+ if (param === '1') {
+ window.sessionStorage.setItem(FLAG_KEY, '1')
+ return true
+ }
+ if (param === '0') {
+ window.sessionStorage.removeItem(FLAG_KEY)
+ return false
+ }
+ return window.sessionStorage.getItem(FLAG_KEY) === '1'
+ } catch (e) {
+ return false
+ }
+}
+
+function apply() {
+ if (typeof document === 'undefined') return
+ // Use a data attribute, not a class — react-helmet-async resets the
+ // root className on every render, which would wipe a class we added.
+ // Helmet only manages className, not data attributes.
+ if (readAndPersist()) {
+ document.documentElement.setAttribute(ATTR, '1')
+ } else {
+ document.documentElement.removeAttribute(ATTR)
+ }
+}
+
+if (typeof window !== 'undefined') {
+ apply()
+}
+
+export function onRouteUpdate() {
+ apply()
+}
diff --git a/src/src/components/TestedAgainst/index.jsx b/src/src/components/TestedAgainst/index.jsx
new file mode 100644
index 00000000..0fbc5750
--- /dev/null
+++ b/src/src/components/TestedAgainst/index.jsx
@@ -0,0 +1,29 @@
+import React from 'react'
+import styles from './styles.module.css'
+
+export default function TestedAgainst({ avocado, jetpack, boards = [] }) {
+ const parts = []
+ if (avocado) parts.push(Avocado {avocado})
+ if (jetpack) parts.push(JetPack {jetpack})
+ if (boards.length > 0) {
+ parts.push(
+
+ {boards.join(', ')}
+
+ )
+ }
+
+ if (parts.length === 0) return null
+
+ return (
+