Skip to content

Jetson edge-compute spec

The Jetson Orin Nano Super Dev Kit (8GB, 67 TOPS, 1024 CUDA cores) is the inference + recording + dashboard box for Phase I. It must boot headless, auto-recover from process crashes and power events, expose itself only through Tailscale, decode 2 concurrent H.264 sub-streams via NVDEC, run YOLOv8n in TensorRT (person class only), write events to SQLite, serve a Flask/FastAPI dashboard on :5000, and segment-record video to a 4TB NVMe for ~120 days of rolling history.

Both Jetsons (LBZF + ITBA twin) are pre-flashed in California per ADR-006 before the 2026-05-15 BA flight — once the LBZF unit lands in Pereira, the only inbound channel is Tailscale, so every “what happens when X breaks” path must be in place before takeoff.

ItemChoiceNotes
SBCNVIDIA Jetson Orin Nano Super Developer Kit (8GB)MAXN power profile for super-mode 67 TOPS
CarrierBundled with dev kitThe bare module variant is harder to deploy — verify dev-kit SKU on box
EnclosureWaveshare aluminum Case A (Amazon B0CG38BS5S)Passive — fan NOT in the box. Add Waveshare PWM fan B0C1TYVDTF + low-profile M.2 NVMe heatsink + thermal pad
Primary storageSamsung 990 EVO Plus 4TB NVMe (M.2 2280, PCIe Gen4/5)Trains down to Gen3 x4 on the Orin carrier — fine
Boot mediaSanDisk 64GB Extreme microSDInitial JetPack boot only; migrate root to NVMe via jetsonhacks/bootFromExternalStorage after first boot
PowerIncluded 19V barrel-jack supplyDo NOT power via USB-C — community reports of brownout / recovery-mode failures over USB-C
InputLogitech MK120 wired keyboard/mouseCA bring-up only; runs headless after deploy
ItemChoiceWhy
OSJetPack 6.0 or 6.1 (Ubuntu 22.04 LTS arm64)NOT 6.2 — JetPack 6.2 ships TensorRT 10.x which currently breaks INT8 quantization for YOLOv8 per Ultralytics docs. 6.0/6.1 ships TRT 8.6 (ADR-003)
Hostnamelbzf-jetson-01 (LBZF) / itba-jetson-01 (ITBA twin)Tailscale auto-uses hostname
Default userlbzf (LBZF) / itba (ITBA twin); delete nvidia / ubuntu after first bootFactory-default nvidia is a brute-force magnet
Localeen_US.UTF-8 system locale; dashboard strings localized to Spanish in the frontend bucketLogs in English, UI in ES
Time zoneAmerica/Bogota (LBZF) / America/Argentina/Buenos_Aires (ITBA)Cycle-event timestamps must match local whiteboard hora

Root partition lives on the NVMe (not microSD); the microSD is recovery-only after the post-first-boot migration.

MountDeviceFSOptionsSizePurpose
//dev/nvme0n1p1ext4defaults~50 GBOS, packages, app code
/var/log/dev/nvme0n1p2ext4noatime~20 GBJournal + app logs, rotated
/data/dev/nvme0n1p3ext4noatime,lazytime~3.9 TBRolling video buffer + SQLite + Excel exports
swap/swapfileswapfile-backed8 GBPredictable I/O — avoid zram for Phase I

Filesystem choice: ext4 with noatime on /data. F2FS has weaker unclean-shutdown recovery — bad fit for factory power. The rolling video buffer is pre-allocated as a ring of fixed-size files rather than growing organically; this keeps free-space behavior predictable and lets the eviction script (below) operate by oldest-file-first without surprising the underlying FS.

/data/
video/
cam1/YYYY/MM/DD/cam1_HHMMSS.mp4
cam2/...
db/
lbzf.sqlite # canonical store
lbzf.sqlite-wal # SQLite WAL mode (concurrent dashboard reads safe)
exports/
INDICADORES_YYYY-MM.xlsx
models/
yolov8n.pt # source weights
yolov8n.engine # TensorRT engine (Jetson-specific; rebuilt per device)
logs/
pipeline.log

