Skip to content

Communication Interfaces

External-bus inputs the firmware consumes. WiFi/SSE/web is in Network & Web System; on-board chip drivers (ADS, INA, DS18B20, BMP388, IMU) are in Sensor Systems.

Bus Pins Speed / mode Variable gate Init in
I²C (shared) SDA=9, SCL=10 800 kHz always on initializeHardware()
NMEA2000 (CAN) RX=16, TX=17 Library-default CAN NMEA2KData == 1 initializeHardware()
Victron VE.Direct Serial1 RX=7 19200 8N1, inverted VeData == 1 initializeHardware()
NMEA0183 Serial2 RX=6 19200 8N1 (read in NMEA0183 work-in-progress path) initializeHardware()
OneWire (DS18B20) GPIO13 OneWire always on (Core 0) implicit via sensors.begin()

I²C clock note

Wire.setClock(800000) is currently in use — out of spec for LSM6DSOX (rated 400 kHz max) and ADS1115 in Fast-mode, but empirically stable in this design across multiple sessions with zero error counts (imu_i2c_error_count, imu_unknown_tag_count, adsI2CErrorCount all stay at 0). Gives ~15 % faster IMU drain vs 400 kHz. Revert to 400000 if any of those three counters ever go non-zero — that is the diagnostic trigger.

Wire.setTimeOut(15) (15 ms) is a defensive cap added April 2026 to bound stalled transactions.


NMEA2000 (CAN)

CAN driver from tNMEA2000_esp32, configured for PGN dispatch through a fixed handler table.

Init

NMEA2000.SetForwardType(tNMEA2000::fwdt_Text);
NMEA2000.SetForwardStream(OutputStream);     // OutputStream = &Serial
NMEA2000.EnableForward(false);               // don't print every received frame
NMEA2000.SetMsgHandler(HandleNMEA2000Msg);
NMEA2000.Open();

Dispatch — HandleNMEA2000Msg(N2kMsg)

Linear table walk. NMEA2KVerbose == 1 adds a serial print of the incoming PGN before dispatch.

tNMEA2000Handler NMEA2000Handlers[] = {
  { 126992L, &SystemTime },
  { 127245L, &Rudder },
  { 127250L, &Heading },
  { 127257L, &Attitude },
  { 127506L, &DCStatus },
  { 127513L, &BatteryConfigurationStatus },
  { 128259L, &Speed },                  // Boat speed through water (SOW)
  { 128267L, &WaterDepth },
  { 129026L, &COGSOG },
  { 129029L, &GNSS },
  { 129540L, &GNSSSatsInView },
  { 130306L, &WindSpeed },
  { 0, 0 }
};

NMEA2000.ParseMessages() is called from loop() only when NMEA2KData == 1 and hardwarePresent == 1. Each handler is responsible for its own throttle.

Per-PGN handlers

PGN Handler Throttle Variables set MARK_FRESH
126992 SystemTime SystemTime() none feeds syncTimeFromGPS(SystemDate, SystemTime) (no IDX_ — tempLastSuccessMillis-equivalent tracked inside the time-sync state machine)
127245 Rudder Rudder() none (parsed, not stored)
127250 Heading Heading() 2 s HeadingNMEA (deg, from radians) IDX_HEADING_NMEA
127257 Attitude Attitude() none (parsed, printed only)
127506 DCStatus DCStatus() none (parsed, printed only)
127513 BatteryConfigurationStatus BatteryConfigurationStatus() none (parsed, printed only)
128259 Speed Speed() none (parsed, printed only)
128267 WaterDepth WaterDepth() none (parsed, printed only)
129026 COGSOG COGSOG() 2 s COGNMEA (deg), SOGNMEA (knots), MaxSpeed/MaxSpeed_AllTime, wmIgnUpdate(wmIgn_SOG) IDX_COG_NMEA, IDX_SOG_NMEA
129029 GNSS GNSS() 2 s LatitudeNMEA, LongitudeNMEA, SatelliteCountNMEA. Sanity: |lat| ≤ 90, |lon| ≤ 180, sats > 0, none NaN/zero IDX_LATITUDE_NMEA, IDX_LONGITUDE_NMEA, IDX_SATELLITE_COUNT
129540 GNSSSatsInView GNSSSatsInView() none (verbose print only when NMEA2KVerbose)
130306 WindSpeed WindSpeed() 2 s ApparentWindSpeedNMEA (knots, from m/s), ApparentWindAngleNMEA (deg); UpdateWindMaximums() IDX_APPARENT_WIND_SPEED, IDX_APPARENT_WIND_ANGLE

