9. SOFTWARE STACK

rev. 3: runs on the OptiPlex 5080 (Linux), CPU-only. No GPU, so model choices are bounded by CPU inference speed (~5–15 tok/s on a 3–4B model; drops sharply above ~8B). The local model's job is to be fast and good at routing/classifying + simple answers, NOT to be smart. Real reasoning goes to Fireworks. The whole stack is a router with a small near-brain and a big far-brain.

Carputer OptiPlex 5080 (Linux Mint 22.3, headless β€” Cinnamon disabled, multi-user.target):

Pi 3 B+ (always-on, Raspberry Pi OS Lite β€” NO audio, NO mic, NO TTS):

9.1 Model Choices (June 2026)

Local (on the OptiPlex, via Ollama):

Cloud (rev 3.34 β€” OpenRouter primary, Venice for privacy, Fireworks shelved):

9.2 The Router (local-vs-cloud β€” rev 3.34, OpenRouter)

The local Qwen3 4B does a fast first-pass classification on every utterance, deciding local-vs-cloud BEFORE generating a full answer. Five strategies, mixable:

  1. Rule-based (start here): commands/state-queries β†’ no LLM at all (regex/keyword β†’ relay/DI/vehicle_state); short/simple/offline/RAG-lookup β†’ local; deep reasoning/long-context/"analyze/explain/write code" β†’ OpenRouter.
  2. Local-LLM-as-router: the 4B classifies every query ({"route":"local"|"cloud"}) and gatekeeps the expensive model.
  3. Confidence/fallback: local attempts; low confidence β†’ retry on OpenRouter Qwen3 235B.
  4. Capability-based (practical mix): voice command/car control β†’ no LLM; quick chat/"what's my temp" β†’ local; RAG over docs β†’ local + ChromaDB (private); heavy reasoning/code/research β†’ OpenRouter; anything offline β†’ local only (graceful degradation).
  5. Ensemble-matrix (rev 3.34, cell-up only): when accuracy outranks latency (explain/recommend/route-planning/cross-border-cost), fan out 3 models in parallel β€” local Qwen3 4B + OpenRouter Qwen3 235B + OpenRouter Qwen3-Next 80B :free β€” and pick the best. Two judge modes: (a) deterministic β€” string-similarity + numeric agreement (clear winner on unit/price math); (b) LLM judge β€” Qwen3 235B graded the 3 candidates and returned WINNER: <letter> + one-line reason. Wall time β‰ˆ slowest of the three (typically 3-5s). Cost ceiling at the heavy end (1000 q/day, every query ensembled) β‰ˆ ~450 MB/month cell + ~$0.10/day OpenRouter, well inside Keepgo 10 GB + single-digit-$/month. Offline β†’ strategy 5 collapses to local-only (rev-3.4 graceful-degradation rule unchanged). Validated empirically in scripts/slm-bench.py --ensemble.

Key rules: a cloud timeout silently falls back to local (never errors). High-power actions require confirmation before any relay fires. Offline-resilience matters a lot for a truck frequently out of signal in BC.

Local-fast floor (rev 3.34): The benchmark showed sub-4B models break unit/price math β€” Llama 3.2 1B returned 67 for 35 mph β†’ km/h (correct 56.33), Phi-4-mini and Gemma 3 4B failed similar conversions. Do not regress the "local-fast" tier below Qwen3 4B for the sake of tok/s. Half of what gets asked in a truck is conversion math; a "fast" model that's wrong is worse than a slower one that's right.

RAG split: retrieval is ALWAYS local (private docs, fast CPU vector search). Only the synthesis step is routable β€” short factual lookup β†’ local 4B; deeper synthesis β†’ Fireworks with retrieved chunks injected. For the personal life archive you may pin synthesis to local on privacy grounds. Heavy ingestion (PDF/Word/Excel/email/photos, nightly NAS rsync) lives at home on Z440 #1 (Β§11) β€” the truck carries a small synced subset (build doc, manuals, CAN notes, field reference); in-truck ingestion is a lightweight pypdf/unstructured β†’ chunk β†’ nomic-embed-text β†’ ChromaDB pipeline for small drops only.

RAM budget β€” ⚠️ the box ships with 16GB, not 32GB: local LLM (~3GB) + whisper (~2GB) + ChromaDB (~2GB) + Flask/app (~1GB) + Frigate routing + OS/Mint (~2-3GB) + browsers/go2rtc β‰ˆ 12–15GB β†’ at 16GB total that's only ~1-4GB headroom = TIGHT (risk of swapping under load, which kills latency). Highest-value upgrade in the whole build: add a 16GB DDR4 SODIMM β†’ 32GB (empty slot, 64GB max) β€” then the budget is comfortable. Teardown: the as-bought stick is a single Kingston 16GB DDR4-3200 SO-DIMM = single-channel right now; add a matching 16GB SO-DIMM (~$25-35 used) β†’ 32GB dual-channel β€” dual-channel also widens the UHD 630 iGPU's shared memory bandwidth β†’ better Quick Sync camera decode, so it helps both the LLM headroom and video. Treat 32GB as a need-soon, not optional, if you run LLM + RAG + Frigate together. Disk, not CPU or RAM, is the real constraint once cameras record (see Β§7.3).

