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>
392 lines
13 KiB
Markdown
392 lines
13 KiB
Markdown
# 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
|
||
|
||
1. PlatformIO project with correct board and library configuration
|
||
2. Pin mapping header for TTGO T-Call v1.4
|
||
3. RS485 Modbus RTU driver for Acuvim II
|
||
4. Acuvim II register definitions and data structures
|
||
5. Polling task that reads all inverter parameters
|
||
6. 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`
|
||
|
||
```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-kit` is 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 = qio` to 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
|
||
|
||
```cpp
|
||
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 `AcuvimData` struct
|
||
- Handle communication errors with retry logic
|
||
- Report read success/failure statistics
|
||
|
||
**Key methods:**
|
||
|
||
```cpp
|
||
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:**
|
||
|
||
```cpp
|
||
// 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:
|
||
|
||
```cpp
|
||
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 = false` and set `error_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.
|
||
|
||
```cpp
|
||
// 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](acuvim-spec-02.md)
|