commit 41e04e2a7a4c57e215a1f615c9da89fd3d84bae1 Author: moyongming Date: Wed Aug 13 17:15:40 2025 +0800 Init diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..079c44e --- /dev/null +++ b/.clang-format @@ -0,0 +1,167 @@ +--- +Language: Cpp +BasedOnStyle: Google +AccessModifierOffset: -4 +AlignAfterOpenBracket: Align +AlignConsecutiveMacros: true +AlignConsecutiveAssignments: true +AlignConsecutiveDeclarations: false +AlignEscapedNewlines: Left +AlignOperands: true +AlignTrailingComments: true +AllowAllArgumentsOnNextLine: true +AllowAllConstructorInitializersOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Never +AllowShortCaseLabelsOnASingleLine: false +AllowShortFunctionsOnASingleLine: false +AllowShortLambdasOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: true +AlwaysBreakAfterDefinitionReturnType: None +AlwaysBreakAfterReturnType: None +AlwaysBreakBeforeMultilineStrings: true +AlwaysBreakTemplateDeclarations: Yes +BinPackArguments: true +BinPackParameters: true +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: false + AfterEnum: false + AfterFunction: true + AfterNamespace: false + AfterObjCDeclaration: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false + IndentBraces: false + SplitEmptyFunction: true + SplitEmptyRecord: true + SplitEmptyNamespace: true +BreakBeforeBinaryOperators: None +BreakBeforeBraces: Custom +BreakBeforeInheritanceComma: false +BreakInheritanceList: BeforeColon +BreakBeforeTernaryOperators: true +BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: BeforeColon +BreakAfterJavaFieldAnnotations: false +BreakStringLiterals: true +ColumnLimit: 120 +CommentPragmas: '^ IWYU pragma:' +CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 +Cpp11BracedListStyle: true +DeriveLineEnding: true +DerivePointerAlignment: true +DisableFormat: false +ExperimentalAutoDetectBinPacking: false +FixNamespaceComments: true +ForEachMacros: + - foreach + - Q_FOREACH + - BOOST_FOREACH +IncludeBlocks: Regroup +IncludeCategories: + - Regex: '^' + Priority: 2 + SortPriority: 0 + - Regex: '^<.*\.h>' + Priority: 1 + SortPriority: 0 + - Regex: '^<.*' + Priority: 2 + SortPriority: 0 + - Regex: '.*' + Priority: 3 + SortPriority: 0 +IncludeIsMainRegex: '([-_](test|unittest))?$' +IncludeIsMainSourceRegex: '' +IndentCaseLabels: true +IndentGotoLabels: true +IndentPPDirectives: None +IndentWidth: 4 +IndentWrappedFunctionNames: false +JavaScriptQuotes: Leave +JavaScriptWrapImports: true +KeepEmptyLinesAtTheStartOfBlocks: false +MacroBlockBegin: '' +MacroBlockEnd: '' +MaxEmptyLinesToKeep: 1 +NamespaceIndentation: None +ObjCBinPackProtocolList: Never +ObjCBlockIndentWidth: 2 +ObjCSpaceAfterProperty: false +ObjCSpaceBeforeProtocolList: true +PenaltyBreakAssignment: 2 +PenaltyBreakBeforeFirstCallParameter: 1 +PenaltyBreakComment: 300 +PenaltyBreakFirstLessLess: 120 +PenaltyBreakString: 1000 +PenaltyBreakTemplateDeclaration: 10 +PenaltyExcessCharacter: 1000000 +PenaltyReturnTypeOnItsOwnLine: 200 +PointerAlignment: Left +RawStringFormats: + - Language: Cpp + Delimiters: + - cc + - CC + - cpp + - Cpp + - CPP + - 'c++' + - 'C++' + CanonicalDelimiter: '' + BasedOnStyle: google + - Language: TextProto + Delimiters: + - pb + - PB + - proto + - PROTO + EnclosingFunctions: + - EqualsProto + - EquivToProto + - PARSE_PARTIAL_TEXT_PROTO + - PARSE_TEST_PROTO + - PARSE_TEXT_PROTO + - ParseTextOrDie + - ParseTextProtoOrDie + CanonicalDelimiter: '' + BasedOnStyle: google +ReflowComments: true +SortIncludes: false +SortUsingDeclarations: true +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeParens: ControlStatements +SpaceBeforeRangeBasedForLoopColon: true +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesBeforeTrailingComments: 2 +SpacesInAngles: false +SpacesInConditionalStatement: false +SpacesInContainerLiterals: true +SpacesInCStyleCastParentheses: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +SpaceBeforeSquareBrackets: false +Standard: Auto +StatementMacros: + - Q_UNUSED + - QT_REQUIRE_VERSION +TabWidth: 4 +UseCRLF: false +UseTab: Never +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..259148f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6831ef1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 M5Stack Technology CO LTD + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..28401dc --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# M5SwitchC6 + +## Overview + +### SKU:K140 + +ESP-NOW based library to control and manage Switch-C6 smart switch devices. + +## Related Link + +- [Function Guide (CN)](./README_FUNCTION_CN.md) +- [Function Guide (EN)](./README_FUNCTION_EN.md) + +## Required Libraries: + +- [Arduino-ESP32](https://github.com/espressif/arduino-esp32) +- [M5Unified (optional)](https://github.com/m5stack/M5Unified) + +## License + +- [M5SwitchC6 - MIT](LICENSE) diff --git a/README_FUNCTION_CN.md b/README_FUNCTION_CN.md new file mode 100644 index 0000000..191a733 --- /dev/null +++ b/README_FUNCTION_CN.md @@ -0,0 +1,741 @@ +# M5-SwitchC6 库 + +## 概述 + +M5-SwitchC6 是一个基于ESP-NOW协议的智能开关控制库,用于控制和管理 Switch-C6 开关设备。该库提供了完整的设备控制API,支持开关控制、状态查询、版本查询、响应管理等功能。 + +### 主要特性 + +- **开关控制**: 远程开启/关闭Switch-C6设备 +- **状态查询**: 实时查询设备状态和电压信息 +- **版本查询**: 获取设备固件版本信息 +- **响应管理**: 支异步通信,带超时机制 +- **信道管理**: 支持设置主机ESP-NOW通信信道 +- **设备发现**: 自动发现和管理多个设备 +- **信号监控**: 获取设备信号强度(RSSI)信息 +- **广播接收**: 自动接收设备按钮广播数据 +- **多设备支持**: 支持同时管理多个Switch-C6设备 +- **设备清理**: 智能清理离线设备和过期数据,支持自动清理 +- **线程安全**: 使用FreeRTOS互斥锁确保线程安全操作 + +### 支持平台 + +1. **ESP32系列** (推荐) +2. **M5Stack系列设备** + +### 完善的示例项目:https://gitlab.m5stack.com/moyongming/m5iot-switchc6-demo-pio + +## 快速开始 + +### 基础使用 + +```cpp +#include + +M5SwitchC6 switchC6(10); +//填入你的SwitchC6设备MAC地址 +const String deviceMac = "E4B3-2386-18C4"; + +void setup() { + Serial.begin(115200); + + // 设置WiFi为Station模式 + WiFi.mode(WIFI_STA); + WiFi.begin(); + + // 初始化Switch-C6控制器 + if (switchC6.begin()) { + Serial.println("Switch-C6 初始化成功"); + } else { + Serial.println("Switch-C6 初始化失败"); + } +} + +void loop() { + // 执行自动清理检查 + switchC6.checkAutoCleanup(); + + // 开启设备 + if (switchC6.sendSwitchCommand(deviceMac, true)) { + Serial.println("开启命令发送成功"); + } + delay(5000); + + // 查询设备状态 + SwitchC6ParsedData_t response; + if (switchC6.sendStatusQueryWithResponse(deviceMac, 3000, &response)) { + Serial.printf("设备状态: %s\n", response.switchState ? "开启" : "关闭"); + Serial.printf("设备电压: %.2fV\n", response.voltage); + Serial.printf("设备MAC: %s\n", response.macAddress.c_str()); + } else { + Serial.println("状态查询失败或超时"); + } + + delay(5000); + + // 关闭设备 + if (switchC6.sendSwitchCommand(deviceMac, false)) { + Serial.println("关闭命令发送成功"); + } + delay(5000); +} +``` + +### 调试输出激活 + +在调试模式下,可以启用详细日志输出。需要在代码中定义以下宏,并在主函数初始化该串口: + +```cpp +#define M5_MODULE_DEBUG_SERIAL Serial0 +``` + +### M5Stack示例 + +```cpp +#include +#include +#include + +M5SwitchC6 switchC6(10); +//填入你的SwitchC6设备MAC地址 +const String DEVICE_MAC = "E4B3-2386-18C4"; + +void setup() { + Serial.begin(115200); + M5.begin(); + M5.Display.setRotation(1); + M5.Display.setTextSize(2); + + WiFi.mode(WIFI_STA); + WiFi.begin(); + + M5.Display.clear(); + M5.Display.setCursor(10, 10); + M5.Display.println("Switch-C6 Demo"); + + if (switchC6.begin()) { + M5.Display.setCursor(10, 60); + M5.Display.println("A:ON B:OFF C:STATUS"); + } +} + +void loop() { + M5.update(); + + if (M5.BtnA.wasPressed()) { + // 开启设备 + if (switchC6.sendSwitchCommandWithResponse(DEVICE_MAC, true, 10000)) { + Serial.println("开启命令发送成功"); + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.println("Device ON sent"); + } + } + + if (M5.BtnB.wasPressed()) { + // 关闭设备 + if (switchC6.sendSwitchCommandWithResponse(DEVICE_MAC, false, 10000)) { + Serial.println("关闭命令发送成功"); + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.println("Device OFF sent"); + } + } + + if (M5.BtnC.wasPressed()) { + // 查询状态 + SwitchC6ParsedData_t response; + if (switchC6.sendStatusQueryWithResponse(DEVICE_MAC, 10000, &response)) { + Serial.printf("设备状态: %s, 电压: %.2fV\n", + response.switchState ? "开启" : "关闭", response.voltage); + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.printf("S:%s V:%.2fV", + response.switchState ? "ON" : "OFF", response.voltage); + } else { + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.println("Query failed"); + } + } + + delay(100); +} +``` + +## API 参考 + +### 初始化和配置 + +#### `M5SwitchC6(uint16_t max_broadcast_peers)` + +构造函数,创建M5SwitchC6实例 + +- **参数**: + - `max_broadcast_peers` - 支持的最大广播设备数量 + +#### `bool begin()` + +初始化模块,设置ESP-NOW通信 + +- **返回值**: `true` 成功,`false` 失败 + +#### `bool setChannel(uint8_t channel)` + +设置ESP-NOW通信信道(仅高级用户使用) + +- **参数**: + - `channel` - WiFi信道 (1-14) +- **返回值**: `true` 成功,`false` 失败 + +> **注意**: 默认信道已优化,普通用户无需调用此方法。 + +#### `uint8_t getChannel()` + +获取当前通信信道(仅用于调试) + +- **返回值**: 当前WiFi信道号 + +### 设备控制 + +#### `bool sendSwitchCommand(const String& deviceMac, bool switchOn, bool needResponse = false)` + +发送开关控制命令 + +- **参数**: + - `deviceMac` - 设备MAC地址 (格式: "XXXX-XXXX-XXXX") + - `switchOn` - `true` 开启,`false` 关闭 + - `needResponse` - 是否需要响应 +- **返回值**: `true` 发送成功,`false` 发送失败 + +#### `bool sendStatusQuery(const String& deviceMac)` + +发送状态查询命令 + +- **参数**: + - `deviceMac` - 设备MAC地址 +- **返回值**: `true` 发送成功,`false` 发送失败 + +#### `bool sendVersionQuery(const String& deviceMac)` + +发送版本查询命令 + +- **参数**: + - `deviceMac` - 设备MAC地址 +- **返回值**: `true` 发送成功,`false` 发送失败 + +#### `bool sendCustomMessage(const String& message)` + +发送自定义消息 + +- **参数**: + - `message` - 自定义消息字符串 +- **返回值**: `true` 发送成功,`false` 发送失败 + +### 响应管理 + +#### `bool sendSwitchCommandWithResponse(const String& deviceMac, bool switchOn, unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr)` + +发送开关命令并等待响应 + +- **参数**: + - `deviceMac` - 设备MAC地址 + - `switchOn` - 开关状态 + - `timeoutMs` - 超时时间(毫秒),默认5000ms + - `response` - 响应数据指针(可选) +- **返回值**: `true` 收到响应,`false` 超时或失败 + +#### `bool sendStatusQueryWithResponse(const String& deviceMac, unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr)` + +发送状态查询并等待响应 + +- **参数**: + - `deviceMac` - 设备MAC地址 + - `timeoutMs` - 超时时间(毫秒),默认5000ms + - `response` - 响应数据指针(可选) +- **返回值**: `true` 收到响应,`false` 超时或失败 + +#### `bool sendVersionQueryWithResponse(const String& deviceMac, unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr)` + +发送版本查询并等待响应 + +- **参数**: + - `deviceMac` - 设备MAC地址 + - `timeoutMs` - 超时时间(毫秒),默认5000ms + - `response` - 响应数据指针(可选) +- **返回值**: `true` 收到响应,`false` 超时或失败 + +#### `bool isWaitingForResponse()` + +检查是否有等待中的响应请求 + +- **返回值**: `true` 有等待中的请求,`false` 无等待 + +#### `bool getResponseWaitInfo(const String& deviceMac, ResponseWaitInfo* waitInfo)` + +获取指定设备的响应等待信息 + +- **参数**: + - `deviceMac` - 设备MAC地址 + - `waitInfo` - 等待信息结构体指针 +- **返回值**: `true` 设备正在等待响应,`false` 无等待 + +### 设备管理 + +#### `uint16_t getPeerCount()` + +获取检测到的设备数量 + +- **返回值**: 设备数量 + +#### `bool getPeerInfo(uint16_t index, broadcast_packet_with_mac* peerInfo)` + +根据索引获取设备信息 + +- **参数**: + - `index` - 设备索引 (0 到 getPeerCount()-1) + - `peerInfo` - 设备信息结构体指针 +- **返回值**: `true` 获取成功,`false` 索引无效 + +### 高级配置 + +#### `bool setResendInterval(unsigned long intervalMs)` + +设置重发间隔 + +- **参数**: + - `intervalMs` - 重发间隔(毫秒,1-10000ms) +- **返回值**: `true` 设置成功,`false` 参数超出范围 + +#### `unsigned long getResendInterval()` + +获取当前重发间隔 + +- **返回值**: 重发间隔(毫秒) + +### 数据管理 + +#### `bool clearPeersData()` + +清空所有已发现的设备数据 + +- **功能**: 清空所有存储的设备信息,包括MAC地址、最后接收的数据、信号强度和状态信息 +- **返回值**: `true` 清空成功,`false` 清空失败 + +#### `bool clearResponseWaits()` + +清空所有等待中的响应请求 + +- **功能**: 取消所有等待中的响应请求并清空等待队列 +- **返回值**: `true` 清空成功,`false` 清空失败 + +#### `bool clearAllData()` + +清空所有历史数据 + +- **功能**: 执行完整的数据清理,包括已发现的设备和等待中的响应请求 +- **返回值**: `true` 全部清空成功,`false` 部分清空失败 + +#### `bool clearDeviceData(const String& deviceMac)` + +清除指定设备的数据 + +- **参数**: + - `deviceMac` - 设备MAC地址 (格式: "XX:XX:XX:XX:XX:XX" 或 "XXXX-XXXX-XXXX") +- **功能**: 根据MAC地址从设备列表中移除指定设备,同时清除该设备的等待响应 +- **返回值**: `true` 设备找到并清除成功,`false` 设备未找到或清除失败 + +#### `uint16_t clearExpiredDevices(unsigned long timeoutMs = 300000)` + +清除过期设备 + +- **参数**: + - `timeoutMs` - 超时时间(毫秒),默认300000ms(5分钟) +- **功能**: 移除指定时间内未发送任何数据的设备 +- **返回值**: 被移除的设备数量 + +#### `bool setAutoCleanup(unsigned long intervalMs, unsigned long deviceTimeoutMs = 300000)` + +设置自动清理 + +- **参数**: + - `intervalMs` - 清理检查间隔(毫秒,设为0禁用自动清理) + - `deviceTimeoutMs` - 设备超时时间(毫秒),默认300000ms(5分钟) +- **功能**: 启用定期自动清理过期设备的功能 +- **返回值**: `true` 设置成功,`false` 参数无效 + +#### `uint16_t checkAutoCleanup()` + +执行自动清理检查 + +- **功能**: 检查并执行自动清理,应在loop()中定期调用 +- **返回值**: 本次清理移除的设备数量,如果未到清理时间则返回0 + +### 数据解析 + +#### `SwitchC6ParsedData parseDataStructured(const uint8_t *data, int len)` + +解析接收到的数据并返回结构化结果 + +- **参数**: + - `data` - 数据缓冲区指针 + - `len` - 数据长度 +- **返回值**: 解析后的数据结构 + +## 数据结构 + +### SwitchC6ParsedData_t + +设备解析数据结构 + +```cpp +struct SwitchC6ParsedData_t { + SwitchC6CommandType_t commandType; // 命令类型 + String macAddress; // 设备MAC地址字符串 + bool switchState; // 开关状态 (true=开, false=关) + float voltage; // 电压值 (例如: 3.30V) + String version; // 版本号字符串 + bool isValid; // 解析是否成功 + String errorMessage; // 错误信息 +}; +``` + +### broadcast_packet_with_mac + +广播设备信息结构 + +```cpp +struct broadcast_packet_with_mac { + uint8_t senderMac[6]; // 发送者MAC地址 + String lastData; // 最新数据 + uint8_t channel; // 接收数据的信道 + int rssi; // 信号强度 + unsigned long lastReceiveTime; // 最后接收时间 + bool status; // 状态标志 (0:关断 1:开启) +}; +``` + +### SwitchC6CommandType_t + +命令类型枚举 + +```cpp +enum SwitchC6CommandType_t { + COMMAND_UNKNOWN = 0, // 未知命令 + COMMAND_STATUS_RETURN = 1, // 状态返回数据 + COMMAND_BROADCAST = 2, // 按钮广播数据 + COMMAND_VERSION_RETURN = 3 // 版本返回数据 +}; +``` + +### ResponseWaitInfo + +响应等待信息结构 + +```cpp +struct ResponseWaitInfo { + String deviceMac; // 等待响应的设备MAC地址 + unsigned long requestTime; // 请求发送时间 + unsigned long timeoutMs; // 超时时间(毫秒) + bool isWaiting; // 是否正在等待响应 + bool hasResponse; // 是否已收到响应 + SwitchC6ParsedData_t response; // 收到的响应数据 +}; +``` + +## 高级使用 + +### 设备清理功能 + +```cpp +M5SwitchC6 switchC6(20); + +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); + + // 启用自动清理:每30秒检查一次,移除5分钟未响应的设备 + switchC6.setAutoCleanup(30000, 300000); +} + +void loop() { + // 执行自动清理检查 + uint16_t removed = switchC6.checkAutoCleanup(); //不可少了这行,否则不会运行清理函数 + if (removed > 0) { + Serial.printf("自动清理移除了 %d 个设备\n", removed); + } + + // 手动清理特定设备 + if (needToClearDevice) { + if (switchC6.clearDeviceData("AA:BB:CC:DD:EE:FF")) { + Serial.println("指定设备已清除"); + } + } + + // 手动清理过期设备 + if (needToClearExpired) { + uint16_t count = switchC6.clearExpiredDevices(60000); // 清除1分钟未响应的设备 + Serial.printf("清除了 %d 个过期设备\n", count); + } + + delay(1000); +} +``` + +### 多设备管理 + +```cpp +M5SwitchC6 switchC6(20); // 支持20个设备 + +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); +} + +void loop() { + // 获取所有检测到的设备 + uint16_t deviceCount = switchC6.getPeerCount(); + Serial.printf("检测到 %d 个设备\n", deviceCount); + + for (uint16_t i = 0; i < deviceCount; i++) { + broadcast_packet_with_mac peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + Serial.printf("设备 %d: MAC=%02X:%02X:%02X:%02X:%02X:%02X, " + "状态=%s, RSSI=%d, 信道=%d\n", + i, + peerInfo.senderMac[0], peerInfo.senderMac[1], + peerInfo.senderMac[2], peerInfo.senderMac[3], + peerInfo.senderMac[4], peerInfo.senderMac[5], + peerInfo.status ? "开启" : "关闭", + peerInfo.rssi, peerInfo.channel); + } + } + delay(5000); +} +``` + +### 错误处理和重试机制 + +```cpp +bool controlDeviceWithRetry(const String& deviceMac, bool switchOn, int maxRetries = 3) { + for (int i = 0; i < maxRetries; i++) { + SwitchC6ParsedData_t response; + if (switchC6.sendSwitchCommandWithResponse(deviceMac, switchOn, 3000, &response)) { + if (response.isValid && response.switchState == switchOn) { + Serial.printf("设备控制成功,第 %d 次尝试\n", i + 1); + return true; + } + } + Serial.printf("第 %d 次尝试失败,重试中...\n", i + 1); + delay(1000); // 等待1秒后重试 + } + Serial.println("设备控制失败,已达到最大重试次数"); + return false; +} + +void loop() { + // 使用重试机制控制设备 + if (controlDeviceWithRetry("E4B3-2386-18C4", true)) { + Serial.println("设备成功开启"); + } + delay(10000); +} +``` + +### 异步操作示例 + +```cpp +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); +} + +void loop() { + // 发送命令但不等待响应 + switchC6.sendSwitchCommand("E4B3-2386-18C4", true, true); + + // 继续执行其他任务 + doOtherTasks(); + + // 检查是否有等待中的响应 + if (switchC6.isWaitingForResponse()) { + Serial.println("等待设备响应中..."); + } + + delay(100); +} + +void doOtherTasks() { + // 执行其他不依赖Switch-C6响应的任务 + Serial.println("执行其他任务..."); + delay(200); +} +``` + +### 数据管理示例 + +```cpp +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); + + // 设置自动清理:每2分钟检查,移除10分钟未响应的设备 + switchC6.setAutoCleanup(120000, 600000); +} + +void loop() { + // 自动清理会在checkAutoCleanup()中执行 + switchC6.checkAutoCleanup(); + + // 显示当前设备信息 + uint16_t deviceCount = switchC6.getPeerCount(); + Serial.printf("当前检测到 %d 个设备\n", deviceCount); + + // 监控设备状态,清理长时间无响应的设备 + for (uint16_t i = 0; i < deviceCount; i++) { + broadcast_packet_with_mac peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + unsigned long timeSinceLastReceive = millis() - peerInfo.lastReceiveTime; + + // 如果设备超过15分钟未响应,手动清理 + if (timeSinceLastReceive > 900000) { // 15分钟 + String deviceMac = ""; + for (int j = 0; j < 6; j++) { + if (j > 0) deviceMac += ":"; + deviceMac += String(peerInfo.senderMac[j], HEX); + } + + Serial.printf("清理长时间未响应的设备: %s\n", deviceMac.c_str()); + switchC6.clearDeviceData(deviceMac); + break; // 一次只清理一个,避免影响循环 + } + } + } + + delay(5000); +} +``` + +### 智能设备管理 + +```cpp +void smartDeviceManagement() { + // 1. 检查设备数量,避免过多设备占用内存 + uint16_t deviceCount = switchC6.getPeerCount(); + if (deviceCount > 15) { + Serial.println("设备数量过多,清理最旧的设备"); + switchC6.clearExpiredDevices(180000); // 清理3分钟未响应的设备 + } + + // 2. 定期全面清理 + static unsigned long lastFullCleanup = 0; + if (millis() - lastFullCleanup > 1800000) { // 30分钟执行一次全面清理 + Serial.println("执行定期全面清理"); + switchC6.clearAllData(); + lastFullCleanup = millis(); + } + + // 3. 根据信号强度清理弱信号设备 + for (uint16_t i = 0; i < switchC6.getPeerCount(); i++) { + broadcast_packet_with_mac peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + if (peerInfo.rssi < -90) { // 信号强度过弱 + unsigned long timeSinceLastReceive = millis() - peerInfo.lastReceiveTime; + if (timeSinceLastReceive > 300000) { // 且5分钟未响应 + String deviceMac = ""; + for (int j = 0; j < 6; j++) { + if (j > 0) deviceMac += ":"; + deviceMac += String(peerInfo.senderMac[j], HEX); + } + Serial.printf("清理弱信号设备: %s (RSSI: %d)\n", deviceMac.c_str(), peerInfo.rssi); + switchC6.clearDeviceData(deviceMac); + break; + } + } + } + } +} +``` + +## 协议支持 + +### ESP-NOW通信协议 + +支持完整的Switch-C6 ESP-NOW控制协议: + +#### 控制命令格式 + +- **开启设备**: `"1"` 或 `"1;"` (带响应) +- **关闭设备**: `"0"` 或 `"0;"` (带响应) +- **状态查询**: `"?"` 或 `"?;"` (带响应) +- **版本查询**: `"V"` 或 `"V;"` (带响应) + +#### 响应数据格式 + +- **状态响应**: `";状态;电压"` (例如: ";1;3.30") +- **版本响应**: `";版本号"` (例如: ";1.0.0") +- **广播数据**: 设备按钮按下时的广播信息 + +#### MAC地址格式 + +- **标准格式**: `"XXXX-XXXX-XXXX"` (例如: "E4B3-2386-18C4") +- **广播地址**: `"FFFF-FFFF-FFFF"` (接收所有设备广播) + +### 通信特性 + +- **信道范围**: 1-14 (推荐使用6或11) +- **最大设备数**: 可配置,推荐不超过20个 +- **通信距离**: 室内约30-100米,室外约100-300米 +- **数据包大小**: 最大250字节 +- **重发机制**: 可配置重发间隔和次数 + +## 故障排除 + +### 常见问题 + +#### 1. 设备初始化失败 + +```cpp +if (!switchC6.begin()) { + Serial.println("初始化失败,请检查:"); + Serial.println("1. WiFi是否设置为STA模式"); + Serial.println("2. ESP-NOW是否支持"); + Serial.println("3. 内存是否充足"); +} +``` + +#### 2. 设备响应超时 + +```cpp +SwitchC6ParsedData_t response; +if (!switchC6.sendStatusQueryWithResponse(deviceMac, 5000, &response)) { + Serial.println("响应超时,可能原因:"); + Serial.println("1. 设备不在线或信号弱"); + Serial.println("2. MAC地址错误"); + Serial.println("3. 信道不匹配"); + Serial.println("4. 设备忙碌"); +} +``` + +#### 3. MAC地址格式错误 + +```cpp +String deviceMac = "E4B3-2386-18C4"; // 正确格式 +// String deviceMac = "E4:B3:23:86:18:C4"; // 错误格式 +``` + +#### 4. 信道配置问题 + +```cpp +Serial.printf("当前信道: %d\n", switchC6.getChannel()); +``` + +> **注意**: 由于SwitchC6使用的是广播包,且会监听所有信道,故信道设置不重要,如果环境某信道干扰过大,再做更改。 diff --git a/README_FUNCTION_EN.md b/README_FUNCTION_EN.md new file mode 100644 index 0000000..6ee2b64 --- /dev/null +++ b/README_FUNCTION_EN.md @@ -0,0 +1,738 @@ +# M5-SwitchC6 Library + +## Overview + +M5-SwitchC6 is an ESP-NOW based smart switch control library for controlling and managing Switch-C6 switch devices. It provides a complete device control API including switch control, status query, version query, response management, and more. + +### Key Features + +- Switch control: remotely turn Switch-C6 devices on/off +- Status query: get device status and voltage in real time +- Version query: retrieve firmware version +- Response management: async communications with timeout +- Channel management: set host ESP-NOW channel +- Device discovery: automatically discover and manage multiple devices +- Signal monitoring: obtain device RSSI +- Broadcast reception: automatically receive button broadcast data +- Multi-device support: manage multiple Switch-C6 devices concurrently +- Device cleanup: intelligently remove offline devices and stale data; optional auto cleanup +- Thread safety: FreeRTOS mutex-protected operations + +### Supported Platforms + +1. ESP32 family (recommended) +2. M5Stack devices + +### Full example project: https://gitlab.m5stack.com/moyongming/m5iot-switchc6-demo-pio + +## Quick Start + +### Basic usage + +```cpp +#include + +M5SwitchC6 switchC6(10); +// Fill in your Switch-C6 device MAC address +const String deviceMac = "E4B3-2386-18C4"; + +void setup() { + Serial.begin(115200); + + // Set WiFi to Station mode + WiFi.mode(WIFI_STA); + WiFi.begin(); + + // Initialize Switch-C6 controller + if (switchC6.begin()) { + Serial.println("Switch-C6 initialized successfully"); + } else { + Serial.println("Switch-C6 initialization failed"); + } +} + +void loop() { + // Run auto cleanup checks + switchC6.checkAutoCleanup(); + + // Turn device ON + if (switchC6.sendSwitchCommand(deviceMac, true)) { + Serial.println("ON command sent"); + } + delay(5000); + + // Query device status + SwitchC6ParsedData_t response; + if (switchC6.sendStatusQueryWithResponse(deviceMac, 3000, &response)) { + Serial.printf("Status: %s\n", response.switchState ? "ON" : "OFF"); + Serial.printf("Voltage: %.2fV\n", response.voltage); + Serial.printf("MAC: %s\n", response.macAddress.c_str()); + } else { + Serial.println("Status query failed or timed out"); + } + + delay(5000); + + // Turn device OFF + if (switchC6.sendSwitchCommand(deviceMac, false)) { + Serial.println("OFF command sent"); + } + delay(5000); +} +``` + +### Enable debug logs + +In debug builds, you can enable verbose logging by defining the macro below and initializing the chosen serial port in your sketch: + +```cpp +#define M5_MODULE_DEBUG_SERIAL Serial0 +``` + +### M5Stack example + +```cpp +#include +#include +#include + +M5SwitchC6 switchC6(10); +// Fill in your Switch-C6 device MAC address +const String DEVICE_MAC = "E4B3-2386-18C4"; + +void setup() { + Serial.begin(115200); + M5.begin(); + M5.Display.setRotation(1); + M5.Display.setTextSize(2); + + WiFi.mode(WIFI_STA); + WiFi.begin(); + + M5.Display.clear(); + M5.Display.setCursor(10, 10); + M5.Display.println("Switch-C6 Demo"); + + if (switchC6.begin()) { + M5.Display.setCursor(10, 60); + M5.Display.println("A:ON B:OFF C:STATUS"); + } +} + +void loop() { + M5.update(); + + if (M5.BtnA.wasPressed()) { + // Turn ON + if (switchC6.sendSwitchCommandWithResponse(DEVICE_MAC, true, 10000)) { + Serial.println("ON command sent"); + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.println("Device ON sent"); + } + } + + if (M5.BtnB.wasPressed()) { + // Turn OFF + if (switchC6.sendSwitchCommandWithResponse(DEVICE_MAC, false, 10000)) { + Serial.println("OFF command sent"); + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.println("Device OFF sent"); + } + } + + if (M5.BtnC.wasPressed()) { + // Query status + SwitchC6ParsedData_t response; + if (switchC6.sendStatusQueryWithResponse(DEVICE_MAC, 10000, &response)) { + Serial.printf("Status: %s, Voltage: %.2fV\n", + response.switchState ? "ON" : "OFF", response.voltage); + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.printf("S:%s V:%.2fV", + response.switchState ? "ON" : "OFF", response.voltage); + } else { + M5.Display.fillRect(10, 100, 300, 30, BLACK); + M5.Display.setCursor(10, 100); + M5.Display.println("Query failed"); + } + } + + delay(100); +} +``` + +## API Reference + +### Initialization and configuration + +#### `M5SwitchC6(uint16_t max_broadcast_peers)` + +Constructor, creates an M5SwitchC6 instance. + +- Parameter: + - `max_broadcast_peers` - Max number of broadcast devices to support + +#### `bool begin()` + +Initialize the module and set up ESP-NOW communications. + +- Returns: `true` on success, `false` on failure + +#### `bool setChannel(uint8_t channel)` + +Set ESP-NOW communication channel (advanced). + +- Parameter: + - `channel` - WiFi channel (1-14) +- Returns: `true` success, `false` failure + +> Note: The default channel is optimized; most users don’t need to change this. + +#### `uint8_t getChannel()` + +Get the current communication channel (for debugging). + +- Returns: current WiFi channel + +### Device control + +#### `bool sendSwitchCommand(const String& deviceMac, bool switchOn, bool needResponse = false)` + +Send an ON/OFF command. + +- Parameters: + - `deviceMac` - Device MAC (format: "XXXX-XXXX-XXXX") + - `switchOn` - `true` ON, `false` OFF + - `needResponse` - Whether a response is required +- Returns: `true` when sent, `false` on failure + +#### `bool sendStatusQuery(const String& deviceMac)` + +Send a status query command. + +- Parameter: `deviceMac` - Device MAC address +- Returns: `true` when sent, `false` on failure + +#### `bool sendVersionQuery(const String& deviceMac)` + +Send a version query command. + +- Parameter: `deviceMac` - Device MAC address +- Returns: `true` when sent, `false` on failure + +#### `bool sendCustomMessage(const String& message)` + +Send a custom message. + +- Parameter: `message` - Custom message string +- Returns: `true` when sent, `false` on failure + +### Response management + +#### `bool sendSwitchCommandWithResponse(const String& deviceMac, bool switchOn, unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr)` + +Send an ON/OFF command and wait for a response. + +- Parameters: + - `deviceMac` - Device MAC address + - `switchOn` - ON/OFF + - `timeoutMs` - Timeout in milliseconds (default 5000ms) + - `response` - Optional pointer to response struct +- Returns: `true` if response received, otherwise `false` + +#### `bool sendStatusQueryWithResponse(const String& deviceMac, unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr)` + +Send a status query and wait for a response. + +- Parameters: + - `deviceMac` - Device MAC address + - `timeoutMs` - Timeout in milliseconds (default 5000ms) + - `response` - Optional pointer to response struct +- Returns: `true` if response received, otherwise `false` + +#### `bool sendVersionQueryWithResponse(const String& deviceMac, unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr)` + +Send a version query and wait for a response. + +- Parameters: + - `deviceMac` - Device MAC address + - `timeoutMs` - Timeout in milliseconds (default 5000ms) + - `response` - Optional pointer to response struct +- Returns: `true` if response received, otherwise `false` + +#### `bool isWaitingForResponse()` + +Check if there are pending requests waiting for responses. + +- Returns: `true` if waiting, `false` otherwise + +#### `bool getResponseWaitInfo(const String& deviceMac, ResponseWaitInfo* waitInfo)` + +Get waiting info for a specific device. + +- Parameters: + - `deviceMac` - Device MAC address + - `waitInfo` - Pointer to a wait info struct +- Returns: `true` if the device is waiting for a response, otherwise `false` + +### Device management + +#### `uint16_t getPeerCount()` + +Get the number of detected devices. + +- Returns: device count + +#### `bool getPeerInfo(uint16_t index, broadcast_packet_with_mac* peerInfo)` + +Get device info by index. + +- Parameters: + - `index` - Device index (0 to getPeerCount()-1) + - `peerInfo` - Pointer to a device info struct +- Returns: `true` on success, `false` if index is invalid + +### Advanced configuration + +#### `bool setResendInterval(unsigned long intervalMs)` + +Set resend interval. + +- Parameter: + - `intervalMs` - Interval in milliseconds (1-10000ms) +- Returns: `true` on success, `false` if out of range + +#### `unsigned long getResendInterval()` + +Get the current resend interval. + +- Returns: interval in milliseconds + +### Data management + +#### `bool clearPeersData()` + +Clear all discovered device data. + +- Behavior: clear all stored device info including MAC, last payload, RSSI and status +- Returns: `true` on success, `false` on failure + +#### `bool clearResponseWaits()` + +Clear all pending response requests. + +- Behavior: cancel all pending requests and clear the queue +- Returns: `true` on success, `false` on failure + +#### `bool clearAllData()` + +Clear all historical data. + +- Behavior: perform a full cleanup including discovered devices and response waits +- Returns: `true` if all cleared, `false` if partially failed + +#### `bool clearDeviceData(const String& deviceMac)` + +Clear data for a specific device. + +- Parameter: + - `deviceMac` - Device MAC address (format: "XX:XX:XX:XX:XX:XX" or "XXXX-XXXX-XXXX") +- Behavior: remove device from the list and clear associated waits +- Returns: `true` if found and removed, otherwise `false` + +#### `uint16_t clearExpiredDevices(unsigned long timeoutMs = 300000)` + +Remove expired devices. + +- Parameter: + - `timeoutMs` - Timeout in milliseconds, default 300000ms (5 minutes) +- Behavior: remove devices that have not sent any data within the specified time +- Returns: number of removed devices + +#### `bool setAutoCleanup(unsigned long intervalMs, unsigned long deviceTimeoutMs = 300000)` + +Configure automatic cleanup. + +- Parameters: + - `intervalMs` - Check interval in milliseconds (0 disables auto cleanup) + - `deviceTimeoutMs` - Device timeout in milliseconds, default 300000ms (5 minutes) +- Behavior: enable periodic auto cleanup of expired devices +- Returns: `true` on success, `false` if parameters invalid + +#### `uint16_t checkAutoCleanup()` + +Perform auto cleanup check. + +- Behavior: check and perform auto cleanup; call regularly in loop() +- Returns: number of devices removed in this run, or 0 if not time yet + +### Data parsing + +#### `SwitchC6ParsedData parseDataStructured(const uint8_t *data, int len)` + +Parse received data and return a structured result. + +- Parameters: + - `data` - Pointer to the data buffer + - `len` - Data length +- Returns: parsed data structure + +## Data Structures + +### SwitchC6ParsedData_t + +Parsed device data structure + +```cpp +struct SwitchC6ParsedData_t { + SwitchC6CommandType_t commandType; // Command type + String macAddress; // Device MAC address string + bool switchState; // Switch state (true=ON, false=OFF) + float voltage; // Voltage value (e.g., 3.30V) + String version; // Version string + bool isValid; // Whether parsing succeeded + String errorMessage; // Error message if any +}; +``` + +### broadcast_packet_with_mac + +Broadcast device info structure + +```cpp +struct broadcast_packet_with_mac { + uint8_t senderMac[6]; // Sender MAC address + String lastData; // Last received payload + uint8_t channel; // Channel data was received on + int rssi; // Signal strength + unsigned long lastReceiveTime; // Last receive timestamp + bool status; // Status flag (0:OFF 1:ON) +}; +``` + +### SwitchC6CommandType_t + +Command type enum + +```cpp +enum SwitchC6CommandType_t { + COMMAND_UNKNOWN = 0, // Unknown command + COMMAND_STATUS_RETURN = 1, // Status response data + COMMAND_BROADCAST = 2, // Button broadcast data + COMMAND_VERSION_RETURN = 3 // Version response data +}; +``` + +### ResponseWaitInfo + +Response waiting info structure + +```cpp +struct ResponseWaitInfo { + String deviceMac; // Device MAC waiting for response + unsigned long requestTime; // Request sent time + unsigned long timeoutMs; // Timeout in milliseconds + bool isWaiting; // Whether waiting + bool hasResponse; // Whether a response has been received + SwitchC6ParsedData_t response; // Response payload +}; +``` + +## Advanced Usage + +### Device cleanup + +```cpp +M5SwitchC6 switchC6(20); + +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); + + // Enable auto cleanup: check every 30s, remove devices idle for 5 minutes + switchC6.setAutoCleanup(30000, 300000); +} + +void loop() { + // Perform auto cleanup check (don’t miss this, or cleanup won’t run) + uint16_t removed = switchC6.checkAutoCleanup(); + if (removed > 0) { + Serial.printf("Auto cleanup removed %d device(s)\n", removed); + } + + // Manually clear a specific device + if (needToClearDevice) { + if (switchC6.clearDeviceData("AA:BB:CC:DD:EE:FF")) { + Serial.println("Specified device cleared"); + } + } + + // Manually clear expired devices + if (needToClearExpired) { + uint16_t count = switchC6.clearExpiredDevices(60000); // 1-minute idle + Serial.printf("Cleared %d expired device(s)\n", count); + } + + delay(1000); +} +``` + +### Manage multiple devices + +```cpp +M5SwitchC6 switchC6(20); // Support 20 devices + +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); +} + +void loop() { + // Get all detected devices + uint16_t deviceCount = switchC6.getPeerCount(); + Serial.printf("Detected %d device(s)\n", deviceCount); + + for (uint16_t i = 0; i < deviceCount; i++) { + broadcast_packet_with_mac peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + Serial.printf("Device %d: MAC=%02X:%02X:%02X:%02X:%02X:%02X, " + "Status=%s, RSSI=%d, Channel=%d\n", + i, + peerInfo.senderMac[0], peerInfo.senderMac[1], + peerInfo.senderMac[2], peerInfo.senderMac[3], + peerInfo.senderMac[4], peerInfo.senderMac[5], + peerInfo.status ? "ON" : "OFF", + peerInfo.rssi, peerInfo.channel); + } + } + delay(5000); +} +``` + +### Error handling and retry + +```cpp +bool controlDeviceWithRetry(const String& deviceMac, bool switchOn, int maxRetries = 3) { + for (int i = 0; i < maxRetries; i++) { + SwitchC6ParsedData_t response; + if (switchC6.sendSwitchCommandWithResponse(deviceMac, switchOn, 3000, &response)) { + if (response.isValid && response.switchState == switchOn) { + Serial.printf("Success on attempt %d\n", i + 1); + return true; + } + } + Serial.printf("Attempt %d failed, retrying...\n", i + 1); + delay(1000); // Retry after 1 second + } + Serial.println("Control failed, reached max retries"); + return false; +} + +void loop() { + // Use retry mechanism to control the device + if (controlDeviceWithRetry("E4B3-2386-18C4", true)) { + Serial.println("Device turned ON"); + } + delay(10000); +} +``` + +### Asynchronous operations + +```cpp +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); +} + +void loop() { + // Send command without waiting for response + switchC6.sendSwitchCommand("E4B3-2386-18C4", true, true); + + // Continue with other tasks + doOtherTasks(); + + // Check if there are pending responses + if (switchC6.isWaitingForResponse()) { + Serial.println("Waiting for device response..."); + } + + delay(100); +} + +void doOtherTasks() { + // Perform other tasks that don't depend on Switch-C6 responses + Serial.println("Doing other tasks..."); + delay(200); +} +``` + +### Data management example + +```cpp +void setup() { + WiFi.mode(WIFI_STA); + WiFi.begin(); + + switchC6.begin(); + + // Auto cleanup: check every 2 minutes; remove devices idle for 10 minutes + switchC6.setAutoCleanup(120000, 600000); +} + +void loop() { + // Auto cleanup is performed inside checkAutoCleanup() + switchC6.checkAutoCleanup(); + + // Show current device info + uint16_t deviceCount = switchC6.getPeerCount(); + Serial.printf("Currently detected %d device(s)\n", deviceCount); + + // Monitor device status and clean long-idle devices + for (uint16_t i = 0; i < deviceCount; i++) { + broadcast_packet_with_mac peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + unsigned long timeSinceLastReceive = millis() - peerInfo.lastReceiveTime; + + // If device is idle for more than 15 minutes, clear it + if (timeSinceLastReceive > 900000) { // 15 minutes + String deviceMac = ""; + for (int j = 0; j < 6; j++) { + if (j > 0) deviceMac += ":"; + deviceMac += String(peerInfo.senderMac[j], HEX); + } + + Serial.printf("Clearing long-idle device: %s\n", deviceMac.c_str()); + switchC6.clearDeviceData(deviceMac); + break; // Clear one at a time to avoid affecting the loop + } + } + } + + delay(5000); +} +``` + +### Smart device management + +```cpp +void smartDeviceManagement() { + // 1) Check device count to avoid excessive memory usage + uint16_t deviceCount = switchC6.getPeerCount(); + if (deviceCount > 15) { + Serial.println("Too many devices; clearing oldest/idle ones"); + switchC6.clearExpiredDevices(180000); // 3 minutes idle + } + + // 2) Periodic full cleanup + static unsigned long lastFullCleanup = 0; + if (millis() - lastFullCleanup > 1800000) { // Every 30 minutes + Serial.println("Performing periodic full cleanup"); + switchC6.clearAllData(); + lastFullCleanup = millis(); + } + + // 3) Cleanup weak-signal devices + for (uint16_t i = 0; i < switchC6.getPeerCount(); i++) { + broadcast_packet_with_mac peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + if (peerInfo.rssi < -90) { // Very weak signal + unsigned long timeSinceLastReceive = millis() - peerInfo.lastReceiveTime; + if (timeSinceLastReceive > 300000) { // Also idle for 5 minutes + String deviceMac = ""; + for (int j = 0; j < 6; j++) { + if (j > 0) deviceMac += ":"; + deviceMac += String(peerInfo.senderMac[j], HEX); + } + Serial.printf("Clearing weak-signal device: %s (RSSI: %d)\n", deviceMac.c_str(), peerInfo.rssi); + switchC6.clearDeviceData(deviceMac); + break; + } + } + } + } +} +``` + +## Protocol Support + +### ESP-NOW protocol + +Fully supports the Switch-C6 ESP-NOW control protocol: + +#### Control command formats + +- Turn ON: `"1"` or `"1;"` (with response) +- Turn OFF: `"0"` or `"0;"` (with response) +- Status query: `"?"` or `"?;"` (with response) +- Version query: `"V"` or `"V;"` (with response) + +#### Response formats + +- Status response: `";status;voltage"` (e.g., ";1;3.30") +- Version response: `";version"` (e.g., ";1.0.0") +- Broadcast data: button broadcast information from the device + +#### MAC address formats + +- Standard: `"XXXX-XXXX-XXXX"` (e.g., "E4B3-2386-18C4") +- Broadcast: `"FFFF-FFFF-FFFF"` (receive broadcasts from all devices) + +### Communication characteristics + +- Channel range: 1-14 (6 or 11 recommended) +- Max devices: configurable; recommended ≤ 20 +- Range: approx. 30-100 m indoors, 100-300 m outdoors +- Max payload: 250 bytes +- Retries: configurable resend interval and attempts + +## Troubleshooting + +### Common issues + +#### 1) Initialization failed + +```cpp +if (!switchC6.begin()) { + Serial.println("Initialization failed, please check:"); + Serial.println("1. WiFi is set to STA mode"); + Serial.println("2. ESP-NOW is supported"); + Serial.println("3. Sufficient memory is available"); +} +``` + +#### 2) Response timeout + +```cpp +SwitchC6ParsedData_t response; +if (!switchC6.sendStatusQueryWithResponse(deviceMac, 5000, &response)) { + Serial.println("Response timeout, possible reasons:"); + Serial.println("1. Device is offline or signal is weak"); + Serial.println("2. Wrong MAC address"); + Serial.println("3. Channel mismatch"); + Serial.println("4. Device is busy"); +} +``` + +#### 3) MAC address format error + +```cpp +String deviceMac = "E4B3-2386-18C4"; // Correct format +// String deviceMac = "E4:B3:23:86:18:C4"; // Incorrect format +``` + +#### 4) Channel configuration + +```cpp +Serial.printf("Current channel: %d\n", switchC6.getChannel()); +``` + +> Note: SwitchC6 uses broadcast packets and listens on all channels; channel setting is generally not critical. Change it only in high-interference environments. diff --git a/docs/M5SwitchC6_SoftwareFlow_CN_useAI.md b/docs/M5SwitchC6_SoftwareFlow_CN_useAI.md new file mode 100644 index 0000000..d2a0706 --- /dev/null +++ b/docs/M5SwitchC6_SoftwareFlow_CN_useAI.md @@ -0,0 +1,221 @@ +# M5SwitchC6 软件流程图(基于 src/M5Switchc6.cpp) + +更新时间:2025-08-13 + +本文基于 `src/M5Switchc6.cpp` 与 `src/M5Switchc6.h` 提炼核心流程,给出总览流程图与收发应答时序图,帮助理解 ESP-NOW 初始化、数据接收/解析、命令发送与应答等待、设备清理等关键路径。 + +提示:VS Code Markdown 预览已原生支持 Mermaid。打开本文件后,使用“在侧边打开预览”即可查看图表。 + +--- + +## 一、总览流程图 + +### 1.1 初始化流程 (begin) + +```mermaid +flowchart TB + A[begin] --> B{Platform ESP32 or ESP8266} + B -->|No| A0[Not supported return false] + B -->|Yes| C{mutexLock == NULL} + C -->|Yes| C1[Create mutex] + C1 --> D{dataQueue == nullptr} + C -->|No| D + D -->|Yes| D1[Create queue 10] + D1 --> E{parseTaskHandle == nullptr} + D -->|No| E + E -->|Yes| E1[Create task parseDataTask] + E1 --> F{peers == nullptr} + E -->|No| F + F -->|Yes| F1[Alloc init peers max_broadcast_peers] + F1 --> G{WiFi.getMode} + F -->|No| G + G -->|WIFI_OFF| A1[WiFi is off return false] + G -->|WIFI_AP| A2[WiFi is AP mode return false] + G -->|WIFI_STA or WIFI_AP_STA| H[Continue WiFi OK] + H --> I[acquireMutex] + I --> J[esp_now_init] + J -->|ESP_OK| K[Register recv cb lambda] + J -->|fail| J1[releaseMutex return false] + K --> L[Register send cb lambda] + L --> M[releaseMutex return true] +``` + +### 1.2 接收路径 (回调 -> 队列 -> 解析) + +```mermaid +flowchart TB + R0[recv_cb with esp_now_info data len] --> R1{esp_now_info and src_addr and instance} + R1 -->|Yes| R2[acquireMutex] + R1 -->|No| R0A[Log unknown MAC] + R2 --> R3[Extract MAC channel rssi from rx_ctrl] + R3 --> R4[Find or add peer in peers array] + R4 --> R5[Update lastData status lastReceiveTime] + R5 --> R6[releaseMutex] + R6 --> R7[enqueueData with senderMac] + R0A --> R7B[enqueueData without senderMac] + R7 --> Q[(Queue size 10)] + R7B --> Q + Q -->|xQueueReceive 100ms| T[parseDataTask infinite loop] + T --> T1{queuedData received} + T1 -->|Yes| P0[parseData with data len senderMac] + T1 -->|No| T2[vTaskDelay 1ms continue] + P0 --> P1[parseDataStructured] + P1 -->|isValid true| P2[handleReceivedResponse with senderMac] + P1 -->|isValid false| P3[log Parse error] + T2 --> T +``` + +### 1.3 发送路径 (构建命令 -> 发送广播) + +```mermaid +flowchart TB + S1[sendSwitchCommand / sendStatusQuery / sendVersionQuery / sendCustomMessage] --> S2[Validate channel 1-14] + S2 -->|invalid| S2A[return false] + S2 -->|valid| S3[Validate MAC format] + S3 -->|invalid| S3A[return false] + S3 -->|valid| S4[Build command string with channel] + S4 --> S5[acquireMutex] + S5 --> S6[Check broadcast peer exists] + S6 -->|not exist| S6A[Add broadcast peer FF:FF:FF:FF:FF:FF] + S6 -->|exist| S7 + S6A --> S7[esp_now_send broadcast] + S7 --> S8[releaseMutex] + S8 -->|ESP_OK| S9[Log success] + S8 -->|fail| S10[Log fail] + S7 --> S11[send_cb status log] +``` + +### 1.4 应答等待机制 (可选) + +```mermaid +flowchart TB + W0[sendSwitchCommandWithResponse] --> W1[addResponseWait deviceMac timeoutMs] + W1 -->|success| W2[Send with needResponse true] + W1 -->|fail| W1A[return false] + W2 -->|success| W3[millis startTime lastSendTime] + W2 -->|fail| W2A[removeResponseWait return false] + W3 --> W4{millis - startTime < timeoutMs} + W4 -->|Yes| W5[checkResponseTimeouts] + W5 --> W6[findResponseWait index] + W6 -->|hasResponse true| W7[Copy response removeResponseWait return true] + W6 -->|hasResponse false| W8{millis - lastSendTime >= resendInterval} + W8 -->|Yes| W9[Resend command update lastSendTime] + W8 -->|No| W10[delay 1ms] + W9 --> W4 + W10 --> W4 + W4 -->|No timeout| W11[removeResponseWait return false] + + %% 从接收路径的响应匹配 + P2[handleReceivedResponse from RX path] -->|match normalized senderMAC| W6 +``` + +### 1.5 自动清理机制 (可选) + +```mermaid +flowchart TB + GC0[setAutoCleanup intervalMs deviceTimeoutMs] --> GC1[Save autoCleanupInterval deviceTimeout lastCleanupTime] + GC2[checkAutoCleanup] --> GC3{autoCleanupInterval == 0} + GC3 -->|Yes disabled| GC5[return 0] + GC3 -->|No| GC4{currentTime - lastCleanupTime >= autoCleanupInterval} + GC4 -->|No| GC5 + GC4 -->|Yes| GC6[clearExpiredDevices deviceTimeout] + GC6 --> GC7[Remove expired devices and response waits] + GC7 --> GC8[Update lastCleanupTime return removedCount] +``` + +说明: + +- **初始化流程**:严格按照源码顺序,包含所有条件检查(mutex/queue/task/peers的nullptr检查,WiFi模式的具体枚举值检查) +- **接收路径**:体现了 ESP-NOW 回调的完整处理逻辑,包括 esp_now_info 检查、MAC提取、peers更新、队列入队、解析任务循环 +- **发送路径**:包含参数验证(信道范围、MAC格式)、命令字符串构建、广播peer确保存在等完整步骤 +- **WithResponse机制**:详细展示了超时循环、重发间隔检查、响应匹配的完整逻辑,以及各种失败情况的处理 +- **自动清理**:体现了间隔检查、过期设备移除的完整流程,包括禁用状态的处理 + +--- + +## 二、收发与应答时序图 + +```mermaid +sequenceDiagram + participant App + participant M5 as M5SwitchC6 + participant ESP as ESP_NOW + participant Q as dataQueue + participant T as parseTask + + App->>M5: sendSwitchCommandWithResponse(deviceMac, switchOn, timeoutMs) + M5->>M5: addResponseWait(deviceMac, timeoutMs) + alt addResponseWait success + M5->>M5: sendSwitchCommand(deviceMac, switchOn, needResponse=true) + alt sendSwitchCommand success + M5->>M5: normalizeMacAddress + build command string + M5->>M5: acquireMutex + M5->>ESP: esp_now_send(FF:FF:FF:FF:FF:FF, command) + ESP-->>M5: send_cb(mac, status) + M5->>M5: releaseMutex + + Note over M5: Start timeout loop with resend logic + + loop while millis() - startTime < timeoutMs + M5->>M5: checkResponseTimeouts() + M5->>M5: findResponseWait(deviceMac) + alt hasResponse == true + M5->>App: return response data (success) + else resend interval reached + M5->>ESP: esp_now_send (resend command) + else + M5->>M5: delay(1) + end + end + + M5->>App: timeout - return false + else + M5->>M5: removeResponseWait(deviceMac) + M5->>App: send failed - return false + end + else + M5->>App: addResponseWait failed - return false + end + + Note over ESP,M5: Remote device receives and replies + + ESP-->>M5: recv_cb(esp_now_info, data, len) + M5->>M5: acquireMutex + M5->>M5: extract senderMAC from esp_now_info.src_addr + M5->>M5: find/add peer in peers array + M5->>M5: update peers MAC/channel/RSSI/lastData/status/time + M5->>M5: releaseMutex + M5->>Q: enqueueData(data, len, senderMAC) + + T->>Q: xQueueReceive(dataQueue, queuedData, 100ms) + Q-->>T: data, len, senderMAC + T->>M5: parseData(data, len, senderMAC) + M5->>M5: parseDataStructured(data, len) -> parsed + alt parsed.isValid == true + M5->>M5: handleReceivedResponse(parsed, senderMacStr) + M5->>M5: findResponseWait(senderMacStr) + alt waiting device found + M5->>M5: set hasResponse = true, copy parsed data + Note over M5: This breaks the timeout loop above + end + else + M5->>M5: log parse error + end +``` + +--- + +## 参考与扩展 + +- 关键接口: + + - 初始化与通道:`begin()`, `setChannel()`, `getChannel()` + - 发送:`sendSwitchCommand()`, `sendStatusQuery()`, `sendVersionQuery()`, `sendCustomMessage()` + - 带应答:`sendSwitchCommandWithResponse()`, `sendStatusQueryWithResponse()`, `sendVersionQueryWithResponse()` + - 解析:`parseData()`, `parseDataStructured()` + - 等待/匹配:`addResponseWait()`, `findResponseWait()`, `removeResponseWait()`, `checkResponseTimeouts()`, `handleReceivedResponse()` + - 清理:`clearPeersData()`, `clearResponseWaits()`, `clearAllData()`, `clearDeviceData()`, `clearExpiredDevices()`, `setAutoCleanup()`, `checkAutoCleanup()` +- 预览方式: + + 1) 在 VS Code 中打开本文件 + 2) 右上角“在侧边打开预览”(或快捷键 Ctrl+K V) diff --git a/docs/SwitchC6-ESP-NOW-控制协议-20250722.md b/docs/SwitchC6-ESP-NOW-控制协议-20250722.md new file mode 100644 index 0000000..ba8fb48 --- /dev/null +++ b/docs/SwitchC6-ESP-NOW-控制协议-20250722.md @@ -0,0 +1,200 @@ +# SwitchC6 ESP-NOW控制协议 + +## SwitchC6开(不返回) + +* 命令 + + 主机发送 + ```C + "XXXX-XXXX-XXXX=1;ch=n" + ``` +* 例子 + + 主机发送 + ```C + "AABB-CCDD-EEFF=1;ch=6" 或 "aabb-ccdd-eeff=1;ch=6" 或 "aaBB-CCdd-eeFF=1;ch=6" + ``` +## SwitchC6开(带返回) + +* 命令 + + 主机发送 + ```C + "XXXX-XXXX-XXXX=1;ch=n;" + ``` + + SwitchC6返回 + ```C + "YYYY-YYYY-YYYY;1;z.zzV" + ``` +* 例子 + + 主机发送 + ```C + "AABB-CCDD-EEFF=1;ch=6;" 或 "aabb-ccdd-eeff=1;ch=6;" 或 "aaBB-CCdd-eeFF=1;ch=6;" + ``` + + SwitchC6返回 + ```C + "1122-AABB-CCGG;1;3.30V" 或 "1122-aabb-ccgg;1;3.30V" 或 "1122-aaBB-ccgg;1;3.30V" + ``` + +## SwitchC6关(不返回) + +* 命令 + + 主机发送 + ```C + "YYYY-YYYY-YYYY=0;ch=n" + ``` +* 例子 + + 主机发送 + ```C + "AABB-CCDD-EEFF=0;ch=6" 或 "aabb-ccdd-eeff=0;ch=6" 或 "aaBB-CCdd-eeFF=0;ch=6" + ``` + +## SwitchC6关(带返回) + +* 命令 + + 主机发送 + ```C + "XXXX-XXXX-XXXX=0;ch=n;" + ``` + + SwitchC6返回 + ```C + "YYYY-YYYY-YYYY;0;z.zzV" + ``` +* 例子 + + 主机发送 + ```C + "AABB-CCDD-EEFF=0;ch=6;" 或 "aabb-ccdd-eeff=0;ch=6;" 或 "aaBB-CCdd-eeFF=0;ch=6;" + ``` + + SwitchC6返回 + ```C + "1122-AABB-CCGG;0;3.30V" 或 "1122-aabb-ccgg;0;3.30V" 或 "1122-aaBB-ccgg;0;3.30V" + ``` + +## 读取设备状态 + +* 命令 + + 主机发送 + ```C + "XXXX-XXXX-XXXX=?;ch=n;" + ``` + + SwitchC6返回 + ```C + "YYYY-YYYY-YYYY;0;z.zzV" 或 "YYYY-YYYY-YYYY;1;z.zzV" + ``` +* 例子 + + 主机发送 + ```C + "AABB-CCDD-EEFF=?;ch=6;" 或 "aabb-ccdd-eeff=?;ch=6;" 或 "aaBB-CCdd-eeFF=?;ch=6;" + ``` + + SwitchC6返回 + ```C + "1122-AABB-CCGG;0;3.30V" 或 "1122-aabb-ccgg;0;3.30V" 或 "1122-aaBB-ccgg;0;3.30V" + "1122-AABB-CCGG;1;3.30V" 或 "1122-aabb-ccgg;1;3.30V" 或 "1122-aaBB-ccgg;1;3.30V" + ``` + +## 版本查询 + +* 命令 + + 主机发送 + + ```C + "XXXX-XXXX-XXXX=V;ch=n;" + ``` + + SwitchC6返回 + + ```C + "YYYY-YYYY-YYYY;ver" + ``` + +* 例子 + + 主机发送 + + ```C + "AABB-CCDD-EEFF=V;ch=6;" 或 "aabb-ccdd-eeff=V;ch=6;" 或 "aaBB-CCdd-eeFF=V;ch=6;" + ``` + + SwitchC6返回 + + ```C + "1122-AABB-CCGG;1.0.0" 或 "1122-aabb-ccgg;1.0.0" 或 "1122-aaBB-ccgg;1.0.0" + ``` + +## Button广播 + +* 单击一次 + + SwitchC6广播并且开关状态切换一次 + ```C + "FFFF-FFFF-FFFF;0;z.zzV" 或 "FFFF-FFFF-FFFF;1;z.zzV" + ``` + +* 长按5s + + SwitchC6广播 + ```C + "FFFF-FFFF-FFFF;0;z.zzV" 或 "FFFF-FFFF-FFFF;1;z.zzV" + ``` + +### 说明: + +* 数据发送和接收均采用字符串格式 +* "XXXX-XXXX-XXXX" 表示单火设备的 MAC 地址,使用字符形式,兼容大小写 +* "YYYY-YYYY-YYYY" 表示主机的 MAC 地址,使用字符形式,兼容大小写 +* 设备的通信采用广播方式,主机需要将广播地址(FF:FF:FF:FF:FF:FF)添加为对等节点。SwitchC6 也通过广播发送返回数据。主机可以通过 "YYYY-YYYY-YYYY" 判断接收到的数据包是否属于本设备,而 SwitchC6 则通过 "XXXX-XXXX-XXXX" 判断是否为需要自己处理的数据包 +* "ch=n" 中的 n 表示主机的 Wi-Fi 通道,取值范围为 1~14,主机发送的命令中必须填入正确的通道 +* 主机发送的命令中,MAC 地址后紧跟的第一个字符为 "=" +* SwitchC6 返回的命令中,MAC 地址后紧跟的第一个字符为 ";" +* "z.zzV" 表示电容电压 +* 返回数据中的继电器状态与电容电压使用 ";" 分隔 +* SwitchC6 是否需要回复由主机发送数据包的最后一位是否为 ";" 来判断:如果末尾是 ";",则需要返回数据 +* LED 灯状态与继电器状态绑定:继电器闭合时,LED 灯点亮;继电器断开时,LED 灯熄灭 +* 按下按钮时,设备会广播自身状态。Switch-C6 会在 1~14 通道各发送一次数据包,且数据包中携带的地址为 FFFF-FFFF-FFFF(广播地址)。 + +## ESP32-IDF使用示例 + +* esp now 发送 + + ```C + // 广播 MAC 地址(表示将数据发送给所有接收范围内的 ESP-NOW 设备) + const uint8_t broadcast_mac_addr[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + + // 目标设备的 MAC 地址(此处为 SwitchC6 的 MAC 地址) + const uint8_t switchc6_mac[6] = {0xE4, 0xB3, 0x23, 0x86, 0x18, 0xC4}; + + /** + * @brief 需要发送的控制指令列表(字符串格式) + * + * 格式:MAC=操作;ch=信道; + * - 操作 1 表示开,0 表示关,? 表示读取状态 + * - ch=6 表示当前 Wi-Fi 工作信道为 6 + * - 最后的分号(;)代表是否需要设备响应(有则需要响应) + */ + const char *send_cmd[] = { + "E4B3-2386-18C4=1;ch=6", // 开启继电器,不需要设备响应 + "E4B3-2386-18C4=1;ch=6;", // 开启继电器,需要设备响应 + "E4B3-2386-18C4=0;ch=6", // 关闭继电器,不需要设备响应 + "E4B3-2386-18C4=0;ch=6;", // 关闭继电器,需要设备响应 + "E4B3-2386-18C4=?;ch=6;", // 查询继电器状态,需要设备响应 + }; + + // 使用 ESP-NOW 发送其中一条控制指令 + // 此处发送第 0 条指令,即:开启继电器,且不等待响应 + esp_now_send(broadcast_mac_addr, (const uint8_t *)send_cmd[0], strlen(send_cmd[0])); + + ``` \ No newline at end of file diff --git a/docs/SwitchC6-ESP-NOW-控制协议-20250722.pdf b/docs/SwitchC6-ESP-NOW-控制协议-20250722.pdf new file mode 100644 index 0000000..52fa5b8 Binary files /dev/null and b/docs/SwitchC6-ESP-NOW-控制协议-20250722.pdf differ diff --git a/examples/Broadcast_Scan/Broadcast_Scan.ino b/examples/Broadcast_Scan/Broadcast_Scan.ino new file mode 100644 index 0000000..a8a030d --- /dev/null +++ b/examples/Broadcast_Scan/Broadcast_Scan.ino @@ -0,0 +1,108 @@ +// Switch-C6 ESP-NOW 广播扫描器 / Switch-C6 ESP-NOW Broadcast Scanner +// 使用 M5SwitchC6 库监听和解析广播数据 / Listen and parse broadcast frames using M5SwitchC6 +// 串口输出可见: MAC地址 / RSSI / 信道 / 状态 / 电压 / Serial output: MAC / RSSI / Channel / State / Voltage + +#include +#include +#include + +// ===== 可调参数 / Configurable parameters ===== +static const uint8_t FIXED_CHANNEL = 6; // 目标信道,需与对端一致;设为 0 则不改 / Target channel; must match the peer; set to 0 to keep unchanged +static const uint16_t MAX_DEVICES = 20; // 最大设备数量 / Maximum number of devices +// =================== + +// 创建 M5SwitchC6 实例 / Create M5SwitchC6 instance +M5SwitchC6 switchC6(MAX_DEVICES); + +// 上次显示设备列表的时间 / Timestamp of last device list print +unsigned long lastDisplayTime = 0; +const unsigned long DISPLAY_INTERVAL = 5000; // 每5秒显示一次设备列表 / Print the device list every 5 seconds + +void setup() { + Serial.begin(115200); + delay(200); + + Serial.println("========================================"); + Serial.println("Switch-C6 Broadcast Scanner Starting"); + Serial.println("========================================"); + + // 1) 启动 Wi-Fi:STA(Station,工作站)模式 / Start Wi-Fi in STA (station) mode + WiFi.mode(WIFI_STA); + WiFi.begin(); // 仅初始化协议栈,不必真的连 AP / Initialize stack only; no real AP connection + WiFi.disconnect(false, true); // 不关闭无线,仅清理历史热点 / Keep radio on; clear saved hotspots + + // 2) 初始化 M5SwitchC6 库 / Initialize M5SwitchC6 library + if (!switchC6.begin()) { + Serial.println("ERROR: M5SwitchC6 initialization failed!"); + while (true) delay(1000); + } + + // 3) 设置信道 / Set Wi-Fi channel + if (FIXED_CHANNEL >= 1 && FIXED_CHANNEL <= 14) { + if (switchC6.setChannel(FIXED_CHANNEL)) { + Serial.printf("Channel set successfully: %u\n", FIXED_CHANNEL); + } else { + Serial.printf("Channel set failed: %u\n", FIXED_CHANNEL); + } + } + + uint8_t currentChannel = switchC6.getChannel(); + Serial.printf("Current channel: %u\n", currentChannel); + + // 4) 设置自动清理过期设备(每60秒检查一次,设备超5分钟无响应则清理) / Enable auto cleanup of expired devices (check every 60 s; remove if silent > 5 min) + switchC6.setAutoCleanup(60000, 300000); + + Serial.println("ESP-NOW broadcast scanner ready"); + Serial.println("========================================"); +} + +void loop() { + // 检查是否有自动清理过期设备 / Check whether expired devices were auto-cleaned + uint16_t removedCount = switchC6.checkAutoCleanup(); + if (removedCount > 0) { + Serial.printf("Auto cleaned %u expired devices\n", removedCount); + } + + // 定期显示设备列表 / Periodically display the device list + unsigned long currentTime = millis(); + if (currentTime - lastDisplayTime >= DISPLAY_INTERVAL) { + lastDisplayTime = currentTime; + + uint16_t peerCount = switchC6.getPeerCount(); + + if (peerCount > 0) { + Serial.println("========================================"); + Serial.printf("Discovered devices count: %u\n", peerCount); + Serial.println("----------------------------------------"); + + for (uint16_t i = 0; i < peerCount; i++) { + broadcast_packet_with_mac_t peerInfo; + if (switchC6.getPeerInfo(i, &peerInfo)) { + // 转换MAC地址为字符串格式 / Convert MAC address to string + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", + peerInfo.senderMac[0], peerInfo.senderMac[1], peerInfo.senderMac[2], + peerInfo.senderMac[3], peerInfo.senderMac[4], peerInfo.senderMac[5]); + + // 计算设备静默时间 / Calculate device silent time + unsigned long silentTime = (currentTime - peerInfo.lastReceiveTime) / 1000; + + Serial.printf("Device %d:\n", i + 1); + Serial.printf(" MAC: %s\n", macStr); + Serial.printf(" Channel: %u\n", peerInfo.channel); + Serial.printf(" RSSI: %d dBm\n", peerInfo.rssi); + Serial.printf(" Status: %s\n", peerInfo.status ? "ON" : "OFF"); + Serial.printf(" Last data: %s\n", peerInfo.lastData.c_str()); + Serial.printf(" Silent time: %lu seconds\n", silentTime); + Serial.println(); + } + } + Serial.println("========================================"); + } else { + Serial.println("No devices discovered yet..."); + } + } + + // 主循环延迟 / Main loop delay + delay(100); +} \ No newline at end of file diff --git a/examples/Controller/Controller.ino b/examples/Controller/Controller.ino new file mode 100644 index 0000000..7fa588c --- /dev/null +++ b/examples/Controller/Controller.ino @@ -0,0 +1,319 @@ +#include +#include +#include + +// 创建 M5SwitchC6 实例,最大节点数量为16,可自定义大小 / Create M5SwitchC6 instance with max peers set to 16, customizable size +M5SwitchC6 switchC6(16); +//填入你的SwitchC6设备MAC地址 / Fill in your SwitchC6 device MAC address +const String DEVICE_MAC = "E4B3-2386-18C4"; + +//是否启用wifi连接 / Enable Wi-Fi connection +// #define USE_WIFI +#ifdef USE_WIFI +const char *ssid = "your_ssid"; +const char *password = "your_password"; +#endif + +// 等待动画控制变量 / Control flag for waiting animation +volatile bool showAnimation = false; +TaskHandle_t animationTaskHandle = NULL; +SemaphoreHandle_t displayMutex = NULL; + +// 等待动画任务 / Waiting animation task +void animationTask(void *parameter) { + while (true) { + if (showAnimation) { + // 获取互斥锁 / Acquire mutex + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + int centerX = M5.Display.width() / 2; + int centerY = M5.Display.height() / 2; + static int dots = 1; + + // 清除点号区域 - 只清除动画区域 / Clear dots area (only the animation region) + M5.Display.fillRect(centerX - 30, centerY, 60, 20, BLACK); + + // 显示当前数量的点号 / Draw current number of dots + M5.Display.setCursor(centerX - dots * 6, centerY); + M5.Display.setTextColor(WHITE); + for (int i = 0; i < dots; i++) { + M5.Display.print("."); + } + + // 释放互斥锁 / Release mutex + xSemaphoreGive(displayMutex); + + dots++; + if (dots > 5) { + dots = 1; + } + } + + vTaskDelay(200 / portTICK_PERIOD_MS); // 200ms延迟 / 200 ms delay + } else { + // 清除动画时也需要获取锁 / Also need the lock when clearing animation + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(50)) == pdTRUE) { + int centerX = M5.Display.width() / 2; + int centerY = M5.Display.height() / 2 + 20; + + // 清除动画区域 / Clear animation region + M5.Display.fillRect(centerX - 30, centerY - 10, 60, 30, BLACK); + + + xSemaphoreGive(displayMutex); + } + vTaskDelay(100 / portTICK_PERIOD_MS); + } + } +} + +// 开始等待动画 / Start waiting animation +void startWaitingAnimation() { + showAnimation = true; +} + +// 停止等待动画 / Stop waiting animation +void stopWaitingAnimation() { + showAnimation = false; +} + +void setup() { + Serial.begin(115200); + M5.begin(); + // M5.Display.setRotation(1); + M5.Display.setTextSize(2); + + WiFi.mode(WIFI_STA); + +#ifdef USE_WIFI + // 连接到Wi-Fi网络 / Connect to Wi-Fi network + WiFi.begin(ssid, password); + if (WiFi.status() != WL_CONNECTED) { + Serial.print("Connecting to Wi-Fi"); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.println("\nConnected to Wi-Fi!"); + // 串口打印连接信息 / Print connection info to serial + Serial.printf("SSID: %s\r\n", WiFi.SSID().c_str()); + Serial.printf("IP Address: %s\r\n", WiFi.localIP().toString().c_str()); + Serial.printf("MAC Address: %s\r\n", WiFi.macAddress().c_str()); + Serial.printf("Channel: %d\r\n", WiFi.channel()); + + } else { + Serial.println("Already connected to Wi-Fi."); + } +#else + WiFi.begin(); + switchC6.setChannel(11); // 设置信道为11 / Set channel to 11 +#endif + + M5.Display.clear(); + + // 创建互斥锁 / Create mutex + displayMutex = xSemaphoreCreateMutex(); + + // 创建canvas,使用正确的屏幕尺寸 / Prepare display with proper size + M5.Display.fillScreen(BLACK); + M5.Display.setTextSize(2); + M5.Display.setTextColor(WHITE); + + M5.Display.setCursor(10, 10); + M5.Display.println("M5Switchc6 Controller"); + + if (switchC6.begin()) { + M5.Display.setCursor(10, 60); + M5.Display.println("A:ON B:OFF C:STATUS"); + } + + + // 创建等待动画任务 / Create waiting animation task + xTaskCreate( + animationTask, // 任务函数 / Task function + "AnimationTask", // 任务名称 / Task name + 4096, // 增加堆栈大小 / Stack size + NULL, // 参数 / Parameter + 1, // 优先级 / Priority + &animationTaskHandle // 任务句柄 / Task handle + ); +} + +void loop() { + M5.update(); + + // 先定义按钮变量,在互斥锁外部 / Define button layout variables outside the mutex + int screenWidth = M5.Display.width(); + int screenHeight = M5.Display.height(); + int buttonWidth = screenWidth / 3; + int buttonHeight = 40; + int buttonY = screenHeight - buttonHeight; + int button3Width = screenWidth - buttonWidth * 2; // 剩余宽度 / Remaining width + + // 获取互斥锁后再操作canvas / Lock before drawing on the display + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + // 绘制3个按钮 / Draw 3 buttons + // 按钮A - ON / Button A - ON + M5.Display.fillRect(0, buttonY, buttonWidth, buttonHeight, GREEN); + M5.Display.drawRect(0, buttonY, buttonWidth, buttonHeight, WHITE); + M5.Display.setTextColor(BLACK); + M5.Display.setCursor(buttonWidth/2 - 10, buttonY + 15); + M5.Display.println("ON"); + + // 按钮B - OFF / Button B - OFF + M5.Display.fillRect(buttonWidth, buttonY, buttonWidth, buttonHeight, RED); + M5.Display.drawRect(buttonWidth, buttonY, buttonWidth, buttonHeight, WHITE); + M5.Display.setTextColor(WHITE); + M5.Display.setCursor(buttonWidth + buttonWidth/2 - 15, buttonY + 15); + M5.Display.println("OFF"); + + // 按钮C - STATUS(使用计算后的宽度避免溢出) / Button C - STATUS (use computed width to avoid overflow) + M5.Display.fillRect(buttonWidth * 2, buttonY, button3Width, buttonHeight, BLUE); + M5.Display.drawRect(buttonWidth * 2, buttonY, button3Width, buttonHeight, WHITE); + M5.Display.setTextColor(WHITE); + M5.Display.setCursor(buttonWidth * 2 + button3Width/2 - 25, buttonY + 15); + M5.Display.println("STATUS"); + + M5.Display.setTextColor(WHITE); // 恢复默认文本颜色 / Restore default text color + + + + // 释放互斥锁 / Release mutex + xSemaphoreGive(displayMutex); + } + bool deviceOn = false; + bool deviceOff = false; + bool deviceStatus = false; + + // 检测物理按键(适用于有按键的设备) / Handle physical buttons (if available) + if (M5.BtnA.wasPressed()) { + deviceOn = true; + } + if (M5.BtnB.wasPressed()) { + deviceOff = true; + } + if (M5.BtnC.wasPressed()) { + deviceStatus = true; + } + + // 检测触摸事件(适用于有触摸屏的设备) / Handle touch events (if touch screen) + auto touch = M5.Touch.getDetail(); + if (touch.wasPressed()) { + int touchX = touch.x; + int touchY = touch.y; + + // 检查是否点击了按钮区域 / Check whether a button area is tapped + if (touchY >= buttonY) { + if (touchX < buttonWidth) { + deviceOn = true; + } else if (touchX < buttonWidth * 2) { + deviceOff = true; + } else { + deviceStatus = true; + } + } + } + + // 执行相应的设备控制逻辑 / Execute device control logic + if (deviceOn) { + // 开始等待动画 / Start waiting animation + startWaitingAnimation(); + + // 开启设备 / Turn device ON + if (switchC6.sendSwitchCommandWithResponse(DEVICE_MAC, true, 10000)) { + Serial.println("开启命令发送成功"); // ON command sent successfully + stopWaitingAnimation(); + + // 获取互斥锁后更新状态显示 / Update status display with mutex + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + M5.Display.fillRect(10, 160, 300, 30, BLACK); + M5.Display.setCursor(10, 160); + M5.Display.setTextColor(WHITE); + M5.Display.println("Device ON sent"); + + xSemaphoreGive(displayMutex); + } + } + else { + Serial.println("开启命令发送失败"); // Failed to send ON command + stopWaitingAnimation(); + + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + M5.Display.fillRect(10, 160, 300, 30, BLACK); + M5.Display.setCursor(10, 160); + M5.Display.setTextColor(WHITE); + M5.Display.println("Device ON failed"); + + xSemaphoreGive(displayMutex); + } + } + } + + if (deviceOff) { + // 开始等待动画 / Start waiting animation + startWaitingAnimation(); + + // 关闭设备 / Turn device OFF + if (switchC6.sendSwitchCommandWithResponse(DEVICE_MAC, false, 10000)) { + Serial.println("关闭命令发送成功"); // OFF command sent successfully + stopWaitingAnimation(); + + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + M5.Display.fillRect(10, 160, 300, 30, BLACK); + M5.Display.setCursor(10, 160); + M5.Display.setTextColor(WHITE); + M5.Display.println("Device OFF sent"); + + xSemaphoreGive(displayMutex); + } + } + else { + Serial.println("关闭命令发送失败"); // Failed to send OFF command + stopWaitingAnimation(); + + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + M5.Display.fillRect(10, 160, 300, 30, BLACK); + M5.Display.setCursor(10, 160); + M5.Display.setTextColor(WHITE); + M5.Display.println("Device OFF failed"); + + xSemaphoreGive(displayMutex); + } + } + } + + if (deviceStatus) { + // 开始等待动画 / Start waiting animation + startWaitingAnimation(); + + // 查询状态 / Query status + SwitchC6ParsedData_t response; + if (switchC6.sendStatusQueryWithResponse(DEVICE_MAC, 10000, &response)) { + Serial.printf("设备状态: %s, 电压: %.2fV\n", + response.switchState ? "开启" : "关闭", response.voltage); // Device status and voltage + stopWaitingAnimation(); + + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + M5.Display.fillRect(10, 160, 300, 30, BLACK); + M5.Display.setCursor(10, 160); + M5.Display.setTextColor(WHITE); + M5.Display.printf("S:%s V:%.2fV", + response.switchState ? "ON" : "OFF", response.voltage); + + xSemaphoreGive(displayMutex); + } + } else { + stopWaitingAnimation(); + + if (xSemaphoreTake(displayMutex, pdMS_TO_TICKS(100)) == pdTRUE) { + M5.Display.fillRect(10, 160, 300, 30, BLACK); + M5.Display.setCursor(10, 160); + M5.Display.setTextColor(WHITE); + M5.Display.println("Query failed"); + + xSemaphoreGive(displayMutex); + } + } + } + + delay(20); // 减少循环频率,降低系统负载 / Slow down the loop to reduce load +} \ No newline at end of file diff --git a/library.json b/library.json new file mode 100644 index 0000000..2705f34 --- /dev/null +++ b/library.json @@ -0,0 +1,24 @@ +{ + "name": "M5SwitchC6-ESP-NOW", + "description": "M5Stack Library of M5SwitchC6", + "keywords": "M5Stack,M5SwitchC6", + "authors": { + "name": "M5Stack", + "url": "http://www.m5stack.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/m5stack/M5SwitchC6-ESP-NOW.git" + }, + "version": "1.0.0", + "frameworks": [ + "arduino" + ], + "platforms": [ + "espressif32", + "native" + ], + "headers": "M5Switchc6.h", + "dependencies": { + } +} \ No newline at end of file diff --git a/library.properties b/library.properties new file mode 100644 index 0000000..fc71535 --- /dev/null +++ b/library.properties @@ -0,0 +1,10 @@ +name=M5SwitchC6-ESP-NOW +version=1.1.2 +author=M5Stack +maintainer=M5Stack +sentence=M5Stack Library of M5SwitchC6 +paragraph=M5Stack,M5SwitchC6, See more on http://M5Stack.com +category=Device Control +url=https://github.com/m5stack/M5SwitchC6-ESP-NOW.git +architectures=esp32 +includes=M5Switchc6.h \ No newline at end of file diff --git a/src/M5Switchc6.cpp b/src/M5Switchc6.cpp new file mode 100644 index 0000000..6a3bb47 --- /dev/null +++ b/src/M5Switchc6.cpp @@ -0,0 +1,1463 @@ +/* + * SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + * SPDX-License-Identifier: MIT + */ + +#include "M5Switchc6.h" + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) +// 静态成员变量定义 / Static member variable definitions +SemaphoreHandle_t M5SwitchC6::mutexLock = nullptr; +M5SwitchC6* M5SwitchC6::instance = nullptr; + +// 队列相关静态变量 / Queue-related static variables +QueueHandle_t M5SwitchC6::dataQueue = nullptr; +TaskHandle_t M5SwitchC6::parseTaskHandle = nullptr; +#endif + +void M5SwitchC6::acquireMutex() +{ + + if (mutexLock == nullptr) { + mutexLock = xSemaphoreCreateMutex(); + } + if (mutexLock != nullptr) { + xSemaphoreTake(mutexLock, portMAX_DELAY); + } +} + +void M5SwitchC6::releaseMutex() +{ + if (mutexLock != nullptr) { + xSemaphoreGive(mutexLock); + } +} + +// 构造函数,只做基本的成员变量初始化 / Constructor, only performs basic member variable initialization +M5SwitchC6::M5SwitchC6(uint16_t max_broadcast_peers) : max_broadcast_peers(max_broadcast_peers) { + // 在构造函数中不进行需要串口输出的操作 / Don't perform operations requiring serial output in constructor + // 所有初始化逻辑移到 begin() 方法中 / Move all initialization logic to begin() method + peers = nullptr; + peerCount = 0; + responseWaitCount = 0; + + // 初始化自动清理相关变量 / Initialize auto cleanup related variables + autoCleanupInterval = 0; // 默认禁用自动清理 / Default disable auto cleanup + deviceTimeout = 300000; // 默认5分钟超时 / Default 5 minute timeout + lastCleanupTime = 0; + + // 初始化响应等待数组 / Initialize response wait array + for (uint16_t i = 0; i < MAX_RESPONSE_WAITS; i++) { + responseWaits[i].isWaiting = false; + responseWaits[i].hasResponse = false; + responseWaits[i].deviceMac = ""; + responseWaits[i].requestTime = 0; + responseWaits[i].timeoutMs = 0; + } + + instance = this; // 设置静态实例指针 / Set static instance pointer +} + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) +// 数据解析任务 / Data parsing task +void M5SwitchC6::parseDataTask(void* parameter) { + QueuedData_t queuedData; + + while (true) { + // 从队列中接收数据,等待最多100ms / Receive data from queue, wait up to 100ms + if (xQueueReceive(dataQueue, &queuedData, pdMS_TO_TICKS(100)) == pdTRUE) { + if (instance != nullptr) { + // 在任务上下文中安全地处理数据(带发送方MAC地址) / Safely process data in task context (with sender MAC address) + instance->parseData(queuedData.data, queuedData.len, queuedData.senderMac); + } + + // 释放动态分配的内存 / Release dynamically allocated memory + if (queuedData.data != nullptr) { + delete[] queuedData.data; + } + } + + // 让其他任务有机会运行 / Give other tasks a chance to run + vTaskDelay(pdMS_TO_TICKS(1)); + } +} + +// 将数据加入队列的安全函数 / Safe function to enqueue data +bool M5SwitchC6::enqueueData(const uint8_t* data, int len) { + if (dataQueue == nullptr || data == nullptr || len <= 0) { + return false; + } + + // 创建队列数据结构 / Create queue data structure + QueuedData_t queuedData; + queuedData.len = len; + queuedData.data = new uint8_t[len]; + memset(queuedData.senderMac, 0, 6); // 默认清零 / Default to zero + + if (queuedData.data == nullptr) { + return false; // 内存分配失败 / Memory allocation failed + } + + // 复制数据 / Copy data + memcpy(queuedData.data, data, len); + + // 尝试发送到队列(非阻塞) / Try to send to queue (non-blocking) + if (xQueueSend(dataQueue, &queuedData, 0) != pdTRUE) { + // 队列满了,释放内存 / Queue is full, release memory + delete[] queuedData.data; + return false; + } + + return true; +} + +// 将数据加入队列的安全函数(带发送方MAC地址) / Safe function to enqueue data (with sender MAC address) +bool M5SwitchC6::enqueueData(const uint8_t* data, int len, const uint8_t* senderMac) { + if (dataQueue == nullptr || data == nullptr || len <= 0) { + return false; + } + + // 创建队列数据结构 / Create queue data structure + QueuedData_t queuedData; + queuedData.len = len; + queuedData.data = new uint8_t[len]; + + if (queuedData.data == nullptr) { + return false; // 内存分配失败 / Memory allocation failed + } + + // 复制数据和发送方MAC地址 / Copy data and sender MAC address + memcpy(queuedData.data, data, len); + if (senderMac != nullptr) { + memcpy(queuedData.senderMac, senderMac, 6); + } else { + memset(queuedData.senderMac, 0, 6); + } + + // 尝试发送到队列(非阻塞) / Try to send to queue (non-blocking) + if (xQueueSend(dataQueue, &queuedData, 0) != pdTRUE) { + // 队列满了,释放内存 / Queue is full, release memory + delete[] queuedData.data; + return false; + } + + return true; +} +#endif + +// 析构函数,ESP 平台上删除互斥锁 / Destructor, delete mutex lock on ESP platform +M5SwitchC6::~M5SwitchC6() { + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + // 停止解析任务 / Stop parsing task + if (parseTaskHandle != nullptr) { + vTaskDelete(parseTaskHandle); + parseTaskHandle = nullptr; + } + + // 删除队列 / Delete queue + if (dataQueue != nullptr) { + vQueueDelete(dataQueue); + dataQueue = nullptr; + } +#endif + + // 释放动态分配的内存 / Release dynamically allocated memory + if (peers != nullptr) { + delete[] peers; + peers = nullptr; + } + + // 清理静态实例指针 / Clear static instance pointer + if (instance == this) { + instance = nullptr; + } + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + if (mutexLock != NULL) { + vSemaphoreDelete(mutexLock); + mutexLock = NULL; + } +#endif +} + +// 初始化模块 / Initialize module +bool M5SwitchC6::begin() { + //检查当前是不是 ESP32 或 ESP8266 平台 / Check if current platform is ESP32 or ESP8266 +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) + serialPrintln("M5SwitchC6 is only supported on ESP32 or ESP8266 platforms, please check your platform."); + return false; +#endif + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + // 创建互斥锁 / Create mutex lock + if (mutexLock == NULL) { + mutexLock = xSemaphoreCreateMutex(); + if (mutexLock == NULL) { + serialPrintln("Failed to create mutex lock."); + return false; + } else { + serialPrintln("Mutex lock created successfully."); + } + } + + // 创建数据队列(队列大小为10) / Create data queue (queue size is 10) + if (dataQueue == nullptr) { + dataQueue = xQueueCreate(10, sizeof(QueuedData_t)); + if (dataQueue == nullptr) { + serialPrintln("Failed to create data queue."); + return false; + } else { + serialPrintln("Data queue created successfully."); + } + } + + // 创建数据解析任务 / Create data parsing task + if (parseTaskHandle == nullptr) { + BaseType_t result = xTaskCreate( + parseDataTask, // 任务函数 / Task function + "ParseDataTask", // 任务名称 / Task name + 4096, // 栈大小(字节) / Stack size (bytes) + nullptr, // 任务参数 / Task parameter + 1, // 任务优先级 / Task priority + &parseTaskHandle // 任务句柄 / Task handle + ); + + if (result != pdPASS) { + serialPrintln("Failed to create parse data task."); + return false; + } else { + serialPrintln("Parse data task created successfully."); + } + } + + // 分配内存给peers数组 / Allocate memory for peers array + if (peers == nullptr) { + peers = new broadcast_packet_with_mac_t[max_broadcast_peers]; + if (peers == nullptr) { + serialPrintln("Failed to allocate memory for broadcast peers."); + return false; + } + // 初始化peers数组 / Initialize peers array + for (uint16_t i = 0; i < max_broadcast_peers; i++) { + memset(peers[i].senderMac, 0, sizeof(peers[i].senderMac)); + peers[i].lastData = ""; + peers[i].channel = 0; + peers[i].rssi = 0; + peers[i].lastReceiveTime = 0; + peers[i].status = false; + } + serialPrintln("M5SwitchC6 initialized with max_broadcast_peers: " + String(max_broadcast_peers)); + } + + // 检查当前esp的wifi是否为 STA 模式 / Check if current ESP WiFi is in STA mode + if (WiFi.getMode() == WIFI_OFF) { + serialPrintln("WiFi is off. Please set the WiFi mode to STA or AP_STA and call WiFi.begin() before using the M5SwitchC6."); + return false; + } + else if(WiFi.getMode() == WIFI_AP) { + serialPrintln("WiFi is in AP mode. Please set the WiFi mode to STA or AP_STA and call WiFi.begin() before using the M5SwitchC6."); + return false; + } + else if(WiFi.getMode() == WIFI_STA || WiFi.getMode() == WIFI_AP_STA) { + serialPrintln("WiFi is in STA or AP_STA mode. M5SwitchC6 begin now."); + } else { + serialPrintln("Unknown WiFi mode. Please set the WiFi mode to STA or AP_STA and call WiFi.begin() before using the M5SwitchC6."); + return false; + } + + // 获取互斥锁 / Acquire mutex lock + acquireMutex(); + + // 初始化esp_now / Initialize esp_now + if (esp_now_init() != ESP_OK) { + serialPrintln("Failed to initialize ESP-NOW."); + releaseMutex(); + return false; + } + serialPrintln("ESP-NOW initialized successfully."); + + // 注册接收回调函数 / Register receive callback function + esp_now_register_recv_cb([](const esp_now_recv_info_t *esp_now_info, const uint8_t *data, int len) { + if (esp_now_info && esp_now_info->src_addr && instance) { + // 获取互斥锁,保护 peers 操作 / Acquire mutex lock to protect peers operations + instance->acquireMutex(); + + const uint8_t *mac = esp_now_info->src_addr; + char* tempStr = new char[len + 1]; + memcpy(tempStr, data, len); + tempStr[len] = '\0'; + String dataStr = String(tempStr); + delete[] tempStr; + int channel = 0; + int rssi = 0; + + // 获取信道和信号强度信息 / Get channel and signal strength information + if (esp_now_info->rx_ctrl) { + channel = esp_now_info->rx_ctrl->channel; + rssi = esp_now_info->rx_ctrl->rssi; + } + + int idx = -1; + + // 查找已有节点 / Find existing node + for (int i = 0; i < instance->peerCount; ++i) { + bool same = true; + for (int j = 0; j < 6; ++j) { + if (instance->peers[i].senderMac[j] != mac[j]) { + same = false; + break; + } + } + if (same) { + idx = i; + break; + } + } + + // 新节点添加 / Add new node + if (idx < 0 && instance->peerCount < instance->max_broadcast_peers) { + idx = instance->peerCount; + memcpy(instance->peers[idx].senderMac, mac, 6); + instance->peerCount++; + } + + // 更新数据 / Update data + if (idx >= 0) { + instance->peers[idx].lastData = dataStr; + instance->peers[idx].channel = channel; + instance->peers[idx].rssi = rssi; + instance->peers[idx].lastReceiveTime = millis(); + //MAC;status;voltage (例如: 1122-AABB-CCGG;1;3.30V) / Example: 1122-AABB-CCGG;1;3.30V + if (dataStr.indexOf(';') != -1) { + // 解析按键状态 / Parse button status + int statusIndex = dataStr.indexOf(';'); + if (statusIndex != -1) { + String statusStr = dataStr.substring(statusIndex + 1); + instance->peers[idx].status = (statusStr.startsWith("1") || statusStr.startsWith("true")); + } + } else { + instance->peers[idx].status = false; // 默认状态为关闭 / Default status is off + } + } + + // 释放互斥锁 / Release mutex lock + instance->releaseMutex(); + + // 构建完整的日志消息,使用字符数组避免String拼接的问题 / Build complete log message using character array to avoid String concatenation issues + char logBuffer[512] = {0}; + char macStr[18] = {0}; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + + snprintf(logBuffer, sizeof(logBuffer), + "ESPNOW data received [MAC:%s]<%d> Channel:%d RSSI:%ddBm Data: %s", + macStr, idx, channel, rssi, dataStr.c_str()); + serialPrintf("%s\r\n", logBuffer); + + // 将数据加入队列而不是直接调用parseData(传递发送方MAC地址) / Add data to queue instead of directly calling parseData (pass sender MAC address) + if (!instance->enqueueData(data, len, mac)) { + serialPrintf("Warning: Failed to enqueue data for parsing\r\n"); + } + + } else { + char logBuffer[512] = {0}; + snprintf(logBuffer, sizeof(logBuffer), "ESPNOW data received [MAC:UNKNOWN]: %s", + (data && len > 0) ? (const char *)data : "NULL"); + serialPrintf("%s\r\n", logBuffer); + + // 即使MAC未知,也尝试解析数据(无发送方MAC) / Even if MAC is unknown, try to parse data (without sender MAC) + if (instance != nullptr && data != nullptr && len > 0) { + if (!instance->enqueueData(data, len)) { + serialPrintf("Warning: Failed to enqueue unknown MAC data for parsing\r\n"); + } + } + } + }); + + // 注册发送回调函数 / Register send callback function + // 兼容新旧ESP-IDF版本 / Compatible with old and new ESP-IDF versions +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) + // 新版本ESP-IDF (5.5.0+) 使用wifi_tx_info_t / New ESP-IDF version (5.5.0+) uses wifi_tx_info_t + esp_now_register_send_cb([](const wifi_tx_info_t *tx_info, esp_now_send_status_t status) { + char logBuffer[128]; + char macStr[18]; + const uint8_t *mac = tx_info->des_addr; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + snprintf(logBuffer, sizeof(logBuffer), "ESPNOW send status: [MAC:%s]: %d", macStr, status); + serialPrintf("%s\r\n", logBuffer); + }); +#else + // 旧版本ESP-IDF (<5.5.0) 使用uint8_t* / Old ESP-IDF version (<5.5.0) uses uint8_t* + esp_now_register_send_cb([](const uint8_t *mac, esp_now_send_status_t status) { + char logBuffer[128]; + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + snprintf(logBuffer, sizeof(logBuffer), "ESPNOW send status: [MAC:%s]: %d", macStr, status); + serialPrintf("%s\r\n", logBuffer); + }); +#endif + + // 释放互斥锁 / Release mutex lock + releaseMutex(); + +#endif + return true; +} + +// 设置WiFi信道 / Set WiFi channel +bool M5SwitchC6::setChannel(uint8_t channel) { + // 检查信道范围 / Check channel range + if (channel < 1 || channel > 14) { + serialPrintln("Invalid channel. Channel must be between 1 and 14."); + return false; + } + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + // 获取互斥锁 / Acquire mutex lock + acquireMutex(); + + // 设置WiFi信道 / Set WiFi channel + if (esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE) == ESP_OK) { + serialPrintln("WiFi channel set to: " + String(channel)); + releaseMutex(); + return true; + } else { + serialPrintln("Failed to set WiFi channel to: " + String(channel)); + releaseMutex(); + return false; + } +#else + serialPrintln("setChannel is only supported on ESP32 or ESP8266 platforms."); + return false; +#endif +} + +// 获取当前WiFi信道 / Get current WiFi channel +uint8_t M5SwitchC6::getChannel() { +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + uint8_t primary_channel = 0; + wifi_second_chan_t second_channel = WIFI_SECOND_CHAN_NONE; + + if (esp_wifi_get_channel(&primary_channel, &second_channel) == ESP_OK) { + String logMsg = "Current WiFi channel: " + String(primary_channel); + serialPrintf("%s\r\n", logMsg.c_str()); + return primary_channel; + } else { + String logMsg = "Failed to get current WiFi channel."; + serialPrintf("%s\r\n", logMsg.c_str()); + return 0; + } +#else + serialPrintln("getChannel is only supported on ESP32 or ESP8266 platforms."); + return 0; +#endif +} + +// 获取节点数量 / Get peer count +uint16_t M5SwitchC6::getPeerCount() { + return peerCount; +} + +// 获取节点信息 / Get peer information +bool M5SwitchC6::getPeerInfo(uint16_t index, broadcast_packet_with_mac_t* peerInfo) { + if (index >= peerCount || peerInfo == nullptr || peers == nullptr) { + return false; + } + + // 复制节点信息 / Copy peer information + memcpy(peerInfo->senderMac, peers[index].senderMac, 6); + peerInfo->lastData = peers[index].lastData; + peerInfo->channel = peers[index].channel; + peerInfo->rssi = peers[index].rssi; + peerInfo->lastReceiveTime = peers[index].lastReceiveTime; + peerInfo->status = peers[index].status; + + + return true; +} + +// 数据解析(带发送方MAC地址) / Data parsing (with sender MAC address) +void M5SwitchC6::parseData(const uint8_t *data, int len, const uint8_t *senderMac) { + // 调用结构化解析函数 / Call structured parsing function + SwitchC6ParsedData_t parsed = parseDataStructured(data, len); + + // 转换发送方MAC为字符串格式 / Convert sender MAC to string format + String senderMacStr = ""; + if (senderMac != nullptr) { + char macStr[18]; + snprintf(macStr, sizeof(macStr), "%02X%02X-%02X%02X-%02X%02X", + senderMac[0], senderMac[1], senderMac[2], + senderMac[3], senderMac[4], senderMac[5]); + senderMacStr = String(macStr); + } + + // 打印解析结果 / Print parsing results + if (parsed.isValid) { + char logBuffer[512]; + switch (parsed.commandType) { + case COMMAND_STATUS_RETURN: + snprintf(logBuffer, sizeof(logBuffer), + "Device Status Response: DataMAC=%s, SenderMAC=%s, State=%s, Voltage=%.2fV", + parsed.macAddress.c_str(), senderMacStr.c_str(), + parsed.switchState ? "ON" : "OFF", parsed.voltage); + break; + case COMMAND_BROADCAST: + snprintf(logBuffer, sizeof(logBuffer), + "Device Button Broadcast: DataMAC=%s, SenderMAC=%s, State=%s, Voltage=%.2fV", + parsed.macAddress.c_str(), senderMacStr.c_str(), + parsed.switchState ? "ON" : "OFF", parsed.voltage); + break; + case COMMAND_VERSION_RETURN: + snprintf(logBuffer, sizeof(logBuffer), + "Device Version Response: DataMAC=%s, SenderMAC=%s, Version=%s", + parsed.macAddress.c_str(), senderMacStr.c_str(), parsed.version.c_str()); + break; + default: + snprintf(logBuffer, sizeof(logBuffer), "Unknown Device Response Type"); + break; + } + serialPrintf("Parsed: %s\r\n", logBuffer); + + // 检查是否有等待这个响应的请求(使用发送方MAC) / Check if there's a request waiting for this response (using sender MAC) + handleReceivedResponse(parsed, senderMacStr); + } else { + serialPrintf("Parse Error: %s\r\n", parsed.errorMessage.c_str()); + } +} + +// 结构化数据解析函数 / Structured data parsing function +SwitchC6ParsedData_t M5SwitchC6::parseDataStructured(const uint8_t *data, int len) { + SwitchC6ParsedData_t result; + result.isValid = false; + result.commandType = COMMAND_UNKNOWN; + result.switchState = false; + result.voltage = 0.0f; + result.version = ""; + + // 检查数据有效性 / Check data validity + if (data == nullptr || len <= 0) { + result.errorMessage = "Invalid data or length"; + return result; + } + + // 确保字符串以null结尾 / Ensure string is null-terminated + char* dataStr = new char[len + 1]; + memcpy(dataStr, data, len); + dataStr[len] = '\0'; + + String receivedData = String(dataStr); + delete[] dataStr; + + receivedData.trim(); // 去除首尾空白 / Remove leading and trailing whitespace + + if (receivedData.length() == 0) { + result.errorMessage = "Empty data"; + return result; + } + + // 作为主机,我们主要接收从机的状态返回、广播数据和版本查询响应 / As master, we mainly receive slave status returns, broadcast data and version query responses + // 从机数据格式: / Slave data format: + // 状态/广播: MAC;status;voltage (MAC为从机地址或FFFF-FFFF-FFFF广播地址) / Status/broadcast: MAC;status;voltage (MAC is slave address or FFFF-FFFF-FFFF broadcast address) + // 版本响应: MAC;version (例如: 1122-AABB-CCGG;1.0.0) / Version response: MAC;version (e.g.: 1122-AABB-CCGG;1.0.0) + + // 检查数据是否包含分号(从机返回数据的特征) / Check if data contains semicolon (characteristic of slave return data) + if (receivedData.indexOf(';') > 0) { + // 解析从机返回数据格式 / Parse slave return data format + int firstSemicolon = receivedData.indexOf(';'); + String macPart = receivedData.substring(0, firstSemicolon); + String remainingPart = receivedData.substring(firstSemicolon + 1); + + if (!isValidMacFormat(macPart)) { + result.errorMessage = "Invalid MAC address format in device response"; + return result; + } + + result.macAddress = normalizeMacAddress(macPart); + + // 检查第二个分号的存在来判断数据类型 / Check for second semicolon to determine data type + int secondSemicolon = remainingPart.indexOf(';'); + if (secondSemicolon > 0) { + // 有第二个分号,说明是状态数据格式: MAC;status;voltage / Has second semicolon, indicating status data format: MAC;status;voltage + String statusStr = remainingPart.substring(0, secondSemicolon); + String voltageStr = remainingPart.substring(secondSemicolon + 1); + + // 检查是否为广播地址(按钮按下时的广播) / Check if broadcast address (broadcast when button is pressed) + if (isBroadcastAddress(result.macAddress)) { + result.commandType = COMMAND_BROADCAST; + } else { + result.commandType = COMMAND_STATUS_RETURN; + } + + // 解析状态 / Parse status + if (statusStr == "1") { + result.switchState = true; + } else if (statusStr == "0") { + result.switchState = false; + } else { + result.errorMessage = "Invalid status value: " + statusStr; + return result; + } + + // 解析电压 / Parse voltage + result.voltage = parseVoltage(voltageStr); + if (result.voltage < 0) { + result.errorMessage = "Invalid voltage format: " + voltageStr; + return result; + } + + result.isValid = true; + } else { + // 没有第二个分号,说明是版本数据格式: MAC;version / No second semicolon, indicating version data format: MAC;version + result.commandType = COMMAND_VERSION_RETURN; + result.version = remainingPart; + + // 简单验证版本格式(至少应该包含数字和点) / Simple version format validation (should at least contain numbers and dots) + if (result.version.length() == 0) { + result.errorMessage = "Empty version string"; + return result; + } + + result.isValid = true; + } + } else { + result.errorMessage = "Unrecognized device response format - expecting MAC;status;voltage or MAC;version"; + return result; + } + + return result; +} + +// 检查是否为广播地址 / Check if broadcast address +bool M5SwitchC6::isBroadcastAddress(const String& macStr) { + // 统一成规范化的连字符大写格式后再比较 / Normalize to standardized hyphen uppercase format before comparison + String norm = normalizeMacAddress(macStr); + return norm == "FFFF-FFFF-FFFF"; +} + +// 检查MAC地址格式是否有效 / Check if MAC address format is valid +bool M5SwitchC6::isValidMacFormat(const String& macStr) { + // 支持两种格式: / Support two formats: + // 1) 连字符格式:"XXXX-XXXX-XXXX"(长度14,4和9位是'-') / 1) Hyphen format: "XXXX-XXXX-XXXX" (length 14, positions 4 and 9 are '-') + // 2) 冒号格式: "XX:XX:XX:XX:XX:XX"(长度17,2/5/8/11/14位是':') / 2) Colon format: "XX:XX:XX:XX:XX:XX" (length 17, positions 2/5/8/11/14 are ':') + int len = macStr.length(); + + auto isHex = [](char c) { + return (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'); + }; + + if (len == 14 && macStr.charAt(4) == '-' && macStr.charAt(9) == '-') { + // 验证连字符格式的十六进制字符 / Validate hexadecimal characters in hyphen format + for (int i = 0; i < len; i++) { + if (i == 4 || i == 9) continue; + if (!isHex(macStr.charAt(i))) return false; + } + return true; + } + + if (len == 17 && macStr.charAt(2) == ':' && macStr.charAt(5) == ':' && + macStr.charAt(8) == ':' && macStr.charAt(11) == ':' && macStr.charAt(14) == ':') { + // 验证冒号格式的十六进制字符 / Validate hexadecimal characters in colon format + for (int i = 0; i < len; i++) { + if (i == 2 || i == 5 || i == 8 || i == 11 || i == 14) continue; + if (!isHex(macStr.charAt(i))) return false; + } + return true; + } + + return false; +} + +// 规范化MAC地址格式(转为大写并输出为连字符格式:XXXX-XXXX-XXXX) / Normalize MAC address format (convert to uppercase and output as hyphen format: XXXX-XXXX-XXXX) +String M5SwitchC6::normalizeMacAddress(const String& macStr) { + // 提取十六进制字符,忽略分隔符('-'、':' 等) / Extract hexadecimal characters, ignore separators ('-', ':' etc.) + String hexOnly = ""; + hexOnly.reserve(12); + for (int i = 0; i < macStr.length(); i++) { + char c = macStr.charAt(i); + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f')) { + // 转为大写后加入 / Convert to uppercase and add + if (c >= 'a' && c <= 'f') c = c - 'a' + 'A'; + hexOnly += c; + } + } + + if (hexOnly.length() == 12) { + // 重组为 XXXX-XXXX-XXXX / Reformat as XXXX-XXXX-XXXX + return hexOnly.substring(0, 4) + "-" + hexOnly.substring(4, 8) + "-" + hexOnly.substring(8, 12); + } + + // 长度异常时,退化为原字符串的大写并将冒号替换为连字符(以尽量保持兼容) / When length is abnormal, fallback to uppercase original string with colons replaced by hyphens (to maintain compatibility) + String upper = macStr; + upper.toUpperCase(); + upper.replace(':', '-'); + return upper; +} + +// 发送开关控制命令 / Send switch control command +bool M5SwitchC6::sendSwitchCommand(const String& deviceMac, bool switchOn, bool needResponse) { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) + serialPrintln("sendSwitchCommand is only supported on ESP32 or ESP8266 platforms."); + return false; +#endif + + // 获取当前信道 / Get current channel + uint8_t channel = getChannel(); + if (channel < 1 || channel > 14) { + serialPrintln("Invalid current channel for sendSwitchCommand. Channel must be between 1 and 14."); + return false; + } + + // 检查MAC地址格式 / Check MAC address format + if (!isValidMacFormat(deviceMac)) { + serialPrintln("Invalid MAC address format for sendSwitchCommand: " + deviceMac); + return false; + } + + // 构建命令字符串 / Build command string + // 格式: "XXXX-XXXX-XXXX=1;ch=n" 或 "XXXX-XXXX-XXXX=1;ch=n;" / Format: "XXXX-XXXX-XXXX=1;ch=n" or "XXXX-XXXX-XXXX=1;ch=n;" + String command = normalizeMacAddress(deviceMac) + "=" + (switchOn ? "1" : "0") + ";ch=" + String(channel); + if (needResponse) { + command += ";"; + } + + return sendCustomMessage(command); +} + +// 发送状态查询命令 / Send status query command +bool M5SwitchC6::sendStatusQuery(const String& deviceMac) { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) + serialPrintln("sendStatusQuery is only supported on ESP32 or ESP8266 platforms."); + return false; +#endif + + // 获取当前信道 / Get current channel + uint8_t channel = getChannel(); + if (channel < 1 || channel > 14) { + serialPrintln("Invalid current channel for sendStatusQuery. Channel must be between 1 and 14."); + return false; + } + + // 检查MAC地址格式 / Check MAC address format + if (!isValidMacFormat(deviceMac)) { + serialPrintln("Invalid MAC address format for sendStatusQuery: " + deviceMac); + return false; + } + + // 构建查询命令字符串 / Build query command string + // 格式: "XXXX-XXXX-XXXX=?;ch=n;" / Format: "XXXX-XXXX-XXXX=?;ch=n;" + String command = normalizeMacAddress(deviceMac) + "=?;ch=" + String(channel) + ";"; + + return sendCustomMessage(command); +} + +// 发送版本查询命令 / Send version query command +bool M5SwitchC6::sendVersionQuery(const String& deviceMac) { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) + serialPrintln("sendVersionQuery is only supported on ESP32 or ESP8266 platforms."); + return false; +#endif + + // 获取当前信道 / Get current channel + uint8_t channel = getChannel(); + if (channel < 1 || channel > 14) { + serialPrintln("Invalid current channel for sendVersionQuery. Channel must be between 1 and 14."); + return false; + } + + // 检查MAC地址格式 / Check MAC address format + if (!isValidMacFormat(deviceMac)) { + serialPrintln("Invalid MAC address format for sendVersionQuery: " + deviceMac); + return false; + } + + // 构建版本查询命令字符串 / Build version query command string + // 格式: "XXXX-XXXX-XXXX=V;ch=n;" / Format: "XXXX-XXXX-XXXX=V;ch=n;" + String command = normalizeMacAddress(deviceMac) + "=V;ch=" + String(channel) + ";"; + + return sendCustomMessage(command); +} + +// 发送自定义消息 / Send custom message +bool M5SwitchC6::sendCustomMessage(const String& message) { +#if !defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP8266) + serialPrintln("sendCustomMessage is only supported on ESP32 or ESP8266 platforms."); + return false; +#endif + + if (message.length() == 0) { + serialPrintln("Cannot send empty message."); + return false; + } + + // 获取互斥锁 / Acquire mutex lock + acquireMutex(); + + // 添加广播对等节点(如果还没添加) / Add broadcast peer node (if not already added) + uint8_t broadcastMac[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + esp_now_peer_info_t peerInfo; + memset(&peerInfo, 0, sizeof(peerInfo)); + memcpy(peerInfo.peer_addr, broadcastMac, 6); + peerInfo.channel = 0; // 0表示使用当前信道 / 0 means use current channel + peerInfo.encrypt = false; + + // 检查是否已经添加了广播对等节点 / Check if broadcast peer node has already been added + if (!esp_now_is_peer_exist(broadcastMac)) { + esp_err_t result = esp_now_add_peer(&peerInfo); + if (result != ESP_OK) { + serialPrintln("Failed to add broadcast peer for sending. Error: " + String(result)); + releaseMutex(); + return false; + } + serialPrintln("Broadcast peer added successfully for sending."); + } + + // 发送数据 / Send data + esp_err_t result = esp_now_send(broadcastMac, (uint8_t*)message.c_str(), message.length()); + + // 释放互斥锁 / Release mutex lock + releaseMutex(); + + if (result == ESP_OK) { + serialPrintln("Message sent successfully: " + message); + return true; + } else { + serialPrintln("Failed to send message: " + message + ". Error: " + String(result)); + return false; + } +} + +// 发送开关命令并等待响应 / Send switch command and wait for response +bool M5SwitchC6::sendSwitchCommandWithResponse(const String& deviceMac, bool switchOn, + unsigned long timeoutMs, SwitchC6ParsedData_t* response) { + // 添加响应等待 / Add response wait + if (!addResponseWait(deviceMac, timeoutMs)) { + serialPrintln("Failed to add response wait for device: " + deviceMac); + return false; + } + + // 发送命令(带响应) / Send command (with response) + bool sendResult = sendSwitchCommand(deviceMac, switchOn, true); + if (!sendResult) { + removeResponseWait(deviceMac); + return false; + } + + // 等待响应 / Wait for response + unsigned long startTime = millis(); + unsigned long lastSendTime = startTime; + + while (millis() - startTime < timeoutMs) { + checkResponseTimeouts(); // 检查超时 / Check timeouts + + int waitIndex = findResponseWait(deviceMac); + if (waitIndex >= 0 && responseWaits[waitIndex].hasResponse) { + // 收到响应 / Response received + if (response != nullptr) { + *response = responseWaits[waitIndex].response; + } + removeResponseWait(deviceMac); + serialPrintln("Received response from device: " + deviceMac); + return true; + } + + // 检查是否需要重发命令 / Check if command needs to be resent + unsigned long currentTime = millis(); + if (currentTime - lastSendTime >= resendInterval) { + serialPrintln("Resending command to device: " + deviceMac); + sendSwitchCommand(deviceMac, switchOn, true); + lastSendTime = currentTime; + } + delay(1); // 避免过度占用CPU / Avoid excessive CPU usage + } + + // 超时 / Timeout + removeResponseWait(deviceMac); + serialPrintln("Timeout waiting for response from device: " + deviceMac); + return false; +} + +// 发送状态查询并等待响应 / Send status query and wait for response +bool M5SwitchC6::sendStatusQueryWithResponse(const String& deviceMac, + unsigned long timeoutMs, SwitchC6ParsedData_t* response) { + // 添加响应等待 / Add response wait + if (!addResponseWait(deviceMac, timeoutMs)) { + serialPrintln("Failed to add response wait for device: " + deviceMac); + return false; + } + + // 发送查询命令 / Send query command + bool sendResult = sendStatusQuery(deviceMac); + if (!sendResult) { + removeResponseWait(deviceMac); + return false; + } + + // 等待响应 / Wait for response + unsigned long startTime = millis(); + unsigned long lastSendTime = startTime; + + while (millis() - startTime < timeoutMs) { + checkResponseTimeouts(); // 检查超时 / Check timeouts + + int waitIndex = findResponseWait(deviceMac); + if (waitIndex >= 0 && responseWaits[waitIndex].hasResponse) { + // 收到响应 / Response received + if (response != nullptr) { + *response = responseWaits[waitIndex].response; + } + removeResponseWait(deviceMac); + serialPrintln("Received status response from device: " + deviceMac); + return true; + } + + // 检查是否需要重发命令 / Check if command needs to be resent + unsigned long currentTime = millis(); + if (currentTime - lastSendTime >= resendInterval) { + serialPrintln("Resending status query to device: " + deviceMac); + sendStatusQuery(deviceMac); + lastSendTime = currentTime; + } + + delay(1); // 避免过度占用CPU / Avoid excessive CPU usage + } + + // 超时 / Timeout + removeResponseWait(deviceMac); + serialPrintln("Timeout waiting for status response from device: " + deviceMac); + return false; +} + +// 发送版本查询并等待响应 / Send version query and wait for response +bool M5SwitchC6::sendVersionQueryWithResponse(const String& deviceMac, + unsigned long timeoutMs, SwitchC6ParsedData_t* response) { + // 添加响应等待 / Add response wait + if (!addResponseWait(deviceMac, timeoutMs)) { + serialPrintln("Failed to add response wait for device: " + deviceMac); + return false; + } + + // 发送版本查询命令 / Send version query command + bool sendResult = sendVersionQuery(deviceMac); + if (!sendResult) { + removeResponseWait(deviceMac); + return false; + } + + // 等待响应 / Wait for response + unsigned long startTime = millis(); + unsigned long lastSendTime = startTime; + + while (millis() - startTime < timeoutMs) { + checkResponseTimeouts(); // 检查超时 / Check timeouts + + int waitIndex = findResponseWait(deviceMac); + if (waitIndex >= 0 && responseWaits[waitIndex].hasResponse) { + // 收到响应 / Response received + if (response != nullptr) { + *response = responseWaits[waitIndex].response; + } + removeResponseWait(deviceMac); + serialPrintln("Received version response from device: " + deviceMac); + return true; + } + + // 检查是否需要重发命令 / Check if command needs to be resent + unsigned long currentTime = millis(); + if (currentTime - lastSendTime >= resendInterval) { + serialPrintln("Resending version query to device: " + deviceMac); + sendVersionQuery(deviceMac); + lastSendTime = currentTime; + } + + delay(1); // 避免过度占用CPU / Avoid excessive CPU usage + } + + // 超时 / Timeout + removeResponseWait(deviceMac); + serialPrintln("Timeout waiting for version response from device: " + deviceMac); + return false; +} + +// 检查是否有等待中的响应 / Check if there are pending responses +bool M5SwitchC6::isWaitingForResponse() { + checkResponseTimeouts(); // 先检查超时 / Check timeouts first + return responseWaitCount > 0; +} + +// 获取响应等待信息 / Get response wait information +bool M5SwitchC6::getResponseWaitInfo(const String& deviceMac, ResponseWaitInfo_t* waitInfo) { + if (waitInfo == nullptr) { + return false; + } + + int index = findResponseWait(deviceMac); + if (index >= 0) { + *waitInfo = responseWaits[index]; + return true; + } + return false; +} + +// 添加响应等待 / Add response wait +bool M5SwitchC6::addResponseWait(const String& deviceMac, unsigned long timeoutMs) { + // 检查是否已经在等待这个设备的响应 / Check if already waiting for response from this device + if (findResponseWait(deviceMac) >= 0) { + serialPrintln("Already waiting for response from device: " + deviceMac); + return false; + } + + // 查找空闲的等待槽 / Find available wait slot + for (uint16_t i = 0; i < MAX_RESPONSE_WAITS; i++) { + if (!responseWaits[i].isWaiting) { + responseWaits[i].deviceMac = normalizeMacAddress(deviceMac); + responseWaits[i].requestTime = millis(); + responseWaits[i].timeoutMs = timeoutMs; + responseWaits[i].isWaiting = true; + responseWaits[i].hasResponse = false; + responseWaitCount++; + serialPrintln("Added response wait for device: " + deviceMac + ", timeout: " + String(timeoutMs) + "ms"); + return true; + } + } + + serialPrintln("No available response wait slots for device: " + deviceMac); + return false; +} + +// 移除响应等待 / Remove response wait +void M5SwitchC6::removeResponseWait(const String& deviceMac) { + int index = findResponseWait(deviceMac); + if (index >= 0) { + responseWaits[index].isWaiting = false; + responseWaits[index].hasResponse = false; + responseWaits[index].deviceMac = ""; + responseWaits[index].requestTime = 0; + responseWaits[index].timeoutMs = 0; + responseWaitCount--; + serialPrintln("Removed response wait for device: " + deviceMac); + } +} + +// 查找响应等待 / Find response wait +int M5SwitchC6::findResponseWait(const String& deviceMac) { + String normalizedMac = normalizeMacAddress(deviceMac); + // serialPrintln("findResponseWait: Looking for device: " + normalizedMac); + + for (uint16_t i = 0; i < MAX_RESPONSE_WAITS; i++) { + if (responseWaits[i].isWaiting) { + // serialPrintln("findResponseWait: Slot " + String(i) + " waiting for: " + responseWaits[i].deviceMac); + if (responseWaits[i].deviceMac == normalizedMac) { + serialPrintln("findResponseWait: Found match at index " + String(i)); + return i; + } + } + } + serialPrintln("findResponseWait: No match found for: " + normalizedMac); + return -1; +} + +// 检查响应超时 / Check response timeouts +void M5SwitchC6::checkResponseTimeouts() { + unsigned long currentTime = millis(); + for (uint16_t i = 0; i < MAX_RESPONSE_WAITS; i++) { + if (responseWaits[i].isWaiting && !responseWaits[i].hasResponse) { + if (currentTime - responseWaits[i].requestTime >= responseWaits[i].timeoutMs) { + serialPrintln("Response timeout for device: " + responseWaits[i].deviceMac); + responseWaits[i].isWaiting = false; + responseWaits[i].deviceMac = ""; + responseWaits[i].requestTime = 0; + responseWaits[i].timeoutMs = 0; + responseWaitCount--; + } + } + } +} + +// 处理收到的响应(带ESP-NOW发送方MAC地址) / Handle received response (with ESP-NOW sender MAC address) +void M5SwitchC6::handleReceivedResponse(const SwitchC6ParsedData_t& parsedData, const String& senderMac) { + if (!parsedData.isValid) { + serialPrintln("handleReceivedResponse: Invalid parsed data"); + return; + } + + serialPrintln("handleReceivedResponse: DataMAC=" + parsedData.macAddress + + ", SenderMAC=" + senderMac + + ", CommandType=" + String(parsedData.commandType) + + ", State=" + String(parsedData.switchState ? "ON" : "OFF") + + ", Version=" + parsedData.version); + + // 处理状态返回、广播和版本响应 / Handle status return, broadcast and version response + if (parsedData.commandType != COMMAND_STATUS_RETURN && + parsedData.commandType != COMMAND_BROADCAST && + parsedData.commandType != COMMAND_VERSION_RETURN) { + serialPrintln("handleReceivedResponse: Ignoring unhandled response type"); + return; + } + + // 根据ESP-NOW发送方MAC地址查找等待的响应 / Find waiting response based on ESP-NOW sender MAC address + int index = findResponseWait(senderMac); + if (index >= 0) { + responseWaits[index].hasResponse = true; + responseWaits[index].response = parsedData; + serialPrintln("Matched response from sender: " + senderMac + " for waiting device: " + responseWaits[index].deviceMac); + return; + } + + serialPrintln("handleReceivedResponse: No waiting device found for sender MAC: " + senderMac); +} + +// 处理收到的响应(兼容旧版本,自动查找第一个等待的设备) / Handle received response (compatible with old version, automatically find first waiting device) +void M5SwitchC6::handleReceivedResponse(const SwitchC6ParsedData_t& parsedData) { + if (!parsedData.isValid) { + serialPrintln("handleReceivedResponse: Invalid parsed data"); + return; + } + + serialPrintln("handleReceivedResponse: MAC=" + parsedData.macAddress + + ", CommandType=" + String(parsedData.commandType) + + ", State=" + String(parsedData.switchState ? "ON" : "OFF") + + ", Version=" + parsedData.version); + + // 处理状态返回、广播和版本响应 / Handle status return, broadcast and version response + if (parsedData.commandType != COMMAND_STATUS_RETURN && + parsedData.commandType != COMMAND_BROADCAST && + parsedData.commandType != COMMAND_VERSION_RETURN) { + serialPrintln("handleReceivedResponse: Ignoring unhandled response type"); + return; + } + + // 查找第一个正在等待响应的设备(简化版本) / Find first device waiting for response (simplified version) + serialPrintln("handleReceivedResponse: Looking for any waiting device"); + for (uint16_t i = 0; i < MAX_RESPONSE_WAITS; i++) { + if (responseWaits[i].isWaiting && !responseWaits[i].hasResponse) { + responseWaits[i].hasResponse = true; + responseWaits[i].response = parsedData; + serialPrintln("Matched response for waiting device: " + responseWaits[i].deviceMac + + " (received from device ID: " + parsedData.macAddress + ")"); + return; // 只匹配第一个等待的设备 / Only match first waiting device + } + } + serialPrintln("handleReceivedResponse: No waiting device found for any response"); +} + +/** + * @brief Set the resend interval for ESP-NOW communication. + */ +bool M5SwitchC6::setResendInterval(unsigned long intervalMs) { + // 验证间隔范围:1ms到10000ms (10秒) / Validate interval range: 1ms to 10000ms (10 seconds) + if (intervalMs < 1 || intervalMs > 10000) { + serialPrintln("setResendInterval: Invalid interval " + String(intervalMs) + "ms (range: 1-10000ms)"); + return false; + } + + acquireMutex(); + resendInterval = intervalMs; + releaseMutex(); + + serialPrintln("setResendInterval: Set to " + String(intervalMs) + "ms"); + return true; +} + +/** + * @brief Get the current resend interval. + */ +unsigned long M5SwitchC6::getResendInterval() { + acquireMutex(); + unsigned long interval = resendInterval; + releaseMutex(); + + return interval; +} + +/** + * @brief Clear all discovered peer devices data. + */ +bool M5SwitchC6::clearPeersData() { + if (peers == nullptr) { + serialPrintln("Peers array is not initialized."); + return false; + } + + acquireMutex(); + + // 清空所有设备数据 / Clear all device data + for (uint16_t i = 0; i < max_broadcast_peers; i++) { + memset(peers[i].senderMac, 0, sizeof(peers[i].senderMac)); + peers[i].lastData = ""; + peers[i].channel = 0; + peers[i].rssi = 0; + peers[i].lastReceiveTime = 0; + peers[i].status = false; + } + + // 重置设备计数 / Reset device count + peerCount = 0; + + releaseMutex(); + + serialPrintln("All peers data cleared. Device count reset to 0."); + return true; +} + +/** + * @brief Clear all pending response wait requests. + */ +bool M5SwitchC6::clearResponseWaits() { + acquireMutex(); + + // 清空所有响应等待 / Clear all response waits + for (uint16_t i = 0; i < MAX_RESPONSE_WAITS; i++) { + responseWaits[i].isWaiting = false; + responseWaits[i].hasResponse = false; + responseWaits[i].deviceMac = ""; + responseWaits[i].requestTime = 0; + responseWaits[i].timeoutMs = 0; + // 清空响应数据 / Clear response data + responseWaits[i].response.isValid = false; + responseWaits[i].response.commandType = COMMAND_UNKNOWN; + responseWaits[i].response.macAddress = ""; + responseWaits[i].response.switchState = false; + responseWaits[i].response.voltage = 0.0f; + responseWaits[i].response.version = ""; + responseWaits[i].response.errorMessage = ""; + } + + // 重置等待计数 / Reset wait count + responseWaitCount = 0; + + releaseMutex(); + + serialPrintln("All response waits cleared. Wait count reset to 0."); + return true; +} + +/** + * @brief Clear all historical data including peers and response waits. + */ +bool M5SwitchC6::clearAllData() { + serialPrintln("Clearing all historical data..."); + + bool peersCleared = clearPeersData(); + bool waitsCleared = clearResponseWaits(); + + if (peersCleared && waitsCleared) { + serialPrintln("All historical data cleared successfully."); + return true; + } else { + serialPrintln("Failed to clear some historical data."); + return false; + } +} + +/** + * @brief Clear specific device data by MAC address. + */ +bool M5SwitchC6::clearDeviceData(const String& deviceMac) { + if (peers == nullptr) { + serialPrintln("Peers array is not initialized."); + return false; + } + + if (deviceMac.isEmpty()) { + serialPrintln("Device MAC address is empty."); + return false; + } + + // 标准化MAC地址格式 / Normalize MAC address format + String normalizedMac = normalizeMacAddress(deviceMac); + bool deviceFound = false; + + acquireMutex(); + + // 查找并移除指定设备 / Find and remove specified device + for (uint16_t i = 0; i < peerCount; i++) { + String currentMac = macToString(peers[i].senderMac); + String normalizedCurrentMac = normalizeMacAddress(currentMac); + + if (normalizedCurrentMac.equals(normalizedMac)) { + serialPrintf("Found device %s at index %d, removing...\n", currentMac.c_str(), i); + + // 将后面的设备前移 / Move subsequent devices forward + for (uint16_t j = i; j < peerCount - 1; j++) { + memcpy(&peers[j], &peers[j + 1], sizeof(broadcast_packet_with_mac_t)); + } + + // 清空最后一个位置 / Clear last position + memset(&peers[peerCount - 1], 0, sizeof(broadcast_packet_with_mac_t)); + peers[peerCount - 1].lastData = ""; + + // 减少设备计数 / Decrease device count + peerCount--; + deviceFound = true; + break; + } + } + + releaseMutex(); + + // 同时清除该设备的等待响应 / Also clear waiting response for this device + if (deviceFound) { + removeResponseWait(normalizedMac); + serialPrintf("Device %s data cleared successfully.\n", deviceMac.c_str()); + return true; + } else { + serialPrintf("Device %s not found in peer list.\n", deviceMac.c_str()); + return false; + } +} + +/** + * @brief Clear devices that haven't been seen for a specified time. + */ +uint16_t M5SwitchC6::clearExpiredDevices(unsigned long timeoutMs) { + if (peers == nullptr) { + serialPrintln("Peers array is not initialized."); + return 0; + } + + unsigned long currentTime = millis(); + uint16_t removedCount = 0; + + acquireMutex(); + + // 从后向前遍历,避免移动元素时的索引问题 / Traverse from back to front to avoid index issues when moving elements + for (int i = peerCount - 1; i >= 0; i--) { + unsigned long timeSinceLastReceive = currentTime - peers[i].lastReceiveTime; + + // 检查是否超时(考虑millis()溢出情况) / Check if timeout (considering millis() overflow) + bool isExpired = false; + if (currentTime >= peers[i].lastReceiveTime) { + isExpired = (timeSinceLastReceive > timeoutMs); + } else { + // millis()溢出的情况 / millis() overflow case + isExpired = ((ULONG_MAX - peers[i].lastReceiveTime + currentTime) > timeoutMs); + } + + if (isExpired) { + String deviceMac = macToString(peers[i].senderMac); + serialPrintf("Removing expired device %s (last seen %lu ms ago)\n", + deviceMac.c_str(), timeSinceLastReceive); + + // 将后面的设备前移 / Move subsequent devices forward + for (uint16_t j = i; j < peerCount - 1; j++) { + memcpy(&peers[j], &peers[j + 1], sizeof(broadcast_packet_with_mac_t)); + } + + // 清空最后一个位置 / Clear last position + memset(&peers[peerCount - 1], 0, sizeof(broadcast_packet_with_mac_t)); + peers[peerCount - 1].lastData = ""; + + // 减少设备计数 / Decrease device count + peerCount--; + removedCount++; + + // 同时清除该设备的等待响应 / Also clear waiting response for this device + removeResponseWait(normalizeMacAddress(deviceMac)); + } + } + + releaseMutex(); + + if (removedCount > 0) { + serialPrintf("Removed %d expired devices (timeout: %lu ms)\n", removedCount, timeoutMs); + } + + return removedCount; +} + +/** + * @brief Set automatic cleanup interval for expired devices. + */ +bool M5SwitchC6::setAutoCleanup(unsigned long intervalMs, unsigned long deviceTimeoutMs) { + if (deviceTimeoutMs == 0) { + serialPrintln("Device timeout cannot be 0."); + return false; + } + + acquireMutex(); + + autoCleanupInterval = intervalMs; + deviceTimeout = deviceTimeoutMs; + lastCleanupTime = millis(); + + releaseMutex(); + + if (intervalMs == 0) { + serialPrintln("Automatic device cleanup disabled."); + } else { + serialPrintf("Automatic device cleanup enabled: interval=%lu ms, device_timeout=%lu ms\n", + intervalMs, deviceTimeoutMs); + } + + return true; +} + +/** + * @brief Check and perform automatic cleanup of expired devices. + */ +uint16_t M5SwitchC6::checkAutoCleanup() { + // 如果自动清理被禁用,直接返回 / Return directly if auto cleanup is disabled + if (autoCleanupInterval == 0) { + return 0; + } + + unsigned long currentTime = millis(); + unsigned long timeSinceLastCleanup; + + // 检查是否到了清理时间(考虑millis()溢出情况) / Check if it's cleanup time (considering millis() overflow) + if (currentTime >= lastCleanupTime) { + timeSinceLastCleanup = currentTime - lastCleanupTime; + } else { + // millis()溢出的情况 / millis() overflow case + timeSinceLastCleanup = ULONG_MAX - lastCleanupTime + currentTime; + } + + if (timeSinceLastCleanup >= autoCleanupInterval) { + // 执行自动清理 / Execute auto cleanup + uint16_t removedCount = clearExpiredDevices(deviceTimeout); + + // 更新上次清理时间 / Update last cleanup time + acquireMutex(); + lastCleanupTime = currentTime; + releaseMutex(); + + if (removedCount > 0) { + serialPrintf("Auto cleanup completed: %d devices removed\n", removedCount); + } + + return removedCount; + } + + return 0; +} + +// 解析电压字符串的辅助函数 / Helper function to parse voltage string +float M5SwitchC6::parseVoltage(const String& voltageStr) { + // 移除单位 'V' 并转换为浮点数 / Remove unit 'V' and convert to float + String cleanStr = voltageStr; + cleanStr.replace("V", ""); + cleanStr.replace("v", ""); + cleanStr.trim(); + + if (cleanStr.length() == 0) { + return -1.0f; // 表示解析失败 / Indicates parsing failure + } + + float voltage = cleanStr.toFloat(); + + // 检查toFloat是否成功解析(如果输入是"0"或"0.0"等有效的零值,这是正确的) / Check if toFloat successfully parsed (if input is "0" or "0.0" etc. valid zero values, this is correct) + if (voltage == 0.0f && cleanStr != "0" && cleanStr != "0.0" && cleanStr != "0.00" && !cleanStr.startsWith("0.")) { + return -1.0f; // toFloat失败,返回了默认值0.0 / toFloat failed, returned default value 0.0 + } + + // 验证电压范围(考虑SwitchC6的实际工作范围:通常在2.0V到4.2V之间,但允许更宽的范围以适应不同情况) / Validate voltage range (considering actual working range of SwitchC6: usually between 2.0V and 4.2V, but allow wider range for different situations) + if (voltage < 0.0f || voltage > 6.0f) { + return -1.0f; // 表示解析失败 / Indicates parsing failure + } + + return voltage; +} \ No newline at end of file diff --git a/src/M5Switchc6.h b/src/M5Switchc6.h new file mode 100644 index 0000000..525aea1 --- /dev/null +++ b/src/M5Switchc6.h @@ -0,0 +1,509 @@ +/* + *SPDX-FileCopyrightText: 2025 M5Stack Technology CO LTD + * + *SPDX-License-Identifier: MIT + */ + +#ifndef __M5SWITCHC6_H__ +#define __M5SWITCHC6_H__ + +#include "Arduino.h" +#include + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) +#include +#include +#include +#include +#include +#include +#include +#endif + +#define M5_DEVICE_NAME "M5SwitchC6" + +// #define M5_MODULE_DEBUG_SERIAL Serial // The corresponding serial port must be initialized before use +// This macro definition can be annotated without sending and receiving data prints +// Define the serial port you want to use, e.g., Serial1 or Serial2 +#define FORMAT_TIMESTAMP() ([]() -> String { \ + unsigned long ms = millis(); \ + unsigned long total_seconds = ms / 1000; \ + unsigned long hours = total_seconds / 3600; \ + unsigned long minutes = (total_seconds % 3600) / 60; \ + unsigned long seconds = total_seconds % 60; \ + float sec_frac = seconds + (ms % 1000) / 1000.0f; \ + char buf[20]; \ + snprintf(buf, sizeof(buf), "%02luh:%02lum:%.3lfs\0", hours, minutes, sec_frac); \ + return String(buf); \ +})() + +#if defined M5_MODULE_DEBUG_SERIAL +#define serialPrint(...) do { \ + char _msg_buf[256]; \ + String _timestamp = FORMAT_TIMESTAMP(); \ + snprintf(_msg_buf, sizeof(_msg_buf), "[at %s] %s: ", _timestamp.c_str(), M5_DEVICE_NAME); \ + M5_MODULE_DEBUG_SERIAL.print(_msg_buf); \ + M5_MODULE_DEBUG_SERIAL.print(__VA_ARGS__); \ + M5_MODULE_DEBUG_SERIAL.flush(); \ +} while(0) + +#define serialPrintln(...) do { \ + char _msg_buf[256]; \ + String _timestamp = FORMAT_TIMESTAMP(); \ + snprintf(_msg_buf, sizeof(_msg_buf), "[at %s] %s: ", _timestamp.c_str(), M5_DEVICE_NAME); \ + M5_MODULE_DEBUG_SERIAL.print(_msg_buf); \ + M5_MODULE_DEBUG_SERIAL.println(__VA_ARGS__); \ + M5_MODULE_DEBUG_SERIAL.flush(); \ +} while(0) + +#define serialPrintf(...) do { \ + char _msg_buf[256]; \ + String _timestamp = FORMAT_TIMESTAMP(); \ + snprintf(_msg_buf, sizeof(_msg_buf), "[at %s] %s: ", _timestamp.c_str(), M5_DEVICE_NAME); \ + M5_MODULE_DEBUG_SERIAL.print(_msg_buf); \ + M5_MODULE_DEBUG_SERIAL.printf(__VA_ARGS__); \ + M5_MODULE_DEBUG_SERIAL.flush(); \ +} while(0) + +#define serialFlush() M5_MODULE_DEBUG_SERIAL.flush() +#else +#define serialPrint(...) +#define serialPrintln(...) +#define serialPrintf(...) +#define serialFlush() +#endif + +/** + * @brief Structure to hold broadcast packet information. + * 用于保存广播包发送者和其最新数据 + */ +struct broadcast_packet_with_mac_t { + uint8_t senderMac[6]; + String lastData; + uint8_t channel; + int rssi; // 信号强度 + unsigned long lastReceiveTime; // 最后接收时间 + bool status; // 状态标志 0:关断 1:开启 +}; + +/** + * @brief Enumeration for Switch-C6 command types + * 作为主机,主要接收和解析从机的状态返回和广播数据 + */ +enum SwitchC6CommandType_t { + COMMAND_UNKNOWN = 0, + COMMAND_STATUS_RETURN = 1, // ;状态;电压 从机状态返回数据 + COMMAND_BROADCAST = 2, // 从机按钮广播数据 + COMMAND_VERSION_RETURN = 3 // ;版本号 从机版本返回数据 +}; + +/** + * @brief Structure to hold parsed Switch-C6 device data (Host side) + * 主机端解析从机设备数据的结构体 + */ +struct SwitchC6ParsedData_t { + SwitchC6CommandType_t commandType; // 命令类型(状态返回或广播) + String macAddress; // 设备MAC地址字符串 (格式: XXXX-XXXX-XXXX,广播时为FFFF-FFFF-FFFF) + bool switchState; // 开关状态 (true=开, false=关) + float voltage; // 电压值 (例如: 3.30) + String version; // 版本号字符串 (例如: "1.0.0") + bool isValid; // 解析是否成功 + String errorMessage; // 错误信息 +}; + +/** + * @brief Structure to hold response waiting information + * 响应等待信息结构体 + */ +struct ResponseWaitInfo_t { + String deviceMac; // 等待响应的设备MAC地址 + unsigned long requestTime; // 请求发送时间 + unsigned long timeoutMs; // 超时时间(毫秒) + bool isWaiting; // 是否正在等待响应 + bool hasResponse; // 是否已收到响应 + SwitchC6ParsedData_t response; // 收到的响应数据 +}; + +#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) +/** + * @brief Structure to hold queued data for processing + */ +struct QueuedData_t { + uint8_t* data; + int len; + uint8_t senderMac[6]; // ESP-NOW发送方MAC地址 +}; +#endif + +class M5SwitchC6 { +public: + /** 构造和析构函数 */ + /** + * @brief Constructs a new M5SwitchC6 object. + * + * This constructor initializes the M5SwitchC6 instance and creates a mutex lock + * for thread-safe operations on ESP32 or ESP8266 platforms. + */ + M5SwitchC6(uint16_t max_broadcast_peers); + ~M5SwitchC6(); + + /** + * @brief Initializes the module. + * + * This function initializes the M5SwitchC6 module, setting up necessary configurations + * and preparing it for use. It should be called before any other operations on the module. + * 初始化模块,设置必要的配置并准备使用 + */ + bool begin(); + + /** + * @brief Sets the WiFi channel for ESP-NOW communication. + * + * This function sets the WiFi channel for ESP-NOW communication. The channel must be + * between 1 and 14. Note that both devices must be on the same channel to communicate. + * 设置ESP-NOW通信的WiFi信道 + */ + bool setChannel(uint8_t channel); + + /** + * @brief Gets the current WiFi channel. + * + * This function returns the current WiFi channel being used for communication. + * 获取当前使用的WiFi信道 + */ + uint8_t getChannel(); + + /** + * @brief Gets the number of detected peers. + * + * @return The number of peers that have been detected. + * 获取检测到的设备数量 + */ + uint16_t getPeerCount(); + + /** + * @brief Gets peer information by index. + * + * @param index The index of the peer (0 to getPeerCount()-1). + * @param peerInfo Pointer to structure to store peer information. + * @return true if peer exists and information was retrieved, false otherwise. + * 根据索引获取设备信息 + */ + bool getPeerInfo(uint16_t index, broadcast_packet_with_mac_t* peerInfo); + + /** + * @brief Parses received device data with sender MAC address. + * 解析接收的数据,并包含发送者的MAC地址 + * + * @param data Pointer to the received data buffer + * @param len Length of the received data + * @param senderMac ESP-NOW sender MAC address + */ + void parseData(const uint8_t *data, int len, const uint8_t *senderMac); + + /** + * @brief Parses received device data and returns structured result (Host side). + * 解析接收到的数据并返回结构化结果 + * + * @param data Pointer to the received data buffer + * @param len Length of the received data + * @return SwitchC6ParsedData_t structure containing parsed device information + */ + SwitchC6ParsedData_t parseDataStructured(const uint8_t *data, int len); + + /** + * @brief Send control command to Switch-C6 device. + * 发送控制命令到Switch-C6设备 + * + * 发送控制命令到Switch-C6设备,使用当前信道 + * + * @param deviceMac Target device MAC address in format "XXXX-XXXX-XXXX" + * @param switchOn true to turn on, false to turn off + * @param needResponse true if response is needed (adds ';' at the end) + * @return true if command was sent successfully, false otherwise + */ + bool sendSwitchCommand(const String& deviceMac, bool switchOn, bool needResponse = false); + + /** + * @brief Send status query to Switch-C6 device. + * 发送状态查询命令到Switch-C6设备 + * + * 发送状态查询命令到Switch-C6设备,使用当前信道 + * + * @param deviceMac Target device MAC address in format "XXXX-XXXX-XXXX" + * @return true if query was sent successfully, false otherwise + */ + bool sendStatusQuery(const String& deviceMac); + + /** + * @brief Send version query to Switch-C6 device. + * 发送版本查询命令到Switch-C6设备 + * + * 发送版本查询命令到Switch-C6设备,使用当前信道 + * + * @param deviceMac Target device MAC address in format "XXXX-XXXX-XXXX" + * @return true if query was sent successfully, false otherwise + */ + bool sendVersionQuery(const String& deviceMac); + + /** + * @brief Send custom message via ESP-NOW. + * 通过ESP-NOW发送自定义消息 + * + * 发送自定义消息(通用发送函数) + * + * @param message Custom message string to send + * @return true if message was sent successfully, false otherwise + */ + bool sendCustomMessage(const String& message); + + /** + * @brief Send switch command and wait for response. + * 发送开关命令并等待响应 + * + * 发送开关命令并等待响应,使用当前信道 + * + * @param deviceMac Target device MAC address in format "XXXX-XXXX-XXXX" + * @param switchOn true to turn on, false to turn off + * @param timeoutMs Timeout in milliseconds (default 5000ms) + * @param response Pointer to store the response data (optional) + * @return true if response received within timeout, false otherwise + */ + bool sendSwitchCommandWithResponse(const String& deviceMac, bool switchOn, + unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr); + + /** + * @brief Send status query and wait for response. + * 发送状态查询并等待响应 + * + * 发送状态查询并等待响应,使用当前信道 + * + * @param deviceMac Target device MAC address in format "XXXX-XXXX-XXXX" + * @param timeoutMs Timeout in milliseconds (default 5000ms) + * @param response Pointer to store the response data (optional) + * @return true if response received within timeout, false otherwise + */ + bool sendStatusQueryWithResponse(const String& deviceMac, + unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr); + + /** + * @brief Send version query and wait for response. + * 发送版本查询并等待响应 + * + * 发送版本查询并等待响应,使用当前信道 + * + * @param deviceMac Target device MAC address in format "XXXX-XXXX-XXXX" + * @param timeoutMs Timeout in milliseconds (default 5000ms) + * @param response Pointer to store the response data (optional) + * @return true if response received within timeout, false otherwise + */ + bool sendVersionQueryWithResponse(const String& deviceMac, + unsigned long timeoutMs = 5000, SwitchC6ParsedData_t* response = nullptr); + + /** + * @brief Check if there are any pending response waits. + * 检查是否有等待中的响应请求 + * + * @return true if waiting for any response, false otherwise + */ + bool isWaitingForResponse(); + + /** + * @brief Get response waiting information for a specific device. + * 获取指定设备的响应等待信息 + * + * @param deviceMac Device MAC address + * @param waitInfo Pointer to store wait information + * @return true if device is being waited for, false otherwise + */ + bool getResponseWaitInfo(const String& deviceMac, ResponseWaitInfo_t* waitInfo); + + /** + * @brief Set the resend interval for ESP-NOW communication. + * 设置ESP-NOW通信的重发间隔 + * + * @param intervalMs Resend interval in milliseconds (1-10000ms) + * @return true if interval was set successfully, false if out of range + */ + bool setResendInterval(unsigned long intervalMs); + + /** + * @brief Get the current resend interval. + * 获取当前的重发间隔 + * + * @return Current resend interval in milliseconds + */ + unsigned long getResendInterval(); + + /** + * @brief Clear all discovered peer devices data. + * 清空所有已发现的设备数据 + * + * This function clears all stored peer device information including MAC addresses, + * last received data, signal strength (RSSI), and status information. + * 此函数清空所有存储的设备信息,包括MAC地址、最后接收的数据、信号强度和状态信息。 + * + * @return true if peers data was cleared successfully, false otherwise + */ + bool clearPeersData(); + + /** + * @brief Clear all pending response wait requests. + * 清空所有等待中的响应请求 + * + * This function cancels all pending response wait requests and clears the wait queue. + * 此函数取消所有等待中的响应请求并清空等待队列。 + * + * @return true if response waits were cleared successfully, false otherwise + */ + bool clearResponseWaits(); + + /** + * @brief Clear all historical data including peers and response waits. + * 清空所有历史数据,包括设备信息和响应等待 + * + * This function performs a complete cleanup of all stored data, including + * discovered devices and pending response requests. + * 此函数执行完整的数据清理,包括已发现的设备和等待中的响应请求。 + * + * @return true if all data was cleared successfully, false otherwise + */ + bool clearAllData(); + + /** + * @brief Clear specific device data by MAC address. + * 根据MAC地址清除指定设备数据 + * + * This function removes a specific device from the peer list by its MAC address. + * Both peer data and any pending response waits for this device will be cleared. + * 此函数根据MAC地址从设备列表中移除指定设备。 + * 设备数据和该设备的等待响应都会被清除。 + * + * @param deviceMac Device MAC address in format "XX:XX:XX:XX:XX:XX" or "XXXX-XXXX-XXXX" + * @return true if device was found and cleared successfully, false otherwise + */ + bool clearDeviceData(const String& deviceMac); + + /** + * @brief Clear devices that haven't been seen for a specified time. + * 清除指定时间内未收到消息的设备 + * + * This function removes devices from the peer list if they haven't sent any + * data for longer than the specified timeout period. + * 此函数移除在指定超时时间内未发送任何数据的设备。 + * + * @param timeoutMs Timeout in milliseconds (devices not seen for this long will be removed) + * @return Number of devices that were removed + */ + uint16_t clearExpiredDevices(unsigned long timeoutMs = 300000); // 默认5分钟 + + /** + * @brief Set automatic cleanup interval for expired devices. + * 设置过期设备的自动清理间隔 + * + * This function enables automatic cleanup of devices that haven't been seen + * for a specified period. Set to 0 to disable automatic cleanup. + * 此函数启用对指定时间内未见到的设备的自动清理。设置为0禁用自动清理。 + * + * @param intervalMs Cleanup interval in milliseconds (0 to disable) + * @param deviceTimeoutMs Device timeout in milliseconds (devices not seen for this long will be removed) + * @return true if settings were applied successfully, false otherwise + */ + bool setAutoCleanup(unsigned long intervalMs, unsigned long deviceTimeoutMs = 300000); + + /** + * @brief Check and perform automatic cleanup of expired devices. + * 检查并执行过期设备的自动清理 + * + * This function should be called periodically (e.g., in loop()) to perform + * automatic cleanup of expired devices based on the settings from setAutoCleanup(). + * 此函数应定期调用(例如在loop()中)以基于setAutoCleanup()的设置执行过期设备的自动清理。 + * + * @return Number of devices that were removed, or 0 if no cleanup was needed + */ + uint16_t checkAutoCleanup(); + + +private: + + uint16_t max_broadcast_peers = 16; // 最大广播节点数量 + broadcast_packet_with_mac_t* peers = nullptr; // 广播节点数组 + uint16_t peerCount = 0; // 当前节点数量 + + // 响应管理相关变量 + static const uint16_t MAX_RESPONSE_WAITS = 16; // 最大同时等待响应的数量 + ResponseWaitInfo_t responseWaits[MAX_RESPONSE_WAITS]; // 响应等待数组 + uint16_t responseWaitCount = 0; // 当前等待响应的数量 + unsigned long resendInterval = 0; // 重发间隔(毫秒) + + // 自动清理相关变量 + unsigned long autoCleanupInterval = 0; // 自动清理间隔(毫秒,0表示禁用) + unsigned long deviceTimeout = 300000; // 设备超时时间(毫秒,默认5分钟) + unsigned long lastCleanupTime = 0; // 上次清理时间 + + // 静态实例指针,用于在回调函数中访问类成员 + static M5SwitchC6* instance; + + // MAC地址转字符串的辅助函数 + static String macToString(const uint8_t *mac) { + char buf[18]; + snprintf(buf, sizeof(buf), "%02X:%02X:%02X:%02X:%02X:%02X", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + return String(buf); + } + + /** + * @brief Acquires a mutex lock. + * + * This function attempts to acquire a mutex lock to ensure thread-safe access + * to shared resources. It should be paired with a corresponding call to + * releaseMutex() to prevent deadlocks. + */ + #if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266) + /** FreeRTOS 互斥锁句柄 */ + static SemaphoreHandle_t mutexLock; + + /** 队列相关静态变量 */ + static QueueHandle_t dataQueue; + static TaskHandle_t parseTaskHandle; + + /** 队列相关方法 */ + static void parseDataTask(void* parameter); + bool enqueueData(const uint8_t* data, int len); + bool enqueueData(const uint8_t* data, int len, const uint8_t* senderMac); + #endif + /** 获取互斥锁 */ + void acquireMutex(); + + /** + * @brief Releases a mutex lock. + * + * This function releases a previously acquired mutex lock, allowing other + * threads to access shared resources. It should only be called after + * successfully acquiring the mutex with acquireMutex(). + */ + /** 释放互斥锁 */ + void releaseMutex(); + + /** + * @brief Helper functions for data parsing + */ + bool isBroadcastAddress(const String& macStr); + bool isValidMacFormat(const String& macStr); + String normalizeMacAddress(const String& macStr); + float parseVoltage(const String& voltageStr); + + /** + * @brief Helper functions for response management + */ + bool addResponseWait(const String& deviceMac, unsigned long timeoutMs); + void removeResponseWait(const String& deviceMac); + int findResponseWait(const String& deviceMac); + void checkResponseTimeouts(); + void handleReceivedResponse(const SwitchC6ParsedData_t& parsedData); + void handleReceivedResponse(const SwitchC6ParsedData_t& parsedData, const String& senderMac); + + +}; + +#endif // __M5SWITCHC6_H__ \ No newline at end of file