Skip to content

External Interfaces

Beyond its own on-board sensors, the regulator listens to the rest of the boat: the NMEA2000 backbone, an NMEA0183 serial feed, a Victron solar charger, and a digital temperature probe on the alternator itself. This page covers what each interface carries, how it is wired, and where the parsing happens in the firmware.

Interface summary

Interface Pins Speed / mode Enabled by
Shared sensor bus (I²C) SDA = GPIO9, SCL = GPIO10 400 kHz (in-spec Fast-mode) always on
NMEA2000 (CAN) RX = GPIO16, TX = GPIO17 standard CAN NMEA2KData user setting
Victron VE.Direct (serial) RX = GPIO7 on Serial1 19200 8N1, inverted logic VeData user setting
NMEA0183 (serial) RX = GPIO6 on Serial2 19200 8N1, normal logic parser not yet enabled
1-Wire (DS18B20) GPIO13 1-Wire always on

All of these are brought up in initializeHardware().

Shared sensor bus (I²C)

The on-board chips — battery monitor (INA228), analog converter (ADS1115), motion sensor (LSM6DSOX IMU), and barometer (BMP390) — share one I²C bus. Two details matter for contributors:

  • The clock is 400 kHz, which is within specification (Fast-mode) for every chip on the bus. An earlier experiment ran the bus faster, out of spec; it was reverted after a sensor dropped off the bus, and the comment at the Wire.setClock() call in initializeHardware() records that history. Per-device I²C error counters (for example adsI2CErrorCount) are the tripwire for bus health.
  • A short transaction timeout (Wire.setTimeOut()) bounds how long a wedged device can stall a bus read, so an electrical glitch costs milliseconds instead of hanging the control loop.

The individual chip drivers are documented separately; this page is only about the bus they share.

NMEA2000 (CAN)

The regulator is a read-only listener on the NMEA2000 backbone — it consumes data but does not claim a source address or transmit its own messages.

The CAN driver (tNMEA2000_esp32) delivers every received message to one dispatch function, HandleNMEA2000Msg(), which walks a fixed table (NMEA2000Handlers[]) mapping message types (PGNs) to handler functions. NMEA2000.ParseMessages() is pumped from the main loop only when the user has enabled NMEA2000 input (NMEA2KData) and real hardware is present.

Data consumed from the bus:

  • Position and satellites (GNSS() handler) — latitude, longitude, satellite count, with sanity checks before anything is stored.
  • Course and speed over ground (COGSOG() handler) — also feeds the session and lifetime maximum-speed records.
  • Heading (Heading() handler).
  • Apparent wind (WindSpeed() handler) — apparent wind speed and angle, plus wind maximums.
  • Speed through water (Speed() handler) — stored separately from speed over ground because the sailing-performance system prefers through-water speed, which excludes current. See Sailing Performance.
  • GPS time (SystemTime() handler) — feeds syncTimeFromGPS(), the GPS leg of the time-sync chain. The chain has three tiers: network time is preferred when the boat has internet; GPS is the fallback; and below both, a boot soft-clock estimate (restoreSoftClock(), source label TIME_ESTIMATED) adopts the retained RTC or an NVS-stored epoch (SoftClockEp) so an offline or AP-mode device still has a usable timebase. The estimate snaps to the real value as soon as network or GPS reports.

Several other message types (rudder, attitude, depth, DC status) are parsed and printed for diagnostics but not stored.

High-rate messages — heading, wind, position, course/speed — are individually throttled inside their handlers (roughly every couple of seconds) so a chatty bus cannot dominate the loop.

From the raw apparent-wind and motion data, calculateDerivedMetrics() computes true wind speed and angle, leeway, and velocity made good (VMG) every tick.

Victron VE.Direct (serial)

A Victron solar charge controller broadcasts plain-text status frames continuously on its VE.Direct port. The regulator receives them on Serial1 with inverted logic (note the final argument in the Serial1.begin() call — getting this wrong yields garbage, not silence).

ReadVEData() runs every couple of seconds when the user has enabled Victron input (VeData). It drains the serial buffer through the VE.Direct frame parser with a per-call budget so a chatty device cannot starve the main loop, then picks named fields out of the decoded frame. Three of them carry real weight:

  • Battery voltage (VE.Direct VVictronVoltage) — display and cross-check only. The regulator's own voltage reading always comes from its battery monitor; the Victron value is never used for control.
  • Battery current (IVictronCurrent) — can serve as the battery-current source for display and accounting, but only when the user explicitly selects it (BatteryCurrentSource).
  • Panel power (PPV) — feeds session and lifetime solar energy accumulators. Fractional watt-hours carry over between calls so slow trickles are not rounded away, and lifetime totals persist across reboots.

The rest are dashboard readouts, taken as reported: panel voltage (VPV), charge state (CS), tracker mode (MPPT), error code (ERR), and the yield history (H20H23 — today's and yesterday's yield and peak power). The Victron device's own state-of-charge field is deliberately not read; the regulator computes state of charge itself from current.

The three weighted values each pass a plausibility check before they are accepted.

NMEA0183 (serial)

Serial2 is initialized at 19200 baud, normal logic, with the expectation of a single combined NMEA0183 feed (for example from a multiplexer that aggregates the boat's instruments onto one wire). The parser is not currently wired into the main loop — the library include in Xregulator.ino carries a comment to that effect. The wiring and baud-rate expectation are documented here so that when the parser is re-enabled, an existing installation will already be feeding the right pin.

1-Wire temperature probe (DS18B20)

The alternator temperature probe is a DS18B20 digital sensor on its own 1-Wire bus (GPIO13). It is deliberately kept off the main core: a dedicated task (TempTask) on the second core owns the bus, performs the slow conversion-and-read cycle, validates every read with a CRC check, and retries suspect reads. The control loop on the main core only ever reads the resulting shared value (AlternatorTemperatureF), so a slow or flaky sensor can never stall field control.

Data freshness

Every externally sourced value has a "last updated" timestamp, bumped by the MARK_FRESH macro only after a successful parse and sanity check. These timestamps stream to the dashboard on their own channel (the TimestampData server-sent event), and the browser greys out any field whose source has gone quiet — so a disconnected wind transducer shows up as visibly stale data rather than a frozen number.