Radiant Core

Glow 2027 Project Knowledge Base — Cary, NC Post Office plaza

Wes Swain / Geeksmithing LLC
Thure / When Geeks Craft
Updated: May 2026
01 — Project overview

Radiant Core is a temporary interactive art installation for Glow 2027 in Cary, NC. It transforms passive observers into active participants through motion, voice, and presence detection driving synchronized LED displays and generative animations.

DetailValue
FestivalGlow 2027, Cary, NC — Post Office plaza
ArtistsWes Swain / Geeksmithing, LLC + Thure / When Geeks Craft
BackgroundPac-Man LED Matrix Lantern (Jan 2025) — WLED + ESP32 experience
Artistic intentPrompt questions about the relevance of the humble pixel in modern life and our relationship with it
02 — Physical structure

Overall dimensions

  • Total height: 7.5 ft
  • Base: trapezoidal, 7 ft wide, 2" steel tubing, ~1,562 lbs — Config B, wind load verified
  • Outer cube: 5 ft cuboid framework atop the base
  • Inner cube: 3 ft solid cube, suspended diagonally inside outer cube

LED display surfaces

  • 4 outward-facing sides of inner 3 ft cube: P10 LED matrix panels
  • Outer 5 ft cube: LED strips tracing perimeter of each of 4 outward-facing sides — 16 lengths total, picture-frame style on each cardinal face
  • No strips on frame edges or diagonal struts

Panel layout — per face

SpecValue
Grid per face3 wide × 6 tall = 18 panels
Resolution per face96 × 96 pixels
Face dimension960mm × 960mm — designed to 960mm (~45mm over 3 ft / 915mm)

3D printed pixel isolation grids

  • Material: matte black PETG or ASA — UV stable, no PLA outdoors
  • 1:1 true pixel: 10mm cell per LED — ~80+ hrs print time for 4 faces
  • 2×2 chunky pixel: 20mm cell — retro look, bold at distance, best crowd appeal
  • Plan: mix styles across faces — each face different character per artistic intent
03 — LED technology

Matrix panels — P10 (confirmed, all faces)

P10 (10mm pitch) is the only panel choice. Finer pitches (P6, P4, P3) are not viable — 3D-printed pixel isolation grids become impractical below 10mm cell size.

SpecValue
Pixel pitch10mm
Panel size320mm × 160mm (32×16 px)
Resolution per face96×96 pixels
Panels required72 panels (4 faces × 18)
Panels ordered80 panels (72 + 8 spares)
Panel cost$13/panel incl. shipping — $1,040 total for 80
Weather ratingIP65 outdoor
InterfaceHUB75 — driven by Colorlight 5A-75E cards

Matrix driver cards — Colorlight 5A-75E (confirmed)

5A-75E vs 5A-75BThe 5A-75E is the current model — same price, 32 RGB data groups vs 16 on older 75B, higher pixel capacity. Always buy 75E.
SpecValue
Quantity4 cards (one per face)
HUB75 ports16× onboard — no adapter boards needed
Max pixels per card256×1024 — vastly exceeds 96×96
RGB data groups32 (vs. 16 on older 75B)
Refresh rate240Hz
InputGigabit Ethernet — receives DDP/E1.31 from Jetson
Cost estimate~$15–25/card — ~$60–100 for 4

LED strips — outer frame faces

  • 16 lengths total — 4 per outward-facing side of the 5 ft outer cube, picture-frame style
  • Total run: ~20m (16 × ~1.25m per side)
  • SK6812 RGBW 5V — recommended. WLED native, white channel for better glow.
  • WS2812B 5V — familiar fallback from Pac-Man build
  • Controllers: ESP32 nodes running WLED — permanent display layer

Storage — NVMe per Jetson

  • 500GB M.2 2280 M-key NVMe per Jetson unit — confirmed slot: M.2 Key M with x4 PCIe Gen3
  • Do not use microSD for production — slow random I/O, corruption risk at festivals
  • Recommended: WD Black SN770 500GB or Kingston NV2 500GB — ~$35–40/unit
  • 500GB provides headroom for raw camera logging for offline analysis or model retraining
