---
title: "Software"
space: "MPPT Charger"
url: "https://docs.modbee.org/mppt-charger/software"
updated: "2026-10-25"
---

# Software

Firmware architecture, start-up behaviour, configuration and the web interface protocol. For the method-by-method API see [API_REFERENCE.md](/mppt-charger/api-reference).

## Layout

```
src/main.cpp                    Application: setup() and loop()
lib/ModbeeMPPT/src/
  ModbeeMPPT.h/.cpp             Top-level controller: start-up, periodic tasks, status LED
  ModbeeMpptAPI.h/.cpp          Validated, unit-based interface to the BQ25798
  ModbeeMpptConfig.h/.cpp       Settings: defaults, validation, JSON persistence on LittleFS
  ModbeeMpptWebServer.h/.cpp    Wi-Fi access point, captive DNS, HTTP and WebSocket server
  ModbeeMpptPowerSave.h/.cpp    Wi-Fi timeout, WIFI button, CPU frequency, light sleep
  ModbeeMpptLog.h/.cpp          Energy and peak-power statistics persisted to LittleFS
  ModbeeMpptDebug.h/.cpp        Serial status and register dumps
  ModbeeMpptGlobal.h            Common includes
lib/bq25798/                    BQ25798 register driver (derived from the Adafruit library)
data/                           Web interface (LittleFS image)
```

Third-party libraries (vendored in `lib/`):

