Tau.Acuvim/docs/acuvim-spec-01.md
Renier Forster 84a0668c54 Initial commit: Tau Acuvim IoT monitoring system
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>
2026-05-16 19:05:32 +02:00

13 KiB
Raw Blame History

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

[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: 120019200)
  • Data bits: 8
  • Parity: None
  • Stop bits: 1
  • Slave address: 1 (configurable: 1247)

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 AcuvimData struct
  • 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 = 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.

// 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