AI tiers (matched to compute tiers):

ESP32 8DI-8DO (~0.5-1W): DO triggers + DI wires + LVD + proximity β†’ MQTT β†’ Pi wake coordinator
Pi 3 B+ (~3-5W):   MQTT broker, wake logic, ffmpeg recording (NO voice, NO LLM)
OptiPlex (~35W):   router, local LLM (Qwen3 4B), Porcupine, whisper.cpp, TTS (Piper/Kokoro),
                   RAG retrieval, CAN, Flask UI, camera record/restream, PipeWire audio
OpenRouter (cloud, rev 3.34): heavy reasoning (Qwen3 235B-A22B-2507; Qwen3-Next 80B :free as ensemble leg)
Venice (cloud, privacy lane): zero-log path for personal-context queries
[ Pi 5 + Hailo-8L, ~1yr out: takes over all camera record/restream/detect ]

Conversation handling: LLM returns action + spoken response in ONE JSON so mid-conversation commands don't break context. Multi-action arrays; confirmation for high-power actions. Three-layer memory: session + 5-min recent-context + persistent memory.json (LLM-decided saves); "Hey truck forget everything" clears it.

Multi-screen: WebSocket push to named screens (?screen=radio/tablet); voice targets a screen ("on the radio"); each screen independently shows its own page; one always-open kiosk browser per screen.

Audio: S24 Ultra pairs to OptiPlex via the built-in Intel AX201 BT 5.2 (A2DP sink β€” no USB dongle). OptiPlex 3.5mm AUX β†’ Uconnect AUX input. PipeWire module-role-ducking auto-ducks S24 music when TTS speaks β€” no app integration needed. Music = any app on S24 Ultra. Mic = reSpeaker XVF3800 USB array, headliner/overhead mount (Β§9.1).

Flask pages: /dashboard, /controls, /cameras, /gauges, /trans (8HP70 monitor), /rag, /music. WebSocket-synced across screens, large touch targets, dark theme.

9.3 Truck RAG, Facts, History & Replication (rev 3.5)

The truck carries a small offline-resilience subset; the big personal archive lives on the home RAG box (Β§11), queried over Tailscale when you have signal. Three distinct stores, each with the right tool:

A) Structured FACTS β†’ direct lookup, NOT RAG. Exact values that a fuzzy vector search could paraphrase wrong belong in a small table (SQLite/config) the rule-based tier answers instantly, offline, no LLM:

B) DOCS β†’ the actual RAG (ChromaDB + nomic-embed-text on the OptiPlex). Unstructured prose worth semantic search:

C) Usage/event HISTORY β†’ structured event-log, NOT RAG. "When were the KC lights on / when did the ESP32 fire a relay" is a SQL question, not a semantic one:

OptiPlex-asleep behavior: RAG, voice, and the LLM all run on the OptiPlex. While it hibernates, the Pi serves facts + control + status from its own data; anything needing the LLM/RAG/voice wakes the OptiPlex (~20s), then answers.

3-tier history replication (edge β†’ master β†’ archive):

Pi 3 B+   = gapless RECORDER / buffer (rolling recent window; small disk)
   └─(append-only, delta-on-wake)β†’ OptiPlex 5080 = MASTER of record (full history, 1TB) + analyst
        └─(periodic, over Tailscale)β†’ Home = offsite ARCHIVE / backup (later)

One-way, append-only β†’ no merge conflicts. Logs are tiny (years β‰ˆ tens-hundreds of MB; the 4TB is for video). The Pi prunes once the 5080 confirms receipt. ⚠️ Until home exists the 5080 is the ONLY full copy β€” back it up to the owned NAS or Hetzner over Tailscale (nightly 3am window, Β§5.4) so nothing is single-copy. It's the recorded DATA that flows up the tiers β€” the Pi's software/config is version-controlled separately, not mirrored.

9.4 CAN-write voice control of HVAC (Phase 3 future-work, rev 3.35)

Stub, not buildable yet β€” captured here so the architecture doesn't get lost on the next pass. Phase 1 (Phoenix install) and Phase 2 (CAN-read via SH-C31G into a vehicle_state dict) carry NO Phase 3 dependency and ship on the current rev. Phase 3 is the future-work layer that adds voice control of HVAC/heated-seats/defrost without replacing what Phoenix already does.

