SYSTEM ARCHITECTURE

Firmware Specifications & Developer Manual

An in-depth guide detailing Haxel's multi-tasking FreeRTOS scheduling model, safety state machines, firmware API interfaces, and advanced mathematical expression scripting.

01 // OVERVIEW

Architectural Philosophy

Haxel is designed to bridge the gap between abstract software concepts and high-performance physical haptics. Instead of treating haptic feedback as an arbitrary series of on/off motor commands, Haxel models tactile actuation as a continuous, mathematically-defined waveform stream.

The platform decouples visual interaction (handled in the client web UI) from real-time execution (processed locally on the ESP32 chip). By utilizing a local mathematical expression evaluator in firmware, users can develop, deploy, and evaluate complex haptic behaviors dynamically without needing to recompile or flash code.

02 // EMBEDDED CONCURRENCY

FreeRTOS Concurrency Model

To achieve low-latency audio processing alongside responsive network control, Haxel distributes its workloads across both cores of the ESP32 using a 4-task FreeRTOS scheduling architecture:

Task Name Core Pinned Priority Frequency Functionality Description
engine_task Core 1 Priority 5 (RT) 1,000 Hz Evaluates pattern mathematics, processes envelopes, and commits duty cycles to the HAL driver.
audio_task Core 1 Priority 4 (RT) DMA Driven Collects I2S microphone samples, runs the 256-point FFT, and computes envelope statistics.
web_task Core 0 Priority 2 (Normal) Asynchronous Drives the captive portal DNS, handles standard HTTP API endpoints, and handles active WebSockets.
housekeeping Core 0 Priority 1 (Low) 10 Hz Manages mDNS broadcasting, WiFi status checks, system statistics, and writes configuration to LittleFS.

Priority Separation (Core 1 vs. Core 0)

Real-Time Isolation: All network operations (Wi-Fi stack, TCP/IP, WebSockets) are restricted to Core 0. This guarantees that network traffic spikes or socket re-connections cannot block the high-priority real-time tasks (Engine and Audio) on Core 1, ensuring jitter remains below 1.4 ms even under heavy web traffic.

03 // HARDWARE WATCHDOG

Safety Watchdogs & FAULT States

When driving physical, inductive actuators like LRA and ERM motors, firmware safety is critical to prevent hardware damage due to bugs or thermal buildup. Haxel provides a built-in safety state machine:

