{"id":20612913,"url":"https://github.com/78/esp-ml307","last_synced_at":"2025-10-21T06:50:34.466Z","repository":{"id":262778468,"uuid":"864420714","full_name":"78/esp-ml307","owner":"78","description":"ESP32 ML307 AT Modem","archived":false,"fork":false,"pushed_at":"2025-09-25T23:48:26.000Z","size":168,"stargazers_count":48,"open_issues_count":4,"forks_count":34,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-09-26T01:26:00.258Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C++","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/78.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2024-09-28T07:05:49.000Z","updated_at":"2025-09-25T23:48:30.000Z","dependencies_parsed_at":"2025-04-15T07:09:33.837Z","dependency_job_id":"aed63946-7999-43b0-85b2-b421883cab84","html_url":"https://github.com/78/esp-ml307","commit_stats":null,"previous_names":["78/esp-ml307"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/78/esp-ml307","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/78%2Fesp-ml307","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/78%2Fesp-ml307/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/78%2Fesp-ml307/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/78%2Fesp-ml307/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/78","download_url":"https://codeload.github.com/78/esp-ml307/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/78%2Fesp-ml307/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":280219365,"owners_count":26292796,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-21T02:00:06.614Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-16T11:08:21.451Z","updated_at":"2025-10-21T06:50:34.459Z","avatar_url":"https://github.com/78.png","language":"C++","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ML307 / Quectel-E Series Cat.1 AT Modem (v3.0)\n\n这是一个适用于 ML307R / EC801E / NT26K LTE Cat.1 模组的组件。\n本项目最初为 https://github.com/78/xiaozhi-esp32 项目创建。\n\n出现 UART_FIFO_OVF 需要设置 CONFIG_UART_ISR_IN_IRAM=y，其他 IO 如 LVGL 放在 CPU1\n\n## 🆕 版本 3.0 新特性\n\n- **自动模组检测**: 自动识别 ML307 和 EC801E 模组\n- **统一接口**: 通过 `NetworkInterface` 基类提供一致的API\n- **智能内存管理**: 使用 `std::unique_ptr` 确保内存安全\n- **简化的API**: 更加直观和易用的接口设计\n\n## 功能特性\n\n- AT 命令\n- MQTT / MQTTS\n- HTTP / HTTPS\n- TCP / SSL TCP\n- UDP\n- WebSocket\n- 自动模组检测和初始化\n\n## 支持的模组\n\n- ML307R\n- ML307A\n- EC801E \\*\n- NT26K \\*\n\n\\* 需要在购买时咨询是否已烧录支持 SSL TCP 的固件\n\n## 快速开始\n\n### 基础用法\n\n```cpp\n#include \"esp_log.h\"\n#include \"at_modem.h\"\n\nstatic const char *TAG = \"ML307_DEMO\";\n\nextern \"C\" void app_main(void) {\n    // 自动检测并初始化模组\n    auto modem = AtModem::Detect(GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_15, 921600);\n    \n    if (!modem) {\n        ESP_LOGE(TAG, \"模组检测失败\");\n        return;\n    }\n    \n    // 设置网络状态回调\n    modem-\u003eOnNetworkStateChanged([](bool ready) {\n        ESP_LOGI(TAG, \"网络状态: %s\", ready ? \"已连接\" : \"已断开\");\n    });\n    \n    // 等待网络就绪\n    NetworkStatus status = modem-\u003eWaitForNetworkReady(30000);\n    if (status != NetworkStatus::Ready) {\n        ESP_LOGE(TAG, \"网络连接失败\");\n        return;\n    }\n    \n    // 打印模组信息\n    ESP_LOGI(TAG, \"模组版本: %s\", modem-\u003eGetModuleRevision().c_str());\n    ESP_LOGI(TAG, \"IMEI: %s\", modem-\u003eGetImei().c_str());\n    ESP_LOGI(TAG, \"ICCID: %s\", modem-\u003eGetIccid().c_str());\n    ESP_LOGI(TAG, \"运营商: %s\", modem-\u003eGetCarrierName().c_str());\n    ESP_LOGI(TAG, \"信号强度: %d\", modem-\u003eGetCsq());\n}\n```\n\n### HTTP 客户端\n\n```cpp\nvoid TestHttp(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    ESP_LOGI(TAG, \"开始 HTTP 测试\");\n\n    // 创建 HTTP 客户端\n    auto http = modem-\u003eCreateHttp(0);\n    \n    // 设置请求头\n    http-\u003eSetHeader(\"User-Agent\", \"Xiaozhi/3.0.0\");\n    http-\u003eSetTimeout(10000);\n    \n    // 发送 GET 请求\n    if (http-\u003eOpen(\"GET\", \"https://httpbin.org/json\")) {\n        ESP_LOGI(TAG, \"HTTP 状态码: %d\", http-\u003eGetStatusCode());\n        ESP_LOGI(TAG, \"响应内容长度: %zu bytes\", http-\u003eGetBodyLength());\n        \n        // 读取响应内容\n        std::string response = http-\u003eReadAll();\n        ESP_LOGI(TAG, \"响应内容: %s\", response.c_str());\n        \n        http-\u003eClose();\n    } else {\n        ESP_LOGE(TAG, \"HTTP 请求失败\");\n    }\n    \n    // unique_ptr 会自动释放内存，无需手动 delete\n}\n```\n\n### MQTT 客户端\n\n```cpp\nvoid TestMqtt(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    ESP_LOGI(TAG, \"开始 MQTT 测试\");\n\n    // 创建 MQTT 客户端\n    auto mqtt = modem-\u003eCreateMqtt(0);\n    \n    // 设置回调函数\n    mqtt-\u003eOnConnected([]() {\n        ESP_LOGI(TAG, \"MQTT 连接成功\");\n    });\n    \n    mqtt-\u003eOnDisconnected([]() {\n        ESP_LOGI(TAG, \"MQTT 连接断开\");\n    });\n    \n    mqtt-\u003eOnMessage([](const std::string\u0026 topic, const std::string\u0026 payload) {\n        ESP_LOGI(TAG, \"收到消息 [%s]: %s\", topic.c_str(), payload.c_str());\n    });\n    \n    // 连接到 MQTT 代理\n    if (mqtt-\u003eConnect(\"broker.emqx.io\", 1883, \"esp32_client\", \"\", \"\")) {\n        // 订阅主题\n        mqtt-\u003eSubscribe(\"test/esp32/message\");\n        \n        // 发布消息\n        mqtt-\u003ePublish(\"test/esp32/hello\", \"Hello from ESP32!\");\n        \n        // 等待一段时间接收消息\n        vTaskDelay(pdMS_TO_TICKS(5000));\n        \n        mqtt-\u003eDisconnect();\n    } else {\n        ESP_LOGE(TAG, \"MQTT 连接失败\");\n    }\n    \n    // unique_ptr 会自动释放内存，无需手动 delete\n}\n```\n\n### WebSocket 客户端\n\n```cpp\nvoid TestWebSocket(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    ESP_LOGI(TAG, \"开始 WebSocket 测试\");\n\n    // 创建 WebSocket 客户端\n    auto ws = modem-\u003eCreateWebSocket(0);\n    \n    // 设置请求头\n    ws-\u003eSetHeader(\"Protocol-Version\", \"3\");\n    \n    // 设置回调函数\n    ws-\u003eOnConnected([]() {\n        ESP_LOGI(TAG, \"WebSocket 连接成功\");\n    });\n    \n    ws-\u003eOnData([](const char* data, size_t length, bool binary) {\n        ESP_LOGI(TAG, \"收到数据: %.*s\", (int)length, data);\n    });\n    \n    ws-\u003eOnDisconnected([]() {\n        ESP_LOGI(TAG, \"WebSocket 连接断开\");\n    });\n    \n    ws-\u003eOnError([](int error) {\n        ESP_LOGE(TAG, \"WebSocket 错误: %d\", error);\n    });\n    \n    // 连接到 WebSocket 服务器\n    if (ws-\u003eConnect(\"wss://echo.websocket.org/\")) {\n        // 发送消息\n        for (int i = 0; i \u003c 5; i++) {\n            std::string message = \"{\\\"type\\\": \\\"ping\\\", \\\"id\\\": \" + std::to_string(i) + \"}\";\n            ws-\u003eSend(message);\n            vTaskDelay(pdMS_TO_TICKS(1000));\n        }\n        \n        ws-\u003eClose();\n    } else {\n        ESP_LOGE(TAG, \"WebSocket 连接失败\");\n    }\n    \n    // unique_ptr 会自动释放内存，无需手动 delete\n}\n```\n\n### TCP 客户端\n\n```cpp\nvoid TestTcp(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    ESP_LOGI(TAG, \"开始 TCP 测试\");\n\n    // 创建 TCP 客户端\n    auto tcp = modem-\u003eCreateTcp(0);\n    \n    // 设置数据接收回调\n    tcp-\u003eOnStream([](const std::string\u0026 data) {\n        ESP_LOGI(TAG, \"TCP 接收数据: %s\", data.c_str());\n    });\n    \n    // 设置断开连接回调\n    tcp-\u003eOnDisconnected([]() {\n        ESP_LOGI(TAG, \"TCP 连接已断开\");\n    });\n    \n    if (tcp-\u003eConnect(\"httpbin.org\", 80)) {\n        // 发送 HTTP 请求\n        std::string request = \"GET /ip HTTP/1.1\\r\\nHost: httpbin.org\\r\\nConnection: close\\r\\n\\r\\n\";\n        int sent = tcp-\u003eSend(request);\n        ESP_LOGI(TAG, \"TCP 发送了 %d 字节\", sent);\n        \n        // 等待接收响应（通过回调处理）\n        vTaskDelay(pdMS_TO_TICKS(3000));\n        \n        tcp-\u003eDisconnect();\n    } else {\n        ESP_LOGE(TAG, \"TCP 连接失败\");\n    }\n    \n    // unique_ptr 会自动释放内存，无需手动 delete\n}\n```\n\n### UDP 客户端\n\n```cpp\nvoid TestUdp(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    ESP_LOGI(TAG, \"开始 UDP 测试\");\n\n    // 创建 UDP 客户端\n    auto udp = modem-\u003eCreateUdp(0);\n    \n    // 设置数据接收回调\n    udp-\u003eOnMessage([](const std::string\u0026 data) {\n        ESP_LOGI(TAG, \"UDP 接收数据: %s\", data.c_str());\n    });\n    \n    // 连接到 UDP 服务器\n    if (udp-\u003eConnect(\"8.8.8.8\", 53)) {\n        // 发送简单的测试数据\n        std::string test_data = \"Hello UDP Server!\";\n        int sent = udp-\u003eSend(test_data);\n        ESP_LOGI(TAG, \"UDP 发送了 %d 字节\", sent);\n        \n        // 等待接收响应（通过回调处理）\n        vTaskDelay(pdMS_TO_TICKS(2000));\n        \n        udp-\u003eDisconnect();\n    } else {\n        ESP_LOGE(TAG, \"UDP 连接失败\");\n    }\n    \n    // unique_ptr 会自动释放内存，无需手动 delete\n}\n```\n\n## 高级用法\n\n### 直接访问 AtUart\n\n```cpp\nvoid DirectAtCommand(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    // 获取共享的 AtUart 实例\n    auto uart = modem-\u003eGetAtUart();\n    \n    // 发送自定义 AT 命令\n    if (uart-\u003eSendCommand(\"AT+CSQ\", 1000)) {\n        std::string response = uart-\u003eGetResponse();\n        ESP_LOGI(TAG, \"信号强度查询结果: %s\", response.c_str());\n    }\n    \n    // 可以在多个地方安全地持有 uart 引用\n    std::shared_ptr\u003cAtUart\u003e my_uart = modem-\u003eGetAtUart();\n    // my_uart 可以在其他线程或对象中安全使用\n}\n```\n\n### 网络状态监控\n\n```cpp\nvoid MonitorNetwork(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    // 监控网络状态变化\n    modem-\u003eOnNetworkStateChanged([\u0026modem](bool ready) {\n        if (ready) {\n            ESP_LOGI(TAG, \"网络已就绪\");\n            ESP_LOGI(TAG, \"信号强度: %d\", modem-\u003eGetCsq());\n            \n            auto reg_state = modem-\u003eGetRegistrationState();\n            ESP_LOGI(TAG, \"注册状态: %s\", reg_state.ToString().c_str());\n        } else {\n            ESP_LOGE(TAG, \"网络连接丢失\");\n        }\n    });\n    \n    // 检查网络状态\n    if (modem-\u003enetwork_ready()) {\n        ESP_LOGI(TAG, \"当前网络状态: 已连接\");\n    } else {\n        ESP_LOGI(TAG, \"当前网络状态: 未连接\");\n    }\n}\n```\n\n### 提前释放网络对象\n\n```cpp\nvoid EarlyReleaseExample(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    // 创建 HTTP 客户端\n    auto http = modem-\u003eCreateHttp(0);\n    \n    // 使用完毕后提前释放\n    http-\u003eClose();\n    http.reset(); // 显式释放内存\n    \n    // 或者让 unique_ptr 在作用域结束时自动释放\n    {\n        auto tcp = modem-\u003eCreateTcp(0);\n        tcp-\u003eConnect(\"example.com\", 80);\n        // 作用域结束时 tcp 自动释放\n    }\n    \n    // 此时 tcp 已经自动释放，可以创建新的连接\n    auto udp = modem-\u003eCreateUdp(0);\n    // ...\n}\n```\n\n## 错误处理\n\n```cpp\nvoid HandleErrors(std::unique_ptr\u003cAtModem\u003e\u0026 modem) {\n    // 等待网络就绪，处理各种错误情况\n    NetworkStatus status = modem-\u003eWaitForNetworkReady(30000);\n    \n    switch (status) {\n        case NetworkStatus::Ready:\n            ESP_LOGI(TAG, \"网络连接成功\");\n            break;\n        case NetworkStatus::ErrorInsertPin:\n            ESP_LOGE(TAG, \"SIM 卡未插入或 PIN 码错误\");\n            break;\n        case NetworkStatus::ErrorRegistrationDenied:\n            ESP_LOGE(TAG, \"网络注册被拒绝\");\n            break;\n        case NetworkStatus::ErrorTimeout:\n            ESP_LOGE(TAG, \"网络连接超时\");\n            break;\n        default:\n            ESP_LOGE(TAG, \"未知网络错误\");\n            break;\n    }\n}\n```\n\n## 迁移指南 (v2.x → v3.0)\n\n### 旧版本 (v2.x)\n\n```cpp\n// 旧方式：需要明确指定模组类型和GPIO引脚\nMl307AtModem modem(GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_15);\nNetworkStatus status = modem.WaitForNetworkReady();\n\nMl307Http http(modem);\nhttp.Open(\"GET\", \"https://example.com\");\n```\n\n### 新版本 (v3.0)\n\n```cpp\n// 新方式：自动检测模组类型，使用智能指针管理内存\nauto modem = AtModem::Detect(GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_15);\nNetworkStatus status = modem-\u003eWaitForNetworkReady();\n\nauto http = modem-\u003eCreateHttp(0);\nhttp-\u003eOpen(\"GET\", \"https://example.com\");\n// 无需手动 delete，unique_ptr 自动管理内存\n```\n\n## 架构优势\n\n1. **自动化**: 无需手动指定模组类型，提高代码通用性\n2. **统一接口**: 不同模组使用相同的API\n3. **代码复用**: 避免重复实现相同功能\n4. **易于维护**: 公共逻辑集中管理\n5. **扩展性**: 便于添加新的模组类型支持\n6. **内存安全**: `std::unique_ptr` 提供自动内存管理，避免内存泄漏\n7. **线程安全**: 支持多线程安全访问\n8. **RAII 原则**: 资源获取即初始化，作用域结束时自动释放\n\n## 注意事项\n\n1. 构造函数已变化，现在使用 `AtModem::Detect()` 方法\n2. 协议客户端需要通过 `CreateXxx()` 方法创建，返回 `std::unique_ptr`\n3. **无需手动 delete**，`std::unique_ptr` 会自动管理内存\n4. 网络状态通过回调函数异步通知\n5. `GetAtUart()` 返回 `shared_ptr\u003cAtUart\u003e`，支持安全共享\n6. 如果需要提前释放网络对象，可以调用 `.reset()` 方法\n7. 所有网络接口方法现在都有默认参数 `connect_id = -1`\n\n## 作者\n\n- 虾哥 Terrence (terrence@tenclass.com)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F78%2Fesp-ml307","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F78%2Fesp-ml307","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F78%2Fesp-ml307/lists"}