Analytics and Advanced Features¶
Features layered on top of the basic regulator: weather-aware charging, alternator lifetime modeling, motion and comfort analytics from the inertial sensor, and the local sensor-history buffer that feeds the optional cloud sync. Each feature is independent — disabling any one of them does not affect the others or the core charging loop.
For how the charging decision itself is made, see Charging Control. For the sensors that feed these features, see Sensors. For how results reach the dashboard, see Telemetry Pipeline.
Weather Mode — Solar Forecast Charging Holiday¶
When the solar forecast indicates the panels will cover the next few days, the regulator skips alternator charging entirely, saving engine fuel and alternator wear.
Where the forecast comes from¶
The fetch worker (executeFetchWeatherData(), run on the networking core) queries Open-Meteo, a free public forecast API that needs no key. It requests the daily shortwave-radiation total for today, tomorrow, and the day after at the boat's current coordinates.
Each day's radiation figure is converted into an expected solar harvest in kilowatt-hours (pKwHrToday, pKwHrTomorrow, pKwHr2days) using two user settings: the installed panel nameplate wattage (SolarWatts) and a real-world derating factor (performanceRatio, typically 0.75–0.85 to account for wiring, angle, and temperature losses).
The decision — analyzeWeatherMode()¶
A day counts as a "good solar day" if its expected harvest meets the user's threshold (UVThresholdHigh). If at least two of the three forecast days are good, weather mode activates (currentWeatherMode = 1). The two-of-three vote prevents a single optimistic forecast day from suppressing charging ahead of what turns out to be a cloudy stretch.
When weather mode is active and the feature is enabled (weatherModeEnabled), the per-tick control snapshot (buildTickSnapshot()) clears the charging-enabled flag — the field stays off exactly as if the user had switched charging off, and everything downstream (staging, safeties, telemetry) sees a normal "not charging" state.
Refresh gating — updateWeatherMode()¶
The refresh logic runs from the main loop whenever WiFi is connected:
- If the feature is disabled, weather mode is forced off and nothing else happens.
- If the existing forecast is still fresh (within
WeatherUpdateInterval), it re-runs the decision on the cached data. - Network fetches are deferred until the field has been off and settled for a while (
fieldOffSettled()) — the regulator never does discretionary network work while actively charging. Stale data is still analyzed in the meantime. - When a refresh is due, a fetch request is queued to the background HTTPS worker; if the queue is full it retries shortly.
There is also a manual "update now" button handled by triggerWeatherUpdate(), which performs the same fetch immediately but first checks for a GPS fix, a reasonably strong WiFi signal, and that the device is in normal client (station) mode — each failure prints a plain-language reason to the console.
Where the coordinates come from¶
Position for the forecast follows the firmware's general GPS source hierarchy, resolved every loop pass by resolveSources():
- Sticky manual override first. If the user has typed coordinates into Weather Mode (
gpsManualActive), those win over every live source and stay in force until explicitly cleared. - Otherwise the user-selected source mode (
gpsTimeSourceMode) applies: automatic mode prefers boat instruments (NMEA2000) and falls back to the phone's GPS when the boat feed goes stale; forced-NMEA and forced-phone modes honor the user's choice even when stale.
If no position is available at all, the fetch is skipped and an error is reported rather than requesting a forecast for latitude zero, longitude zero.
Alternator Lifetime Modeling — calculateThermalStress()¶
Three independent damage accumulators track the three things that actually wear out in an alternator: winding insulation, bearing grease, and brushes. The model runs every couple of seconds whenever the alternator temperature reading (TempToUse) and RPM pass sanity checks — implausible or missing readings are skipped so a bad sensor can never accumulate phantom damage.
The three wear models¶
- Insulation follows the classic chemistry-rate-versus-temperature relationship (Arrhenius equation): insulation life roughly halves for every ~10 °C the winding runs above its rating point. The predicted life is clamped to a ceiling so a cold start does not show an implausibly long lifetime.
- Grease uses the bearing-industry rule of thumb — life halves for a fixed temperature step above a reference baseline — multiplied by a speed factor, since higher shaft RPM churns the grease harder. Lifetimes are normalized to a reference alternator speed, with a floor on RPM to avoid divide-by-zero at standstill.
- Brushes are dominated by mechanical wear (proportional to RPM) with a mild temperature factor layered on, floored so an implausibly cold reading can't flip the math.
Because only one temperature probe exists, the winding, bearing, and brush temperatures are all currently estimated as the sensed temperature plus a single user-settable offset (WindingTempOffset). This is a deliberate placeholder until real-world data justifies separate per-component offsets.
Damage accumulation and outputs¶
Each pass converts elapsed time into a fraction of each component's predicted life and adds it to a running total (CumulativeInsulationDamage, CumulativeGreaseDamage, CumulativeBrushDamage, each clamped to the 0–1 range). From these come the dashboard values:
- Remaining-life percentages (
InsulationLifePercent,GreaseLifePercent,BrushLifePercent). PredictedLifeHours— the lifetime of whichever component is wearing fastest at current operating conditions. A hot, fast steady state therefore shows a much shorter predicted life than a cool idle; the number is an instantaneous rate, not a countdown clock.LifeIndicatorColor— a simple green/yellow/red bucket of the predicted-life figure for at-a-glance display.
Persistence¶
The cumulative damage values persist in flash key-value storage (NVS), written through the change-detecting batch saver (saveNVSDataFull()) and restored at boot by loadNVSData(). The saver only writes values that actually changed, and fires at field-off edges and shutdown rather than continuously, so flash wear stays negligible. The accumulators are monotonic by design — damage doesn't undo.
This per-component wear model is complementary to the empirical Charging-System Health system, which compares measured output against the alternator's own best-ever performance.
IMU Analytics — Motion, Comfort, and Sea State¶
updateAccelMetrics() runs every loop pass and processes the sample ring filled by the inertial sensor's FIFO drain (see Sensors → LSM6DSOX). It turns raw acceleration and rotation into boat-level metrics: heel angle, sea-state severity, and estimated seasickness risk.
Real-time outputs¶
Fused tilt angles (imu_heel_deg, imu_pitch_deg) come from a complementary filter blending accelerometer and gyro. Yaw rate (imu_yaw_rate_dps) is read directly from the gyro, and per-sample vertical and total acceleration magnitudes (imu_vertical_accel_g, imu_total_accel_g) feed the impact detection below.
Rolling-window metrics¶
Sliding windows summarize recent motion: one-minute range and mean-deviation figures for heel and pitch (imu_heel_change_60s, imu_heel_deviation_60s, and pitch equivalents), plus two-minute peak-deviation versions and a compass-based heading swing (imu_heading_swing_120s) that reports peak-to-peak yaw wander — useful for judging anchor sailing.
Comfort and seasickness estimates¶
imu_msi_score— a motion-sickness index derived from frequency-weighted vertical-acceleration RMS, following the published Lawther & Griffin seasickness research. The weighting emphasizes the low-frequency band where the human inner ear is most sensitive. Zero is flat calm; one hundred is severe.imu_vomit_pct— a power-law estimate of what fraction of people would be sick after a couple of hours at the current motion level, from the same research lineage.imu_anchorage_comfort— a 0–100 heuristic blending roll deviation, the sickness index, and slam events into a single anchorage-comfort score.
Impact and capsize counters¶
Vertical-acceleration spikes above a user-settable threshold (SLAM_THRESHOLD_G) count as slams — tracked per session (imu_slam_count) and for the lifetime of the device (imu_slam_count_lifetime). Extreme heel or pitch beyond their thresholds (CAPSIZE_THRESHOLD_DEG, PITCHPOLE_THRESHOLD_DEG) increment lifetime capsize and pitchpole counters; these trigger an immediate synchronous flash save (saveNVSDataFull()), because a boat that has just capsized may be about to lose power.
The detection thresholds are user-configurable settings persisted in flash key-value storage (NVS) via settingWrite() like any other setting; the counters themselves persist through the batch saver.
Sea-state binning¶
Time is bucketed into gentle / moderate / rough / extreme sea-state minutes based on the motion-sickness index, kept separately for moving versus stationary (imu_min_moving_gentle through imu_min_stat_extreme). These minute counters persist across reboots and build a long-term picture of what conditions the boat actually lives in. They also feed the sailing analytics described in Sailing Performance.
Sensor History — Local Ring Buffer and Optional Cloud Sync¶
The firmware keeps its own multi-day sensor history on the device, independent of any internet connection. With cloud features enabled, the device periodically uploads that history to the project's servers so the owner can see long-term trends from anywhere.
Window aggregation¶
A window accumulator (SensorWindow, instance currentWindow) is updated every loop pass by updateSensorWindow(): running minimum / maximum / average for voltages, currents, temperatures, and RPM, plus IMU, GPS, and weather summaries. At a fixed cadence (firmware constant SENSOR_UPLOAD_INTERVAL), uploadSensorHistory() closes the window, and resetSensorWindow() re-seeds the extrema sentinels for the next one.
The PSRAM ring — sensorRing[]¶
Each closed window becomes a snapshot (SensorSnapshot) pushed into a large ring buffer in external RAM (PSRAM) by pushSensorSnapshot() — several days of history with no flash wear at all, following the project-wide rule that big buffers live in PSRAM (see Memory and Persistence). Ring index updates are guarded by a critical section because the upload worker on the other core pops entries concurrently, and an in-flight marker (sensorRingInFlightIndex) prevents a snapshot from being overwritten while it is being transmitted.
Upload path¶
When cloud features are enabled (CloudFeatures), uploadBufferedRecords() periodically drains the ring: it serializes a batch of snapshots to JSON and hands it to the background HTTPS worker. Uploads are gated the same way as all discretionary network traffic — only after the field has been off and settled, and throttled to a modest cadence — except when the user presses the "Upload Cloud Now" button, which sets a bypass flag (forceCloudFlushPending) and drains the ring back-to-back. Successful uploads pop entries from the ring tail; failures leave them in place for retry, so nothing is lost to a flaky connection.
Separately from sensor data, a periodic configuration snapshot (buildConfigPayload()) uploads the device's current user settings under the same gating, letting the owner's cloud view detect drift between what the dashboard shows and what the device is actually running.
With cloud features disabled, the window accumulator and ring still run — history simply accumulates locally and is served to the dashboard only.
Surviving shutdown¶
During the staged ignition-off shutdown, if the post-charging cloud-drain window expires with records still in the ring, dumpSensorRingToLittleFS() writes the remaining snapshots to a flash file (a magic-numbered, versioned binary), and restoreSensorRingFromLittleFS() reloads them on the next boot. A blocking flash write is acceptable at that point because the field is already off and the device is about to sleep. The result: unsent history survives a complete power-down as long as the shutdown sequence reached its final phase.
Cross-references¶
- Charging decision, stages, and override modes → Charging Control
- The sensors feeding all of this (IMU FIFO drain, temperature probe, battery monitor) → Sensors
- How values reach the dashboard (channels, cadence, integrity checks) → Telemetry Pipeline
- NVS settings layer and the batch persistence saver → Memory and Persistence
- Empirical alternator health scoring → Charging-System Health
- Networking, the background HTTPS worker, and upload gating → Networking and Web Server