The Control Problem
A hot tub or spa imposes a set of control requirements that push beyond what a simple timer or thermostat can handle. The heater must never run without confirmed water flow, or it will burn out the element within minutes. The filter schedule must pause automatically when the jets or blower are running, because both loads together will exceed the panel's single-phase supply rating. RGB mood lighting needs smooth PWM dimming, not relay switching. Modern installations also demand connectivity: the property manager must be able to monitor status, adjust setpoints, and troubleshoot remotely, without a site visit for every routine change.
This reference design uses the EQSP32C controller. It demonstrates how a single 16-channel controller handles all outputs, all inputs, all logic, and all connectivity, while the property manager accesses the system through a smartphone app with no native app installation required.
I/O Inventory
This installation uses 9 of the EQSP32C's 16 software-configurable channels, leaving 7 free for future expansion. The complete channel assignment is shown below.
| Ch. | Signal | Mode | Type | Notes |
|---|---|---|---|---|
| 1 | Water temperature | TIN | Temp. input | NTC thermistor, β=3950, Rref=10 kΩ @ 25°C. Library returns °C; application averages 256 samples for noise rejection. |
| 2 | Flow switch | SWT | Digital in | Debounced switch input. Immediate heater cutout if open; also published as status telemetry. |
| 3 | Jets panel button | SWT | Digital in | Each rising edge cycles jet state: off → jet 1 → jet 2 → both → off. |
| 4 | Lights panel button | SWT | Digital in | Debounced toggle for on/off. Color set via MQTT only. |
| 5 | Bubbles panel button | SWT | Digital in | Debounced toggle for blower pump. |
| 7 | Filter pump relay | RELAY | Relay out | Hold power 350 ms, derate to 250 ms. Disabled while jets or blower run. |
| 9 | RGB red channel | POUT | PWM out | Three POUT channels at 1 kHz. Payload is a single 0xRRGGBB integer via MQTT; firmware splits into per-channel PWM values (0–1000 scale). |
| 10 | RGB green channel | POUT | PWM out | |
| 11 | RGB blue channel | POUT | PWM out | |
| 12 | Jet pump 1 relay | RELAY | Relay out | Hold 350 ms / derate 250 ms. Addressable individually or as a pair via MQTT. |
| 13 | Jet pump 2 relay | RELAY | Relay out | Hold 350 ms / derate 250 ms. |
| 14 | Blower/bubbles relay | RELAY | Relay out | Hold 350 ms / derate 250 ms. Triggers filter load-shedding when active. |
| 16 | Heater relay | RELAY | Relay out | Hold 350 ms / derate 500 ms. Requires both flow confirmed and system power on. |
7 channels free. Channels 6, 8, 15, and others remain unassigned. Candidates for expansion: UV disinfection enable, chemical dosing pump, ozone generator, towel rail heater, or a second temperature zone.
Panel Architecture and Wiring
The EQSP32C operates at 24 V DC and its output channels source up to 1 A each, which is sufficient to drive 24 V relay coils directly. The relay contacts then switch the 230 V AC loads: jet pumps, blower, filter pump, and heater. This is the same two-stage architecture used by most professional panel builders: the controller never sees AC voltage on its output terminals.
The heater relay is DPST (double-pole single-throw), breaking both the live and neutral conductors. This is a code requirement in wet-environment installations across most jurisdictions, and it prevents leakage current from energising the heating element when the relay is commanded off. All other loads use SPST relays.
A ground-fault circuit interrupter (GFCI/RCD) is fitted upstream of all 230 V AC loads. This device is independent of the EQSP32 and provides the primary safety layer against ground faults in the wet zone. The EQSP32's flow-switch interlock is a secondary, application-level protection for the heater element specifically.
Relay derate pattern
The EQSP32 RELAY mode applies full drive current for a configurable hold period, then reduces to a lower sustain current. This reduces relay coil heating significantly in installations where relays may remain energised for hours at a time. The fill valve solenoid uses a shorter hold time (50 ms vs 350 ms for the pumps) because solenoid valves pull in faster than mechanical relay armatures and benefit from a more aggressive derate.
Temperature Control and Heater Interlock
The water temperature is read by a 10 kΩ NTC thermistor connected in a voltage divider configuration to the EQSP32 TIN input on channel 1. The EQSP32 library handles the β-model conversion internally (β=3950, Rref=10 kΩ at 25°C), returning a calibrated temperature in tenths of a degree Celsius. The firmware maintains a 256-sample ring buffer of raw readings and averages them before use, providing effective noise rejection without phase lag on slow thermal processes.
Thermostat logic
The setpoint is configurable via MQTT in the range 0–45°C (default 20°C). The controller uses a symmetric ±1°C hysteresis band to prevent relay chatter near the setpoint:
| Condition | Action |
|---|---|
| Water temp < setpoint −1°C AND flow confirmed | Heater ON |
| Water temp > setpoint +1°C AND flow confirmed | Heater OFF |
| Within band (setpoint −1 to +1°C) | Heater state latched (no change) |
| Flow switch open (no water flow) | Heater OFF immediately, every control cycle |
| System power off | Heater OFF, jets and filter off |
| Temperature reading out of range (<0 or >60°C) | Reading set to error sentinel (99), heater off |
No delay on flow loss. The heater cutout on flow-switch open is evaluated every control loop iteration (10 ms). There is intentionally no debounce delay on this path: if water flow stops for any reason, the heater de-energises within one scan cycle. Open-circuit (broken thermistor wire) and short-circuit conditions are also detected by the TIN driver and treated as faults.
Filter Scheduling and Load Management
The filter pump runs on a recurring one-hour cycle. Within each hour the filter runs for a configurable period of 0 to 60 minutes, then rests for the remainder. Setting it to zero disables scheduled filtering entirely. The cycle restarts automatically at power-up. Whenever the jets or blower are active, the filter switches off and its timer pauses rather than resets, so the filter always completes its scheduled runtime regardless of interruptions. This prevents the jets and filter from simultaneously loading the single-phase supply, which would exceed the panel's rating.
AI Programming: a Natural Fit for the Control Engineer
Look at the control requirements for this installation: a thermostat with hysteresis, a flow-switch safety interlock that overrides everything else, a filter cycle that yields to the jets, a swimjet timeout. Every one of these is a condition with a consequence — precisely the kind of structured, causal reasoning a PLC programmer does instinctively. Writing it in ladder is natural. What is less natural is translating that logic into C++ firmware, managing timers in code, wiring up MQTT topics, and building the smartphone interface on top. That work traditionally belongs to a software developer, and the hand-off between disciplines introduces delay, misinterpretation, and rework.
With the EQ-AI Coding Assistant, that hand-off disappears. The entire hot tub firmware — thermostat logic, flow interlock, filter scheduling, jets timeout, MQTT publishing — was generated by describing each requirement in plain English, in the same terms a controls engineer would use to brief a colleague. EQ-AI produced working C++ code. The engineer reviewed it against logic they already understood. No software developer was involved.
From Ladder Logic to AI Prompt
A PLC programmer approaching the heater interlock would instinctively reach for a set/reset latch with a safety override rung. The logic is straightforward to express in ladder: three rungs, each with a clear condition and a clear action. The EQSP32 firmware implements exactly the same logic, and it was generated by describing that logic to EQ-AI in plain English rather than drawing rungs on a screen.
The comparison below shows the same requirement expressed in both forms. The ladder is how a PLC programmer thinks about the problem. The prompt is what produces the working firmware.
Left Rail (Power) Right Rail (Neutral)
| |
|== RUNG 1: Latch Heater ON ========================================|
| |
| System_Power Water_Flow Temp < (SP-1°C) Heater_ON |
|--------| |--------------| |--------------| |--------------(S)-----|
| |
| |
|== RUNG 2: Normal Cut-off =========================================|
| |
| Temp > (SP+1°C) Heater_OFF |
|--------| |------------------------------------------------(R)-----|
| |
| |
|== RUNG 3: Safety Interlock & Power-Down Override =================|
| |
| Water_Flow Heater_OFF |
|--------|/|------------+-----------------------------------(R)-----|
| | |
| System_Power | |
|--------|/|------------| |
| |
Control a heater relay on output channel 16. Turn the heater ON when all three conditions are true: - system power is enabled - the flow switch on channel 2 is closed - water temperature is more than 1°C below the desired setpoint Turn the heater OFF when temperature rises more than 1°C above the setpoint. If the flow switch opens at any time, turn the heater off immediately with no debounce delay. This is a safety interlock — evaluate it every control scan, before all other heater logic.
EQ-AI generated the firmware implementation from this description. The output correctly handled the hysteresis band, the set/reset latch structure, and the priority ordering of the flow-loss safety rung over the temperature conditions — without the engineer specifying the implementation mechanism. For a PLC programmer, the translation is natural: wherever you would draw a rung, you write a sentence.
Connecting to the Internet
Remote access is no longer optional. Property managers, maintenance teams, and end users expect to check status and make adjustments from a phone, whether they are on-site or across the country. Connectivity also enables proactive service: a technician can verify water temperature and heater state before driving to a site, log historical data to identify recurring faults, and push configuration changes without a physical visit. Any product that lacks this capability is increasingly difficult to justify against alternatives that include it as standard.
MQTT is the protocol that makes this possible. The EQSP32 publishes the state of every element — heater, filter pump, jets, blower, and lights — to a cloud broker in real time, and accepts commands on the same connection. A smartphone app subscribes to those topics and gives the property manager full visibility and control from anywhere, with no port-forwarding or VPN required. The broker also retains the last known value of every topic, so the app displays current status the moment it opens, without waiting for the controller to publish again.
How the Webapp Was Built: an Agentic Development Workflow
The webapp was not designed screen by screen in a visual editor or assembled from a component library. The development process started with a single structured prompt given to a Claude AI agent running inside the Cursor IDE. The prompt described the desired interface in plain terms (four screens, a temperature gauge, rotary dials, a colour wheel, a bottom nav bar) and pointed the agent at two sources of truth: the EQSP32 firmware source file and the MQTT broker credentials. The agent read the firmware, identified every entity registration call, reconstructed the complete topic list from those registrations, connected to the broker to verify retained values, and then wrote the entire PWA (HTML, CSS, JavaScript, service worker, and manifest) as a single coherent codebase. The first version ran in a browser and communicated correctly with the physical controller in under an hour.
I have an EQSP32 IoT controller running the firmware in LimniJacuzziMQTT.ino. Read the source file and extract every MQTT topic the device publishes and subscribes to — they follow the pattern state/EQ_<deviceId>/<entity_name> and cmd/EQ_<deviceId>/<entity_name>. The device ID is LimniTub. The broker is EMQX Cloud at p6c2051c.ala.eu-central-1.emqxsl.com, port 8084 over WebSocket Secure, with the credentials I will provide. Build a mobile-first progressive web app in vanilla HTML/CSS/JS (no framework, MQTT.js via CDN) that connects to this broker and provides full control of the hot tub. The UI should have a dark industrial theme and four screens accessible from a bottom nav bar: a main screen with an SVG arc temperature gauge, status chips for heater/filter/flow, and buttons for jets 1, jets 2, bubbles, lights, and a master power toggle; a heater screen with a rotary canvas dial for the temperature setpoint; a lights screen with a canvas HSV colour wheel that sends a single 0xRRGGBB integer to the colour topic; and a filter screen with a rotary dial for filter run duration. Every button should show an orange pending state immediately on tap, clearing only when the corresponding status topic arrives from the controller. Track controller liveness separately from broker connectivity: if no state/EQ_LimniTub/ message arrives for 15 seconds, mark the device offline even if the WebSocket is still connected. Auto-reconnect to the broker every 4 seconds on disconnect. Include a PWA manifest and service worker that cache the app shell for offline loading. The app should be installable via Add to Home Screen on iOS and Android.
For a controls engineer, the closest analogy is asking a colleague to read your PLC program and build you a SCADA screen for it. Normally that exchange takes days: you document the tag database, hand it to the SCADA developer, answer questions about what each tag means, review a first draft, and iterate. Here, the agent performed all of that autonomously. It did not need a tag list because it could read the source of truth directly. It did not need to ask what st_water_flow means because it could see the flow-switch interlock logic in the firmware and infer the correct label, colour coding, and behaviour for the corresponding indicator. The mapping from controller state to interface element was derived, not specified.
What this means in practice is that the HMI and the firmware stayed in sync without any manual coordination effort. When a new topic was added to the firmware, the agent could be asked to add the corresponding indicator to the right screen with another short prompt. There was no tag database to update, no separate configuration file to edit, no HMI software licence to open. The webapp is plain text, versioned in a git repository alongside the firmware, readable and modifiable by any engineer who can open a text editor. That combination of AI-generated speed and full human auditability is what makes the agentic development workflow genuinely different from either hand-coding a UI or using a proprietary IoT platform.
Webapp as Smartphone Interface
The control interface is a progressive web app (PWA): a static HTML/CSS/JavaScript application served from a web server and accessed through the smartphone's browser. Once visited, the user taps "Add to Home Screen" and the app installs like a native application, with its own icon, launch screen, and full-screen display mode. There is no App Store review process, no user account, and no subscription.
Why a webapp instead of a native or proprietary app
A number of commercial IoT platforms offer pre-built mobile applications that can be connected to an MQTT device. These platforms are convenient at the prototype stage, but they introduce dependencies that are problematic in a professional or commercial installation. The webapp approach used here avoids all of them:
No App Store review process
Publishing a native app to Google Play or the Apple App Store involves account fees, compliance reviews, binary submissions, and release cycles that can take days to weeks. A webapp is deployed by copying files to a web server. Updates are live the moment the file is saved: no review, no waiting, no version management across stores.
Installable on any device
PWA installation via "Add to Home Screen" works on iOS Safari, Android Chrome, and desktop browsers. One codebase covers all platforms with no platform-specific builds required.
Webapp screen layout
The app has four screens, each accessible from a bottom navigation bar:
- Main: SVG arc gauge showing live water temperature and setpoint, status chips for heater, filter, and flow switch, four actuator buttons (jets 1, jets 2, bubbles, lights), and a master power toggle. Tapping the temperature gauge sends a force-update command. WiFi RSSI and controller online status are shown in the header.
- Heater: A rotary dial for adjusting the temperature setpoint (10–42°C). The dial updates the MQTT setpoint topic on release, not on every degree of movement.
- Lights: A canvas-rendered HSV colour wheel. Releasing the picker sends the new colour as a single 0xRRGGBB integer. The colour preview updates from the controller's published R/G/B values to confirm the command was received.
- Filter: A rotary dial for the filter run duration (0–60 minutes), with a live indicator showing filter pump status.
The app monitors controller liveness independently of the MQTT broker connection. If no status message arrives from the controller for 15 seconds, the interface shows the controller as offline even if the broker WebSocket is still connected. Reconnection to the broker is automatic with a 4-second retry interval.
Bill of Materials
| Qty | Item | Description | Notes |
|---|---|---|---|
| 1 | EQSP32C | 16-channel industrial IoT controller, DIN-rail, WiFi/BLE/Ethernet | Erqos Technologies |
| 1 | 24 V DC PSU | 220 V AC to 24 V DC, 2.5 A (Meanwell HDR-60-24 or equivalent) | Powers EQSP32C and relay coils |
| 1 | GFCI/RCD | 30 mA ground-fault circuit interrupter, 2-pole or 4-pole | Upstream of all wet-zone 230 V AC loads |
| 3 | Panel air buttons | Pneumatic (air-pulse) switches for jets, lights, bubbles; electrically isolated | Spa-rated, wet-zone safe |
| 5 | SPST relays (24 V coil) | For filter pump, jet pump 1, jet pump 2, blower, drain valve | Rated for motor load; e.g. Finder 40.52 |
| 1 | DPST relay (24 V coil) | For heater (breaks both live and neutral) | Required for wet-environment heater isolation |
| 1 | NTC thermistor | 10 kΩ @ 25°C, β=3950 (e.g. NTCLE413E2103F106A) | Waterproof probe, immersed in water flow path |
| 1 | Water flow switch | Paddle-type or differential pressure flow switch, SPST N/O | Heater safety interlock |
Why EQSP32 for This Application
16 software-configurable I/O channels
Every channel can be independently set as RELAY, POUT (PWM), TIN (thermistor), SWT (debounced switch), or AIN (analog). No fixed I/O allocation, no need for separate relay modules or PWM drivers for the RGB lighting.
Solid-state outputs drive relay coils directly
The EQSP32C's 1 A solid-state outputs can sink 24 V relay coils without intermediate transistor stages. The same outputs that switch small 24 V coils can also drive solenoid valves, LED drivers, or indicator lamps, with no external drive circuitry.
Native MQTT over TLS
MQTT with TLS is handled by the EQSP32 library, including certificate management and automatic reconnection. The firmware sketch contains only application logic. No MQTT library integration, no TLS handshake code, no reconnect loop to write.
Provisioning without code changes
WiFi credentials, MQTT broker address, and device identity are configured over Bluetooth from the EQSP32 smartphone provisioning app. No firmware recompilation is needed when installing the controller at a new site or when the WiFi network changes.
EQ-AI firmware generation
The application firmware was generated almost entirely by EQ-AI, the EQSP32-specific AI coding assistant, from a plain-language description of the required behaviour. The integrator reviewed, tested, and adjusted the output. Total development time from specification to running firmware: under two days.
Ready to build a similar system?
The full source code for the EQSP32 firmware and the webapp is available on request. Our engineering team can also design and deliver a complete integration for your application.
Get started with EQSP32