04 — Compute architecture
DECISION: Jetson Orin Nano(s) + Colorlight 5A-75E + 4× USB webcams

Confirmed components

  • Central compute: 1 or 2× Jetson Orin Nano modules (~$249 each) — real CUDA, 1024-core Ampere GPU, 40 TOPS
  • Storage: 500GB M.2 NVMe per unit (~$35–40 each)
  • Matrix drivers: Colorlight 5A-75E (one per face, four total) — receive DDP/E1.31 UDP from Jetson(s)
  • Strip controllers: ESP32 + WLED — permanent display layer, receive E1.31
  • Cameras: 4× Logitech C920/C922 USB webcams — one per face, Sony IMX sensor, native MJPEG hardware compression
  • Camera upgrade path: OAK-D offloads 100% of CV compute from Jetson GPU if needed — strong contingency

1 Jetson vs. 2 Jetsons — open question (pending benchmark)

The 2-Jetson split is increasingly appealing. Full 4-face synchronized animation is not a hard requirement — face-independent interaction may serve the artistic intent better anyway. Decision deferred until benchmark results on first unit.

1 Jetson ($249)2 Jetsons ($498)
Camera load4 cameras, 4 async threads2 cameras each — more headroom
Inference headroomTight if all models activeComfortable — half the load each
Cross-face syncTrivial — shared memoryNeeds MQTT/UDP coordination
Full sync animationEasyHarder — inter-unit latency
Whisper / LLMOne instanceOne unit, results shared via MQTT
Failure modeSingle point of failureOne unit fails, others continue
ComplexitySimplerMore config + MQTT glue

Compute tier reference (historical)

TierHardwareCostStatus
1 — ESP32ESP32 + WLED$25–50/nodeRetained — display layer
2 — Pi 5Raspberry Pi 5 8GB$200–240SKIPPED
3 — JetsonJetson Orin Nano$249/unitCONFIRMED — 1 or 2 units
4 — Full PCMini PC + GPU$900–1,400Deferred — post-Glow if TD needed
05 — Software stack

Display / LED

  • Colorlight 5A-75E: receives DDP/E1.31 UDP from Jetson, drives P10 HUB75 panels directly
  • WLED on ESP32: drives LED strips, receives E1.31 from Jetson
  • Python sacn / python-e131: Jetson sends frame data via UDP to all display nodes

AI / Vision (phase 2 — deferred until visual pipeline validated)

LLM / Whisper stack deferred. Validate the visual pipeline on hardware first. AI layer is additive — it does not affect the core LED interaction system.
  • TensorRT: YOLOv8n-pose (nano variant), FP16, exported on-device — primary inference engine
  • MediaPipe: fallback / supplement for face and hand landmarks
  • Whisper.cpp: local STT — model size TBD pending Orin Nano benchmark (Large may be too heavy)
  • Ollama (Phi-3 Mini): local LLM, ~1s response, structured JSON output — deferred
  • Piper TTS: local text-to-speech for compliment engine — deferred

Orchestration / Glue

  • Python 3.11+ asyncio: non-blocking camera + audio + LED pipelines
  • MQTT: pub/sub — "Presence on north face" → all nodes react
  • FastAPI: REST + WebSocket on Jetson — control panel and sensor routing (on isolated CPU core)
  • librosa: beat detection, FFT, RMS for crowd noise / music reactivity

Remote monitoring — Particle Boron LTE-M

Cellular health monitoring independent of festival Wi-Fi. Jetson publishes status JSON to Boron over USB serial every 60s; Boron forwards to Particle Cloud via LTE-M. Webhook to Discord/Slack for alerts.