Time sync from GPS

SystemTime PGN drives syncTimeFromGPS(daysSince1970, secondsSinceMidnight) (in 4_functions.ino). The time-sync state machine prefers NTP when available (in MODE_CLIENT with WiFi), falls back to GPS, and persists offset via saveTimeSyncState() / loadTimeSyncState(). Used for timestamping sensor-history records before cloud upload.

True wind / VMG derivation

calculateDerivedMetrics() (every tick when currentMode != MODE_CONFIG) combines ApparentWindSpeedNMEA/ApparentWindAngleNMEA with SOGNMEA/HeadingNMEA to produce TrueWindSpeedNMEA, TrueWindAngleNMEA, LeewayNMEA, VMGNMEA. These four feed MARK_FRESH(IDX_TRUE_WIND_*), IDX_LEEWAY, IDX_VMG. VMGUseTrueWind toggles whether VMG is computed against apparent or true wind angle.


Victron VE.Direct (Serial1, inverted)

Serial1 RX = GPIO 7 (TX disabled with -1), inverted logic (SERIAL_8N1, 7, -1, 1 — the final 1 is invert). The VE.Direct protocol broadcasts ASCII frames at 19200 baud continuously while the Victron device is connected.

ReadVEData() — every 2 s

Early-returns if VeData != 1 or fewer than 2000 ms since last read. Drains everything currently in Serial1's buffer through myve.rxData(). After each byte, walks the parsed frame's name/value table:

Field Stored variable Conversion Sanity MARK_FRESH
V (battery volts × 1000) VictronVoltage / 1000 > 0 && < 100 V IDX_VICTRON_VOLTAGE
I (battery current × 1000) VictronCurrent / 1000 > −1000 && < 1000 A IDX_VICTRON_CURRENT
PPV (panel power) solarPower_W (local) direct W 0 ≤ W < 10000

Solar energy accumulation

When PPV is valid, the elapsed time since lastSolarEnergyUpdate is multiplied by solarPower_W and divided by 3600 to get Wh. Two accumulators (solarEnergyAccumulator session, solarEnergyAccumulator_AllTime lifetime) carry float fractions; only integer Wh roll into SolarChargedEnergy and SolarChargedEnergy_AllTime, the fraction stays in the accumulator across calls. Lifetime totals persist in NVS.

Timing telemetry

VeTime = micros() delta of one drain — shown on the ESP32 stats panel.

Why VE.Direct is a secondary battery source

getBatteryCurrent() returns VictronCurrent when BatteryCurrentSource == 3 (the user explicitly opts in). getBatteryVoltage() does not fall through to Victron — it always returns INA228 IBV. The Victron voltage exists only for display and the user-driven shunt-gain auto-correction.


NMEA0183 (Serial2, normal logic)

Serial2 RX = GPIO 6, 19200 8N1, normal logic (SERIAL_8N1, 6, -1, 0). The intended source is a Yacht Devices NMEA0183 multiplexer that aggregates the boat's serial sentences and feeds them in on one wire.

Currently the firmware includes <TinyGPSPlus.h> but the parser is not wired into the main loop — the comment in Xregulator.ino notes "was working great but not currently implemented." The Serial2 init runs and Serial2.flush() clears the buffer, but no read pump exists yet. NMEA0183 fields in the data freshness table (IDX_HEADING_NMEA etc.) are populated by NMEA2K only at present.

Documentation kept here so the wiring expectation stays accurate: when the parser comes back online, the existing Yacht Devices feed will already be reaching the right pin at the right baud rate.


OneWire (DS18B20, GPIO13)

Driver covered in Sensor Systems → DS18B20. The bus runs on Core 0 in TempTask; Core 1 only reads AlternatorTemperatureF. Adding a second sensor on the same wire requires expanding the address discovery in TempTask — currently only sensors.getAddress(tempDeviceAddress, 0) is called (index 0 only).


Cross-PGN / Cross-Protocol Notes

  • Heading and wind both throttle to 2 s — these are high-rate PGNs that would otherwise dominate the dispatch table.
  • GNSS and COGSOG also throttle to 2 s — paired so the cloud sensor-history snapshot sees consistent lat/lon/COG/SOG at each upload.
  • MARK_FRESH is only called on successful parse + sanity-check pass. A failed parse logs to OutputStream (Serial) if NMEA2KVerbose == 1 but leaves the timestamp un-bumped, so the dashboard greys the field.
  • No PGN-level acks — the regulator is read-only on NMEA2000. It does not transmit its own DC status PGN even though it has the data; doing so would require the device to claim a source address and become a fully participating node.