System packages (apt):

  • python3.11, python3.11-venv, python3-pip
  • GStreamer: gstreamer1.0-tools gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav
  • NVIDIA GStreamer plugins ship with JetPack (libgstnvvideo4linux2.so provides nvv4l2decoder)
  • tailscale from the official Tailscale apt repo, pinned to a known version
  • sqlite3, tmux, htop, iotop, nvtop, ufw
  • unattended-upgrades — security patches only, never unattended kernel jumps

OpenCV with GStreamer is non-negotiable. Default JetPack OpenCV often ships without GStreamer support, which silently breaks the entire RTSP pipeline. Either:

  • Rebuild OpenCV from source with -DWITH_GSTREAMER=ON, or
  • Use a Q-engineering prebuilt wheel.

Verify with:

import cv2; print(cv2.getBuildInformation())

and grep for GStreamer: YES. The G1 gate (10-gates/G1-argentina-demo.md) has this as a check.

Python packages (pip into /opt/lbzf/venv):

  • ultralytics (pinned to a version bench-tested against TensorRT 8.6 on JetPack 6.0/6.1)
  • flask or fastapi+uvicorn (Phase I default = Flask)
  • pandas, openpyxl
  • tensorrt (pre-installed via JetPack; verify import tensorrt works with --system-site-packages in the venv)

The TensorRT engine is hardware-bound — an engine compiled on a MacBook will not run on a Jetson, and an engine compiled on the LBZF Jetson is not guaranteed to run on the ITBA Jetson even though they’re the same SKU. Compile on each device individually (ADR-003, ADR-006).

  1. Copy yolov8n.pt to /data/models/ on the target Jetson.

  2. Export to TensorRT on the Jetson itself:

    Terminal window
    yolo export model=/data/models/yolov8n.pt format=engine device=0 half=True
  3. Verify FP16 inference on a sample frame; record per-frame latency. Phase I targets per-frame inference < 50ms under concurrent 2-stream load.

Per-camera pipeline (one process or thread per stream):

rtspsrc location=rtsp://<user>:<pwd>@<cam-ip>:554/cam/realmonitor?channel=1&subtype=1
→ rtph264depay
→ h264parse
→ nvv4l2decoder
→ appsink

Amcrest exposes RTSP at /cam/realmonitor?channel=1&subtype=1 (see network-and-tailscale.md for the full URL pattern and credentials). Sub-stream presets are 704×480 / 352×240 / CIF / QCIF; 640×480 is NOT a preset. Ingest the closest preset and downscale on the Jetson to whatever the model expects (ADR-004).

Inference runs at 3–5 fps, downscaled on the Jetson from the ingested sub-stream (revised down from the v1.9 plan per ADR-004). Sewing-workstation cycles are 10–90 seconds long; 3–5 fps gives 5–25 frames per cycle, plenty for “person enters ROI → person leaves ROI” with hysteresis, and keeps the Jetson well below thermal throttling and NVMe TBW concerns.

The same RTSP stream is tee’d to a recording branch using splitmuxsink (10-minute segments). Phase I records the sub-stream only; main-stream recording is deferred to Phase II forensic playback.

File: /etc/systemd/system/lbzf-monitor.service

[Unit]
Description=LBZF CV monitor (capture + inference + recording + dashboard)
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
Type=simple
User=lbzf
Group=lbzf
WorkingDirectory=/opt/lbzf/app
ExecStart=/opt/lbzf/venv/bin/python -m lbzf.main
Restart=always
RestartSec=10
StandardOutput=append:/data/logs/pipeline.log
StandardError=append:/data/logs/pipeline.log
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/data /var/log
ProtectHome=yes
PrivateTmp=yes
MemoryMax=6G
TasksMax=512
[Install]
WantedBy=multi-user.target

A separate lbzf-dashboard.service runs the Flask app so a dashboard crash doesn’t take down recording. If the backend bucket consolidates them, document the rationale there.