FieldSourceNotes
uptime_sJetson systemSeconds since last boot
cpu_temp_c / gpu_temp_cJetson thermalAlert threshold ~85°C
cpu_load_pct/proc/stat
visitor_countOrchestratorRunning total since boot
current_modeOrchestratorActive interaction mode name
cameras_okCamera health checkBitmask: 1 bit per face camera
error_flagsOrchestratorBitmask for fault conditions

Browser control panel — intended architecture

FastAPI on Jetson serves a live control UI — like WLED or LedFX. Any device on the same local network opens it in a browser. No SSH, no config files, no festival fumbling in the dark.

ControlDescription
Mode switchingSelect active interaction mode from full inventory
Parameter slidersSpeed, intensity, palette, particle count, decay rate per mode
Palette pickerLive color palette swap across all faces
Face overrideForce a specific mode on one face independently
Trigger effectsFire one-shot effects (fireworks, ignite, crowd burst) manually
Visitor statsLive visitor count, dwell times, current crowd level
Boron healthJetson temps, uptime, camera status, error flags
LLM mood overrideManually set palette/speed/mode bias (phase 2)
Key principle: clean parameter API first. Every animation function takes (frame_buffer, input_state, params). Control UI just pushes params JSON over WebSocket. Build the engine right — UI is a thin shell. ~1 week effort once API is clean. Do not build the UI first.
06 — Interaction modes

Body / skeleton driven

Silhouette mirror
Pose rendered as pixel silhouette on the face in front of the visitor. The core stated vision.
Core vision
Shadow puppets
Silhouette as dark cutout against glowing background. Color shifts with movement energy.
High crowd appeal
Skeleton painter
Joints as colored dots, limbs as bright lines. Movements leave a fading long-exposure trail.
Visually striking
Particle attractor
Drifting particles cluster around your skeleton joints acting as gravity wells.
Magical feeling
Puppet / avatar
Pose drives a stylized pixel art character that mirrors your movements with personality.
Kid magnet
Warp field
Flowing ambient animation distorted by your body — like dipping your hand in a stream.
Subtle + beautiful
Gesture commands
Both hands raised = crowd mode. T-pose = freeze. Pointed arm = spotlight follows.
Gameable
Four-face sync
All cameras share state. Person on one face causes ripples on adjacent faces.
Makes cube feel alive

Voice / audio driven

Word painter
Whisper transcribes speech. Words scroll across the face. Emotional words shift palette.
Instant engagement
Shout to ignite
Audio RMS threshold → cube explodes into fire animation. Zero latency. No Whisper needed.
Festival goldZero latency
Beat sync
librosa beat detection. Strips pulse on the beat. Dances to festival stage music automatically.
Always-on ambient
LLM word → visual
Whisper → Ollama returns JSON descriptor. "Ocean" = blue slow waves. "Chaos" = glitch.
Core visionPhase 2
Compliment engine
Cube "notices" someone. Piper TTS speaks LLM-generated observation through base speaker.
MemorablePhase 2
Frequency spectrum
FFT of mic → live equalizer. Low frequencies warm at bottom, highs cool at top.
Always looks great

Crowd / multi-person

Crowd density meter
Person count across 4 cameras → outer frame brightness. More people = more excited cube.
Self-reinforcing
Tug of war
Two opposite faces. Pixel rope on perimeter. Movement energy pulls it toward your side.
Instantly competitive
Color ownership
Each face has a color that bleeds around perimeter. Four-way competition to fill it.
Naturally collaborative
Collective breath
Cube breathes in sync with visitor's subtle torso oscillation. Meditative contrast mode.
Meditative
Legacy / ghost mode
Previous visitor's pose lingers as fading echo. "You are not the first."
PoeticFits artistic intent
Synchronized strangers
Two people on different faces match poses → celebratory animation.
Unexpected delight

Games

Simon says (pose)
Cube displays target pose. 5s to match. TensorRT compares keypoints. Gets faster each round.
Competitive + clear
Dodge the pixels
Falling blocks on panel. Your silhouette is the blocker. Physically dodge, lean, duck.
Physical + active
Keep the light alive
Glowing orb dims unless someone is moving. Walk away and it dies. Cooperative by default.
No instructions needed
Pixel draw
Wrist keypoint paints pixels. Left hand erases. Drawings layer under the next visitor's.
Creative + persistent
07 — Orchestration model

