Sensors¶
External data sources (VE.Direct, NMEA2000, NMEA0183) are on External Interfaces.
The shared I²C bus runs at standard Fast-mode speed (400 kHz) with a short bus timeout, so a wedged device costs milliseconds, not a hung loop. Every sample that passes validation stamps a freshness timestamp (MARK_FRESH(IDX_*)); consumers check staleness against those stamps rather than using the last value indefinitely.
ADS1115 Four-Channel Analog Converter (I²C 0x48)¶
Configured for a wide input range and its maximum sample rate, in single-shot triggered mode. The library wrapper is used only for setup and conversion triggering; conversion-register reads are done as raw two-phase I²C transactions so the firmware can avoid the library's blocking poll and time bus stalls in microseconds. Search 5_functions.ino for _ReadAnalogInputs_inner.
Non-blocking state machine¶
A three-state machine drives each conversion without blocking the loop:
- Idle: select the next channel, trigger a conversion, note the time.
- Wait: a time-based ready check (a few milliseconds, with margin over the theoretical conversion time); a timeout retries the same channel.
- Read result: raw register read with microsecond timing, channel-specific scaling, a plausibility-band check, then the freshness stamp. Repeated consecutive failures set
ADS1115Disconnectedso a dead chip cannot stall the system.
Sample sequence¶
The channel order interleaves alternator current between every other measurement:
adsSeq = { CH1, CH0, CH1, CH2, CH1, CH3 }
CH1 (alternator current) gets half the slots, since it feeds the fastest control loop; battery voltage, RPM, and the thermistor each get one slot per cycle.
Each step waits ADS_CONVERSION_MS (3 ms) after its trigger, and the next channel is triggered as soon as the result is read, so the main loop's cadence sets the real slot length rather than a fixed grid. Measured on the bench with the field on, CH1 (three of the six slots) refreshes about every 13 ms, with gaps from 10 ms to about 30 ms (the longest field-on gap seen was 34 ms). The other three channels refresh once per cycle, every third CH1 reading: about every 40 ms, stretching to 70–80 ms in the worst field-on cycles. With the field off, background work such as flash writes can stretch any channel's gap to a few hundred milliseconds. The firmware measures CH1's real rate at run time (ch1SampleHz() in 7_functions.ino: about 75 samples per second, 95% of intervals under about 20 ms) and treats it as the hard ceiling on what any swept-sine measurement can resolve: sweepAliasLimitHz() caps sweeps at a third of it (about 25 Hz), because oversampling a held value adds no information.
What each channel measures¶
| Ch | Variable | Source | Notes |
|---|---|---|---|
| 0 | BatteryV (V) |
High-ratio resistor divider | Used for the cross-sensor checks on the safeties page, disagreement and plausibility, never for normal control; it replaces the main reading only when the owner runs without the INA228 under Run Without Failed Sensors |
| 1 | MeasuredAmps (A) |
Hall-effect current clamp, mid-rail centered | Scale selected by AmpSensorRange; optional sign flip (InvertAltAmps); user offset (AlternatorCOffset) and temperature-compensated zero-correction offset (DynamicAltCurrentZero) subtracted |
| 2 | RPM |
LM2907 frequency-to-voltage converter (see LM2907 front end) | Scaled by RPMScalingFactor; very low readings forced to zero; a lone jump from near zero to above 1000 is held back until a second reading confirms it |
| 3 | temperatureThermistor (°F) |
NTC thermistor via op-amp chain | Converted by the Beta-equation helper; search 5_functions.ino for thermistorTempC. The thermistor constants (R_fixed, R0, Beta, T0_C) are user settings persisted in NVS |
Each channel has its own plausibility band; readings outside it are discarded and never stamped fresh, and the channel holds its last good value. The stale rules count ADS cycles, so one long loop pass does not read as a dead input, with a 3 s wall-clock backstop that counts only while the state machine is stepping. CH0 goes stale after four failed reads in a row, which the second-battery-voltage protection acts on. CH2 reads as a stopped engine (RPM 0) after ten, and so do all consumers while the chip is disconnected. Every committed CH2 reading is stamped, whatever its value, including a held-back lone jump. The engine-stopped and spin-down cuts count these stamps to confirm a second low reading. The RPM gate counts them the same way, except while the tach-lie cut is armed, when it counts only readings that were not held back. If none arrives, the RPM gate and the shutdown spin-down cut fire after 160 ms and the engine-stopped cut after 300 ms, so a stalled channel cannot hold the field on. CH1 is stale after 10 s without an in-range reading, which cuts the field.
Why CH1 is special¶
CH1 sets the control loop's rate. Every confirmed sample:
- Has its arrival time logged in a large PSRAM ring that measures the real sample rate and its worst gaps (
ch1_recordin 7_functions.ino). - Updates the smoothing filters the current loop can act on (exponential and moving-average variants, so the user-selectable signal sources are always populated). The current-excess supervisor keeps its own average of the raw reading.
- Updates session and lifetime peak-current watermarks.
- Sets a freshness flag that gates one pass of the control function: the control loop runs per fresh sample, not on a wall-clock timer.
Performance counters (I²C error count, slow-read count, worst-case timings) are published to the dashboard so a marginal bus shows up as numbers rather than unexplained stalls.
INA228 Battery Voltage and Current Monitor (I²C 0x40)¶
A high-resolution delta-sigma converter measuring battery bus voltage (IBV) and shunt current (Bcur). It is the only voltage source for all control and safety voltage decisions (unless the owner runs without it under Run Without Failed Sensors), and it also drives the hardware overvoltage backup: its alert pin physically pulls the field enable line down with no firmware involvement (see Safeties and Protections).
Two sampling modes¶
The chip is reconfigured on the field-state edge (inaFastModeActive):
- Field on, fast mode: 4-sample averaging with 540 µs bus and shunt conversions plus a 50 µs die-temperature conversion (~4.5 ms update), polled on the first main-loop pass at least 5 ms after the previous read, so reads occur about every 6 ms (up to about 18 ms worst case). Feeds the voltage loop, the rate-of-rise prediction, and load-dump detection. Because the hardware alert compares against the chip's averaged value, it also sets the alert's filter depth.
- Field off, slow mode: 128-sample averaging with 4120 µs bus and shunt conversions (~1.06 s update), polled about once a second. Quiet idle monitoring; the hardware alert compares against the same deep average, which has no effect with the field already off: the pin simply holds the enable line low while the condition persists.
Reconfiguration costs a few register writes on the transition edge only. On the switch to fast mode, the smoothed-voltage filter is reseeded so the voltage loop starts from the present reading, not a stale average.
Per-read pipeline¶
Each read fetches bus voltage and shunt voltage, and checks the shunt read for a bus error or a not-a-number result. The battery voltage correction comes next. When Shunt Location (ShuntLead) is Negative lead, a shunt is installed with its resistance set (HAS_BATT_SHUNT), and the shunt read is good, the shunt's own drop is subtracted from the bus voltage: the raw shunt volts, signed by InvertBattAmps, with none of the current-path calibrations applied. With the Regulator Ground Wire on the load side of a negative-lead shunt, board ground sits below the battery post by that drop, so the uncorrected reading runs high while charging (INA228). The bus voltage is then validated (bus error, not-a-number, and a plausibility window above 5 V and below a ceiling that scales with the voltage class, capped at the chip's 85 V), committed as IBV, stamped fresh, and fed to the smoothing filters, so every consumer sees the corrected value. The hardware overvoltage alert still compares the chip's raw bus voltage. A good shunt read then yields current from the user's configured shunt resistance (ShuntResistanceMicroOhm), with the calibration offset (BatteryCOffset) and optional inversion (InvertBattAmps) applied. Only validated reads stamp freshness; a failed read holds the last good value. The stale rule counts reads rather than wall-clock time, with a backstop: ten failed reads in a row or 1 s without a valid one while the field is on (three reads or 5 s while it is off) marks the main battery voltage stale, which cuts the field in Auto mode. Three seconds of I²C errors also marks the chip disconnected. Derived per read:
IBV_filtered: a time-constant-aware exponential smooth used for display and the voltage-loop error term (getFiltV()).g_dBcur_dt: battery-current slew rate in amps per second, the input to load-dump detection.- Watermarks: session and lifetime voltage extremes, plus per-ignition-cycle high/low tracking.
The hardware overvoltage threshold (VoltageHardwareLimit, the Hardware Shutdown Voltage) is its own absolute, persisted user setting, not derived from the chemistry or the bulk target. It is reprogrammed into the chip when the setting itself is changed and when a system-voltage-class change rescales it. Because a chip that resets comes back with its limit at the power-on maximum, the firmware also re-applies the whole configuration when the chip answers again after I²C errors, and reads the threshold back every 30 s to re-apply it on a mismatch. How to place it is covered on Protection Timing; search 5_functions.ino for updateINA228OvervoltageThreshold.
Accessor convention¶
getBatteryVoltage() → IBV (control + safety source of truth)
getBatteryCurrent() → Bcur (the regulator's own shunt; a network monitor's current never feeds it)
getFiltV() → smoothed IBV (display, never safety)
BatteryV from the ADS1115 is not a normal control signal. It exists so two independent sensors can cross-check each other, and it becomes the battery voltage only while the owner runs without the INA228 reading (Main battery voltage under Run Without Failed Sensors), where it feeds the software over-voltage cut and the fixed-duty cap.
DS18B20 Temperature Probes (OneWire)¶
Runs entirely in its own task (TempTask) on the second CPU core, because a full conversion takes most of a second, and running it in the main loop would disrupt control timing. The task feeds a heartbeat that the main core watches (the T4 protection on the safeties page).
The per-iteration sequence: heartbeat, a few cooperative gates (over-the-air-update in progress, hardware-absent flag, core-busy flag), re-enumeration if a probe was lost, an adaptive poll cadence (5 s after a good read, a 1 s retry after a failure, 60 s with the engine stopped and no dashboard connected, and one forced cycle the moment the engine starts), a single Skip-ROM convert command that every probe on the bus obeys at once, a non-blocking conversion wait, then one scratchpad read per probe.
One bus, up to six probes, three roles¶
The 1-Wire bus (GPIO13) is multi-drop. owEnumerate() searches it, keeps every DS18B20 with a valid ROM code in a small registry (owProbes, six entries), and binds probes to roles (alternator, battery, extra) by stored ROM code rather than by position, because the search order is by serial number and would otherwise shuffle when a probe is added. The bindings persist in NVS (owAddrAlt / owAddrBatt / owAddrExtra); the dashboard reads the registry from /tempSensors and assigns roles under Setup → Temperature → Temperature Sensors. A single-probe device binds that probe to the alternator role automatically, so nothing has to be assigned by hand to keep working, but only when the alternator role has never been assigned, the alternator's Temp Source is the digital probe, and both the battery and extra probe enables are off. A stored alternator probe that has gone missing is reported as a fault, never replaced by another probe on the bus. On a thermistor install, or once either of those enables is on, a lone probe is left unassigned: it is presumed to be a battery or extra probe not yet assigned a role.
Only the alternator role feeds AlternatorTemperatureF and the thermal protections; a failed read on the battery or extra probe never affects the alternator staleness cut. The task never writes flash itself; an auto-bind is queued and persisted from the main loop's field-off block.
Five-check validation¶
Every raw reading must pass all five before it is accepted:
| Check | What it catches | Failure counter |
|---|---|---|
| CRC of the scratchpad (one immediate retry) | Bus noise, marginal pull-up | tempCrcFailCount (tempCrcRecoveredCount on retry success) |
| All-bytes-0xFF | Sensor physically disconnected | tempAllFFCount |
| Power-on reset signature | Sensor rebooted and is returning its placeholder value | tempPowerOn85Count |
| Resolution-config drift | Sensor lost its configured resolution; firmware rewrites it and re-converts | tempResolutionFixCount |
| Plausible range | Impossible temperatures | tempOutOfRangeCount |
A passing read on the alternator probe updates AlternatorTemperatureF, the session and lifetime maxima, and the freshness stamp; a passing read on the battery or extra probe updates BatteryTempProbeF / ExtraTempF and its own freshness stamp, but only while that role's enable toggle is on. Persistent failures queue a throttled console warning.
Every counter above is exposed under Live Data → ESP32, so a marginal crimp or noisy bus shows its specific failure mode rather than a generic "temperature stale." A rising recovered-CRC count with a steady temperature is an early noise warning before anything goes stale.
BMP388 Board Temperature and Barometric Pressure (I²C)¶
The barometer (a BMP388; the pin-compatible BMP390 fits the same footprint, which is why the hardware page carries both names) runs heavy pressure oversampling with infinite-impulse-response filtering, in forced one-shot mode inside the same non-blocking state machine as the ADS1115: trigger a conversion every four seconds, poll for completion, validate plausible pressure and temperature ranges, convert, stamp fresh. The cycle is sized against the freshness timeout: at four seconds a single missed read still leaves the value eight seconds old, inside the ten-second window that consumers treat as stale. The first sample after boot is discarded, and a timeout with a throttled warning covers a wedged conversion.
Use sites: ambientTemp (displayed as board temperature, and since the sensor sits on the powered board it reads warmer than true ambient) is a display and telemetry value, and it is one of the two candidate regressors in the alternator-current zero-drift fit, which each day picks whichever of the board or the alternator probe explains its own recorded drift better. No battery calculation reads it. baroPressure is logged for trip analytics and weather correlation.
LSM6DSOX Six-Axis IMU (I²C)¶
Accelerometer and gyroscope in hardware-FIFO mode, drained from the main loop after the control function so an IMU bus stall can never delay control-critical reads.
Initialization (imuInit() in 4_functions.ino)¶
Hard-fails (sets imuEnabled = false rather than running in a degraded mode) on any of: no I²C acknowledge at the address, a wrong chip-identity register (WHO_AM_I), or any failure configuring sensor enables, output data rates, FIFO batching rates, or continuous-FIFO mode. The accelerometer runs at roughly a hundred samples per second (margin over the tens-of-milliseconds slam pulses it must catch); the gyro at about half that, sufficient for hull roll and pitch dynamics. After configuration the init reads a few FIFO entries and prints their decoded sensor tags as an empirical self-check.
Draining (drainIMUFifo())¶
Polled on a short interval, pulling a capped number of FIFO entries per poll (MAX_FIFO_DRAIN_PER_POLL) so the cost per loop pass is capped. Each entry carries a tag identifying accelerometer, gyro, or temperature data; samples are written to a PSRAM ring with separate accel and gyro pointers. An error tracker (imuRecordI2CError()) counts errors inside a 60-second window and auto-disables the IMU at ten, with a console message.
Windowed motion metrics (updateAccelMetrics())¶
Aggregated over a configurable window when motion sensing is enabled (on enable, stale FIFO backlog is flushed first): attitude, slam/capsize/pitchpole event counting, comfort and seasickness estimates, and deviation statistics. What each metric means and how it is derived is covered under IMU Analytics. The event thresholds (CAPSIZE_THRESHOLD_DEG, PITCHPOLE_THRESHOLD_DEG, SLAM_THRESHOLD_G) are ordinary NVS settings loaded by InitSystemSettings(), with the counters themselves persisted in NVS.
These feed both the live tipping display and the slower summary statistics on the dashboard.
Data Freshness (the DataIndex enum)¶
Every sensor source has an index into a timestamp table; MARK_FRESH stamps it and IS_STALE compares against a default 10 s timeout, with the safety consumers (the current, voltage, and temperature staleness protections) applying their own windows. The roster, in Xregulator.ino near the DataIndex enum:
| Group | Indices | Use |
|---|---|---|
| Electrical | battery voltage (both sensors), battery current, alternator current | Control and safety; stale alternator current and either stale battery voltage are each a field-cut reason |
| Engine | RPM | Ceiling table and the three RPM cuts (the RPM gate on an energized field, the engine-stopped cut, and the shutdown spin-down cut), each of which waits for a second fresh RPM stamp. The tach-lie cut's 3 s count, and the RPM gate while that cut is armed, read a second stamp that a held-back reading does not move |
| Thermal | alternator probe, thermistor, battery probe, extra probe, INA228 die temperature | Whichever alternator source is selected (TempSource) drives derating and the staleness protection; the battery probe is a candidate in the battery-temperature source chain, the extra probe feeds its own alarms; the die temperature feeds the field-drive heat limit, and 10 s without it is a field-cut reason |
| Environment | barometric pressure, board temperature | Analytics, display, and the board branch of the alternator-current zero-drift fit |
| Motion | IMU | Tipping and comfort metrics |
| Derived | duty cycle, field telemetry, state of charge, charge-time estimates, and similar | Dashboard display |
| External | NMEA 2000 position/wind/speed/depth fields, received battery status, Victron voltage, current and battery temperature, RV-C battery temperature, charge-limit follow, NMEA 0183 | Dashboard display, grayed out when stale; the three battery-temperature feeds are candidates in the battery-temperature source chain |
Disconnect Flags¶
Each device has a runtime "disconnected" flag so a failed chip stops being read instead of crashing the loop:
| Flag | Set by | Effect |
|---|---|---|
ADS1115Disconnected |
Failed init at boot, or five consecutive bus failures at run time | The ADS state machine is skipped and the chip is retried every 30 s; engine speed reads 0 and the other channels go stale until it answers |
INADisconnected |
Failed init at boot (INA.begin() did not answer), or 3 s of I²C errors at run time |
The chip is still polled at the slow cadence; values hold and go stale, and the firmware's alert check is skipped. The first answered read re-initializes the chip and re-arms its over-voltage threshold |
BMP388Disconnected |
Failed init at boot; begin() is retried once after a short delay to recover from a transient bus NAK, and only a second failure sets the flag (with a console warning) |
Conversions are never triggered, so board temperature and barometric pressure stop updating; boot continues normally |
imuEnabled = false |
Init failure, or the error-rate trip (ten I²C errors inside one 60-second window) | FIFO drain early-returns |
| Temperature task | Self-recovers via re-enumeration retries | The staleness protection cuts the field if readings stop |
All five are set by the firmware itself, and none is exposed as a settings parameter. The ADS1115 and INA228 flags clear by themselves once the chip answers again; the BMP388 and IMU flags take a reboot after the wiring is fixed. For bench work the fake-data injection path (ReadAnalogInputs_Fake) covers most development scenarios more cleanly.