Three layers:

  1. systemd Restart=always — process-level crash recovery, 10s backoff.
  2. Hardware watchdog — enable Jetson’s /dev/watchdog with a 60s timeout; systemd’s RuntimeWatchdogSec=30s in /etc/systemd/system.conf triggers a reset if PID 1 hangs.
  3. /healthz endpointlbzf-monitor returns 200 if all cameras are streaming and the last cycle event is within 2× max_expected_cycle_time per camera (not globally — SAM varies per station). A 60s cron polls; 3 consecutive non-200s calls systemctl restart lbzf-monitor.

A lbzf-eviction.timer fires every 10 minutes and runs /opt/lbzf/bin/evict.py:

THRESHOLD = 0.92
TARGET = 0.80
while disk_usage('/data') > THRESHOLD:
oldest = sorted(glob('/data/video/cam*/**/*.mp4'), key=os.path.getmtime)[0]
os.remove(oldest)
if disk_usage('/data') < TARGET: break

Tunables in /etc/lbzf/eviction.conf. SQLite + exports are never evicted; only video segments under /data/video/ are eligible.

lbzf-monitor issues PRAGMA wal_checkpoint(TRUNCATE) once per hour so the -wal sidecar doesn’t grow unbounded.

systemd-timesyncd enabled, pointing to:

  • LBZF: 0.co.pool.ntp.org, 1.co.pool.ntp.org, time.cloudflare.com (fallback)
  • ITBA: 0.ar.pool.ntp.org, 1.ar.pool.ntp.org, time.cloudflare.com

Verify with timedatectl status after first boot.

  1. Delete factory nvidia / ubuntu accounts after lbzf / itba is created.
  2. Strong passphrase in 1Password and key-based SSH only. Sophia + Andrew public keys baked into ~lbzf/.ssh/authorized_keys; ITBA team keys baked into ~itba/.ssh/authorized_keys per argentina-twin-set.md.
  3. /etc/ssh/sshd_config:
    • PasswordAuthentication no
    • PermitRootLogin no
    • AllowUsers lbzf (or itba)
  4. ufw default deny inbound; allow:
    • tailscale0 interface (everything from Tailscale, per ADR-005)
    • eth0 from 192.168.1.0/24 on TCP 5000 (dashboard, local LAN)
    • Outbound RTSP to the Amcrest camera VLAN
  5. unattended-upgrades with Unattended-Upgrade::Automatic-Reboot "false" — no 4am reboot during a shift.

Per ADR-005, Tailscale is the only remote-access surface for Phase I.

LBZF:

Terminal window
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --ssh --hostname=lbzf-jetson-01 --advertise-tags=tag:lbzf-jetson

ITBA twin (installed, NOT yet authenticated — ITBA brings it up on first boot in BA):

Terminal window
sudo tailscale up --ssh --hostname=itba-jetson-01 --advertise-tags=tag:itba-dev

Mark both devices “Key expiry: never” in the Tailscale admin console — the default 180-day expiry would silently disconnect the Jetson and recovery would be a flight.

  • Network (network-and-tailscale.md): IP plan, RTSP URL pattern, Tailscale ACLs.
  • Backend bucket: the actual lbzf.main module, the /healthz contract, the dashboard binding.
  • Frontend bucket: dashboard rendering on :5000, Spanish localization.
  • Business bucket: confidentiality — video files contain identifiable operators; 120-day retention and Confidencial — Uso Interno classification are enforced via the Tailscale ACL.

Tracked at the G1 gate. Summary:

  1. Pre-flight (CA, before 2026-05-15) — flash both Jetsons with JetPack 6.0/6.1; migrate root to NVMe; build OpenCV with GStreamer; compile TensorRT engine on each Jetson; smoke-test against one Amcrest camera.
  2. 2026-05-15 — Sophia + Armando fly CA → BA with both Jetsons in carry-on.
  3. Pereira install (July 2026) — LBZF Jetson goes live on site (see install-runbook.md).