Layered stack, not discrete mode switching:

1
Base layer — always on
Silhouette mirror or warp field always visible and running. Never blank.
2
Voice / crowd events
Interrupt with 3-second overlays on top of base. Beat sync modulates brightness of whatever is running.
Rotation timer
Every 90–120s the base layer rotates. Games triggered explicitly, not in rotation.
Presence gate
All interactive modes require detection. No one for 30s → drop to idle attractor.
Crowd gate
Multi-person modes only unlock when 2+ faces occupied. Easter eggs.
AI
LLM mood director — 10s tick (phase 2)
Input: time, crowd, weather, emotion, last phrase. Output: mood JSON biasing palette/speed/mode.
Outer frame = nervous system
Strips reflect global state. Panels = focused interaction. Always in dialogue, never redundant.
LLM animation schema pattern Prompt Ollama to return structured JSON: {"mode":"crowd_noise","palette":"fire","speed":0.8,"intensity":0.9} — tiny output, fast on Jetson.
08 — Multi-model sensing pipeline
0
Camera input — always running
4× C920/C922 USB webcams. MJPEG 848×480 30fps. Hardware decode via nvjpegdec.
1
Body pose — always on
YOLOv8n-pose via TensorRT FP16. 17 keypoints per person, bounding boxes, person count, movement energy. ~2–4ms per frame.
2A
Face emotion — conditional (phase 2)
Activates when head keypoint above confidence threshold. MediaPipe 478 landmarks. Emotion CNN at ~5–10fps.
2B
Hand gesture — conditional (phase 2)
Activates when wrist enters trigger zone. MediaPipe 21 hand keypoints. Gesture classifier.
3
Sensor fusion — every frame
Unified per-person state object. Temporal smoothing. Cross-face state via MQTT. Clean state dict per face.
4
LLM mood director — 10s tick (phase 2)
Ollama Phi-3 Mini. Input: emotion, crowd, energy, time, weather, last phrase. Output: mood JSON.
5
Orchestrator → display — every frame
DDP UDP to 4× Colorlight 5A-75E + E1.31 to ESP32 strip nodes. Target: 25fps.
Key insight: conditional activation. Body pose runs always. Face/hand models only fire when triggered. In a real festival scenario all three are rarely active simultaneously — that's the headroom needed.
09 — Visual pipeline implementation
★ IMMEDIATE PRIORITY — validate visual pipeline on hardware before adding LLM/Whisper layer

Model selection plain english

YOLOv8 is the AI model that looks at a camera frame and finds people's bodies and joint positions — shoulders, elbows, wrists, hips etc. It comes in different sizes: nano, small, medium, large, extra-large. Bigger = more accurate but slower.

For your use case the nano variant (YOLOv8n) is plenty accurate and fast enough to run on 4 cameras at once. Using anything larger just burns compute you don't have for accuracy you don't need.

TensorRT is NVIDIA's way of taking that AI model and compiling it specifically for your exact GPU so it runs as fast as physically possible. FP16 means it uses half-precision math — the Ampere GPU has dedicated hardware for it, so it runs roughly twice as fast with no meaningful quality loss for pose detection.

The imgsz flag just tells TensorRT what resolution the camera frames will be. If you don't match it to your actual capture resolution, the model wastes time padding or rescaling every frame before it can even look at it.

  • Model: YOLOv8n-pose — "n" = nano size tier in Ultralytics naming (not Jetson Nano specific). ~3.2M parameters, highly efficient for multi-stream.
  • Do not use YOLOv8s/m/l/x-pose — too heavy for 4-stream inference on a single Orin Nano
  • Precision: TensorRT FP16 — leverages Ampere tensor cores, sub-10ms latency per frame
  • CRITICAL: export the .engine file on the Jetson itself — TensorRT engines are hardware-specific, a desktop-built engine will not run on the Orin Nano
