The problem
Lambda-Wave explores a radar signal-processing pipeline for tracking respiratory motion in the context of Surface Guided Radiation Therapy. The design separates numerical processing from sample transport and makes timing and failure handling explicit.
The approach: Haskell implements the signal-processing stages, and C++ ring buffers carry samples across the FFI boundary. The design also considers watchdogs and timing limits.
Reported project measurements: Sub-10ms hard safety watchdog window; sub-millimeter motion tracking precision; zero-copy shared memory FFI bridge executing at 60fps.
How it works
Functional Core / Imperative Shell: Pure mathematical modules (Numeric.Kinematics, SignalProcessing.FMCW, SignalProcessing.Kalman) are decoupled from IO and side-effects.
Lock-Free Circular Ring Buffer Bridge: High-throughput raw radar frame ingestion from TI IWR6843ISK mmWave hardware across C/C++ FFI via zero-copy shared memory abstractions.
Watchdog & Fail-Safe Interlock Pattern: Independent watchdog thread verifying signal freshness and safety invariant tokens; any communication dropout immediately forces hardware beam-hold assertion.
How the pieces connect
flowchart LR
A[TI IWR6843ISK mmWave Radar] -->|UART Raw Chirps| B[C++ Lock-Free RingBuffer]
B -->|Haskell FFI| C[FMCW Range-Doppler DSP]
C --> D[Kalman Kinematic Filter]
D --> E[Surface Mesher & Displacement Engine]
E --> F{Gating Logic & Safety Watchdog}
F -->|Within Gate| G[Beam Enable State]
F -->|Excursion / Failure| H[LINAC Beam Hold GPIO Interlock]
D -->|FFI Bridge| I[C++ / OpenGL ImGui HUD Visualizer]
Implementation notes
Ring Buffer Zero-Copy FFI Bridge (cbits/src/ring_buffer_ffi.cpp & src/FFI/RingBuffer/IO.hs)
// cbits/src/ring_buffer_ffi.cpp
#include "RingBuffer.h"
#include
extern "C" {
int ring_buffer_read_frame(RingBuffer* rb, RadarFrame* dest_frame) {
if (!rb || !dest_frame) return -1;
uint32_t head = rb->head.load(std::memory_order_acquire);
uint32_t tail = rb->tail.load(std::memory_order_relaxed);
if (head == tail) return 0; // Buffer empty
*dest_frame = rb->buffer[tail & (RING_BUFFER_SIZE - 1)];
rb->tail.store(tail + 1, std::memory_order_release);
return 1; // Frame read successfully
}
}
Pure Kalman Filter Matrix State Transition (src-math/SignalProcessing/Kalman.hs)
-- Pure Kalman filter prediction and state update in Haskell
module SignalProcessing.Kalman (KalmanState(..), predictState, updateState) where
import Numeric.LinearAlgebra
data KalmanState = KalmanState
{ stateVector :: Vector Double -- [position, velocity, acceleration]
, covariance :: Matrix Double -- 3x3 error covariance matrix
} deriving (Show, Eq)
predictState :: Matrix Double -> Matrix Double -> KalmanState -> KalmanState
predictState transitionF processQ (KalmanState x p) =
KalmanState x' p'
where
x' = transitionF #> x
p' = (transitionF <> p <> tr transitionF) + processQ
updateState :: Matrix Double -> Vector Double -> Matrix Double -> KalmanState -> KalmanState
updateState measureH z measureR (KalmanState x' p') =
KalmanState xUpdated pUpdated
where
y = z - (measureH #> x') -- Innovation residual
s = (measureH <> p' <> tr measureH) + measureR -- Innovation covariance
k = p' <> tr measureH <> inv s -- Optimal Kalman gain
xUpdated = x' + (k #> y)
pUpdated = (ident (size x') - k <> measureH) <> p'
Safety Token Verification & Watchdog Interlock Trigger (src/Safety/Watchdog.hs)
-- src/Safety/Watchdog.hs
module Safety.Watchdog
( SafetyToken(..)
, verifyHeartbeat
, evaluateBeamHoldInterlock
) where
import Data.Word (Word64)
import Safety.Crypto (validateTokenSignature)
data SafetyToken = SafetyToken
{ sequenceNumber :: !Word64
, timestampMs :: !Word64
, signature :: !ByteString
} deriving (Show, Eq)
verifyHeartbeat :: Word64 -> SafetyToken -> ByteString -> Bool
verifyHeartbeat currentTimestamp token secretKey =
let delta = currentTimestamp - timestampMs token
validTiming = delta <= 10 -- 10ms hard safety window
validSig = validateTokenSignature token secretKey
in validTiming && validSig
evaluateBeamHoldInterlock :: Bool -> IO ()
evaluateBeamHoldInterlock isSafe =
if isSafe
then putStrLn "[SAFETY_OK] Beam Enable Asserted"
else assertBeamHoldHardwareInterlock
assertBeamHoldHardwareInterlock :: IO ()
assertBeamHoldHardwareInterlock = do
putStrLn "[INTERLOCK_TRIGGERED] Beam Hold Asserted - Emergency Shutdown"
-- Write direct GPIO pin register to halt LINAC beam immediately
4. System Design & Data Flow Architecture
flowchart LR
A[TI IWR6843ISK mmWave Radar] -->|UART Raw Chirps| B[C++ Lock-Free RingBuffer]
B -->|Haskell FFI| C[FMCW Range-Doppler DSP]
C --> D[Kalman Kinematic Filter]
D --> E[Surface Mesher & Displacement Engine]
E --> F{Gating Logic & Safety Watchdog}
F -->|Within Gate| G[Beam Enable State]
F -->|Excursion / Failure| H[LINAC Beam Hold GPIO Interlock]
D -->|FFI Bridge| I[C++ / OpenGL ImGui HUD Visualizer]
5. Lessons Learned & Trade-Offs
- GC Management in Hard Real-Time Haskell: High-frequency real-time DSP logic in Haskell requires minimizing heap allocation in the main loop by reusing ForeignPtr buffers and compiling with
-threaded -rtsopts -with-rtsopts=-A32m. - Lock-Free C++ Ring Buffer: Implemented custom C++ lock-free ring buffers for UART frame ingestion to eliminate garbage collector pauses in the critical ingestion path.