From 6f1c69d1bb404458469b3ed18ffcaa3849d39887 Mon Sep 17 00:00:00 2001 From: Dylan Luo Date: Fri, 27 Feb 2026 18:10:00 +0800 Subject: [PATCH 1/3] add M5StamPLC_IO_Manager for multidevice manager --- .../StamPLC_IO/GetStatus/GetStatus.ino | 106 ++++----------- .../HotPlugDetection/HotPlugDetection.ino | 128 ------------------ .../MultiDeviceHotPlug/MultiDeviceHotPlug.ino | 77 +++++++++++ .../StamPLC_IO/PWM_Control/PWM_Control.ino | 69 +++------- examples/Modules/StamPLC_IO/Relay/Relay.ino | 15 +- src/modules/M5StamPLC_IO.cpp | 108 +++++++++++++++ src/modules/M5StamPLC_IO.h | 127 +++++++++++++++++ 7 files changed, 365 insertions(+), 265 deletions(-) delete mode 100644 examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino create mode 100644 examples/Modules/StamPLC_IO/MultiDeviceHotPlug/MultiDeviceHotPlug.ino diff --git a/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino b/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino index 39afeab..c2430b8 100644 --- a/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino +++ b/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino @@ -8,12 +8,9 @@ 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); @@ -22,52 +19,31 @@ void setup() 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()); + canvas.printf("Found: 0x%02X FW: 0x%02X\n", stamplc_io.getCurrentAddress(), + 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(sys_status == 0 ? TFT_GREEN : TFT_RED); + canvas.printf("System: %s\n", sys_status == 0 ? "Normal" : "Error"); + 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.println("INA226 Config:"); + for (uint8_t ch = 1; ch <= 2; ch++) { + uint16_t cfg; + if (stamplc_io.readINA226Config(ch, &cfg) == ESP_OK) { + uint8_t vs, vb, avg; + stamplc_io.getINA226ConversionTime(ch, &vs, &vb); + stamplc_io.getINA226Averaging(ch, &avg); + canvas.printf("CH%d: 0x%04X VS=%d VB=%d AVG=%d\n", ch, cfg, vs, vb, avg); + } } canvas.setTextColor(TFT_YELLOW); @@ -88,62 +64,40 @@ void loop() 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 io_ctrl = 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); + canvas.setTextColor(sys_status & (1 << M5StamPLC_IO::SYS_CH1_INA226_ERROR) ? TFT_RED : 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(sys_status & (1 << M5StamPLC_IO::SYS_CH2_INA226_ERROR) ? TFT_RED : 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.printf("Pull-up: CH1=%s CH2=%s\n", + (io_ctrl & (1 << M5StamPLC_IO::BIT_CH1_PU_EN)) ? "ON" : "OFF", + (io_ctrl & (1 << M5StamPLC_IO::BIT_CH2_PU_EN)) ? "ON" : "OFF"); canvas.setTextColor(TFT_MAGENTA); - canvas.printf("Address: 0x%02X->0x%02X \n", stamplc_io.getCurrentAddress(), + canvas.printf("Addr: 0x%02X DIP: 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); - } + canvas.setTextColor(sys_status == 0 ? TFT_GREEN : TFT_RED); + canvas.printf("System: %s\n", sys_status == 0 ? "Normal" : "Error"); } - // Button A: Toggle CH1 pull-up - if (M5StamPLC.BtnA.wasClicked()) { - stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH1_PU_EN); - } + if (M5StamPLC.BtnA.wasClicked()) stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH1_PU_EN); + if (M5StamPLC.BtnB.wasClicked()) stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH2_PU_EN); - // Button B: Toggle CH2 pull-up - if (M5StamPLC.BtnB.wasClicked()) { - stamplc_io.toggleIOBit(M5StamPLC_IO::BIT_CH2_PU_EN); + if (stamplc_io.syncAddress()) { + canvas.printf("Address changed to 0x%02X\n", stamplc_io.getCurrentAddress()); } canvas.pushSprite(0, 0); diff --git a/examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino b/examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino deleted file mode 100644 index 0008c77..0000000 --- a/examples/Modules/StamPLC_IO/HotPlugDetection/HotPlugDetection.ino +++ /dev/null @@ -1,128 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD - * - * SPDX-License-Identifier: MIT - * - * M5StamPLC IO Hot-Plug Detection Example - * - * This example demonstrates hot-plug detection for multiple M5StamPLC IO modules: - * - Automatically scan I2C bus and detect connected modules (0x20-0x2F) - * - Display online modules list - * - Real-time detection of module insertion and removal - */ -#include -#include - -#define MAX_MODULES 16 -#define SCAN_INTERVAL_MS 2000 - -struct ModuleInfo { - uint8_t address; - uint8_t firmware_version; - bool connected; -}; - -ModuleInfo modules[MAX_MODULES]; -uint8_t module_count = 0; -unsigned long last_scan_time = 0; - -void initModules() -{ - for (int i = 0; i < MAX_MODULES; i++) { - modules[i].address = M5StamPLC_IO::I2C_ADDR_MIN + i; - modules[i].firmware_version = 0; - modules[i].connected = false; - } - module_count = 0; -} - -void scanModules() -{ - bool found_map[0x80] = {false}; - m5::In_I2C.scanID(found_map); - - for (int i = 0; i < MAX_MODULES; i++) { - uint8_t addr = M5StamPLC_IO::I2C_ADDR_MIN + i; - if (found_map[addr] && !modules[i].connected) { - M5StamPLC_IO temp_device; - if (temp_device.begin(addr, false)) { - modules[i].address = addr; - modules[i].firmware_version = temp_device.getFirmwareVersion(); - modules[i].connected = true; - module_count++; - Serial.printf("[+] Module connected: 0x%02X (FW: 0x%02X)\n", addr, modules[i].firmware_version); - } - } else if (!found_map[addr] && modules[i].connected) { - Serial.printf("[-] Module disconnected: 0x%02X\n", modules[i].address); - modules[i].connected = false; - module_count--; - } - } -} - -void displayModules() -{ - M5StamPLC.Display.fillScreen(TFT_BLACK); - M5StamPLC.Display.setCursor(0, 0); - - M5StamPLC.Display.setTextColor(TFT_GREENYELLOW); - M5StamPLC.Display.println("=== IO Hot-Plug ==="); - - M5StamPLC.Display.setTextColor(TFT_CYAN); - M5StamPLC.Display.printf("Online: %d\n\n", module_count); - - if (module_count == 0) { - M5StamPLC.Display.setTextColor(TFT_ORANGE); - M5StamPLC.Display.println("No modules found"); - } else { - int displayed = 0; - for (int i = 0; i < MAX_MODULES && displayed < 8; i++) { - if (modules[i].connected) { - M5StamPLC.Display.setTextColor(TFT_GREEN); - M5StamPLC.Display.printf("%d. Addr: 0x%02X\n", displayed + 1, modules[i].address); - M5StamPLC.Display.setTextColor(TFT_YELLOW); - M5StamPLC.Display.printf(" FW: 0x%02X\n", modules[i].firmware_version); - displayed++; - } - } - } -} - -void setup() -{ - M5StamPLC.begin(); - M5StamPLC.Display.setTextScroll(false); - M5StamPLC.Display.setTextSize(1); - M5StamPLC.Display.setFont(&fonts::efontCN_16); - - Serial.begin(115200); - Serial.println("\n=== M5StamPLC IO Hot-Plug Detection ==="); - - initModules(); - - M5StamPLC.Display.fillScreen(TFT_BLACK); - M5StamPLC.Display.setCursor(0, 0); - M5StamPLC.Display.setTextColor(TFT_GREENYELLOW); - M5StamPLC.Display.println("M5StamPLC IO"); - M5StamPLC.Display.println("Hot-Plug Detection"); - M5StamPLC.Display.setTextColor(TFT_CYAN); - M5StamPLC.Display.println("\nScanning..."); - - delay(1000); - scanModules(); - displayModules(); - last_scan_time = millis(); -} - -void loop() -{ - M5StamPLC.update(); - - if (millis() - last_scan_time > SCAN_INTERVAL_MS) { - scanModules(); - displayModules(); - last_scan_time = millis(); - } - - delay(100); -} diff --git a/examples/Modules/StamPLC_IO/MultiDeviceHotPlug/MultiDeviceHotPlug.ino b/examples/Modules/StamPLC_IO/MultiDeviceHotPlug/MultiDeviceHotPlug.ino new file mode 100644 index 0000000..2efd7ef --- /dev/null +++ b/examples/Modules/StamPLC_IO/MultiDeviceHotPlug/MultiDeviceHotPlug.ino @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + * + * M5StamPLC IO Multi-Device Hot-Plug Example + * + * Demonstrates hot-plug and DIP-switch address change detection + * for up to 16 M5StamPLC IO modules (I2C address 0x20-0x2F). + */ +#include +#include + +M5StamPLC_IO_Manager io_mgr; + +void onConnect(M5StamPLC_IO& dev, uint8_t addr) +{ + Serial.printf("[+] 0x%02X online FW=0x%02X DIP=0x%02X\n", addr, dev.getFirmwareVersion(), + dev.getExpectedAddress()); +} + +void onDisconnect(uint8_t addr) +{ + Serial.printf("[-] 0x%02X offline\n", addr); +} + +void onAddrChange(uint8_t old_addr, uint8_t new_addr) +{ + Serial.printf("[~] DIP change: 0x%02X -> 0x%02X, address applied\n", old_addr, new_addr); +} + +void updateDisplay() +{ + M5StamPLC.Display.fillScreen(TFT_BLACK); + M5StamPLC.Display.setCursor(0, 0); + + M5StamPLC.Display.setTextColor(TFT_GREENYELLOW); + M5StamPLC.Display.println("== Multi IO HotPlug =="); + + M5StamPLC.Display.setTextColor(TFT_CYAN); + M5StamPLC.Display.printf("Online: %d\n\n", io_mgr.count()); + + for (uint8_t i = 0; i < io_mgr.count() && i < 6; i++) { + M5StamPLC_IO* dev = io_mgr.get(i); + uint8_t addr = dev->getCurrentAddress(); + M5StamPLC.Display.setTextColor(TFT_GREEN); + M5StamPLC.Display.printf("0x%02X FW:0x%02X", addr, dev->getFirmwareVersion()); + M5StamPLC.Display.setTextColor(TFT_YELLOW); + M5StamPLC.Display.printf(" DIP:0x%02X\n", dev->getExpectedAddress()); + } +} + +void setup() +{ + M5StamPLC.begin(); + M5StamPLC.Display.setTextScroll(false); + M5StamPLC.Display.setTextSize(1); + M5StamPLC.Display.setFont(&fonts::efontCN_16); + + Serial.begin(115200); + Serial.println("\n=== M5StamPLC IO Multi-Device Hot-Plug ==="); + + io_mgr.onConnect(onConnect); + io_mgr.onDisconnect(onDisconnect); + io_mgr.onAddrChange(onAddrChange); + io_mgr.begin(); + + updateDisplay(); +} + +void loop() +{ + M5StamPLC.update(); + io_mgr.update(); + updateDisplay(); + delay(1000); +} diff --git a/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino b/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino index ef764bc..f428c2c 100644 --- a/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino +++ b/examples/Modules/StamPLC_IO/PWM_Control/PWM_Control.ino @@ -5,17 +5,11 @@ * * M5StamPLC IO PWM Control Example * - * This example demonstrates how to use the PWM control function of the M5StamPLC IO module: - * - Switch between IO mode and PWM mode - * - Set PWM frequency (1-100 Hz) - * - Adjust CH1 and CH2 duty cycle (0-1000 thousandths) - * - * Button functions: - * - Button A: Toggle PWM mode on/off - * - Button B: Increase CH1 duty cycle (+10%) - * - Long press B: Decrease CH1 duty cycle (-10%) - * - Button C: Increase CH2 duty cycle (+10%) - * - Long press C: Decrease CH2 duty cycle (-10%) + * Button A: Toggle PWM mode on/off + * Button B: CH1 duty +10% + * Long press B: CH1 duty -10% + * Button C: CH2 duty +10% + * Long press C: CH2 duty -10% */ #include #include @@ -29,8 +23,6 @@ uint16_t ch2_duty = 0; bool btnB_longPressed = false; bool btnC_longPressed = false; -unsigned long lastUpdateTime = 0; - void updateDisplay() { M5StamPLC.Display.fillScreen(TFT_BLACK); @@ -40,17 +32,15 @@ void updateDisplay() M5StamPLC.Display.println("=== PWM Control ==="); M5StamPLC.Display.setTextColor(TFT_CYAN); - M5StamPLC.Display.printf("Mode: %s\n", pwm_mode ? "PWM" : "IO"); - M5StamPLC.Display.printf("Frequency: %d Hz\n", pwm_freq); + M5StamPLC.Display.printf("Mode: %s Freq: %dHz\n", pwm_mode ? "PWM" : "IO", pwm_freq); M5StamPLC.Display.setTextColor(TFT_YELLOW); - M5StamPLC.Display.printf("CH1 Duty: %d.%d%% (%d/1000)\n", ch1_duty / 10, ch1_duty % 10, ch1_duty); - M5StamPLC.Display.printf("CH2 Duty: %d.%d%% (%d/1000)\n", ch2_duty / 10, ch2_duty % 10, ch2_duty); + M5StamPLC.Display.printf("CH1: %d.%d%% (%d/1000)\n", ch1_duty / 10, ch1_duty % 10, ch1_duty); + M5StamPLC.Display.printf("CH2: %d.%d%% (%d/1000)\n", ch2_duty / 10, ch2_duty % 10, ch2_duty); M5StamPLC.Display.setTextColor(TFT_DARKGREY); M5StamPLC.Display.println("A: Toggle PWM mode"); - M5StamPLC.Display.println("B: CH1+ Long press: CH1-"); - M5StamPLC.Display.println("C: CH2+ Long press: CH2-"); + M5StamPLC.Display.println("B/C: duty+ Long: duty-"); } void setup() @@ -61,16 +51,13 @@ void setup() M5StamPLC.Display.setFont(&fonts::efontCN_16); M5StamPLC.Display.println("Try to find M5StamPLC IO module..."); - /* Init M5StamPLC IO */ while (!stamplc_io.begin()) { M5StamPLC.Display.println("Not found, retry in 1s..."); delay(1000); } - M5StamPLC.Display.printf("Found IO module: 0x%02X\n", stamplc_io.getCurrentAddress()); - M5StamPLC.Display.printf("Firmware Version: 0x%02X\n", stamplc_io.getFirmwareVersion()); - - delay(2000); + M5StamPLC.Display.printf("Found: 0x%02X FW: 0x%02X\n", stamplc_io.getCurrentAddress(), + stamplc_io.getFirmwareVersion()); pwm_mode = stamplc_io.getPWMMode(); pwm_freq = stamplc_io.getPWMFrequency(); @@ -83,7 +70,6 @@ void setup() } updateDisplay(); - lastUpdateTime = millis(); } void loop() @@ -92,60 +78,41 @@ void loop() bool needUpdate = false; - // Button A: Toggle PWM mode if (M5StamPLC.BtnA.wasClicked()) { pwm_mode = !pwm_mode; stamplc_io.setPWMMode(pwm_mode); needUpdate = true; } - // Button B: Adjust CH1 duty cycle if (M5StamPLC.BtnB.wasClicked()) { - // Increase 10% (100/1000) - ch1_duty += 100; - if (ch1_duty > 1000) ch1_duty = 1000; + ch1_duty = (ch1_duty + 100 > 1000) ? 1000 : ch1_duty + 100; stamplc_io.setChannelDuty(1, ch1_duty); needUpdate = true; } else if (M5StamPLC.BtnB.pressedFor(500) && !btnB_longPressed) { - // Decrease 10% btnB_longPressed = true; - if (ch1_duty >= 100) { - ch1_duty -= 100; - } else { - ch1_duty = 0; - } + ch1_duty = (ch1_duty >= 100) ? ch1_duty - 100 : 0; stamplc_io.setChannelDuty(1, ch1_duty); needUpdate = true; } else if (!M5StamPLC.BtnB.isPressed()) { btnB_longPressed = false; } - // Button C: Adjust CH2 duty cycle if (M5StamPLC.BtnC.wasClicked()) { - // Increase 10% (100/1000) - ch2_duty += 100; - if (ch2_duty > 1000) ch2_duty = 1000; + ch2_duty = (ch2_duty + 100 > 1000) ? 1000 : ch2_duty + 100; stamplc_io.setChannelDuty(2, ch2_duty); needUpdate = true; } else if (M5StamPLC.BtnC.pressedFor(500) && !btnC_longPressed) { - // Decrease 10% btnC_longPressed = true; - if (ch2_duty >= 100) { - ch2_duty -= 100; - } else { - ch2_duty = 0; - } + ch2_duty = (ch2_duty >= 100) ? ch2_duty - 100 : 0; stamplc_io.setChannelDuty(2, ch2_duty); needUpdate = true; } else if (!M5StamPLC.BtnC.isPressed()) { btnC_longPressed = false; } - // Periodically update display - if (needUpdate || (millis() - lastUpdateTime > 1000)) { - updateDisplay(); - lastUpdateTime = millis(); - } + if (stamplc_io.syncAddress()) needUpdate = true; + + if (needUpdate) updateDisplay(); delay(10); } diff --git a/examples/Modules/StamPLC_IO/Relay/Relay.ino b/examples/Modules/StamPLC_IO/Relay/Relay.ino index db74447..51ca71b 100644 --- a/examples/Modules/StamPLC_IO/Relay/Relay.ino +++ b/examples/Modules/StamPLC_IO/Relay/Relay.ino @@ -7,11 +7,9 @@ #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; +bool relay_state[3] = {false, false, false}; +bool btnB_longPressed = false; +bool btnC_longPressed = false; void setup() { @@ -28,7 +26,6 @@ void setup() 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"); @@ -69,10 +66,8 @@ void loop() 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; + if (stamplc_io.syncAddress()) { + M5StamPLC.Display.printf("Address changed to 0x%02X\n", stamplc_io.getCurrentAddress()); } delay(10); diff --git a/src/modules/M5StamPLC_IO.cpp b/src/modules/M5StamPLC_IO.cpp index 64671f1..a43e01a 100644 --- a/src/modules/M5StamPLC_IO.cpp +++ b/src/modules/M5StamPLC_IO.cpp @@ -237,6 +237,19 @@ void M5StamPLC_IO::setNewAddress(uint8_t newAddr) _current_addr = newAddr; } +bool M5StamPLC_IO::syncAddress() +{ + uint8_t dip_addr = getExpectedAddress(); + + if (dip_addr == _current_addr || dip_addr < I2C_ADDR_MIN || dip_addr > I2C_ADDR_MAX) { + return false; + } + + ESP_LOGI(_tag, "DIP address changed: 0x%02X -> 0x%02X, applying", _current_addr, dip_addr); + setNewAddress(dip_addr); + return true; +} + void M5StamPLC_IO::toggleIOBit(uint8_t bit) { uint8_t oldState = readRegister(REG_IO_CONTROL); @@ -499,3 +512,98 @@ uint16_t M5StamPLC_IO::getChannelDuty(uint8_t channel) return 0; } } + +/* M5StamPLC_IO_Manager */ + +void M5StamPLC_IO_Manager::begin(uint32_t interval_ms) +{ + _interval_ms = interval_ms; + _scan(); + _last_ms = millis(); +} + +void M5StamPLC_IO_Manager::update() +{ + if (millis() - _last_ms >= _interval_ms) { + _last_ms = millis(); // update before scan so _scan() can override to 0 for fast retry + _scan(); + } +} + +M5StamPLC_IO* M5StamPLC_IO_Manager::get(uint8_t index) +{ + uint8_t n = 0; + for (uint8_t i = 0; i < MAX_DEVICES; i++) { + if (_slots[i].connected) { + if (n == index) return &_slots[i].io; + n++; + } + } + return nullptr; +} + +M5StamPLC_IO* M5StamPLC_IO_Manager::getByAddr(uint8_t addr) +{ + if (addr < M5StamPLC_IO::I2C_ADDR_MIN || addr > M5StamPLC_IO::I2C_ADDR_MAX) return nullptr; + uint8_t idx = addr - M5StamPLC_IO::I2C_ADDR_MIN; + return _slots[idx].connected ? &_slots[idx].io : nullptr; +} + +void M5StamPLC_IO_Manager::_scan() +{ + bool found[0x80] = {false}; + m5::In_I2C.scanID(found); + + bool changed = false; + + for (uint8_t i = 0; i < MAX_DEVICES; i++) { + uint8_t addr = M5StamPLC_IO::I2C_ADDR_MIN + i; + Slot& s = _slots[i]; + + if (found[addr] && !s.connected) { + if (s.io.begin(addr, false)) { + s.connected = true; + s.dip_addr = s.io.getExpectedAddress(); + s.fw_ver = s.io.getFirmwareVersion(); + _count++; + changed = true; + if (_on_connect) _on_connect(s.io, addr); + } + } else if (!found[addr] && s.connected) { + s.connected = false; + if (_count > 0) _count--; + changed = true; + if (_on_disconnect) _on_disconnect(addr); + } else if (found[addr] && s.connected) { + uint8_t new_dip = s.io.getExpectedAddress(); + if (new_dip != s.dip_addr && new_dip >= M5StamPLC_IO::I2C_ADDR_MIN && + new_dip <= M5StamPLC_IO::I2C_ADDR_MAX) { + _applyAddrChange(i, new_dip); + changed = true; + } + } + } + + // Any topology change triggers an immediate follow-up scan on the next update() call. + if (changed) { + _last_ms = 0; + } +} + +void M5StamPLC_IO_Manager::_applyAddrChange(uint8_t old_idx, uint8_t new_dip) +{ + uint8_t new_idx = new_dip - M5StamPLC_IO::I2C_ADDR_MIN; + uint8_t old_addr = M5StamPLC_IO::I2C_ADDR_MIN + old_idx; + + if (_slots[new_idx].connected) return; // address conflict, skip + + // Write (new_dip | 0x80): firmware validates bit6:0 == DIP, then updates I2C address. + // After this call, _slots[old_idx].io._current_addr == new_dip internally. + _slots[old_idx].io.setNewAddress(new_dip); + + _slots[new_idx] = _slots[old_idx]; + _slots[new_idx].dip_addr = new_dip; + _slots[old_idx].connected = false; + + if (_on_addr_change) _on_addr_change(old_addr, new_dip); +} diff --git a/src/modules/M5StamPLC_IO.h b/src/modules/M5StamPLC_IO.h index ddb34dd..1611e4b 100644 --- a/src/modules/M5StamPLC_IO.h +++ b/src/modules/M5StamPLC_IO.h @@ -180,6 +180,18 @@ class M5StamPLC_IO { */ void setNewAddress(uint8_t newAddr); + /** + * @brief Check if the DIP-switch address has changed and apply it if so. + * + * Reads REG_ADDR_CONFIG bit6:0 (DIP-switch position) and compares with the + * current I2C address. If different, calls setNewAddress() to apply the change. + * Call this periodically in loop() when DIP-switch hot-swap is needed. + * + * @return true if the address was updated + * @return false if no change detected + */ + bool syncAddress(); + /** * @brief Toggle IO control bit * @@ -314,3 +326,118 @@ class M5StamPLC_IO { protected: uint8_t _current_addr = 0; }; + +/** + * @brief Multi-device hot-plug manager for M5StamPLC IO modules. + * + * Scans the I2C bus periodically (0x20-0x2F), detects module insertion / + * removal, and applies DIP-switch address changes on-the-fly. + * + * Address-change protocol (REG_ADDR_CONFIG = 0xFF): + * Read bit6:0 - current DIP-switch address (hardware, read-only) + * Write bit6:0 + bit7=1 - firmware validates written value == DIP reading, + * on match I2C address is updated, bit7 auto-clears + * + * Usage: + * M5StamPLC_IO_Manager mgr; + * mgr.onConnect([](M5StamPLC_IO& dev, uint8_t addr) { ... }); + * mgr.onDisconnect([](uint8_t addr) { ... }); + * mgr.onAddrChange([](uint8_t old_addr, uint8_t new_addr) { ... }); + * mgr.begin(); // call once in setup() + * mgr.update(); // call in loop() + */ +class M5StamPLC_IO_Manager { +public: + static constexpr uint8_t MAX_DEVICES = 16; + + struct Slot { + M5StamPLC_IO io; + bool connected = false; + uint8_t dip_addr = 0; + uint8_t fw_ver = 0; + }; + + using ConnectCb = void (*)(M5StamPLC_IO& dev, uint8_t addr); + using DisconnectCb = void (*)(uint8_t addr); + using AddrChangeCb = void (*)(uint8_t old_addr, uint8_t new_addr); + + /** + * @brief Initial scan, call once in setup() + * + * @param interval_ms Scan interval in milliseconds (default: 2000) + */ + void begin(uint32_t interval_ms = 2000); + + /** + * @brief Periodic scan, call in loop() + */ + void update(); + + /** + * @brief Register callback invoked when a module is connected + * + * @param cb Callback function pointer: void cb(M5StamPLC_IO& dev, uint8_t addr) + */ + void onConnect(ConnectCb cb) + { + _on_connect = cb; + } + + /** + * @brief Register callback invoked when a module is disconnected + * + * @param cb Callback function pointer: void cb(uint8_t addr) + */ + void onDisconnect(DisconnectCb cb) + { + _on_disconnect = cb; + } + + /** + * @brief Register callback invoked when a module's DIP-switch address changes + * + * @param cb Callback function pointer: void cb(uint8_t old_addr, uint8_t new_addr) + */ + void onAddrChange(AddrChangeCb cb) + { + _on_addr_change = cb; + } + + /** + * @brief Get number of currently connected modules + * + * @return Number of connected modules + */ + uint8_t count() const + { + return _count; + } + + /** + * @brief Get a connected module by sequential index + * + * @param index Index in range 0 to count()-1 + * @return Pointer to M5StamPLC_IO, or nullptr if out of range + */ + M5StamPLC_IO* get(uint8_t index); + + /** + * @brief Get a connected module by its I2C address + * + * @param addr I2C address (0x20-0x2F) + * @return Pointer to M5StamPLC_IO, or nullptr if not connected + */ + M5StamPLC_IO* getByAddr(uint8_t addr); + +private: + Slot _slots[MAX_DEVICES]; + uint8_t _count = 0; + uint32_t _interval_ms = 2000; + unsigned long _last_ms = 0; + ConnectCb _on_connect = nullptr; + DisconnectCb _on_disconnect = nullptr; + AddrChangeCb _on_addr_change = nullptr; + + void _scan(); + void _applyAddrChange(uint8_t old_idx, uint8_t new_dip); +}; From 5b77b840f5bc4b2f52d3e109cc4b07e90d33b8be Mon Sep 17 00:00:00 2001 From: Dylan Luo Date: Sat, 28 Feb 2026 09:14:15 +0800 Subject: [PATCH 2/3] fix formatting issues --- examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino b/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino index c2430b8..b7cc6da 100644 --- a/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino +++ b/examples/Modules/StamPLC_IO/GetStatus/GetStatus.ino @@ -25,8 +25,7 @@ void setup() delay(1000); } - canvas.printf("Found: 0x%02X FW: 0x%02X\n", stamplc_io.getCurrentAddress(), - stamplc_io.getFirmwareVersion()); + canvas.printf("Found: 0x%02X FW: 0x%02X\n", stamplc_io.getCurrentAddress(), stamplc_io.getFirmwareVersion()); uint8_t sys_status = stamplc_io.getSystemStatus(); canvas.setTextColor(sys_status == 0 ? TFT_GREEN : TFT_RED); @@ -81,13 +80,11 @@ void loop() 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_ctrl & (1 << M5StamPLC_IO::BIT_CH1_PU_EN)) ? "ON" : "OFF", + canvas.printf("Pull-up: CH1=%s CH2=%s\n", (io_ctrl & (1 << M5StamPLC_IO::BIT_CH1_PU_EN)) ? "ON" : "OFF", (io_ctrl & (1 << M5StamPLC_IO::BIT_CH2_PU_EN)) ? "ON" : "OFF"); canvas.setTextColor(TFT_MAGENTA); - canvas.printf("Addr: 0x%02X DIP: 0x%02X\n", stamplc_io.getCurrentAddress(), - stamplc_io.getExpectedAddress()); + canvas.printf("Addr: 0x%02X DIP: 0x%02X\n", stamplc_io.getCurrentAddress(), stamplc_io.getExpectedAddress()); canvas.setTextColor(sys_status == 0 ? TFT_GREEN : TFT_RED); canvas.printf("System: %s\n", sys_status == 0 ? "Normal" : "Error"); From fa3929f037ef0a279a97b766eb6a499f2d7d85dc Mon Sep 17 00:00:00 2001 From: Dylan Luo Date: Sat, 28 Feb 2026 09:43:51 +0800 Subject: [PATCH 3/3] update logging levels and improve I2C device scanning in M5StamPLC_IO_Manager --- src/modules/M5StamPLC_IO.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/modules/M5StamPLC_IO.cpp b/src/modules/M5StamPLC_IO.cpp index a43e01a..4ecc6fd 100644 --- a/src/modules/M5StamPLC_IO.cpp +++ b/src/modules/M5StamPLC_IO.cpp @@ -10,7 +10,7 @@ static const char* _tag = "M5StamPLC_IO"; bool M5StamPLC_IO::begin(uint8_t addr, bool debug) { - esp_log_level_set(_tag, debug ? ESP_LOG_DEBUG : ESP_LOG_NONE); + esp_log_level_set(_tag, debug ? ESP_LOG_INFO : ESP_LOG_WARN); if (addr == 0) { _current_addr = scanI2CDevices(); @@ -552,9 +552,13 @@ M5StamPLC_IO* M5StamPLC_IO_Manager::getByAddr(uint8_t addr) void M5StamPLC_IO_Manager::_scan() { bool found[0x80] = {false}; - m5::In_I2C.scanID(found); - - bool changed = false; + bool changed = false; + for (uint8_t addr = M5StamPLC_IO::I2C_ADDR_MIN; addr <= M5StamPLC_IO::I2C_ADDR_MAX; addr++) { + if (m5::In_I2C.scanID(addr)) { + ESP_LOGI(_tag, "Found I2C device: 0x%02X", addr); + found[addr] = true; + } + } for (uint8_t i = 0; i < MAX_DEVICES; i++) { uint8_t addr = M5StamPLC_IO::I2C_ADDR_MIN + i; @@ -595,7 +599,10 @@ void M5StamPLC_IO_Manager::_applyAddrChange(uint8_t old_idx, uint8_t new_dip) uint8_t new_idx = new_dip - M5StamPLC_IO::I2C_ADDR_MIN; uint8_t old_addr = M5StamPLC_IO::I2C_ADDR_MIN + old_idx; - if (_slots[new_idx].connected) return; // address conflict, skip + if (_slots[new_idx].connected) { + ESP_LOGE(_tag, "Address conflict! Dev at 0x%02X wants to move to 0x%02X, but slot is busy.", old_addr, new_dip); + return; + } // Write (new_dip | 0x80): firmware validates bit6:0 == DIP, then updates I2C address. // After this call, _slots[old_idx].io._current_addr == new_dip internally.