StepCommand / Notes
Installpip install ultralytics on Jetson
Exportyolo export model=yolov8n-pose.pt format=engine half=True imgsz=[480,848] device=0
Why imgsz mattersMatching export to capture resolution (848×480) avoids wasted compute on padding/upscaling
Outputyolov8n-pose.engine — use this in production, not the .pt
Hardware-specificAlways export on the Jetson itself — engine will not transfer to other hardware
Expected latency~2–4ms per frame FP16 at 848×480 — 4 sequential frames ~16ms, inside 33ms budget for 30fps
VerifyRun inference benchmark on Jetson before integrating into pipeline

Camera ingest — MJPEG hardware decode plain english

By default if you just open a webcam in Python it soft-decodes the video on the CPU. Four cameras doing that simultaneously would eat all 6 ARM cores just decoding video frames — leaving nothing for the AI or the LED output.

The C920/C922 can compress video internally as JPEG frames before it even leaves the camera. GStreamer with nvjpegdec lets the Jetson's GPU decompress those frames in hardware instead of the CPU — so by the time Python even sees a frame, it cost almost zero CPU to get there.

The memory:NVMM part means the decoded frame stays in GPU memory rather than bouncing out to CPU RAM and then back into GPU for the AI — saves an unnecessary round trip every single frame.

The appsink drop=true line means if the pipeline falls behind, it throws away old frames and always hands you the latest one. You never want a queue of stale frames building up — you want to know where the person is right now.

Do not capture at 1080p and resize in Python. Do not use raw H.264 soft decode. Use C920/C922 native MJPEG at 848×480, decoded on the Jetson GPU via nvjpegdec. Frames stay in unified VRAM.

  • Capture format: MJPEG 848×480 30fps — C920/C922 handles MJPEG compression natively in hardware
  • nvjpegdec: hardware JPEG decode on Jetson GPU — near-zero CPU utilization for video ingest
  • memory:NVMM flag: keeps decoded frames in Jetson unified VRAM, avoids CPU/GPU memory transfer
  • appsink drop=true max-buffers=1: always processes the latest frame, never builds a backlog
  • One isolated thread or multiprocessing worker per camera writing latest frame to shared memory
GStreamer elementPurpose
v4l2src device=/dev/videoNCapture from USB webcam by device index
image/jpeg, width=848, height=480, framerate=30/1Force MJPEG at 848×480 30fps at hardware layer
nvjpegdecHardware-accelerated JPEG decode on Jetson GPU
video/x-raw(memory:NVMM)Frame stays in unified VRAM — no CPU bounce
nvvidconvHardware memory format converter
video/x-raw, format=BGRxConvert to OpenCV/TensorRT compatible format
appsink drop=true max-buffers=1Drop stale frames, guarantee zero latency

Threading & CPU affinity plain english

The Jetson has 6 CPU cores. If you let the OS freely schedule everything, your LED output loop might get interrupted mid-packet by a web request hitting the control panel. That causes a visible flicker on the panels.

The fix is to pin each workload to a specific core — camera ingest here, AI inference here, LED output here, web server there — and they never step on each other. Think of it like dedicating lanes on a highway instead of letting all traffic merge freely.

The LED output core is the most important one to isolate. It needs to fire a UDP packet every 40ms like clockwork. Anything else sharing that core is a risk.

Pin each workload to specific cores. The Orin Nano has 6 ARM Cortex-A78AE cores.

Core(s)Assigned workload
Core 0OS, housekeeping, watchdog
Core 1–2GStreamer NVDEC camera ingest threads
Core 3TensorRT inference dispatch + result collection
Core 4sacn / python-e131 UDP output loop — isolated, never interrupted
Core 5FastAPI + MQTT + Boron serial — never on same core as UDP output
  • Use taskset or os.sched_setaffinity() to pin threads to cores
  • Isolating the E1.31/sacn UDP output loop is non-negotiable — HTTP request on same core causes LED frame drops

