Complete IoT monitoring platform for Acuvim II power meters via ESP32. Firmware (Phases 1-7): - ESP32-WROVER-B (TTGO T-Call v1.4) with RS485 Modbus RTU - WiFi STA+AP concurrent mode with GSM/GPRS failover - Transport abstraction layer with 4 priority modes - MQTT protocol with 20 commands, LWT, QoS, exponential backoff - SD card offline buffering with JSONL rotation and non-blocking drain - OTA firmware updates with dual partition rollback protection - Watchdog timer, crash loop detection, Acuvim health monitoring - Captive portal provisioning with AP mode Console backend (Phase 8): - .NET 10 minimal API with PostgreSQL + EF Core - JWT authentication, SignalR real-time updates - MQTTnet 5.x bridge service with health monitoring - Device, telemetry, firmware, alert, group management - Rate limiting, security headers, Swagger/OpenAPI Frontend (Phase 9): - React 18 + TypeScript + Vite with Ant Design 5 - ECharts telemetry visualization, TanStack Query - SignalR live updates, device management UI - Dashboard, fleet management, firmware deployment Testing & Production (Phase 10): - 28 firmware unit tests (Modbus, JSON, config, version) - 23 xUnit backend tests (device, telemetry, command, alert) - Docker Compose with nginx, TLS MQTT, PostgreSQL - Production deployment, commissioning, and troubleshooting docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 KiB
Phase 1: Project Setup, Hardware Abstraction & RS485 Modbus Communication
Objective
Establish the PlatformIO project structure, configure the LILYGO TTGO T-Call v1.4 board, build the hardware abstraction layer, and implement RS485 Modbus RTU communication with the Acuvim II power meter.
Prerequisites
- LILYGO TTGO T-Call v1.4 board
- Acuvim II power meter with RS485 interface
- RS485-to-TTL transceiver module (e.g., MAX485 or SP3485)
- Acuvim II Modbus register map documentation
- USB-C cable for programming
- PlatformIO IDE (VS Code extension)
Deliverables
- PlatformIO project with correct board and library configuration
- Pin mapping header for TTGO T-Call v1.4
- RS485 Modbus RTU driver for Acuvim II
- Acuvim II register definitions and data structures
- Polling task that reads all inverter parameters
- Serial debug output confirming successful reads
1.1 PlatformIO Project Structure
Tau.Acuvim/
├── firmware/
│ ├── platformio.ini
│ ├── src/
│ │ └── main.cpp
│ ├── include/
│ │ ├── pin_config.h
│ │ ├── acuvim_registers.h
│ │ ├── acuvim_reader.h
│ │ └── config_manager.h
│ ├── lib/
│ │ └── README.md
│ ├── data/ # LittleFS web files (Phase 3)
│ └── test/
├── console/ # Console app (Phase 8-9)
├── docs/
│ └── acuvim-spec-xx.md
└── README.md
1.2 PlatformIO Configuration
platformio.ini
[env:ttgo-t-call]
platform = espressif32
board = esp-wrover-kit
framework = arduino
monitor_speed = 115200
upload_speed = 921600
board_build.partitions = min_spiffs.csv
board_build.f_cpu = 240000000L
board_build.f_flash = 80000000L
board_build.flash_mode = qio
lib_deps =
4-20ma/ModbusMaster@^2.0.1
bblanchon/ArduinoJson@^7.0.0
build_flags =
-DBOARD_HAS_PSRAM
-DCORE_DEBUG_LEVEL=3
Notes:
esp-wrover-kitis the closest PlatformIO board definition for the ESP32-WROVER-B module- Partition table will be changed in Phase 6 (OTA) to support two app partitions
- Additional libraries will be added in subsequent phases
1.3 Pin Configuration
pin_config.h
Define all hardware pin assignments for the TTGO T-Call v1.4 in a single header. This is the hardware abstraction layer that will be updated if the board changes.
TTGO T-Call v1.4 Pin Assignments
================================
Modem (SIM800L) Pins:
MODEM_RST = GPIO 5
MODEM_PWRKEY = GPIO 4
MODEM_POWER_ON = GPIO 23
MODEM_TX = GPIO 27
MODEM_RX = GPIO 26
LED:
LED_PIN = GPIO 13
I2C (IP5306 Power Management):
I2C_SDA = GPIO 21
I2C_SCL = GPIO 22
RS485 (Acuvim II) - User-assigned:
RS485_RX = GPIO 32
RS485_TX = GPIO 33
RS485_DE_RE = GPIO 25 # Direction control (DE+RE tied together)
SD Card (SPI) - User-assigned (Phase 5):
SD_CS = GPIO 15
SD_MOSI = GPIO 2
SD_MISO = GPIO 14
SD_CLK = GPIO 12
Available GPIOs (unused):
GPIO 0 (BOOT - avoid for general use)
GPIO 34 (input only)
GPIO 35 (input only)
GPIO 36 (input only, VP)
GPIO 39 (input only, VN)
Important constraints:
- GPIO 6-11 are used by internal flash (do not use)
- GPIO 34-39 are input-only (no pull-up/pull-down)
- GPIO 2 must be LOW during boot (SD MOSI — acceptable since SPI is idle at boot)
- GPIO 12 (MTDI) controls flash voltage on WROVER — if using SD on GPIO 12, set efuse or use
board_build.flash_mode = qioto avoid boot issues
RS485 wiring to MAX485/SP3485:
ESP32 GPIO 33 (TX) ───→ DI (Driver Input)
ESP32 GPIO 32 (RX) ←─── RO (Receiver Output)
ESP32 GPIO 25 (DE) ───→ DE + RE (tied together)
MAX485 A ────────────→ Acuvim II RS485 A (+ / Data+)
MAX485 B ────────────→ Acuvim II RS485 B (- / Data-)
GND ─────────────────→ Acuvim II GND (common ground)
1.4 Acuvim II Modbus Register Map
The Acuvim II uses Modbus RTU over RS485 with the following defaults:
- Baud rate: 9600 (configurable: 1200–19200)
- Data bits: 8
- Parity: None
- Stop bits: 1
- Slave address: 1 (configurable: 1–247)
Key Register Groups
Define these in acuvim_registers.h:
Register Address | Length | Parameter | Unit | Data Type | Scale
------------------|--------|------------------------|---------|--------------|-------
0x0000 | 2 | Phase A Voltage (Ua) | V | Float32 | -
0x0002 | 2 | Phase B Voltage (Ub) | V | Float32 | -
0x0004 | 2 | Phase C Voltage (Uc) | V | Float32 | -
0x0006 | 2 | Phase A Current (Ia) | A | Float32 | -
0x0008 | 2 | Phase B Current (Ib) | A | Float32 | -
0x000A | 2 | Phase C Current (Ic) | A | Float32 | -
0x000C | 2 | Active Power (P) | kW | Float32 | -
0x000E | 2 | Reactive Power (Q) | kVAR | Float32 | -
0x0010 | 2 | Apparent Power (S) | kVA | Float32 | -
0x0012 | 2 | Power Factor (PF) | - | Float32 | -
0x0014 | 2 | Frequency (F) | Hz | Float32 | -
0x0016 | 2 | Phase A Power (Pa) | kW | Float32 | -
0x0018 | 2 | Phase B Power (Pb) | kW | Float32 | -
0x001A | 2 | Phase C Power (Pc) | kW | Float32 | -
0x001C | 2 | Line Voltage AB (Uab) | V | Float32 | -
0x001E | 2 | Line Voltage BC (Ubc) | V | Float32 | -
0x0020 | 2 | Line Voltage CA (Uca) | V | Float32 | -
Energy Registers (Accumulated):
0x0100 | 2 | Import Active Energy | kWh | Float32 | -
0x0102 | 2 | Export Active Energy | kWh | Float32 | -
0x0104 | 2 | Import Reactive Energy | kVARh | Float32 | -
0x0106 | 2 | Export Reactive Energy | kVARh | Float32 | -
Demand Registers:
0x0200 | 2 | Active Power Demand | kW | Float32 | -
0x0202 | 2 | Max Active Demand | kW | Float32 | -
0x0204 | 2 | Reactive Power Demand | kVAR | Float32 | -
THD Registers:
0x0300 | 2 | Voltage THD A | % | Float32 | -
0x0302 | 2 | Voltage THD B | % | Float32 | -
0x0304 | 2 | Voltage THD C | % | Float32 | -
0x0306 | 2 | Current THD A | % | Float32 | -
0x0308 | 2 | Current THD B | % | Float32 | -
0x030A | 2 | Current THD C | % | Float32 | -
Note: Register addresses and data types must be verified against the actual Acuvim II documentation for the specific model (Acuvim II, IIR, IIE, IIW). The above is representative — the actual register map from Accuenergy should be used as the source of truth.
Data Structure
struct AcuvimData {
// Phase Voltages
float voltage_a; // V
float voltage_b; // V
float voltage_c; // V
// Phase Currents
float current_a; // A
float current_b; // A
float current_c; // A
// Power
float active_power; // kW (total)
float reactive_power; // kVAR (total)
float apparent_power; // kVA (total)
float power_factor; // -1.0 to 1.0
// Frequency
float frequency; // Hz
// Per-Phase Power
float power_a; // kW
float power_b; // kW
float power_c; // kW
// Line Voltages
float voltage_ab; // V
float voltage_bc; // V
float voltage_ca; // V
// Energy (Accumulated)
float import_active_energy; // kWh
float export_active_energy; // kWh
float import_reactive_energy; // kVARh
float export_reactive_energy; // kVARh
// Demand
float active_demand; // kW
float max_active_demand; // kW
float reactive_demand; // kVAR
// THD
float thd_voltage_a; // %
float thd_voltage_b; // %
float thd_voltage_c; // %
float thd_current_a; // %
float thd_current_b; // %
float thd_current_c; // %
// Metadata
uint32_t timestamp; // epoch seconds
bool valid; // true if read succeeded
uint8_t error_code; // 0 = success, else Modbus exception
};
1.5 Modbus RTU Driver Implementation
acuvim_reader.h / acuvim_reader.cpp
Class: AcuvimReader
Responsibilities:
- Initialize HardwareSerial (Serial2) for RS485 at configured baud rate
- Control RS485 direction pin (DE/RE) for half-duplex operation
- Read register blocks using ModbusMaster library
- Parse Float32 values from Modbus register pairs (big-endian IEEE 754)
- Populate
AcuvimDatastruct - Handle communication errors with retry logic
- Report read success/failure statistics
Key methods:
class AcuvimReader {
public:
bool begin(uint8_t slaveAddress = 1, uint32_t baudRate = 9600);
bool readAll(AcuvimData& data);
bool readVoltages(AcuvimData& data);
bool readCurrents(AcuvimData& data);
bool readPower(AcuvimData& data);
bool readEnergy(AcuvimData& data);
bool readDemand(AcuvimData& data);
bool readTHD(AcuvimData& data);
uint32_t getSuccessCount() const;
uint32_t getErrorCount() const;
uint8_t getLastError() const;
private:
ModbusMaster modbus;
uint8_t slaveAddr;
float readFloat32(uint16_t reg);
bool readRegisterBlock(uint16_t startReg, uint16_t count, uint16_t* buffer);
static void preTransmission();
static void postTransmission();
};
RS485 direction control callbacks:
// Called before Modbus transmission — set DE/RE HIGH to enable transmit
void AcuvimReader::preTransmission() {
digitalWrite(RS485_DE_RE, HIGH);
}
// Called after Modbus transmission — set DE/RE LOW to enable receive
void AcuvimReader::postTransmission() {
digitalWrite(RS485_DE_RE, LOW);
}
Float32 parsing from Modbus registers:
The Acuvim II stores 32-bit floats across two consecutive 16-bit registers (big-endian). The ModbusMaster library returns registers in order, so:
float AcuvimReader::readFloat32(uint16_t reg) {
uint32_t raw = ((uint32_t)modbus.getResponseBuffer(reg) << 16)
| (uint32_t)modbus.getResponseBuffer(reg + 1);
float value;
memcpy(&value, &raw, sizeof(float));
return value;
}
Error Handling
- Max 3 retries per register block read with 50ms delay between retries
- On persistent failure, mark
AcuvimData.valid = falseand seterror_code - Track cumulative success/error counts for health reporting (Phase 7)
- Log errors to Serial for debugging
1.6 Main Application (Phase 1 Scope)
main.cpp
Phase 1 main loop is minimal — it initializes the RS485 interface, polls the Acuvim II at a configurable interval (default: 5 seconds), and prints results to Serial for verification.
// Pseudocode for Phase 1 main.cpp
void setup() {
Serial.begin(115200);
acuvimReader.begin(SLAVE_ADDRESS, BAUD_RATE);
}
void loop() {
AcuvimData data;
if (acuvimReader.readAll(data)) {
printAcuvimData(data); // Serial debug output
} else {
Serial.printf("Read failed, error: %d\n", data.error_code);
}
delay(pollInterval);
}
1.7 Testing & Validation
| Test | Method | Pass Criteria |
|---|---|---|
| Build succeeds | pio run |
Zero errors, zero warnings |
| Upload to board | pio run -t upload |
Firmware flashes successfully |
| Serial output | pio device monitor |
Prints structured data at poll interval |
| Modbus read | Connect to Acuvim II | Valid voltage/current/power readings |
| Error handling | Disconnect RS485 | Error reported, no crash, recovery on reconnect |
| Baud rate mismatch | Set wrong baud | Error reported, no hang |
| Slave address mismatch | Set wrong address | Timeout reported, no hang |
| Continuous operation | Run for 1 hour | No memory leaks, stable read rate |
1.8 Dependencies
| Library | Version | Purpose |
|---|---|---|
| ModbusMaster | ^2.0.1 | Modbus RTU master protocol |
| ArduinoJson | ^7.0.0 | JSON serialization (used from Phase 2 onward) |
1.9 Phase 1 Completion Criteria
- PlatformIO project builds and uploads to TTGO T-Call v1.4
- RS485 communication established with Acuvim II
- All register groups read successfully (voltages, currents, power, energy, demand, THD)
- Float32 values parsed correctly from Modbus register pairs
- Error handling tested (disconnection, timeout, wrong address)
- Data printed to Serial in human-readable format
- Pin configuration documented and verified against hardware
Next Phase: Phase 2 — WiFi, MQTT & NVS Configuration