void Engine::tick() { if (state_ == EngineState::FAULT) { // Cease all motor driver outputs immediately driver_->writeDuty(0, 0.0f); return; } // ... regular haptic execution loop ... }

Emergency Shutdown

Calling the function raiseFault(const char* code) locks the engine into the FAULT state. Haptics are instantly cut off and will not resume automatically under any circumstances.

Fault Recovery Loop

To clear a fault, the user must resolve the hardware issue and send a {"clear": true} state patch via the Web API. Only after this explicit reset can the system be put back into PLAYING mode.

04 // EXPRESSION ENGINE

Pattern Scripting Guide

Haxel's expression engine runs math scripting expressions at 1 kHz in real time. Scripts have access to system variables and standard math helpers:

Variable Type Units Definition & Usage
t / time float Seconds Pattern time since boot, pre-multiplied by playback speed. Monotonic — wrap it yourself with % or frac() for cyclic patterns.
dt / delta float Seconds Time since the previous evaluation. One engine tick (0.001 s) on hardware, one animation frame (~0.016 s) in the browser simulator. Integrate with dt and both behave identically.
freq float Hertz Reserved frequency-shift parameter (currently a constant 150.0).
speed float Scalar The global playback speed coefficient (default is 1.0).
intensity float 0.0 to 1.0 The master intensity slider value. Applied to your output automatically — read it only if you want non-linear responses.
floor float 0.0 to 1.0 The motor startup-floor calibration value. Applied downstream by the engine; exposed for scripts that shape around it.
PI / TAU const Radians 3.14159… and 6.28318…. sin(t * TAU) is exactly one cycle per second.

Persistent variables: any name you assign (env = env + ...) survives between ticks and starts at 0. This is what makes envelopes, filters, and physics possible — see the live examples in Section 05.

Delta Time Scripting Techniques

Because t is global, making patterns that are phase-independent or require persistent local accumulation requires delta-time integration. The code snippets below demonstrate advanced script behaviors:

01 // Delta-Time LFO Phase Accumulator

Integrates a changing rate into a phase so speed sweeps never click or jump:

// Accumulate phase dynamically using dt
rate = 2 + sin(t) * 1.5;
phase = frac(phase + dt * rate);
wave(phase)

02 // Simulated State-Variable Filter (SVF)

Integrates state variables to low-pass filter noise, creating a resonant physical sweep:

// Low-pass filter loop via delta-time integration
q = 0.4;
f = 6 * dt;
bp = bp + f * (noise(t * 3) - lp - q * bp);
lp = lp + f * bp;
clamp(lp, 0, 1)

03 // Envelope Follower with Attack/Release

Models physical inertia by lagging a target signal at different rise/fall rates:

// Fast attack, slow release
target = square(t / 2, 0.5);
k = target > env ? 12 : 2.5;
env = env + (target - env) * min(1, dt * k);
env

04 // Decaying Noise Burst

Shapes smooth noise into organic, textured pulses using an exponential decay envelope:

// pow(2.718, -x) is e^-x — a natural decay curve
envelope = pow(2.718, -5 * (t % 1.5));
noise(t * 20) * envelope

Every one of these techniques — and every built-in function — is demonstrated with a live-rendered waveform in Section 05: Live Coding Examples below.

05 // LIVE CODING EXAMPLES

Learn by Feel: The Live Pattern Cookbook

Every canvas below runs the code above it through the same expression compiler the firmware uses, in real time, right now. What you see scrolling past is exactly the motor power the ESP32 would output. Copy any snippet into the Pattern Studio, or use it as a starting point on the device portal.

The examples escalate: constants, then oscillators, then wave shaping, then rhythm and randomness, then stateful dt patterns, then audio reactivity, and finally a full combination. One rule holds throughout: layer one dominant voice over one quiet bed. Sum three loud signals and skin stops reading them as anything — it all becomes white noise.

Function Cheat Sheet

FunctionReturnsDescription
sin(x) cos(x) tan(x)-1..1 (tan: ±∞)Radians trig. tan() explodes near π/2 — always clamp() it.
wave(x)0..1Haptics-friendly sine: one full 0→1→0 cycle per whole number of x.
triangle(x)0..1Linear rise then fall, one cycle per whole number of x.
square(x, duty)0 or 1On for the first duty fraction of each cycle (default 0.5).
frac(x)0..1Fractional part — a free sawtooth ramp.
abs(x) sqrt(x) pow(x, y)variesRectify, fatten (sqrt), or sharpen (pow with y > 1) a wave.
floor(x) ceil(x) round(x)integerQuantizers — turn ramps into staircases, times into beat indices.
min(a, b) max(a, b) clamp(x, lo, hi)variesLimiters. max() is also the cleanest way to layer two voices.
mix(a, b, k) / lerp(a, b, k)variesBlend from a to b as k goes 0→1.
step(edge, x)0 or 1Hard gate: 1 when xedge.
smoothstep(lo, hi, x)0..1Eased gate: fades from 0 to 1 between lo and hi.
time(period)0..1A looping clock: ramps 0→1 every period seconds.
random()0..1New value every tick — raw white noise. Always shape it.
hash(n)0..1Deterministic pseudo-random: same n always gives the same value.
noise(x) / perlin1d(x)0..1Smooth Perlin noise — organic drift instead of chaos.
vu() peak()0..1Microphone volume envelope; peak() is vu() with a 1.2× boost.
bass() mid() treble()0..1Averaged FFT energy in the low / middle / high bands.
band(n)0..1A single FFT bin, n = 0 (deepest bass) … 31 (highest treble).
beat() pitch()0..1Convenience helpers: a ready-made pulse train and a slowly wandering tone tracker.
06 // DATA SPECIFICATION

WebSocket & REST API Protocol

The ESP32 hosts a JSON REST API and a WebSocket server at ws:///ws. Clients can query the status and send updates using raw JSON objects.

GET /json/state

Returns the active status of the haptic engine, parameters, and segment lists:

{ "on": true, "mute": false, "intensity": 0.75, "speed": 1.0, "pattern": "bass_booster", "channels": [ { "on": true, "intensity": 1.0 } ], "info": { "version": "1.2.0", "uptime_ms": 32840, "heap_free": 184752 } }

POST /json/state (State Patch)

Applies incremental updates to the active engine state. Unspecified parameters are preserved:

{ "on": true, "intensity": 0.85, "pattern": "rhythmic_pulse", "params": { "freqShift": 150.0, "decayRate": 5.5 } }