diff --git a/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino b/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino new file mode 100644 index 0000000..39afeab --- /dev/null +++ b/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino @@ -0,0 +1,151 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#include +#include + +M5Canvas canvas(&M5StamPLC.Display); +M5StamPLC_IO stamplc_io; +uint8_t expected_address = 0; +uint8_t last_expected_address = 0; + +void setup() +{ + /* Init M5StamPLC */ + M5StamPLC.begin(); + canvas.createSprite(M5StamPLC.Display.width(), M5StamPLC.Display.height()); + canvas.setTextScroll(true); + canvas.fillScreen(TFT_BLACK); + canvas.setTextSize(1); + canvas.setFont(&fonts::efontCN_16); + canvas.println("Try to find M5StamPLC IO"); + + /* Init M5StamPLC IO */ + while (!stamplc_io.begin()) { + canvas.println("M5StamPLC_IO not found, retry in 1s..."); + canvas.pushSprite(0, 0); + delay(1000); + } + + last_expected_address = expected_address = stamplc_io.getExpectedAddress(); + canvas.printf("M5StamPLC IO found in 0x%02X\n", stamplc_io.getCurrentAddress()); + canvas.printf("Firmware Version: 0x%02X\n", stamplc_io.getFirmwareVersion()); + + uint8_t sys_status = stamplc_io.getSystemStatus(); + if (sys_status == 0) { + canvas.setTextColor(TFT_GREEN); + canvas.println("System status: Normal"); + } else { + canvas.setTextColor(TFT_RED); + canvas.printf("System status: 0x%02X\n", sys_status); + if (sys_status & (1 << M5StamPLC_IO::SYS_CH1_INA226_ERROR)) { + canvas.println("- CH1 INA226 Error"); + } + if (sys_status & (1 << M5StamPLC_IO::SYS_CH2_INA226_ERROR)) { + canvas.println("- CH2 INA226 Error"); + } + } + + canvas.setTextColor(TFT_CYAN); + canvas.println("INA226 Configuration:"); + + uint16_t config_ch1, config_ch2; + if (stamplc_io.readINA226Config(1, &config_ch1) == ESP_OK) { + canvas.printf("CH1: 0x%04X", config_ch1); + + uint8_t vshct, vbusct, avg; + stamplc_io.getINA226ConversionTime(1, &vshct, &vbusct); + stamplc_io.getINA226Averaging(1, &avg); + canvas.printf(" VS=%d VB=%d AVG=%d\n", vshct, vbusct, avg); + } + + if (stamplc_io.readINA226Config(2, &config_ch2) == ESP_OK) { + canvas.printf("CH2: 0x%04X", config_ch2); + + uint8_t vshct, vbusct, avg; + stamplc_io.getINA226ConversionTime(2, &vshct, &vbusct); + stamplc_io.getINA226Averaging(2, &avg); + canvas.printf(" VS=%d VB=%d AVG=%d\n", vshct, vbusct, avg); + } + + canvas.setTextColor(TFT_YELLOW); + canvas.println("press BtnC to start monitoring"); + canvas.pushSprite(0, 0); + while (!M5StamPLC.BtnC.isPressed()) { + M5StamPLC.update(); + delay(10); + } + canvas.clear(); +} + +void loop() +{ + M5StamPLC.update(); + + static unsigned long last_update = 0; + if (millis() - last_update > 1000) { + last_update = millis(); + + // Batch read all channels data + int16_t v1, v2; + int32_t i1, i2; + stamplc_io.readAllChannelsData(&v1, &i1, &v2, &i2); + + // Read IO control state + uint8_t io_control = stamplc_io.readRegister(M5StamPLC_IO::REG_IO_CONTROL); + uint8_t sys_status = stamplc_io.getSystemStatus(); + + // Display data + canvas.fillScreen(TFT_BLACK); + canvas.setCursor(0, 0); + + // CH1 status + if (sys_status & (1 << M5StamPLC_IO::SYS_CH1_INA226_ERROR)) { + canvas.setTextColor(TFT_RED); + } else { + canvas.setTextColor(TFT_GREEN); + } + canvas.printf("CH1: %d.%02dV %duA \n", v1 / 1000, abs((v1 % 1000)) / 10, i1); + + // CH2 status + if (sys_status & (1 << M5StamPLC_IO::SYS_CH2_INA226_ERROR)) { + canvas.setTextColor(TFT_RED); + } else { + canvas.setTextColor(TFT_GREEN); + } + canvas.printf("CH2: %d.%02dV %duA \n", v2 / 1000, abs((v2 % 1000)) / 10, i2); + + canvas.setTextColor(TFT_YELLOW); + canvas.printf("Pull-up: CH1=%s CH2=%s \n", + (io_control & (1 << M5StamPLC_IO::BIT_CH1_PU_EN)) ? "ON" : "OFF", + (io_control & (1 << M5StamPLC_IO::BIT_CH2_PU_EN)) ? "ON" : "OFF"); + + canvas.setTextColor(TFT_MAGENTA); + canvas.printf("Address: 0x%02X->0x%02X \n", stamplc_io.getCurrentAddress(), + stamplc_io.getExpectedAddress()); + + // System status + if (sys_status == 0) { + canvas.setTextColor(TFT_GREEN); + canvas.println("System: Normal "); + } else { + canvas.setTextColor(TFT_RED); + canvas.printf("System: Error(0x%02X) \n", sys_status); + } + } + + // Button A: Toggle CH1 pull-up + if (M5StamPLC.BtnA.wasClicked()) { + stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH1_PU_EN); + } + + // Button B: Toggle CH2 pull-up + if (M5StamPLC.BtnB.wasClicked()) { + stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH2_PU_EN); + } + + canvas.pushSprite(0, 0); + delay(10); +} diff --git a/examples/Modules/StamPLC_IO/Relay/Relay.ino b/examples/Modules/StamPLC_IO/Relay/Relay.ino new file mode 100644 index 0000000..db74447 --- /dev/null +++ b/examples/Modules/StamPLC_IO/Relay/Relay.ino @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#include +#include + +M5StamPLC_IO stamplc_io; +bool relay_state[3] = {false, false, false}; +uint8_t expected_address = 0; +uint8_t last_expected_address = 0; +bool btnB_longPressed = false; +bool btnC_longPressed = false; + +void setup() +{ + /* Init M5StamPLC*/ + M5StamPLC.begin(); + M5StamPLC.Display.setTextScroll(true); + M5StamPLC.Display.setTextSize(1); + M5StamPLC.Display.setFont(&fonts::efontCN_16); + M5StamPLC.Display.println("Try to find M5StamPLC IO"); + + /* Init M5StamPLC IO */ + while (!stamplc_io.begin()) { + M5StamPLC.Display.println("M5StamPLC_IO not found, retry in 1s..."); + delay(1000); + } + + last_expected_address = expected_address = stamplc_io.getExpectedAddress(); + M5StamPLC.Display.printf("M5StamPLC IO found in 0x%02X\n", stamplc_io.getCurrentAddress()); + M5StamPLC.Display.printf("Firmware Version: 0x%02X\n", stamplc_io.getFirmwareVersion()); + M5StamPLC.Display.printf("push A/B/C to switch relay\n"); + M5StamPLC.Display.printf("long press B/C to switch pull-up\n"); +} + +void loop() +{ + M5StamPLC.update(); + + if (M5StamPLC.BtnA.wasClicked()) { + M5StamPLC.Display.printf("BtnA switch relay 0 to %s\n", relay_state[0] ? "OFF" : "ON"); + relay_state[0] = !relay_state[0]; + stamplc_io.setRelayState(M5StamPLC_IO::BIT_RELAY_TRIG, relay_state[0]); + } + + if (M5StamPLC.BtnB.wasClicked()) { + M5StamPLC.Display.printf("BtnB switch relay 1 to %s\n", relay_state[1] ? "OFF" : "ON"); + relay_state[1] = !relay_state[1]; + stamplc_io.setRelayState(M5StamPLC_IO::BIT_EX_CTR_1, relay_state[1]); + } else if (M5StamPLC.BtnB.pressedFor(1000) && !btnB_longPressed) { + btnB_longPressed = true; + M5StamPLC.Display.printf("switch CH1 pull-up\n"); + stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH1_PU_EN); + } else if (!M5StamPLC.BtnB.isPressed()) { + btnB_longPressed = false; + } + + if (M5StamPLC.BtnC.wasClicked()) { + M5StamPLC.Display.printf("BtnC switch relay 2 to %s\n", relay_state[2] ? "OFF" : "ON"); + relay_state[2] = !relay_state[2]; + stamplc_io.setRelayState(M5StamPLC_IO::BIT_EX_CTR_2, relay_state[2]); + } else if (M5StamPLC.BtnC.pressedFor(1000) && !btnC_longPressed) { + btnC_longPressed = true; + M5StamPLC.Display.printf("switch CH2 pull-up\n"); + stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH2_PU_EN); + } else if (!M5StamPLC.BtnC.isPressed()) { + btnC_longPressed = false; + } + + expected_address = stamplc_io.getExpectedAddress(); + if (expected_address != last_expected_address) { + M5StamPLC.Display.printf("Expected Address 0x%02X -> 0x%02X\n", last_expected_address, expected_address); + last_expected_address = expected_address; + } + + delay(10); +} diff --git a/src/M5StamPLC.h b/src/M5StamPLC.h index 2c31b01..0d4e843 100644 --- a/src/M5StamPLC.h +++ b/src/M5StamPLC.h @@ -9,6 +9,7 @@ #include "utils/lm75b/lm75b.h" #include "utils/rx8130/rx8130.h" #include "modules/M5StamPLC_AC.h" +#include "modules/M5StamPLC_IO.h" #include #include #include diff --git a/src/modules/M5StamPLC_IO.cpp b/src/modules/M5StamPLC_IO.cpp new file mode 100644 index 0000000..1c5522d --- /dev/null +++ b/src/modules/M5StamPLC_IO.cpp @@ -0,0 +1,395 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#include "M5StamPLC_IO.h" +#include + +static const char* _tag = "M5StamPLC_IO"; + +bool M5StamPLC_IO::begin(uint8_t addr) +{ + if (addr == 0) { + _current_addr = scanI2CDevices(); + if (_current_addr == 0) { + ESP_LOGE(_tag, "No I2C device found in range 0x%02X-0x%02X", I2C_ADDR_MIN, I2C_ADDR_MAX); + return false; + } + } else { + if (addr < I2C_ADDR_MIN || addr > I2C_ADDR_MAX) { + ESP_LOGE(_tag, "Invalid I2C address: 0x%02X", addr); + return false; + } + _current_addr = addr; + } + + ESP_LOGI(_tag, "Found I2C device at address: 0x%02X", _current_addr); + + if (!waitForSystemReady()) { + ESP_LOGW(_tag, "System ready timeout, but continuing"); + } + + uint8_t firmware_version = getFirmwareVersion(); + ESP_LOGI(_tag, "Firmware version: 0x%02X", firmware_version); + + uint8_t io_state = readRegister(REG_IO_CONTROL); + uint8_t desired_state = io_state & ~((1 << BIT_CH1_PU_EN) | (1 << BIT_CH2_PU_EN)); + if (desired_state != io_state) { + ESP_LOGI(_tag, "Disabling CH1 and CH2 pull-ups: 0x%02X -> 0x%02X", io_state, desired_state); + writeRegister(REG_IO_CONTROL, desired_state); + } + + return true; +} + +uint8_t M5StamPLC_IO::scanI2CDevices() +{ + bool i2c_devices[0x80] = {false}; + m5::In_I2C.scanID(i2c_devices); + + for (uint8_t addr = I2C_ADDR_MIN; addr <= I2C_ADDR_MAX; addr++) { + if (i2c_devices[addr]) { + ESP_LOGI(_tag, "Found I2C device: 0x%02X", addr); + return addr; + } + } + + ESP_LOGW(_tag, "No I2C device found in range 0x%02X-0x%02X", I2C_ADDR_MIN, I2C_ADDR_MAX); + return 0; +} + +bool M5StamPLC_IO::waitForSystemReady(uint32_t timeout_ms) +{ + unsigned long start_time = millis(); + + while (millis() - start_time < timeout_ms) { + uint8_t status = getSystemStatus(); + + if (status == 0) { + ESP_LOGI(_tag, "System ready, INA226 initialized"); + return true; + } + + if ((status & (1 << SYS_CH1_INA226_ERROR)) != 0) { + ESP_LOGD(_tag, "Waiting for CH1 INA226 initialization..."); + } + if ((status & (1 << SYS_CH2_INA226_ERROR)) != 0) { + ESP_LOGD(_tag, "Waiting for CH2 INA226 initialization..."); + } + + delay(100); + } + + ESP_LOGW(_tag, "System ready timeout, final status: 0x%02X", getSystemStatus()); + return false; +} + +uint8_t M5StamPLC_IO::readRegister(uint8_t reg) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return 0; + } + + uint8_t value = m5::In_I2C.readRegister8(_current_addr, reg, 400000); + + if (value == 0x00 || value == 0xFF) { + delay(10); + uint8_t retry_value = m5::In_I2C.readRegister8(_current_addr, reg, 100000); + if (retry_value != value) { + ESP_LOGD(_tag, "Retry read register 0x%02X: 0x%02X -> 0x%02X", reg, value, retry_value); + value = retry_value; + } + } + + return value; +} + +void M5StamPLC_IO::writeRegister(uint8_t reg, uint8_t value) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return; + } + + bool success = m5::In_I2C.writeRegister8(_current_addr, reg, value, 400000); + if (!success) { + ESP_LOGW(_tag, "Failed to write register 0x%02X = 0x%02X", reg, value); + } +} + +int16_t M5StamPLC_IO::readVoltage(uint8_t channel) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return 0; + } + + uint8_t data[2]; + uint8_t start_reg; + + if (channel == 1) { + start_reg = REG_V_CH1_LSB; + } else if (channel == 2) { + start_reg = REG_V_CH2_LSB; + } else { + ESP_LOGW(_tag, "Invalid channel: %d", channel); + return 0; + } + + if (m5::In_I2C.readRegister(_current_addr, start_reg, data, 2, 400000)) { + uint16_t raw_value = (data[1] << 8) | data[0]; + return (int16_t)raw_value; + } else { + ESP_LOGW(_tag, "Failed to read CH%d voltage", channel); + return 0; + } +} + +int32_t M5StamPLC_IO::readCurrent(uint8_t channel) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return 0; + } + + uint8_t data[4]; + uint8_t start_reg; + + if (channel == 1) { + start_reg = REG_I_CH1_LSB; + } else if (channel == 2) { + start_reg = REG_I_CH2_LSB; + } else { + ESP_LOGW(_tag, "Invalid channel: %d", channel); + return 0; + } + + if (m5::In_I2C.readRegister(_current_addr, start_reg, data, 4, 400000)) { + uint32_t raw_value = ((uint32_t)data[3] << 24) | ((uint32_t)data[2] << 16) | ((uint32_t)data[1] << 8) | data[0]; + return (int32_t)raw_value; + } else { + ESP_LOGW(_tag, "Failed to read CH%d current", channel); + return 0; + } +} + +void M5StamPLC_IO::readAllChannelsData(int16_t* v1, int32_t* i1, int16_t* v2, int32_t* i2) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + *v1 = *i1 = *v2 = *i2 = 0; + return; + } + + uint8_t data[12]; + + if (m5::In_I2C.readRegister(_current_addr, REG_V_CH1_LSB, data, 12, 400000)) { + uint16_t v1_raw = (data[1] << 8) | data[0]; + *v1 = (int16_t)v1_raw; + + uint32_t i1_raw = ((uint32_t)data[5] << 24) | ((uint32_t)data[4] << 16) | ((uint32_t)data[3] << 8) | data[2]; + *i1 = (int32_t)i1_raw; + + uint16_t v2_raw = (data[7] << 8) | data[6]; + *v2 = (int16_t)v2_raw; + + uint32_t i2_raw = ((uint32_t)data[11] << 24) | ((uint32_t)data[10] << 16) | ((uint32_t)data[9] << 8) | data[8]; + *i2 = (int32_t)i2_raw; + + ESP_LOGD(_tag, "Batch read: CH1=%dmV/%duA, CH2=%dmV/%duA", *v1, *i1, *v2, *i2); + } else { + ESP_LOGW(_tag, "Failed to batch read"); + *v1 = *i1 = *v2 = *i2 = 0; + } +} + +uint8_t M5StamPLC_IO::getSystemStatus() +{ + return readRegister(REG_SYSTEM_STATUS); +} + +uint8_t M5StamPLC_IO::getFirmwareVersion() +{ + return readRegister(REG_FIRMWARE_VER); +} + +uint8_t M5StamPLC_IO::getExpectedAddress() +{ + uint8_t config = readRegister(REG_ADDR_CONFIG); + return config & 0x7F; +} + +void M5StamPLC_IO::setNewAddress(uint8_t newAddr) +{ + if (newAddr < I2C_ADDR_MIN || newAddr > I2C_ADDR_MAX) { + ESP_LOGW(_tag, "Invalid address: 0x%02X, must be in range 0x%02X-0x%02X", newAddr, I2C_ADDR_MIN, I2C_ADDR_MAX); + return; + } + + uint8_t config = newAddr | 0x80; + writeRegister(REG_ADDR_CONFIG, config); + + ESP_LOGI(_tag, "Set new address: 0x%02X", newAddr); + _current_addr = newAddr; +} + +void M5StamPLC_IO::toggleIOBit(uint8_t bit) +{ + uint8_t oldState = readRegister(REG_IO_CONTROL); + uint8_t newState = oldState ^ (1 << bit); + + ESP_LOGI(_tag, "Toggle IO bit%d: %s -> %s", bit, (oldState & (1 << bit)) ? "ON" : "OFF", + (newState & (1 << bit)) ? "ON" : "OFF"); + + writeRegister(REG_IO_CONTROL, newState); +} + +void M5StamPLC_IO::setRelayState(uint8_t bit, bool state) +{ + uint8_t currentState = readRegister(REG_IO_CONTROL); + uint8_t newState; + + if (state) { + newState = currentState | (1 << bit); + } else { + newState = currentState & ~(1 << bit); + } + + if (newState != currentState) { + ESP_LOGI(_tag, "Set relay bit%d to %s", bit, state ? "ON" : "OFF"); + writeRegister(REG_IO_CONTROL, newState); + } +} + +void M5StamPLC_IO::setAllRelays(bool state) +{ + uint8_t currentState = readRegister(REG_IO_CONTROL); + uint8_t relay_mask = (1 << BIT_RELAY_TRIG) | (1 << BIT_EX_CTR_1) | (1 << BIT_EX_CTR_2); + uint8_t newState; + + if (state) { + newState = currentState | relay_mask; + ESP_LOGI(_tag, "Turn ON all relays"); + } else { + newState = currentState & ~relay_mask; + ESP_LOGI(_tag, "Turn OFF all relays"); + } + + if (newState != currentState) { + ESP_LOGI(_tag, "Relay state: 0x%02X -> 0x%02X", currentState, newState); + writeRegister(REG_IO_CONTROL, newState); + } +} + +esp_err_t M5StamPLC_IO::readINA226Config(uint8_t channel, uint16_t* config) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return ESP_FAIL; + } + + uint8_t data[2]; + uint8_t start_reg; + + if (channel == 1) { + start_reg = REG_INA226_CONFIG_CH1_LSB; + } else if (channel == 2) { + start_reg = REG_INA226_CONFIG_CH2_LSB; + } else { + ESP_LOGW(_tag, "Invalid channel: %d", channel); + return ESP_FAIL; + } + + if (m5::In_I2C.readRegister(_current_addr, start_reg, data, 2, 400000)) { + *config = (data[1] << 8) | data[0]; + ESP_LOGI(_tag, "Read CH%d INA226 config: 0x%04X", channel, *config); + return ESP_OK; + } else { + ESP_LOGW(_tag, "Failed to read CH%d INA226 config", channel); + return ESP_FAIL; + } +} + +void M5StamPLC_IO::writeINA226Config(uint8_t channel, uint16_t config) +{ + if (_current_addr == 0) { + ESP_LOGE(_tag, "Device not initialized"); + return; + } + + uint8_t lsb = config & 0xFF; + uint8_t msb = (config >> 8) & 0xFF; + + if (channel == 1) { + writeRegister(REG_INA226_CONFIG_CH1_LSB, lsb); + writeRegister(REG_INA226_CONFIG_CH1_MSB, msb); + } else if (channel == 2) { + writeRegister(REG_INA226_CONFIG_CH2_LSB, lsb); + writeRegister(REG_INA226_CONFIG_CH2_MSB, msb); + } else { + ESP_LOGW(_tag, "Invalid channel: %d", channel); + return; + } + + ESP_LOGI(_tag, "Write CH%d INA226 config: 0x%04X", channel, config); +} + +esp_err_t M5StamPLC_IO::setINA226ConversionTime(uint8_t channel, uint8_t vshct, uint8_t vbusct) +{ + uint16_t config; + esp_err_t err = readINA226Config(channel, &config); + if (err != ESP_OK) { + return ESP_FAIL; + } + + config &= ~(INA226_VSHCT_MASK | INA226_VBUSCT_MASK); + config |= ((vshct & 0x07) << 3); + config |= ((vbusct & 0x07) << 6); + + writeINA226Config(channel, config); + ESP_LOGI(_tag, "Set CH%d INA226 conversion time: VSHCT=%d, VBUSCT=%d", channel, vshct, vbusct); + return ESP_OK; +} + +esp_err_t M5StamPLC_IO::setINA226Averaging(uint8_t channel, uint8_t avg) +{ + uint16_t config; + esp_err_t err = readINA226Config(channel, &config); + if (err != ESP_OK) { + return ESP_FAIL; + } + + config &= ~INA226_AVG_MASK; + config |= ((avg & 0x07) << 9); + + writeINA226Config(channel, config); + ESP_LOGI(_tag, "Set CH%d INA226 averaging: %d", channel, avg); + return ESP_OK; +} + +esp_err_t M5StamPLC_IO::getINA226ConversionTime(uint8_t channel, uint8_t* vshct, uint8_t* vbusct) +{ + uint16_t config; + esp_err_t err = readINA226Config(channel, &config); + if (err != ESP_OK) { + return ESP_FAIL; + } + + *vshct = (config & INA226_VSHCT_MASK) >> 3; + *vbusct = (config & INA226_VBUSCT_MASK) >> 6; + return ESP_OK; +} + +esp_err_t M5StamPLC_IO::getINA226Averaging(uint8_t channel, uint8_t* avg) +{ + uint16_t config; + esp_err_t err = readINA226Config(channel, &config); + if (err != ESP_OK) { + return ESP_FAIL; + } + + *avg = (config & INA226_AVG_MASK) >> 9; + return ESP_OK; +} diff --git a/src/modules/M5StamPLC_IO.h b/src/modules/M5StamPLC_IO.h new file mode 100644 index 0000000..da39a83 --- /dev/null +++ b/src/modules/M5StamPLC_IO.h @@ -0,0 +1,265 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ +#pragma once +#include +#include +#include + +class M5StamPLC_IO { +public: + // I2C ADDRESS RANGE + static constexpr uint8_t I2C_ADDR_MIN = 0x20; + static constexpr uint8_t I2C_ADDR_MAX = 0x2F; + + // REGISTER ADDRESS DEFINITION + static constexpr uint8_t REG_V_CH1_LSB = 0x00; + static constexpr uint8_t REG_V_CH1_MSB = 0x01; + static constexpr uint8_t REG_I_CH1_LSB = 0x02; + static constexpr uint8_t REG_I_CH1_MSB = 0x03; + static constexpr uint8_t REG_I_CH1_EXT1 = 0x04; + static constexpr uint8_t REG_I_CH1_EXT2 = 0x05; + static constexpr uint8_t REG_V_CH2_LSB = 0x06; + static constexpr uint8_t REG_V_CH2_MSB = 0x07; + static constexpr uint8_t REG_I_CH2_LSB = 0x08; + static constexpr uint8_t REG_I_CH2_MSB = 0x09; + static constexpr uint8_t REG_I_CH2_EXT1 = 0x0A; + static constexpr uint8_t REG_I_CH2_EXT2 = 0x0B; + static constexpr uint8_t REG_IO_CONTROL = 0x10; + static constexpr uint8_t REG_SYSTEM_STATUS = 0xFB; + static constexpr uint8_t REG_FIRMWARE_VER = 0xFE; + static constexpr uint8_t REG_ADDR_CONFIG = 0xFF; + + // INA226 CONFIG REGISTER ADDRESS DEFINITION + static constexpr uint8_t REG_INA226_CONFIG_CH1_LSB = 0x20; + static constexpr uint8_t REG_INA226_CONFIG_CH1_MSB = 0x21; + static constexpr uint8_t REG_INA226_CONFIG_CH2_LSB = 0x22; + static constexpr uint8_t REG_INA226_CONFIG_CH2_MSB = 0x23; + + // INA226 CONFIG BIT DEFINITION + static constexpr uint16_t INA226_VSHCT_MASK = 0x38; + static constexpr uint16_t INA226_VBUSCT_MASK = 0x1C0; + static constexpr uint16_t INA226_AVG_MASK = 0xE00; + + // IO CONTROL BIT DEFINITION + static constexpr uint8_t BIT_EX_CTR_1 = 0; + static constexpr uint8_t BIT_EX_CTR_2 = 1; + static constexpr uint8_t BIT_CH1_PU_EN = 2; + static constexpr uint8_t BIT_CH2_PU_EN = 3; + static constexpr uint8_t BIT_RELAY_TRIG = 4; + + // SYSTEM STATUS BIT DEFINITION + static constexpr uint8_t SYS_CH1_INA226_ERROR = 0; + static constexpr uint8_t SYS_CH2_INA226_ERROR = 1; + + // INA226 CONVERSION TIME DEFINITION + enum INA226_ConversionTime : uint8_t { + TIME_140US = 0x00, + TIME_204US = 0x01, + TIME_332US = 0x02, + TIME_588US = 0x03, + TIME_1_1MS = 0x04, + TIME_2_116MS = 0x05, + TIME_4_156MS = 0x06, + TIME_8_244MS = 0x07 + }; + + // INA226 AVERAGING DEFINITION + enum INA226_Averaging : uint8_t { + AVG_1 = 0x00, + AVG_4 = 0x01, + AVG_16 = 0x02, + AVG_64 = 0x03, + AVG_128 = 0x04, + AVG_256 = 0x05, + AVG_512 = 0x06, + AVG_1024 = 0x07 + }; + + /** + * @brief Initialize M5StamPLC-IO + * + * @param addr I2C address (0x20-0x2F), if 0 will auto-scan + * @return true if initialization successful + * @return false if initialization failed + */ + bool begin(uint8_t addr = 0); + + /** + * @brief Scan for I2C devices + * + * @return I2C address if found, 0 if not found + */ + uint8_t scanI2CDevices(); + + /** + * @brief Wait for system ready + * + * @param timeout_ms timeout in milliseconds + * @return true if system is ready + * @return false if timeout + */ + bool waitForSystemReady(uint32_t timeout_ms = 5000); + + /** + * @brief Read I2C register + * + * @param reg register address + * @return register value + */ + uint8_t readRegister(uint8_t reg); + + /** + * @brief Write I2C register + * + * @param reg register address + * @param value value to write + */ + void writeRegister(uint8_t reg, uint8_t value); + + /** + * @brief Read voltage (mV) + * + * @param channel 1 or 2 + * @return voltage in mV + */ + int16_t readVoltage(uint8_t channel); + + /** + * @brief Read current (uA) + * + * @param channel 1 or 2 + * @return current in uA + */ + int32_t readCurrent(uint8_t channel); + + /** + * @brief Read all channels data at once + * + * @param v1 pointer to CH1 voltage + * @param i1 pointer to CH1 current + * @param v2 pointer to CH2 voltage + * @param i2 pointer to CH2 current + */ + void readAllChannelsData(int16_t* v1, int32_t* i1, int16_t* v2, int32_t* i2); + + /** + * @brief Get system status + * + * @return system status register value + */ + uint8_t getSystemStatus(); + + /** + * @brief Get firmware version + * + * @return firmware version + */ + uint8_t getFirmwareVersion(); + + /** + * @brief Get expected I2C address from configuration + * + * @return expected address + */ + uint8_t getExpectedAddress(); + + /** + * @brief Set new I2C address + * + * @param newAddr new address (0x20-0x2F) + */ + void setNewAddress(uint8_t newAddr); + + /** + * @brief Toggle IO control bit + * + * @param bit bit number to toggle + */ + void toggleIOBit(uint8_t bit); + + /** + * @brief Set relay state + * + * @param bit bit number + * @param state true for ON, false for OFF + */ + void setRelayState(uint8_t bit, bool state); + + /** + * @brief Set all relays state + * + * @param state true for ON, false for OFF + */ + void setAllRelays(bool state); + + /** + * @brief Read INA226 configuration + * + * @param channel 1 or 2 + * @param config pointer to configuration value + * @return ESP_OK on success + */ + esp_err_t readINA226Config(uint8_t channel, uint16_t* config); + + /** + * @brief Write INA226 configuration + * + * @param channel 1 or 2 + * @param config configuration value + */ + void writeINA226Config(uint8_t channel, uint16_t config); + + /** + * @brief Set INA226 conversion time + * + * @param channel 1 or 2 + * @param vshct VSHCT conversion time + * @param vbusct VBUSCT conversion time + * @return ESP_OK on success + */ + esp_err_t setINA226ConversionTime(uint8_t channel, uint8_t vshct, uint8_t vbusct); + + /** + * @brief Set INA226 averaging + * + * @param channel 1 or 2 + * @param avg averaging value + * @return ESP_OK on success + */ + esp_err_t setINA226Averaging(uint8_t channel, uint8_t avg); + + /** + * @brief Get INA226 conversion time + * + * @param channel 1 or 2 + * @param vshct pointer to VSHCT conversion time + * @param vbusct pointer to VBUSCT conversion time + * @return ESP_OK on success + */ + esp_err_t getINA226ConversionTime(uint8_t channel, uint8_t* vshct, uint8_t* vbusct); + + /** + * @brief Get INA226 averaging + * + * @param channel 1 or 2 + * @param avg pointer to averaging value + * @return ESP_OK on success + */ + esp_err_t getINA226Averaging(uint8_t channel, uint8_t* avg); + + /** + * @brief Get current I2C address + * + * @return current I2C address + */ + uint8_t getCurrentAddress() const + { + return _current_addr; + } + +protected: + uint8_t _current_addr = 0; +};