Batched vs. sequential inference plain english

Imagine you have 4 photos to show someone. You could hand them one at a time and get each answer back before handing the next. Or you could stack all 4 and hand them over at once — potentially faster since they can look at them all in one go.

Batched inference is the "stack all 4" approach. It's theoretically faster because the GPU handles them in one pass. The catch: you have to wait until all 4 cameras have a fresh frame before you can fire the batch. If one camera is a few milliseconds behind, everything waits.

Sequential async means you process each camera's frame as soon as it arrives — no waiting. A bit more overhead per frame but no one camera can hold everyone else up. Start here and only switch to batched if you need the extra speed.

Stacking 4 frames into a single TensorRT batch is theoretically faster than 4 sequential calls. In practice frames rarely arrive simultaneously — waiting for the slowest camera to fill the batch adds latency. Benchmark both.

ApproachProCon
Sequential async (4 separate calls)Process each frame as it arrives, no wait4× inference call overhead
Batched (stack 4 frames, 1 call)GPU processes all 4 in one passMust wait for all 4 cameras — slowest gates the batch
  • Start with sequential async (simpler), test batched if headroom is needed

Frame buffer architecture plain english

When the AI finishes processing a camera frame it needs to hand the result to the LED output thread. Python has built-in queues for this but they go through Python's global lock — meaning threads have to take turns accessing them, adding latency on every single frame.

A shared memory ring buffer bypasses that entirely. It's just a chunk of memory that both threads can read and write directly — like a whiteboard between two people in the same room vs. sending messages through a receptionist. Faster, no waiting.

The "ring" part means it wraps around — it holds the last N frames and the output thread always grabs the newest one. Old frames get overwritten automatically. No cleanup needed.

  • Use shared memory ring buffers between inference threads and LED output thread — do not use Python queues
  • Python queues add latency and GIL contention — unacceptable in a real-time LED pipeline
  • Ring buffer holds last N rendered frames per face; LED output thread always reads latest available frame
  • Use multiprocessing.shared_memory or numpy shared arrays for zero-copy frame passing

Camera fault tolerance plain english

If a cable gets kicked at the festival and a camera drops, without fault handling the whole Python process would likely crash or hang waiting for a frame that never comes — taking all 4 faces down with it.

The watchdog is just a simple check running every second: "did I get a new frame from camera 2?" If not, it tells the orchestrator to put that face into ambient animation mode and keep everything else running normally. When the camera comes back, it reattaches automatically.

The key principle: a hardware failure on one face must never be allowed to affect the other three faces or the LED output loop. Failures should be contained and degraded gracefully, not cascaded.

A camera dropping mid-festival must not crash the orchestrator or blank the LED panels.

Failure scenarioRequired behavior
Camera feed drops / corrupt framePer-camera watchdog detects loss within 1s
Face with dead cameraThat face falls back to ambient/idle animation automatically
Camera recoversPipeline reattaches, resumes interactive mode without restart
All cameras deadAll faces fall back to ambient — Boron alert fires immediately
Orchestrator crashWatchdog restarts orchestrator; ESP32 WLED nodes continue running last animation
  • Per-camera watchdog thread: no new frame within 1s → flag camera dead, notify orchestrator
  • Orchestrator checks camera health flags each frame cycle, routes dead-camera faces to ambient mode
  • Never let a camera failure propagate to the UDP output loop — LED panels must keep outputting regardless

UDP output — packet compilation plain english

Every 40ms the Jetson needs to send a UDP packet to each Colorlight card containing the full frame of pixel colors. That's a lot of packets flying out at a steady rhythm.

If you build those packets from scratch each time — allocating memory, building Python objects, converting them — that overhead adds up and can cause the rhythm to stutter. The fix is to pre-allocate the packet buffers once at startup and just overwrite the color values each frame. No memory allocation in the hot loop, no garbage collection surprises mid-festival.