Design principle β€” Phoenix is NOT replaced, it is layered. The temptation when you realize the OptiPlex is ~1000Γ— more powerful than Phoenix's $3 CAN-decoder microcontroller is to drop Phoenix and roll your own everything. Don't. Phoenix at $695 buys you ~150 hr of vehicle-specific R&D (CAN decoder firmware for the 2014 RAM + bezel mold + harness + AM/FM tuner + US warranty); the OptiPlex's wins are in the layer Phoenix can't reach, not by re-doing what Phoenix already does. Keep Phoenix as the day-1 baseline (steering wheel, AC touchscreen, backup cam, AM/FM, factory-feature retention); add OptiPlex as a parallel CAN writer for voice control.

Topology β€” the 3-bus problem the SH-C31G alone CAN'T solve

The 2014 RAM has THREE CAN buses with a Gateway Module between them:

                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
PCM / Trans / ABS ──── C-CAN (500 kbps)     β”‚
                     β”‚   powertrain         β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚ GATEWAY MODULE (selective forwarder)
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
HVAC / Doors / BCM ─── B-CAN (125 kbps)     β”‚
Lights / Heated seatsβ”‚   body               β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
Radio / Steering whl ─ Infotainment-CAN     β”‚
buttons / Backup cam β”‚                      β”‚
                     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                β”‚
                                ↓
                      OBD-II port (READ-FILTERED VIEW
                      of all three; WRITES often blocked
                      by the gateway)

The SH-C31G hangs off OBD-II β†’ reads a diagnostic-filtered view of all three buses, but CAN-writes to B-CAN HVAC frames are usually swallowed by the gateway. This is the gotcha most carputer projects discover the hard way. To actually control HVAC, the OptiPlex needs a second CAN tap at the radio harness behind the gateway β€” same place Phoenix's decoder taps it.

Hardware needed for Phase 3 (~$30-$200)

OptionCostWhat it is
Second USB-CAN dongle wired to radio harness$30-80Same form factor as SH-C31G (Korlan USB2CAN or similar), spliced into the Phoenix harness CAN pair. Cheapest path, most DIY wiring.
Comma.ai Panda$199Purpose-built dual-CAN dongle with bus-bridging firmware β€” already supports Chrysler/Ram in OpenPilot. Cleanest path, no soldering.
CAN gateway-bypass board (DIY)~$40STM32 + dual MCP2515 transceivers acting as a one-way bridge from OBD-II to radio-harness CAN. Power-user move; only if you want full control.

DBC starting point β€” opendbc

Chrysler doesn't publish the CAN message database; community has reverse-engineered ~70% of it:

Voice-control flow (Phase 3 target)

"Hey JARVIS, set driver temp to 72"
   ↓ Porcupine β†’ whisper.cpp β†’ Qwen3 4B local (intent + slot extraction)
   ↓ router maps intent='set_hvac', slot={zone:driver, temp:72}
   ↓ Python builds CAN frame from DBC: ID 0x29C, payload {driver_temp: encode(72)}
   ↓ python-can writes to /dev/ttyUSB1 (second CAN tap, NOT SH-C31G)
   ↓ HVAC controller obeys β†’ actual blower changes
   ↓ Phoenix touchscreen automatically updates (it's reading the same B-CAN bus)
   ↓ TTS: "Driver side, seventy-two."

Both control channels coexist. Touch the Phoenix screen to set temp β†’ CAN frame on B-CAN β†’ both Phoenix and OptiPlex see the new state. Speak to JARVIS β†’ OptiPlex writes the same frame β†’ both screens update. No mode-switching, no override needed.

Risk register

  1. Phoenix decoder + OptiPlex both writing to B-CAN = potential frame collision. Mitigation: OptiPlex writes ONLY in response to voice intent, never on a timer; Phoenix writes ONLY in response to user touch. Watchdog: if OptiPlex frame doesn't take (HVAC controller drops it), retry once, then back off β€” never spam.
  2. Aftermarket tune sensitivity to extra CAN traffic (same as Β§4.2.1 gate #1, raised one tier β€” Phase 3 adds a writer not just a reader). Mitigation: stay on B-CAN HVAC frames, NEVER touch C-CAN powertrain.
  3. Wrong DBC entry = wrong HVAC setting (e.g., asked for 72Β°F driver, got 72Β°F passenger). Mitigation: read-back verify the frame in vehicle_state after every write; speak the actual confirmed temp.
  4. HVAC controller refuses spoofed frame because checksum/counter byte is wrong. Mitigation: opendbc usually has the checksum function; if not, sniff a real Phoenix-originated frame and reverse-engineer the counter.
  5. Insurance/warranty after a claim β€” disclosed CAN-write activity is a gray area. Mitigation: scope writes to non-safety systems (HVAC, heated seats, defrost β€” none of which can cause an accident); keep an event log of every write; never touch ABS/airbag/steering/powertrain frames.

Why not just do this on the Phoenix Android side?

You could β€” Phoenix's Android image already has the AC overlay app, and an APK could in principle add voice control on top. But:

Out of scope for Phase 3 (deferred further)

Phase 3 ship-criteria (not blocking Phase 1/2)


β€Ή 8. CAN BUS INTEGRATION10. THERMAL MANAGEMENT β€Ί