| Library | Version | License | Used for |
|---------|---------|---------|----------|
| [ESP32_SoftWire](https://github.com/qqqlab/ESP32_SoftWire) | 1.0.2 | MIT | Software I2C to the BQ25798 |
| [ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer) | 3.9.4 | LGPL-3.0 | HTTP and WebSocket server |
| [AsyncTCP](https://github.com/ESP32Async/AsyncTCP) | 3.4.5 | LGPL-3.0 | TCP for ESPAsyncWebServer |
| [ArduinoJson](https://arduinojson.org/) | 7.4.1 | MIT | Settings file and WebSocket messages |
| [FastLED](https://github.com/FastLED/FastLED) | 3.9.16 | MIT | WS2812B status LED |

## Application

```cpp
#include "ModbeeMpptGlobal.h"
#include "ModbeeMPPT.h"

ModbeeMPPT modbeeMPPT;

void setup() {
  Serial.begin(115200);
  modbeeMPPT.initializeLEDs();
  modbeeMPPT.begin();          // I2C, charger, settings
  modbeeMPPT.initWebServer();  // Wi-Fi AP, DNS, HTTP, WebSocket
}

void loop() {
  modbeeMPPT.loop();           // periodic tasks, LED, web server, power save
}
```

`src/main.cpp` also prints `printQuickStatus()` every 2 seconds. Swap in one of the other `print*()` functions listed there for more detail.

## Start-up sequence

`ModbeeMPPT::begin()` does the following, in order:

1. **Radios and CPU.** Turns Wi-Fi and Bluetooth off and runs the CPU at 80 MHz.
2. **Charger bus.** Starts software I2C on GPIO3 (SDA) and GPIO2 (SCL) to the BQ25798 at address 0x6B.
3. **Settings.** Mounts LittleFS and loads `/config/mppt_config.json`. If the file is missing or fails validation, the defaults are used.
4. **Fixed charger settings** (`applyCriticalSettings()`):
   - ADC on: continuous, 15-bit
   - **BQ25798 I2C watchdog disabled**, so the charger keeps the host settings if the ESP32-C3 resets or stalls
   - HIZ off, backup mode off
   - 1.5 MHz switching
   - input current optimiser (ICO) off, so it does not interfere with MPPT
   - battery discharge current sensing on
5. **Saved settings.** Writes them to the charger: cell count, charge voltage and current, minimum system voltage, termination, recharge and pre-charge, input limits, VAC OVP, safety timers, MPPT and PFM/OOA.
6. **Battery check.** Reads the battery voltage and estimates state of charge.
7. **Charging enabled.** This happens in all cases, so the board recovers and charges unattended after any power loss. If the battery reads missing or below 5 %, the board starts in *low-power boot* mode with the access point off until the battery recovers above 8 % while charging, or the WIFI button is pressed.
8. **Statistics and power saving.** Loads the saved energy statistics and starts the power-save manager.

Note that until step 5 runs, the BQ25798 operates on its own power-on defaults, which the PROG resistor selects. See [HARDWARE.md](/mppt-charger/hardware#connection-and-disconnection-order) for why the battery should be connected first.

## Runtime

| Task | Interval |
|------|----------|
| Status LED update | 1 s |
| WebSocket data push (when a client is connected) | 1 s |
| Battery presence check | 30 s (`intervals.battery_check`) |
| State-of-charge check | 60 s (`intervals.soc_check`) |
| Re-apply fixed charger settings | 60 s |
| Re-apply all saved settings | 5 min |
| Save energy statistics | 5 min |

Interval changes made in the web interface take effect after a restart.

### Power saving

- **Access point lifetime.** The access point starts at boot (except in low-power boot) and turns off 5 minutes after boot or after the last WIFI button press. A press turns it back on for 5 minutes.
- **CPU frequency.** The CPU runs at 80 MHz, and at 160 MHz while the access point is on after a button press.
- **Light sleep.** When the state of charge is below 20 %, the ESP32-C3 light-sleeps for 10 s per minute. It wakes on its timer or on the WIFI button. The charger keeps running while the MCU sleeps.

### Status LED (WS2812B)

| Colour | Meaning |
|--------|---------|
| Orange | Starting up |
| Red | A BQ25798 fault or regulation flag is set |
| Yellow | Charging |
| Green | Charge complete (termination done) |
| Blue | Not charging |

## Settings file

Settings are stored as JSON in `/config/mppt_config.json` on LittleFS. Default contents:

```json
{
  "battery":   { "type": 1, "cell_count": 3, "charge_voltage": 12.6, "charge_current": 1.0, "min_system_voltage": 10.0 },
  "charging":  { "termination_current": 0.12, "recharge_threshold": 0.4, "precharge_current": 0.2, "precharge_voltage_threshold": 3 },
  "input":     { "voltage_limit": 22.0, "current_limit": 3.0, "vac_ovp_threshold": 26.0 },
  "timers":    { "fast_charge_enable": true, "fast_charge_timer": 2, "precharge_enable": true, "precharge_timer": 0, "topoff_timer": 1 },
  "mppt":      { "voc_percent": 5, "voc_delay": 1, "voc_rate": 0, "enable": true },
  "power":     { "pfm_forward_enable": false, "ooa_forward_enable": true },
  "intervals": { "battery_check": 30000, "soc_check": 60000, "config_apply": 300000 },
  "version": "1.0"
}
```

Enumerated fields:

| Field | Values |
|-------|--------|
| `battery.type` | 0 LiFePO4, 1 Li-Po / Li-ion, 2 lead-acid, 3 custom |
| `charging.precharge_voltage_threshold` | 0 = 15 %, 1 = 62.2 %, 2 = 66.7 %, 3 = 71.4 % of VREG |
| `timers.fast_charge_timer` | 0 = 5 h, 1 = 8 h, 2 = 12 h, 3 = 24 h |
| `timers.precharge_timer` | 0 = 2 h, 1 = 0.5 h |
| `timers.topoff_timer` | 0 = off, 1 = 15 min, 2 = 30 min, 3 = 45 min |
| `mppt.voc_percent` | 0-7 = 56.25, 62.5, 68.75, 75, 81.25, 87.5, 93.75, 100 % of VOC |
| `mppt.voc_delay` | 0 = 50 ms, 1 = 300 ms, 2 = 2 s, 3 = 5 s |
| `mppt.voc_rate` | 0 = 30 s, 1 = 2 min, 2 = 10 min, 3 = 30 min |

`input.voltage_limit` is the VINDPM setting. While MPPT is enabled, the BQ25798 overwrites VINDPM after each VOC measurement.

### Validation

`ModbeeMpptConfig::validateConfig()` checks every value before it is saved or applied. For the battery, the charge voltage must:

- fall within the BQ25798 window for the cell count: 1S 3.0-4.99 V, 2S 5.0-9.99 V, 3S 10.0-13.99 V, 4S 14.0-18.8 V;
- not exceed 3.65 V/cell for LiFePO4 or 4.35 V/cell for Li-Po.

A save from the web interface that fails validation is rejected, and the previous settings stay in force.

Settings can also be changed in code:

```cpp
modbeeMPPT.config.data.charge_current = 2.0f;   // A
if (modbeeMPPT.config.validateConfig()) {
  modbeeMPPT.config.saveConfig();
  modbeeMPPT.config.applyToMPPT(modbeeMPPT.api);
}
```

## Web server

| Item | Value |
|------|-------|
| Wi-Fi mode | Access point only |
| SSID / password | `ModbeeMPPT` / none (open) |
| Address | 192.168.4.1, with captive DNS for all names |
| HTTP | `GET /` → `index.html`, `GET /settings` → `settings.html`, `GET /debug` → `debug.html`; other LittleFS files served statically; unknown paths redirect to `/` |
| WebSocket | `ws://192.168.4.1/ws`, JSON text messages |

To change the SSID or add a password, edit `WIFI_SSID` and `WIFI_PASSWORD` in `lib/ModbeeMPPT/src/ModbeeMpptWebServer.h`. A WPA2 password must be at least 8 characters.

### WebSocket protocol

Client to device, `{"command": "<name>", ...}`:

| Command | Extra fields | Reply `type` |
|---------|-------------|--------------|
| `getSystemData` | | `data` |
| `getSettings` | | `settings` |
| `getDebugData` | | `debug` |
| `saveSettings` | `settings`: object with any of the keys below | `status` |
| `resetDefaults` (alias `resetSettings`) | | `status` |
| `resetStat` | `domain`: `vin1`, `vin2`, `vbus`, `vbat` or `vsys` | `status` |
| `resetBatteryAmpStats` | | `status` |
| `resetBatteryDischargePowerStats` | | `status` |

`saveSettings` keys:

- **Battery:** `batteryType`, `cellCount`, `chargeVoltage`, `chargeCurrent`, `systemVoltage`
- **Charging:** `termCurrent`, `rechargeThreshold`, `prechargeCurrent`, `prechargeVoltageThreshold`
- **Input:** `inputVoltage`, `inputCurrent`, `vacOvp`
- **Timers:** `chargeTimerEnable`, `chargeTimer`, `prechargeTimerEnable`, `prechargeTimer`, `topoffTimer`
- **MPPT:** `mpptEnable`, `vocPercent`, `vocDelay`, `vocRate`
- **Power:** `pfmForwardEnable`, `ooaForwardEnable`
- **Intervals:** `batteryCheckInterval`, `socCheckInterval`

`status` replies are `{"type": "status", "success": true|false, "message": "..."}`.

`data` messages are pushed to all clients every second. They contain voltage, current and power for VAC1, VAC2, VBUS, the battery and the system rail. They also carry:

- **Charge:** state of charge, charge state and `isCharging`
- **Status:** fault flag, battery and die temperature
- **Statistics:** energy totals and peak values

The exact key list is in `ModbeeMpptWebServer::getSystemData()`.

## Serial diagnostics

| Function | Output |
|----------|--------|
| `printQuickStatus()` | Essential status |
| `printStatus()` | Full status |
| `printPowerMeasurements()` | Input, battery and system power |
| `printConfiguration()` | Current charger settings |
| `printFaults()` | Fault status |
| `printRegisterDebug()` | Decoded BQ25798 status registers |
| `printComprehensiveBatteryStatus()` | Battery voltage and SOC detail |

## Build

```bash
pio run                  # build
pio run -t upload        # flash firmware
pio run -t uploadfs      # flash web interface
pio device monitor       # serial console, 115200 baud
```

The `lolin_c3_mini` board definition matches the ESP32-C3FH4 on this board (4 MB flash, native USB). The firmware uses about 92 % of the default 1.25 MB application partition.

## Firmware status

| Feature | Status |
|---------|--------|
| BQ25798 charging, MPPT, dual input, telemetry | Implemented |
| Web dashboard, settings, debug pages | Implemented |
| Settings persistence, energy statistics | Implemented |
| WIFI button, access point timeout, light sleep | Implemented |
| Modbus RTU over RS485 | Planned (hardware fitted) |
| I2C target (slave) telemetry on the Grove port | Planned (hardware fitted) |
| Long-press WIFI button factory reset | Planned |
| Bluetooth LE | Not implemented (radio is turned off) |