And if the AI is running behind and doesn't have a fresh frame ready in time, the output loop should just re-send the last valid frame rather than going blank. The panels should never go dark just because inference is a frame late.

  • Compile DDP / E1.31 packets as low-overhead byte arrays, not Python dicts or objects
  • Pre-allocate packet buffers at startup — no runtime memory allocation in the output loop
  • Target: LED output loop runs at locked 25fps cadence regardless of inference load
  • If inference falls behind, output loop sends last valid frame — never a blank/dropped frame

Benchmark plan — before committing to 1 vs. 2 Jetsons plain english

Before deciding whether to buy a second Jetson, you need real numbers from the actual hardware. The plan is to start simple — one camera, does it hit 30fps? Yes: add a second camera. Still good? Add a third and fourth. If 4 cameras runs comfortably, one Jetson is enough.

If 4 cameras is struggling — say you're only getting 18fps or the CPU is pegged — that's your answer: 2 Jetsons with 2 cameras each. At $249 the second unit is affordable insurance, but run the test first before spending the money.

The last two tests check real-world conditions: not just "can it do pose inference" but "can it do pose inference AND blast UDP packets to the LED panels AND respond to a web request from the control panel, all at the same time, without the panels flickering." That's the actual festival scenario.

TestTargetDecision trigger
YOLOv8n-pose TRT FP16, 1 camera, 848×48030fps+Baseline — must pass
YOLOv8n-pose TRT FP16, 2 cameras async30fps+ eachConfirms 2-Jetson split viability
YOLOv8n-pose TRT FP16, 4 cameras async25fps+ eachIf passes: 1 Jetson sufficient
4 cameras + sacn UDP simultaneouslyNo frame dropsReal-world combined load test
4 cameras + sacn + FastAPI on separate coreNo LED stutter on HTTP requestCPU affinity validation
10 — HTML prototype → production pipeline

The browser tool radiant_core_96x96.html is the canonical animation sandbox. The 96×96 canvas matches physical panel resolution exactly — what you see in the browser is a faithful preview of the physical panels.

StageToolRole
DesignHTML5 Canvas + vanilla JSBuild and tune at 96×96. Fast iteration, no hardware needed.
PortPython + NumPyRewrite as function(frame_buffer, input_state, params) → frame_buffer
TestJetson + ColorlightDrop Python function into pipeline. Verify on real P10 hardware.
IterateHTML → Python syncTweak in HTML for fast feedback, sync to Python version.
The HTML prototype is not throwaway work. It is the animation library. LED shape modes (hex, diamond, circle) are visual sugar for prototyping only — dropped in production. The 3D-printed grids handle the physical aesthetic.

Active effect categories

CategoryModes
PhysicsCloth, rope, falling sand, ball collisions
NatureFlowers, crystal growth, slime mould, mycelium
SpaceWarp speed, meteor, jellyfish, deep sea
FluidReaction-diffusion, viral sim, aurora
ArtStarry Night curl noise, lightsaber/whip
PetsAssorted critter modes

Python libraries for production

  • numpy — frame buffer operations, vectorised math
  • python-e131 / sacn — E1.31 sACN UDP output to Colorlight cards
  • asyncio — non-blocking camera + audio + LED pipelines
  • opencv-python — camera capture and image processing
  • mediapipe — pose, face, and hand landmark detection
11 — Wind load / structural summary
Not for construction. Config B — 7×7 tapered base. Simplified estimate per ASCE 7-22. Requires licensed structural engineer review and stamp.
Key result: self-stable, no ballast required. Safety factor 1.52× at 115 mph design wind — exceeds 1.5× target.
ParameterValue
Design wind speed115 mph — Exposure Category C, Risk Category II
Structure weight1,562 lbs
Total lateral wind force~1,067 lbs at 115 mph
Overturning moment3,599 ft·lbs
Restoring moment5,467 ft·lbs (1,562 lbs × 3.5 ft arm)
Safety factor unballasted1.52× ✓ — exceeds 1.5× target
Sand ballast required0 bags at design wind speed
No-tip limit~165 mph

Keep two 50 lb sand bags on hand at each deployment as emergency reserve regardless.

12 — Transport & deployment

Trailer solution

The 7ft base width (84") exceeds the 82" between-wheel deck width of standard rental equipment trailers. Solution: detachable trapezoidal base side panels that bolt on at the venue, reducing transport width to the rectangular core only.

  • Trailer: 12ft Tilt Deck Flatbed Trailer, Single Axle — United Rentals Cary NC, $64/day
  • Transport config: rectangular core only — narrow enough for trailer deck, fully stable on flatbed
  • Deployed config: trapezoidal side panels bolted back on — full 7ft footprint restored, wind load calc holds as designed
  • Safety factor concern only applies when panels are detached during transport — irrelevant since structure is not standing in wind on the trailer

Detachable trapezoidal base panels

The trapezoidal side panels are fabricated as separate bolt-on sections. They attach to the rectangular core via welded receiver flanges or bolt plates built into the core during fabrication. Wes and Thure are welding the entire structure in-house.

  • Weld receiver flanges or bolt plates onto rectangular core during fabrication — before final welding of core
  • Trapezoidal panels bolt on at venue with through-bolts or quick-release pins
  • When attached: panels restore full 7ft footprint and 3.5ft restoring arm — 1.52× wind load safety factor intact
  • Joint must genuinely transfer lateral load — adequate bolt pattern and flange sizing required, not just cosmetic attachment
  • Structural engineer must review and stamp the bolt joint design as part of final sign-off

Base split concept

Red lines show the detachment point — trapezoidal side panels separate from the rectangular core for transport. Blue outlines show the removable panel sections. When bolted on at the venue the full footprint and wind load safety factor are restored.

Radiant Core base split concept — detachable trapezoidal panels shown in blue, attachment point in red

Transport checklist

ItemDetail
Trailer rentalUnited Rentals Cary — 12ft Tilt Deck Single Axle — $64/day
Confirm deck widthCall United Rentals Cary branch — verify usable deck width accommodates rectangular core
Tow vehicleConfirm tow vehicle capacity vs. trailer weight + structure weight before booking
Load configRectangular core on trailer, trapezoidal panels loaded separately
Strapping4-point tie-down minimum on rectangular core
On-site assemblyBolt trapezoidal panels onto core before raising structure
Sand bagsTwo 50 lb bags on hand at each deployment as emergency ballast reserve
13 — Open questions / next steps
#Question / action item
★ F1FABRICATION: design and weld bolt flanges for detachable trapezoidal base panels into rectangular core before final welding
★ F2FABRICATION: confirm rectangular core width fits Sunbelt 12ft tilt deck — call 919.851.0890
★ F3Confirm tow vehicle towing capacity before trailer booking
★ 1IMMEDIATE: order one Jetson Orin Nano + 500GB NVMe, run visual pipeline benchmark suite before ordering second unit
★ 2IMMEDIATE: benchmark YOLOv8n-pose TensorRT FP16 at 1/2/4 cameras 848×480 — results determine 1 vs. 2 Jetson decision
31 vs. 2 Jetson Nanos — decision pending benchmark results
4LLM / Whisper stack: DEFERRED — validate visual pipeline first
5Decide inner cube face dimension: 960mm or trim to 915mm
63D printed grid design: finalize cell sizes and which face gets which style
7Steel fabrication timeline — wind load engineer review and stamp
8Power budget: total draw for panels + strips + Jetson(s) + audio at full load
9Weatherproofing strategy for Jetson enclosure in base
10Confirm Glow 2027 application / submission requirements and deadlines
11Thure's role: which parts of the build / which software layers
12Festival Wi-Fi availability at Post Office plaza for weather API
13Camera placement: FOV and mounting angle per face for reliable pose detection
14Particle Boron: implement health monitoring script, set up webhook to Discord/Slack