feat: ESP32-S3 双核并行摄像头系统 (AsyncUDP + ESP-NOW)
- 主程序: src/main.cpp - AP模式摄像头推流 + AsyncUDP中断通讯 - 通信模块: UDPPeer类 (UDP + ESP-NOW双通道, Flash配置持久化) - 参考实现: ESPNOW通讯 / bkespnow版本 / 普通连接版本 - clangd配置: --target=arm-none-eabi + __XTENSA__宏 + 工具链sys-include - 硬件: ESP32-S3-WROOM-1-N16R8 + OV2640, 16MB Flash, Octal PSRAM
This commit is contained in:
34
jxbeye/.clangd
Normal file
34
jxbeye/.clangd
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
CompileFlags:
|
||||||
|
Compiler: /home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/bin/xtensa-esp32s3-elf-g++
|
||||||
|
Add:
|
||||||
|
- -D__XTENSA__
|
||||||
|
- -D__xtensa__
|
||||||
|
- -D__XTENSA_EL__
|
||||||
|
- -D__XTENSA_WINDOWED_ABI__
|
||||||
|
- --target=arm-none-eabi
|
||||||
|
- -Wno-unknown-warning-option
|
||||||
|
- -isystem/home/fallensigh/Dev/craic/jxbeye/clangd_compat
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/xtensa-esp32s3-elf/include/c++/8.4.0
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/xtensa-esp32s3-elf/include/c++/8.4.0/xtensa-esp32s3-elf
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/xtensa-esp32s3-elf/include/c++/8.4.0/backward
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/lib/gcc/xtensa-esp32s3-elf/8.4.0/include
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/lib/gcc/xtensa-esp32s3-elf/8.4.0/include-fixed
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/xtensa-esp32s3-elf/sys-include
|
||||||
|
- -isystem/home/fallensigh/.platformio/packages/toolchain-xtensa-esp32s3/xtensa-esp32s3-elf/include
|
||||||
|
Remove:
|
||||||
|
- -mlongcalls
|
||||||
|
- -fstrict-volatile-bitfields
|
||||||
|
- -fno-tree-switch-conversion
|
||||||
|
- -mno-speculative-load-hardening
|
||||||
|
- -fno-split-wide-types
|
||||||
|
- -fbranch-target-load-optimize2
|
||||||
|
- -fno-jump-tables
|
||||||
|
- -freorder-blocks
|
||||||
|
- -fstack-protector
|
||||||
|
|
||||||
|
Diagnostics:
|
||||||
|
UnusedIncludes: Strict
|
||||||
|
Suppress:
|
||||||
|
- drv_unknown_argument
|
||||||
|
- drv_unknown_argument_with_suggestion
|
||||||
|
- fatal_too_many_errors
|
||||||
6
jxbeye/.gitignore
vendored
Normal file
6
jxbeye/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.pio
|
||||||
|
.vscode/.browse.c_cpp.db*
|
||||||
|
.vscode/c_cpp_properties.json
|
||||||
|
.vscode/launch.json
|
||||||
|
.vscode/ipch
|
||||||
|
clangd_compat/
|
||||||
10
jxbeye/.vscode/extensions.json
vendored
Normal file
10
jxbeye/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
// See http://go.microsoft.com/fwlink/?LinkId=827846
|
||||||
|
// for the documentation about the extensions.json format
|
||||||
|
"recommendations": [
|
||||||
|
"platformio.platformio-ide"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": [
|
||||||
|
"ms-vscode.cpptools-extension-pack"
|
||||||
|
]
|
||||||
|
}
|
||||||
52
jxbeye/boards/esp32-s3-wroom-1-n16r8.json
Normal file
52
jxbeye/boards/esp32-s3-wroom-1-n16r8.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"build": {
|
||||||
|
"arduino": {
|
||||||
|
"ldscript": "esp32s3_out.ld",
|
||||||
|
"memory_type": "qio_opi",
|
||||||
|
"partitions": "default_16MB.csv"
|
||||||
|
},
|
||||||
|
"core": "esp32",
|
||||||
|
"__comment_1": "构建配置:启用 USB CDC模式DARDUINO_USB_MODE和DARDUINO_USB_CDC_ON_BOOT均设置为1,否则设置为0",
|
||||||
|
"__comment_2": "构建配置:wsl上课适合关闭USB CDC模式,未来中小学适合开启USB CDC,并去掉usb转串口芯片",
|
||||||
|
"extra_flags": [
|
||||||
|
"-DARDUINO_ESP32_S3R8N16",
|
||||||
|
"-DBOARD_HAS_PSRAM",
|
||||||
|
"-DARDUINO_USB_MODE=0",
|
||||||
|
"-DARDUINO_USB_CDC_ON_BOOT=0",
|
||||||
|
"-DARDUINO_RUNNING_CORE=1",
|
||||||
|
"-DARDUINO_EVENT_RUNNING_CORE=1"
|
||||||
|
],
|
||||||
|
"f_cpu": "240000000L",
|
||||||
|
"f_flash": "80000000L",
|
||||||
|
"flash_mode": "qio",
|
||||||
|
"hwids": [
|
||||||
|
[
|
||||||
|
"0x303A",
|
||||||
|
"0x1001"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"mcu": "esp32s3",
|
||||||
|
"variant": "esp32_s3r8n16"
|
||||||
|
},
|
||||||
|
"connectivity": [
|
||||||
|
"bluetooth",
|
||||||
|
"wifi"
|
||||||
|
],
|
||||||
|
"debug": {
|
||||||
|
"openocd_target": "esp32s3.cfg"
|
||||||
|
},
|
||||||
|
"frameworks": [
|
||||||
|
"arduino",
|
||||||
|
"espidf"
|
||||||
|
],
|
||||||
|
"name": "ESP32 16MB (ESP32S3-R8N16)",
|
||||||
|
"upload": {
|
||||||
|
"flash_size": "16MB",
|
||||||
|
"maximum_ram_size": 327680,
|
||||||
|
"maximum_size": 16777216,
|
||||||
|
"require_upload_port": true,
|
||||||
|
"speed": 1000000
|
||||||
|
},
|
||||||
|
"url": "https://roboting.com.cn/products",
|
||||||
|
"vendor": "wifi_sensors"
|
||||||
|
}
|
||||||
380
jxbeye/compile_commands.json
Normal file
380
jxbeye/compile_commands.json
Normal file
File diff suppressed because one or more lines are too long
37
jxbeye/include/README
Normal file
37
jxbeye/include/README
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
This directory is intended for project header files.
|
||||||
|
|
||||||
|
A header file is a file containing C declarations and macro definitions
|
||||||
|
to be shared between several project source files. You request the use of a
|
||||||
|
header file in your project source file (C, C++, etc) located in `src` folder
|
||||||
|
by including it, with the C preprocessing directive `#include'.
|
||||||
|
|
||||||
|
```src/main.c
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including a header file produces the same results as copying the header file
|
||||||
|
into each source file that needs it. Such copying would be time-consuming
|
||||||
|
and error-prone. With a header file, the related declarations appear
|
||||||
|
in only one place. If they need to be changed, they can be changed in one
|
||||||
|
place, and programs that include the header file will automatically use the
|
||||||
|
new version when next recompiled. The header file eliminates the labor of
|
||||||
|
finding and changing all the copies as well as the risk that a failure to
|
||||||
|
find one copy will result in inconsistencies within a program.
|
||||||
|
|
||||||
|
In C, the convention is to give header files names that end with `.h'.
|
||||||
|
|
||||||
|
Read more about using header files in official GCC documentation:
|
||||||
|
|
||||||
|
* Include Syntax
|
||||||
|
* Include Operation
|
||||||
|
* Once-Only Headers
|
||||||
|
* Computed Includes
|
||||||
|
|
||||||
|
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||||
46
jxbeye/lib/README
Normal file
46
jxbeye/lib/README
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
This directory is intended for project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link into the executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in a separate directory
|
||||||
|
("lib/your_library_name/[Code]").
|
||||||
|
|
||||||
|
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||||
|
| |
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |
|
||||||
|
| |- README --> THIS FILE
|
||||||
|
|
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
Example contents of `src/main.c` using Foo and Bar:
|
||||||
|
```
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||||
|
libraries by scanning project source files.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||||
37
jxbeye/platformio.ini
Normal file
37
jxbeye/platformio.ini
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32s3cam]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-s3-wroom-1-n16r8
|
||||||
|
framework = arduino
|
||||||
|
;esp32-s3-wroom-1-n16r8 ;4d_systems_esp32s3_gen4_r8n16
|
||||||
|
; === 关键修正:PSRAM 配置 ===
|
||||||
|
board_build.psram_type = octal ; ✅ 官方标准标识符(非 opi)
|
||||||
|
board_build.memory_type = qio_octal ; ✅ Flash=QIO, PSRAM=Octal
|
||||||
|
board_build.partitions = default_16MB.csv ; ← 显式指定分区表
|
||||||
|
|
||||||
|
; === 附加必要配置 ===
|
||||||
|
board_build.flash_mode = qio ; ✅ Flash 模式必须为 qio(Quad SPI)
|
||||||
|
board_build.flash_freq = 80m ; ✅ 与 PSRAM 速度匹配
|
||||||
|
board_build.flash_size = 16MB
|
||||||
|
|
||||||
|
|
||||||
|
; === 编译选项 ===
|
||||||
|
build_flags =
|
||||||
|
-DBOARD_HAS_PSRAM
|
||||||
|
-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_INFO
|
||||||
|
|
||||||
|
; upload_port = /dev/ttyCH340
|
||||||
|
; monitor_port = /dev/ttyCH340
|
||||||
|
monitor_speed = 1000000
|
||||||
|
monitor_rts = 0
|
||||||
|
monitor_dtr = 0
|
||||||
|
monitor_filters = default, send_on_enter, time
|
||||||
410
jxbeye/src/EPSNOW通讯
Normal file
410
jxbeye/src/EPSNOW通讯
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
#define ESP_NOW_HOST
|
||||||
|
// ===== 配置宏 =====
|
||||||
|
#ifndef ESP_NOW_DEFAULT_CHANNEL
|
||||||
|
#define ESP_NOW_DEFAULT_CHANNEL 6
|
||||||
|
#endif
|
||||||
|
#ifndef ESP_NOW_MAX_SLAVES
|
||||||
|
#define ESP_NOW_MAX_SLAVES 20
|
||||||
|
#endif
|
||||||
|
#ifndef ESP_NOW_CFG_INTERVAL
|
||||||
|
#define ESP_NOW_CFG_INTERVAL 500
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Flash Key
|
||||||
|
#define HOST_CFG_NS "host_cfg"
|
||||||
|
#define HOST_CHANNEL_KEY "channel"
|
||||||
|
#define SLAVES_NS "slaves"
|
||||||
|
#define SLAVE_CFG_NS "slave_cfg"
|
||||||
|
#define SLAVE_ID_KEY "id"
|
||||||
|
#define SLAVE_HOST_KEY "host"
|
||||||
|
#define SLAVE_CHANNEL_KEY "channel"
|
||||||
|
|
||||||
|
class EspNowPeer {
|
||||||
|
private:
|
||||||
|
bool _initialized = false;
|
||||||
|
bool _ready = false;
|
||||||
|
uint8_t _myId;
|
||||||
|
uint8_t _hostMac[6];
|
||||||
|
uint8_t _currentChannel;
|
||||||
|
|
||||||
|
#ifdef ESP_NOW_HOST
|
||||||
|
uint8_t _slaveMacs[ESP_NOW_MAX_SLAVES + 1][6];
|
||||||
|
bool _slaveActive[ESP_NOW_MAX_SLAVES + 1];
|
||||||
|
Preferences _prefs;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
char _recvBuf[256];
|
||||||
|
bool _hasMsg = false;
|
||||||
|
String _parsedValue;
|
||||||
|
|
||||||
|
volatile bool _waitingReg = false;
|
||||||
|
volatile uint8_t _waitingId = 0;
|
||||||
|
volatile bool _regReceived = false;
|
||||||
|
|
||||||
|
static bool _str2mac(const char* s, uint8_t* m) {
|
||||||
|
return sscanf(s, "%hhx:%hhx:%hhx:%hhx:%hhx:%hhx",
|
||||||
|
&m[0],&m[1],&m[2],&m[3],&m[4],&m[5]) == 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _mac2str(const uint8_t* m, char* s) {
|
||||||
|
sprintf(s, "%02X:%02X:%02X:%02X:%02X:%02X", m[0],m[1],m[2],m[3],m[4],m[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void _onRecv(const uint8_t* mac, const uint8_t* data, int len) {
|
||||||
|
if(_instance) _instance->_handleRecv(mac, data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleRecv(const uint8_t* mac, const uint8_t* data, int len) {
|
||||||
|
if(len >= 256) return;
|
||||||
|
memcpy(_recvBuf, data, len);
|
||||||
|
_recvBuf[len] = '\0';
|
||||||
|
_hasMsg = true;
|
||||||
|
|
||||||
|
#ifdef ESP_NOW_HOST
|
||||||
|
if(_waitingReg && getMessageValue("climac")) {
|
||||||
|
String val = lastValue();
|
||||||
|
int dashPos = val.indexOf('-');
|
||||||
|
if(dashPos > 0) {
|
||||||
|
uint8_t recv_id = val.substring(0, dashPos).toInt();
|
||||||
|
if(recv_id == _waitingId && recv_id >= 1 && recv_id <= ESP_NOW_MAX_SLAVES) {
|
||||||
|
String macStr = val.substring(dashPos + 1);
|
||||||
|
uint8_t recv_mac[6];
|
||||||
|
if(_str2mac(macStr.c_str(), recv_mac)) {
|
||||||
|
_regReceived = true;
|
||||||
|
_registerSlave(recv_id, recv_mac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef ESP_NOW_HOST
|
||||||
|
bool _saveChannel(uint8_t ch) {
|
||||||
|
Preferences prefs;
|
||||||
|
if(prefs.begin(HOST_CFG_NS, false)) {
|
||||||
|
prefs.putUChar(HOST_CHANNEL_KEY, ch);
|
||||||
|
prefs.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t _loadChannel() {
|
||||||
|
Preferences prefs;
|
||||||
|
if(prefs.begin(HOST_CFG_NS, true)) {
|
||||||
|
uint8_t ch = prefs.getUChar(HOST_CHANNEL_KEY, 0);
|
||||||
|
prefs.end();
|
||||||
|
if(ch >= 1 && ch <= 13) return ch;
|
||||||
|
}
|
||||||
|
return ESP_NOW_DEFAULT_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _changeChannel(uint8_t newChannel) {
|
||||||
|
if(newChannel < 1 || newChannel > 13) return false;
|
||||||
|
if(newChannel == _currentChannel) return true;
|
||||||
|
|
||||||
|
_saveChannel(newChannel);
|
||||||
|
esp_wifi_set_channel(newChannel, WIFI_SECOND_CHAN_NONE);
|
||||||
|
_currentChannel = newChannel;
|
||||||
|
|
||||||
|
esp_now_deinit();
|
||||||
|
delay(100);
|
||||||
|
if(esp_now_init() != ESP_OK) return false;
|
||||||
|
esp_now_register_recv_cb(_onRecv);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _registerSlave(uint8_t id, uint8_t* mac) {
|
||||||
|
if(id < 1 || id > ESP_NOW_MAX_SLAVES) return false;
|
||||||
|
if(_slaveActive[id] && memcmp(_slaveMacs[id], mac, 6) == 0) return true;
|
||||||
|
|
||||||
|
memcpy(_slaveMacs[id], mac, 6);
|
||||||
|
_slaveActive[id] = true;
|
||||||
|
|
||||||
|
_prefs.begin(SLAVES_NS, false);
|
||||||
|
_prefs.putBytes((String("s")+String(id)).c_str(), mac, 6);
|
||||||
|
_prefs.end();
|
||||||
|
|
||||||
|
esp_now_peer_info_t p = {};
|
||||||
|
memcpy(p.peer_addr, mac, 6);
|
||||||
|
p.channel = _currentChannel;
|
||||||
|
p.encrypt = false;
|
||||||
|
esp_now_add_peer(&p);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* _findMac(uint8_t id) {
|
||||||
|
if(id < 1 || id > ESP_NOW_MAX_SLAVES) return nullptr;
|
||||||
|
return _slaveActive[id] ? _slaveMacs[id] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadSlaves() {
|
||||||
|
for(int i=0; i<=ESP_NOW_MAX_SLAVES; i++) _slaveActive[i] = false;
|
||||||
|
_prefs.begin(SLAVES_NS, true);
|
||||||
|
for(uint8_t id=1; id<=ESP_NOW_MAX_SLAVES; id++) {
|
||||||
|
uint8_t mac[6];
|
||||||
|
if(_prefs.getBytes((String("s")+String(id)).c_str(), mac, 6) == 6) {
|
||||||
|
memcpy(_slaveMacs[id], mac, 6);
|
||||||
|
_slaveActive[id] = true;
|
||||||
|
esp_now_peer_info_t p = {};
|
||||||
|
memcpy(p.peer_addr, mac, 6);
|
||||||
|
p.channel = _currentChannel;
|
||||||
|
p.encrypt = false;
|
||||||
|
esp_now_add_peer(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_prefs.end();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
bool _send(const uint8_t* mac, const char* key, const char* value) {
|
||||||
|
if(!_ready || !mac || !key || !value) return false;
|
||||||
|
char json[250];
|
||||||
|
snprintf(json, sizeof(json), "{\"%s\":\"%s\"}", key, value);
|
||||||
|
int len = strlen(json);
|
||||||
|
if(len > 250) return false;
|
||||||
|
return esp_now_send(mac, (uint8_t*)json, len) == ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static EspNowPeer* _instance;
|
||||||
|
|
||||||
|
public:
|
||||||
|
EspNowPeer() { _instance = this; }
|
||||||
|
|
||||||
|
bool begin(uint8_t myId = 0, const uint8_t* hostMac = nullptr) {
|
||||||
|
if(_initialized) return true;
|
||||||
|
_myId = myId;
|
||||||
|
if(hostMac) memcpy(_hostMac, hostMac, 6);
|
||||||
|
|
||||||
|
#ifdef ESP_NOW_HOST
|
||||||
|
_currentChannel = _loadChannel();
|
||||||
|
#else
|
||||||
|
_currentChannel = _loadChannelSlave();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.disconnect();
|
||||||
|
esp_wifi_set_channel(_currentChannel, WIFI_SECOND_CHAN_NONE);
|
||||||
|
|
||||||
|
if(esp_now_init() != ESP_OK) return false;
|
||||||
|
esp_now_register_recv_cb(_onRecv);
|
||||||
|
|
||||||
|
#ifdef ESP_NOW_HOST
|
||||||
|
_prefs.begin(SLAVES_NS, false);
|
||||||
|
_loadSlaves();
|
||||||
|
_prefs.end();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
_initialized = true;
|
||||||
|
_ready = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 主机公开接口 =====
|
||||||
|
#ifdef ESP_NOW_HOST
|
||||||
|
void setDevice(uint8_t slaveId, uint8_t channel = 0) {
|
||||||
|
if(!_ready) return;
|
||||||
|
if(channel == 0) channel = _currentChannel;
|
||||||
|
if(channel != _currentChannel) _changeChannel(channel);
|
||||||
|
|
||||||
|
uint8_t myMac[6];
|
||||||
|
esp_read_mac(myMac, ESP_MAC_WIFI_STA);
|
||||||
|
char hostMacStr[18];
|
||||||
|
_mac2str(myMac, hostMacStr);
|
||||||
|
|
||||||
|
_waitingReg = true;
|
||||||
|
_waitingId = slaveId;
|
||||||
|
_regReceived = false;
|
||||||
|
|
||||||
|
uint32_t lastSend = 0;
|
||||||
|
while(!_regReceived) {
|
||||||
|
if(millis() - lastSend >= ESP_NOW_CFG_INTERVAL) {
|
||||||
|
Serial1.printf("CFG:%d:%s:%dWZHY\n", slaveId, hostMacStr, channel);
|
||||||
|
Serial.printf("CFG:%d:%s:%dWZHY\n", slaveId, hostMacStr, channel);
|
||||||
|
lastSend = millis();
|
||||||
|
}
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
_waitingReg = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendMessage(uint8_t slaveId, const String& key, const String& value) {
|
||||||
|
if(!_ready) return false;
|
||||||
|
uint8_t* mac = _findMac(slaveId);
|
||||||
|
if(!mac) return false;
|
||||||
|
return _send(mac, key.c_str(), value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getChannel() { return _currentChannel; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ===== 从机公开接口 =====
|
||||||
|
#ifdef ESP_NOW_SLAVE
|
||||||
|
uint8_t _loadChannelSlave() {
|
||||||
|
Preferences prefs;
|
||||||
|
if(prefs.begin(SLAVE_CFG_NS, true)) {
|
||||||
|
uint8_t ch = prefs.getUChar(SLAVE_CHANNEL_KEY, 0);
|
||||||
|
prefs.end();
|
||||||
|
if(ch >= 1 && ch <= 13) return ch;
|
||||||
|
}
|
||||||
|
return ESP_NOW_DEFAULT_CHANNEL;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _saveChannelSlave(uint8_t ch) {
|
||||||
|
Preferences prefs;
|
||||||
|
if(prefs.begin(SLAVE_CFG_NS, false)) {
|
||||||
|
prefs.putUChar(SLAVE_CHANNEL_KEY, ch);
|
||||||
|
prefs.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 核心接口:解析并应用配置
|
||||||
|
bool applyConfig(uint8_t id, const uint8_t* hostMac, uint8_t channel) {
|
||||||
|
bool changed = false;
|
||||||
|
|
||||||
|
// 信道变更
|
||||||
|
if(channel != _currentChannel && channel >= 1 && channel <= 13) {
|
||||||
|
_saveChannelSlave(channel);
|
||||||
|
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
|
||||||
|
_currentChannel = channel;
|
||||||
|
esp_now_deinit();
|
||||||
|
delay(100);
|
||||||
|
esp_now_init();
|
||||||
|
esp_now_register_recv_cb(_onRecv);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID 变更
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin(SLAVE_CFG_NS, false);
|
||||||
|
if(prefs.getUChar(SLAVE_ID_KEY, 0) != id) {
|
||||||
|
prefs.putUChar(SLAVE_ID_KEY, id);
|
||||||
|
_myId = id;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主机 MAC 变更
|
||||||
|
uint8_t storedHost[6];
|
||||||
|
if(prefs.getBytes(SLAVE_HOST_KEY, storedHost, 6) != 6 || memcmp(storedHost, hostMac, 6) != 0) {
|
||||||
|
prefs.putBytes(SLAVE_HOST_KEY, hostMac, 6);
|
||||||
|
memcpy(_hostMac, hostMac, 6);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
prefs.end();
|
||||||
|
|
||||||
|
// 发送 climac 确认
|
||||||
|
_sendRegistration();
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadConfig() {
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin(SLAVE_CFG_NS, true);
|
||||||
|
_myId = prefs.getUChar(SLAVE_ID_KEY, 0);
|
||||||
|
if(_myId == 0) { prefs.end(); return false; }
|
||||||
|
if(prefs.getBytes(SLAVE_HOST_KEY, _hostMac, 6) != 6) { prefs.end(); return false; }
|
||||||
|
prefs.end();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _sendRegistration() {
|
||||||
|
uint8_t myMac[6];
|
||||||
|
esp_read_mac(myMac, ESP_MAC_WIFI_STA);
|
||||||
|
char macStr[18];
|
||||||
|
_mac2str(myMac, macStr);
|
||||||
|
char content[40];
|
||||||
|
snprintf(content, sizeof(content), "%d-%s", _myId, macStr);
|
||||||
|
return _send(_hostMac, "climac", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendMessage(const String& key, const String& value) {
|
||||||
|
if(!_ready) return false;
|
||||||
|
char content[220];
|
||||||
|
snprintf(content, sizeof(content), "cli-%d-%s", _myId, value.c_str());
|
||||||
|
return _send(_hostMac, key.c_str(), content);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t getChannel() { return _currentChannel; }
|
||||||
|
uint8_t getMyId() { return _myId; }
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ===== 通用接口 =====
|
||||||
|
bool hasMessage() {
|
||||||
|
bool has = _hasMsg;
|
||||||
|
_hasMsg = false;
|
||||||
|
return has;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getMessageValue(const String& key) {
|
||||||
|
_parsedValue = "";
|
||||||
|
if(strlen(_recvBuf) == 0) return false;
|
||||||
|
String pattern = "\"" + key + "\":\"";
|
||||||
|
int start = String(_recvBuf).indexOf(pattern);
|
||||||
|
if(start == -1) return false;
|
||||||
|
start += pattern.length();
|
||||||
|
int end = String(_recvBuf).indexOf("\"", start);
|
||||||
|
if(end == -1) return false;
|
||||||
|
_parsedValue = String(_recvBuf).substring(start, end);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastValue() { return _parsedValue; }
|
||||||
|
void clearMessage() { _recvBuf[0] = '\0'; _parsedValue = ""; _hasMsg = false; }
|
||||||
|
bool isReady() { return _ready; }
|
||||||
|
};
|
||||||
|
|
||||||
|
EspNowPeer* EspNowPeer::_instance = nullptr;
|
||||||
|
|
||||||
|
EspNowPeer peer;
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
|
||||||
|
delay(1000);
|
||||||
|
Serial1.begin(115200, SERIAL_8N1, 1, 2);//K10自身管脚不够利用串口接外部芯片扩展
|
||||||
|
// ✅ 初始化主机(ID=255)
|
||||||
|
peer.begin(255);
|
||||||
|
|
||||||
|
Serial.printf("📡 主机就绪 信道=%d\n", peer.getChannel());
|
||||||
|
|
||||||
|
// ===== 关键函数调用示例 =====
|
||||||
|
|
||||||
|
// 示例 1:配置从机#5(使用当前信道)
|
||||||
|
peer.setDevice(5);
|
||||||
|
|
||||||
|
// 示例 2:配置从机#5 并切换到信道 6
|
||||||
|
// peer.setDevice(5, 6);
|
||||||
|
|
||||||
|
// 示例 3:发送消息到从机#5
|
||||||
|
// peer.sendMessage(5, "c", "GO");
|
||||||
|
|
||||||
|
// 示例 4:发送消息到从机#5(带参数)
|
||||||
|
// peer.sendMessage(5, "set", "speed:100");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// ✅ 接收从机消息
|
||||||
|
if(peer.hasMessage()) {
|
||||||
|
if(peer.getMessageValue("c")) {
|
||||||
|
String val = peer.lastValue();
|
||||||
|
// 解析 cli-<id>-<content> 格式
|
||||||
|
if(val.startsWith("cli-")) {
|
||||||
|
int d1 = val.indexOf('-', 4);
|
||||||
|
if(d1 > 0) {
|
||||||
|
String idStr = val.substring(4, d1);
|
||||||
|
String content = val.substring(d1 + 1);
|
||||||
|
Serial.printf("📥 从机#%s: %s\n", idStr.c_str(), content.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 实际使用时,根据业务逻辑调用:
|
||||||
|
// peer.setDevice(5, 6); // 配置从机
|
||||||
|
peer.sendMessage(5, "c", "GO"); // 发送命令
|
||||||
|
|
||||||
|
delay(100);
|
||||||
|
}
|
||||||
911
jxbeye/src/bkespnow版本
Normal file
911
jxbeye/src/bkespnow版本
Normal file
@@ -0,0 +1,911 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include "esp_camera.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <esp_now.h> // ✅【改动 1】添加 ESP-NOW 头文件
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
// ===== UDP通信类(最小改动)=====
|
||||||
|
class UDPPeer {
|
||||||
|
private:
|
||||||
|
WiFiUDP udp;
|
||||||
|
bool ready = false;
|
||||||
|
uint16_t local_port = 8888;
|
||||||
|
|
||||||
|
// 双缓存
|
||||||
|
String receivedRaw;
|
||||||
|
String parsedValue;
|
||||||
|
|
||||||
|
// 连接参数
|
||||||
|
const char* ssid = nullptr;
|
||||||
|
const char* password = nullptr;
|
||||||
|
IPAddress local_ip;
|
||||||
|
IPAddress gateway;
|
||||||
|
IPAddress subnet;
|
||||||
|
|
||||||
|
// ✅ 监视器IP最后一位
|
||||||
|
uint8_t monitor_ip_last = 101;
|
||||||
|
|
||||||
|
// ✅ 改回普通成员变量(非静态)
|
||||||
|
bool is_initialized = false;
|
||||||
|
|
||||||
|
uint32_t last_maintenance = 0;
|
||||||
|
const uint32_t MAINTENANCE_INTERVAL = 1000;
|
||||||
|
|
||||||
|
// ✅【改动 2】新增:ESP-NOW消息标志
|
||||||
|
bool espnowready=false;
|
||||||
|
bool espnowHasMsg=false;
|
||||||
|
|
||||||
|
void autoMaintain() {
|
||||||
|
if (!is_initialized) return;
|
||||||
|
|
||||||
|
if (millis() - last_maintenance < MAINTENANCE_INTERVAL) return;
|
||||||
|
last_maintenance = millis();
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
WiFi.config(local_ip, gateway, subnet);
|
||||||
|
if (!ready)
|
||||||
|
{
|
||||||
|
//WiFi.disconnect(true);
|
||||||
|
//delay(500);
|
||||||
|
Serial.printf("正在启动网络连接!!!\n");
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(500);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(500);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(500);
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(500);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.printf("网络中断,恢复中!!!\n");
|
||||||
|
WiFi.reconnect();
|
||||||
|
//esp_now_deinit();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
if (ready) {
|
||||||
|
//initEspNowRecv();
|
||||||
|
Serial.printf("✅ 网络就绪: IP=%s 端口=%d\n",
|
||||||
|
WiFi.localIP().toString().c_str(), local_port);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ✅ 新增:访问 is_initialized 的成员方法
|
||||||
|
bool isInitialized() {
|
||||||
|
return is_initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool wifiisready() {
|
||||||
|
return ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool espnowisready(){
|
||||||
|
return espnowready;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setespnowreadyflg()
|
||||||
|
{
|
||||||
|
espnowready=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void resetespnowreadyflg()
|
||||||
|
{
|
||||||
|
espnowready=false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 设置监视器IP
|
||||||
|
void setMonitorIPLast(uint8_t last_octet) {
|
||||||
|
monitor_ip_last = last_octet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 获取完整监视器IP
|
||||||
|
String getMonitorIP() {
|
||||||
|
return String(gateway[0]) + "." + String(gateway[1]) + "." +
|
||||||
|
String(gateway[2]) + "." + String(monitor_ip_last);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connect(const char* _ssid, const char* _pwd,
|
||||||
|
uint8_t gw1, uint8_t gw2, uint8_t gw3, uint8_t gw4,
|
||||||
|
uint8_t local_ip_last, uint16_t port = 8888) {
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
last_maintenance = 0;
|
||||||
|
ready = false;
|
||||||
|
espnowHasMsg = false; // ✅ 重置ESP-NOW标志
|
||||||
|
|
||||||
|
ssid = _ssid;
|
||||||
|
password = _pwd;
|
||||||
|
local_ip = IPAddress(gw1, gw2, gw3, local_ip_last);
|
||||||
|
gateway = IPAddress(gw1, gw2, gw3, gw4);
|
||||||
|
subnet = IPAddress(255, 255, 255, 0);
|
||||||
|
local_port = port;
|
||||||
|
is_initialized = true;
|
||||||
|
|
||||||
|
Serial.printf("📶 wifi:%s, pwd:%s,配置网络: IP=%d.%d.%d.%d 网关=%d.%d.%d.%d 端口=%d\n",
|
||||||
|
ssid, password, gw1, gw2, gw3, local_ip_last, gw1, gw2, gw3, gw4, port);
|
||||||
|
Serial.println("💡 网络将在后台自动连接...");
|
||||||
|
|
||||||
|
autoMaintain();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【改动 3】修改hasMessage():优先检查ESP-NOW
|
||||||
|
bool hasMessage() {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
// 优先检查ESP-NOW消息
|
||||||
|
if (espnowHasMsg) {
|
||||||
|
return true; // 数据已在receivedRaw中
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再检查UDP消息(原有逻辑)
|
||||||
|
receivedRaw = "";
|
||||||
|
parsedValue = "";
|
||||||
|
|
||||||
|
int len = udp.parsePacket();
|
||||||
|
if (len <= 0 || len > 512) return false;
|
||||||
|
|
||||||
|
char buf[513];
|
||||||
|
int read = udp.read((uint8_t*)buf, min(len, 512));
|
||||||
|
if (read > 0) {
|
||||||
|
buf[read] = '\0';
|
||||||
|
receivedRaw = String(buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getMessageValue(const String& key) {
|
||||||
|
parsedValue = "";
|
||||||
|
if (receivedRaw.length() == 0) return false;
|
||||||
|
|
||||||
|
String keyPattern = "\"" + key + "\"";
|
||||||
|
int startIndex = receivedRaw.indexOf(keyPattern);
|
||||||
|
if (startIndex == -1) return false;
|
||||||
|
|
||||||
|
int colonIndex = receivedRaw.indexOf(":", startIndex);
|
||||||
|
if (colonIndex == -1) return false;
|
||||||
|
|
||||||
|
int valueStart = colonIndex + 1;
|
||||||
|
while (valueStart < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueStart) == ' ') valueStart++;
|
||||||
|
|
||||||
|
if (valueStart < receivedRaw.length() && receivedRaw.charAt(valueStart) == '"') {
|
||||||
|
int valueEnd = receivedRaw.indexOf("\"", valueStart + 1);
|
||||||
|
if (valueEnd != -1) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart + 1, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int valueEnd = valueStart;
|
||||||
|
while (valueEnd < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ',' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != '}' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ' ') valueEnd++;
|
||||||
|
while (valueEnd > valueStart && receivedRaw.charAt(valueEnd - 1) == ' ') valueEnd--;
|
||||||
|
if (valueEnd > valueStart) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const char* format, ...) {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready)
|
||||||
|
{
|
||||||
|
Serial.println("连接还未完成!!!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
IPAddress target_ip;
|
||||||
|
if (!target_ip.fromString(target_ip_str)) return false;
|
||||||
|
|
||||||
|
char content[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(content, sizeof(content) - 1, format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
char json[300];
|
||||||
|
snprintf(json, sizeof(json), "{\"%s\":\"%s\"}\r\n", key.c_str(), content);
|
||||||
|
|
||||||
|
return udp.beginPacket(target_ip, local_port) &&
|
||||||
|
udp.write((const uint8_t*)json, strlen(json)) &&
|
||||||
|
udp.endPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const String& value) {
|
||||||
|
return sendTo(target_ip_str, key, "%s", value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastValue() { return parsedValue; }
|
||||||
|
String lastMessage() { return receivedRaw; }
|
||||||
|
String getLocalIP() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized ? WiFi.localIP().toString() : "未配置";
|
||||||
|
}
|
||||||
|
bool isConnected() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized && ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【改动 4】新增:获取本机MAC地址
|
||||||
|
String getMacAddress() {
|
||||||
|
return WiFi.macAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【改动 5】新增:设置ESP-NOW消息(回调调用)
|
||||||
|
void setEspNowMessage(const char* data, int len) {
|
||||||
|
if (len <= 0 || len > 250) return; // ESP-NOW最大250字节
|
||||||
|
char buf[251];
|
||||||
|
memcpy(buf, data, len);
|
||||||
|
buf[len] = '\0';
|
||||||
|
receivedRaw = String(buf);
|
||||||
|
espnowHasMsg = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【改动 6】新增:清除ESP-NOW标志(解析后调用)
|
||||||
|
void clearEspNowFlag() {
|
||||||
|
espnowHasMsg = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern UDPPeer peer;
|
||||||
|
UDPPeer peer;
|
||||||
|
|
||||||
|
// ✅【改动 7】ESP-NOW接收回调(直接调用peer方法)
|
||||||
|
static void OnEspNowRecv(const uint8_t *mac, const uint8_t *data, int len) {
|
||||||
|
peer.setEspNowMessage((const char*)data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅【改动 8】初始化ESP-NOW接收
|
||||||
|
bool initEspNowRecv() {
|
||||||
|
if (esp_now_init() != ESP_OK) {
|
||||||
|
Serial.println("❌ ESP-NOW初始化失败");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_register_recv_cb(OnEspNowRecv);
|
||||||
|
Serial.println("✅ ESP-NOW接收已启用");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 默认配置 =====
|
||||||
|
#define DEFAULT_SSID "ESP32-Camera"
|
||||||
|
#define DEFAULT_PWD "12345678"
|
||||||
|
#define DEFAULT_GW1 192
|
||||||
|
#define DEFAULT_GW2 168
|
||||||
|
#define DEFAULT_GW3 4
|
||||||
|
#define DEFAULT_GW4 1
|
||||||
|
#define DEFAULT_LOCAL_LAST 100
|
||||||
|
#define DEFAULT_MONITOR_LAST 101
|
||||||
|
|
||||||
|
// ===== Flash配置管理 =====
|
||||||
|
void loadConfigFromFlash() {
|
||||||
|
Preferences prefs;
|
||||||
|
if (!prefs.begin("cam_cfg", true)) {
|
||||||
|
Serial.println("⚠️ Flash配置不存在,使用默认值");
|
||||||
|
peer.connect(DEFAULT_SSID, DEFAULT_PWD,
|
||||||
|
DEFAULT_GW1, DEFAULT_GW2, DEFAULT_GW3, DEFAULT_GW4,
|
||||||
|
DEFAULT_LOCAL_LAST);
|
||||||
|
peer.setMonitorIPLast(DEFAULT_MONITOR_LAST);
|
||||||
|
prefs.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ssid = prefs.getString("ssid", DEFAULT_SSID);
|
||||||
|
String pwd = prefs.getString("pwd", DEFAULT_PWD);
|
||||||
|
uint8_t gw1 = prefs.getUChar("gw1", DEFAULT_GW1);
|
||||||
|
uint8_t gw2 = prefs.getUChar("gw2", DEFAULT_GW2);
|
||||||
|
uint8_t gw3 = prefs.getUChar("gw3", DEFAULT_GW3);
|
||||||
|
uint8_t gw4 = prefs.getUChar("gw4", DEFAULT_GW4);
|
||||||
|
uint8_t local_last = prefs.getUChar("local", DEFAULT_LOCAL_LAST);
|
||||||
|
uint8_t monitor_last = prefs.getUChar("monitor", DEFAULT_MONITOR_LAST);
|
||||||
|
|
||||||
|
prefs.end();
|
||||||
|
|
||||||
|
peer.connect(ssid.c_str(), pwd.c_str(), gw1, gw2, gw3, gw4, local_last);
|
||||||
|
peer.setMonitorIPLast(monitor_last);
|
||||||
|
|
||||||
|
Serial.println("✅ 从Flash加载配置成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveConfigToFlash(const char* ssid, const char* pwd,
|
||||||
|
uint8_t gw1, uint8_t gw2, uint8_t gw3, uint8_t gw4,
|
||||||
|
uint8_t local_last, uint8_t monitor_last) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (!prefs.begin("cam_cfg", false)) {
|
||||||
|
Serial.println("❌ Flash配置保存失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.putString("ssid", ssid);
|
||||||
|
prefs.putString("pwd", pwd);
|
||||||
|
prefs.putUChar("gw1", gw1);
|
||||||
|
prefs.putUChar("gw2", gw2);
|
||||||
|
prefs.putUChar("gw3", gw3);
|
||||||
|
prefs.putUChar("gw4", gw4);
|
||||||
|
prefs.putUChar("local", local_last);
|
||||||
|
prefs.putUChar("monitor", monitor_last);
|
||||||
|
|
||||||
|
prefs.end();
|
||||||
|
Serial.println("✅ 配置已保存到Flash");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 串口指令解析 =====
|
||||||
|
void handleSerialCommand() {
|
||||||
|
if (!Serial.available()) return;
|
||||||
|
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
|
||||||
|
if (!cmd.startsWith("wificar:"))
|
||||||
|
{
|
||||||
|
if(cmd.endsWith("LZU")||cmd.endsWith("LZU\r"))
|
||||||
|
{
|
||||||
|
peer.sendTo(peer.getMonitorIP(), "f", cmd);
|
||||||
|
Serial.printf("📤 已发送到 %s: {\"f\":%s}\n", peer.getMonitorIP().c_str(), cmd);
|
||||||
|
}
|
||||||
|
// ✅【改动 9】ESPNOW分支:回复MAC+IP给主机
|
||||||
|
else if(cmd.endsWith("ESPNOW")||cmd.endsWith("ESPNOW\r"))
|
||||||
|
{
|
||||||
|
String mac = peer.getMacAddress();
|
||||||
|
String ip = peer.getLocalIP();
|
||||||
|
char json[150];
|
||||||
|
snprintf(json, sizeof(json), "{\"mac\":\"%s\",\"ip\":\"%s\"}\r\n",
|
||||||
|
mac.c_str(), ip.c_str());
|
||||||
|
bool ok = peer.sendTo(peer.getMonitorIP(), "ESPNOW_REG", json);
|
||||||
|
if(ok) {
|
||||||
|
Serial.printf("✅ ESPNOW注册: MAC=%s IP=%s\n", mac.c_str(), ip.c_str());
|
||||||
|
} else {
|
||||||
|
Serial.printf("❌ 发送ESPNOW注册信息失败\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos[7];
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 8; i < cmd.length() && count < 7; i++) {
|
||||||
|
if (cmd.charAt(i) == ':') {
|
||||||
|
pos[count++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < 7) {
|
||||||
|
Serial.println("❌ 指令格式错误");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ssid = cmd.substring(8, pos[0]);
|
||||||
|
String pwd = cmd.substring(pos[0] + 1, pos[1]);
|
||||||
|
uint8_t gw1 = cmd.substring(pos[1] + 1, pos[2]).toInt();
|
||||||
|
uint8_t gw2 = cmd.substring(pos[2] + 1, pos[3]).toInt();
|
||||||
|
uint8_t gw3 = cmd.substring(pos[3] + 1, pos[4]).toInt();
|
||||||
|
uint8_t gw4 = cmd.substring(pos[4] + 1, pos[5]).toInt();
|
||||||
|
uint8_t local_last = cmd.substring(pos[5] + 1, pos[6]).toInt();
|
||||||
|
uint8_t monitor_last = cmd.substring(pos[6] + 1).toInt();
|
||||||
|
|
||||||
|
peer.connect(ssid.c_str(), pwd.c_str(), gw1, gw2, gw3, gw4, local_last);
|
||||||
|
peer.setMonitorIPLast(monitor_last);
|
||||||
|
saveConfigToFlash(ssid.c_str(), pwd.c_str(), gw1, gw2, gw3, gw4, local_last, monitor_last);
|
||||||
|
|
||||||
|
Serial.println("✅ WiFi配置已更新并保存");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\n╔════════════════════════════════════════════════════════════════╗");
|
||||||
|
Serial.println("║ ESP32-S3 摄像头 UDP+ESP-NOW 通信系统 v1.3 ║");
|
||||||
|
Serial.println("║ 支持Flash配置 | 串口配置 | UDP+ESP-NOW双通道统一解析 ║");
|
||||||
|
Serial.println("╚════════════════════════════════════════════════════════════════╝");
|
||||||
|
|
||||||
|
loadConfigFromFlash();
|
||||||
|
//esp_now_deinit();
|
||||||
|
//initEspNowRecv(); // ✅【改动 10】初始化ESP-NOW接收
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
handleSerialCommand();
|
||||||
|
|
||||||
|
|
||||||
|
if (peer.isInitialized()) {
|
||||||
|
// if (peer.wifiisready())
|
||||||
|
// {
|
||||||
|
// if(!peer.espnowisready())
|
||||||
|
// {
|
||||||
|
// if(initEspNowRecv())
|
||||||
|
// {
|
||||||
|
// peer.setespnowreadyflg();
|
||||||
|
// String mac = peer.getMacAddress();
|
||||||
|
// String ip = peer.getLocalIP();
|
||||||
|
// char json[150];
|
||||||
|
// snprintf(json, sizeof(json), "mac:%s;ip:%s",
|
||||||
|
// mac.c_str(), ip.c_str());
|
||||||
|
// bool ok = peer.sendTo(peer.getMonitorIP(), "r", json);//ESPNOW_REG
|
||||||
|
// if(ok) {
|
||||||
|
// Serial.printf("✅ ESPNOW注册: MAC=%s IP=%s\n", mac.c_str(), ip.c_str());
|
||||||
|
// } else {
|
||||||
|
// Serial.printf("❌ 发送ESPNOW注册信息失败\n");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// else if(peer.espnowisready())
|
||||||
|
// {
|
||||||
|
// peer.resetespnowreadyflg();
|
||||||
|
// esp_now_deinit();
|
||||||
|
// } //k10的总线扩展机制导espnow优势不能有效发挥,每次查阅按钮时,会直接打断espnow通讯,再启动会延迟将近200ms(感觉是硬件通道重启);
|
||||||
|
//若是常规udp通讯,不一定被按钮查阅打断,但是会被此间隔时间冲断,再连接延迟好使120ms左右(感觉是软件握手重启)
|
||||||
|
//也就是采用k10板子,espnow效果反而更差于udp通讯
|
||||||
|
|
||||||
|
|
||||||
|
if (peer.hasMessage()) {
|
||||||
|
if (peer.getMessageValue("c")) {
|
||||||
|
Serial.printf("%sZHY\n", peer.lastValue().c_str());
|
||||||
|
// Serial.printf("📶 Wi-Fi 模式:%s\n",
|
||||||
|
// WiFi.getMode() == WIFI_AP ? "AP" :
|
||||||
|
// WiFi.getMode() == WIFI_STA ? "STA" : "AP+STA");
|
||||||
|
// Serial.printf("📶 信道:%d\n", WiFi.channel());
|
||||||
|
// Serial.printf("📶 ESP-NOW:%s\n", peer.espnowisready()? "已启用" : "未启用");
|
||||||
|
// Serial.printf("📶 状态:%d\n", WiFi.status());
|
||||||
|
}
|
||||||
|
// ✅【改动 11】解析后清除ESP-NOW标志(避免重复处理)
|
||||||
|
peer.clearEspNowFlag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
主机端(espnow版本)
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <esp_now.h> // ✅ 新增:ESP-NOW 头文件
|
||||||
|
|
||||||
|
// ✅ 新增:ESP-NOW 对等节点结构(最多 10 个从机)
|
||||||
|
#define MAX_SLAVES 10
|
||||||
|
typedef struct {
|
||||||
|
uint8_t mac[6];
|
||||||
|
IPAddress ip;
|
||||||
|
bool active;
|
||||||
|
} SlaveNode;
|
||||||
|
|
||||||
|
class UDPPeer {
|
||||||
|
private:
|
||||||
|
WiFiUDP udp;
|
||||||
|
bool ready = false;
|
||||||
|
uint16_t local_port = 8888;
|
||||||
|
|
||||||
|
// 双缓存
|
||||||
|
String receivedRaw;
|
||||||
|
String parsedValue;
|
||||||
|
|
||||||
|
// 连接参数
|
||||||
|
const char* ssid = nullptr;
|
||||||
|
const char* password = nullptr;
|
||||||
|
IPAddress local_ip;
|
||||||
|
IPAddress gateway;
|
||||||
|
IPAddress subnet;
|
||||||
|
|
||||||
|
// ✅ 新增:AP模式标志
|
||||||
|
bool is_ap_mode = false;
|
||||||
|
|
||||||
|
// ✅ 新增:网络配置缓存
|
||||||
|
String current_ssid = "";
|
||||||
|
String current_password = "";
|
||||||
|
uint8_t cached_gw1 = 0, cached_gw2 = 0, cached_gw3 = 0, cached_gw4 = 0;
|
||||||
|
|
||||||
|
// 状态标志
|
||||||
|
bool is_initialized = false;
|
||||||
|
uint32_t last_maintenance = 0;
|
||||||
|
const uint32_t MAINTENANCE_INTERVAL = 1000;
|
||||||
|
|
||||||
|
// ✅ 新增:ESP-NOW 相关
|
||||||
|
bool espnowInited = false;
|
||||||
|
SlaveNode slaves[MAX_SLAVES]; // 从机注册表
|
||||||
|
uint8_t slaveCount = 0;
|
||||||
|
|
||||||
|
// ✅ 新增:解析 MAC 字符串 "AA:BB:CC:DD:EE:FF" → 字节数组
|
||||||
|
bool parseMacString(const String& macStr, uint8_t* macBuf) {
|
||||||
|
int vals[6];
|
||||||
|
if (sscanf(macStr.c_str(), "%x:%x:%x:%x:%x:%x",
|
||||||
|
&vals[0], &vals[1], &vals[2], &vals[3], &vals[4], &vals[5]) != 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < 6; i++) macBuf[i] = (uint8_t)vals[i];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新增:初始化 ESP-NOW(网络就绪后调用)
|
||||||
|
void initEspNow() {
|
||||||
|
if (espnowInited) return;
|
||||||
|
if (esp_now_init() != ESP_OK) {
|
||||||
|
Serial.println("❌ ESP-NOW 初始化失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 注册发送回调(可选,用于确认发送状态)
|
||||||
|
esp_now_register_send_cb([](const uint8_t* mac, esp_now_send_status_t status) {
|
||||||
|
// Serial.printf("ESP-NOW Send to %02x... %s\n", mac[5],
|
||||||
|
// status == ESP_NOW_SEND_SUCCESS ? "OK" : "FAIL");
|
||||||
|
});
|
||||||
|
espnowInited = true;
|
||||||
|
Serial.println("✅ ESP-NOW 已启用");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新增:注册/更新从机(MAC+IP 映射)
|
||||||
|
bool registerSlave(const String& macStr, const String& ipStr) {
|
||||||
|
if (!espnowInited) initEspNow();
|
||||||
|
|
||||||
|
uint8_t mac[6];
|
||||||
|
if (!parseMacString(macStr, mac)) return false;
|
||||||
|
|
||||||
|
IPAddress ip;
|
||||||
|
if (!ip.fromString(ipStr)) return false;
|
||||||
|
|
||||||
|
// 检查 MAC 是否已存在 → 更新 IP
|
||||||
|
for (int i = 0; i < slaveCount; i++) {
|
||||||
|
if (slaves[i].active && memcmp(slaves[i].mac, mac, 6) == 0) {
|
||||||
|
slaves[i].ip = ip;
|
||||||
|
Serial.printf("🔄 更新从机: MAC=%s IP=%s\n", macStr.c_str(), ipStr.c_str());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新从机 → 添加到列表
|
||||||
|
if (slaveCount >= MAX_SLAVES) {
|
||||||
|
Serial.println("⚠️ 从机列表已满,无法添加");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SlaveNode* node = &slaves[slaveCount++];
|
||||||
|
memcpy(node->mac, mac, 6);
|
||||||
|
node->ip = ip;
|
||||||
|
node->active = true;
|
||||||
|
|
||||||
|
// 注册到 ESP-NOW 对等列表
|
||||||
|
esp_now_peer_info_t peerInfo = {};
|
||||||
|
memcpy(peerInfo.peer_addr, mac, 6);
|
||||||
|
peerInfo.channel = 0; // 自动使用 Wi-Fi 当前信道
|
||||||
|
peerInfo.encrypt = false;
|
||||||
|
if (esp_now_add_peer(&peerInfo) == ESP_OK) {
|
||||||
|
Serial.printf("✅ 注册从机: MAC=%s IP=%s ( #%d )\n",
|
||||||
|
macStr.c_str(), ipStr.c_str(), slaveCount);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Serial.println("❌ ESP-NOW 添加对等节点失败");
|
||||||
|
slaveCount--; // 回滚
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新增:根据 IP 查找从机 MAC
|
||||||
|
uint8_t* findSlaveByIP(const String& ipStr) {
|
||||||
|
IPAddress target;
|
||||||
|
if (!target.fromString(ipStr)) return nullptr;
|
||||||
|
|
||||||
|
for (int i = 0; i < slaveCount; i++) {
|
||||||
|
if (slaves[i].active && slaves[i].ip == target) {
|
||||||
|
return slaves[i].mac;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 新增:ESP-NOW 发送(内部使用)
|
||||||
|
bool sendEspNow(const uint8_t* mac, const char* data, int len) {
|
||||||
|
if (!espnowInited) initEspNow();
|
||||||
|
if (!mac || !data || len <= 0 || len > 250) return false; // ESP-NOW 最大 250 字节
|
||||||
|
Serial.printf("📶 信道:%d\n", WiFi.channel());
|
||||||
|
/* int sendreslut=esp_now_send(mac, (uint8_t*)data, len);
|
||||||
|
Serial.printf("📶 Wi-Fi 模式:%s\n",
|
||||||
|
WiFi.getMode() == WIFI_AP ? "AP" :
|
||||||
|
WiFi.getMode() == WIFI_STA ? "STA" : "AP+STA");
|
||||||
|
Serial.printf("📶 信道:%d\n", WiFi.channel());
|
||||||
|
Serial.printf("📶 状态:%d\n", WiFi.status());
|
||||||
|
Serial.printf("📶 ESP-NOW:%s\n", espnowInited ? "已启用" : "未启用");
|
||||||
|
|
||||||
|
|
||||||
|
Serial.printf("ESP-NOW 发送, 信道:%d, mac:%s:%s, 状态:0x%X\n",WiFi.channel(), mac, data, sendreslut); */
|
||||||
|
return esp_now_send(mac, (uint8_t*)data, len)== ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 修正后的轻量维护
|
||||||
|
void autoMaintain() {
|
||||||
|
if (!is_initialized) return;
|
||||||
|
if (millis() - last_maintenance < MAINTENANCE_INTERVAL) return;
|
||||||
|
last_maintenance = millis();
|
||||||
|
|
||||||
|
if (is_ap_mode) {
|
||||||
|
if (!ready) {
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
if (ready) Serial.printf("✅ AP模式UDP就绪: 端口=%d\n", local_port);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
WiFi.config(local_ip, gateway, subnet);
|
||||||
|
if (!ready) {
|
||||||
|
Serial.printf("start local net 正在启动网络连接!!!\n");
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(1000);
|
||||||
|
} else {
|
||||||
|
Serial.printf("reconnect 网络中断,恢复中!!!\n");
|
||||||
|
WiFi.reconnect();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
if (ready) {
|
||||||
|
Serial.printf("✅ 网络就绪: IP=%s 端口=%d\n",
|
||||||
|
WiFi.localIP().toString().c_str(), local_port);
|
||||||
|
// ✅ 网络就绪后初始化 ESP-NOW
|
||||||
|
//initEspNow();
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ✅ STA模式连接
|
||||||
|
bool connect(const char* _ssid, const char* _pwd,
|
||||||
|
uint8_t gw1, uint8_t gw2, uint8_t gw3, uint8_t gw4,
|
||||||
|
uint8_t local_ip_last, uint16_t port = 8888) {
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
last_maintenance = 0;
|
||||||
|
ready = false;
|
||||||
|
ssid = _ssid; password = _pwd;
|
||||||
|
local_ip = IPAddress(gw1, gw2, gw3, local_ip_last);
|
||||||
|
gateway = IPAddress(gw1, gw2, gw3, gw4);
|
||||||
|
subnet = IPAddress(255, 255, 255, 0);
|
||||||
|
local_port = port;
|
||||||
|
is_initialized = true;
|
||||||
|
is_ap_mode = false;
|
||||||
|
|
||||||
|
current_ssid = String(_ssid);
|
||||||
|
current_password = String(_pwd);
|
||||||
|
cached_gw1 = gw1; cached_gw2 = gw2; cached_gw3 = gw3; cached_gw4 = gw4;
|
||||||
|
|
||||||
|
Serial.printf("📶 配置STA网络: IP=%d.%d.%d.%d 网关=%d.%d.%d.%d 端口=%d\n",
|
||||||
|
gw1, gw2, gw3, local_ip_last, gw1, gw2, gw3, gw4, port);
|
||||||
|
Serial.println("💡 网络将在后台自动连接...");
|
||||||
|
|
||||||
|
autoMaintain();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ AP模式连接
|
||||||
|
bool connectAP(const char* _ssid, const char* _pwd, uint16_t port = 8888) {
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
last_maintenance = 0;
|
||||||
|
ready = false;
|
||||||
|
WiFi.softAPConfig(IPAddress(192, 168, 4, 1),
|
||||||
|
IPAddress(192, 168, 4, 1),
|
||||||
|
IPAddress(255, 255, 255, 0));
|
||||||
|
|
||||||
|
if (!WiFi.softAP(_ssid, _pwd, 1, false, 10)) {
|
||||||
|
Serial.println("❌ AP启动失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_port = port;
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
is_initialized = true;
|
||||||
|
is_ap_mode = true;
|
||||||
|
|
||||||
|
current_ssid = String(_ssid);
|
||||||
|
current_password = String(_pwd);
|
||||||
|
cached_gw1 = 192; cached_gw2 = 168; cached_gw3 = 4; cached_gw4 = 1;
|
||||||
|
|
||||||
|
Serial.printf("✅ AP模式启动: 热点=\"%s\" 密码=\"%s\"\n", _ssid, _pwd);
|
||||||
|
Serial.printf("🌐 本机IP: %s | 端口: %d | 客户端: %d/%d\n",
|
||||||
|
WiFi.softAPIP().toString().c_str(), port,
|
||||||
|
WiFi.softAPgetStationNum(), 10);
|
||||||
|
|
||||||
|
// ✅ AP模式也初始化 ESP-NOW
|
||||||
|
initEspNow();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 向小车发送配置
|
||||||
|
void sendConfigToDevice(uint8_t car_last_octet, uint8_t monitor_last_octet) {
|
||||||
|
if (!is_initialized) {
|
||||||
|
Serial.println("❌ 手柄未配置网络,无法发送配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (car_last_octet == cached_gw4 || car_last_octet == 0 || car_last_octet == 255) {
|
||||||
|
Serial.println("❌ 无效的小车编号(需1~254,且不能与网关相同)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t local_last = is_ap_mode ? 1 : local_ip[3];
|
||||||
|
if(monitor_last_octet != 255) local_last = monitor_last_octet;
|
||||||
|
|
||||||
|
Serial.printf("wificar:%s:%s:%d:%d:%d:%d:%d:%d\nWZHY",
|
||||||
|
current_ssid.c_str(), current_password.c_str(),
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, cached_gw4,
|
||||||
|
car_last_octet, local_last);
|
||||||
|
Serial1.printf("wificar:%s:%s:%d:%d:%d:%d:%d:%d\nWZHY",
|
||||||
|
current_ssid.c_str(), current_password.c_str(),
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, cached_gw4,
|
||||||
|
car_last_octet, local_last);
|
||||||
|
|
||||||
|
Serial.printf("✅ 已配置小车 #%d → IP=%d.%d.%d.%d | 监视器=%d.%d.%d.%d\n",
|
||||||
|
car_last_octet,
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, car_last_octet,
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, local_last);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 接收:解析 ESPNOW_REG 并自动注册
|
||||||
|
bool hasMessage() {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
receivedRaw = "";
|
||||||
|
parsedValue = "";
|
||||||
|
|
||||||
|
int len = udp.parsePacket();
|
||||||
|
if (len <= 0 || len > 512) return false;
|
||||||
|
|
||||||
|
char buf[513];
|
||||||
|
int read = udp.read((uint8_t*)buf, min(len, 512));
|
||||||
|
if (read > 0) {
|
||||||
|
buf[read] = '\0';
|
||||||
|
receivedRaw = String(buf);
|
||||||
|
|
||||||
|
// ✅ 解析 ESPNOW_REG 格式: "mac:AA:BB:CC:DD:EE:FF;ip:192.168.4.6"
|
||||||
|
if (getMessageValue("r")) {
|
||||||
|
String raw = lastValue(); // "mac:AA:BB:CC:DD:EE:FF;ip:192.168.4.6"
|
||||||
|
|
||||||
|
// 提取 MAC
|
||||||
|
int macStart = raw.indexOf("mac:");
|
||||||
|
int macEnd = raw.indexOf(";", macStart);
|
||||||
|
// 提取 IP
|
||||||
|
int ipStart = raw.indexOf("ip:", macEnd);
|
||||||
|
|
||||||
|
if (macStart != -1 && macEnd != -1 && ipStart != -1) {
|
||||||
|
String macStr = raw.substring(macStart + 4, macEnd); // +4 跳过 "mac:"
|
||||||
|
String ipStr = raw.substring(ipStart + 3); // +3 跳过 "ip:"
|
||||||
|
|
||||||
|
// ✅ 注册/更新从机
|
||||||
|
registerSlave(macStr, ipStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 解析字段(不变)
|
||||||
|
bool getMessageValue(const String& key) {
|
||||||
|
parsedValue = "";
|
||||||
|
if (receivedRaw.length() == 0) return false;
|
||||||
|
|
||||||
|
String keyPattern = "\"" + key + "\"";
|
||||||
|
int startIndex = receivedRaw.indexOf(keyPattern);
|
||||||
|
if (startIndex == -1) return false;
|
||||||
|
|
||||||
|
int colonIndex = receivedRaw.indexOf(":", startIndex);
|
||||||
|
if (colonIndex == -1) return false;
|
||||||
|
|
||||||
|
int valueStart = colonIndex + 1;
|
||||||
|
while (valueStart < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueStart) == ' ') valueStart++;
|
||||||
|
|
||||||
|
if (valueStart < receivedRaw.length() && receivedRaw.charAt(valueStart) == '"') {
|
||||||
|
int valueEnd = receivedRaw.indexOf("\"", valueStart + 1);
|
||||||
|
if (valueEnd != -1) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart + 1, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int valueEnd = valueStart;
|
||||||
|
while (valueEnd < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ',' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != '}' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ' ') valueEnd++;
|
||||||
|
while (valueEnd > valueStart && receivedRaw.charAt(valueEnd - 1) == ' ') valueEnd--;
|
||||||
|
if (valueEnd > valueStart) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 发送:智能路由(UDP/ESP-NOW 自动选择)
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const char* format, ...) {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
IPAddress target_ip;
|
||||||
|
if (!target_ip.fromString(target_ip_str)) return false;
|
||||||
|
|
||||||
|
// ✅ 智能路由:如果目标 IP 是已注册从机,优先使用 ESP-NOW
|
||||||
|
uint8_t* slaveMac = findSlaveByIP(target_ip_str);
|
||||||
|
if (slaveMac && espnowInited) {
|
||||||
|
// 构造 ESP-NOW 消息(保持 JSON 格式,与 UDP 一致)
|
||||||
|
char content[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(content, sizeof(content) - 1, format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
char json[300];
|
||||||
|
snprintf(json, sizeof(json), "{\"%s\":\"%s\"}", key.c_str(), content);
|
||||||
|
|
||||||
|
// ✅ 使用 ESP-NOW 发送(更快、更低延迟)
|
||||||
|
if (sendEspNow(slaveMac, json, strlen(json))) {
|
||||||
|
Serial.println("⚠️ ESP-NOW 发送");
|
||||||
|
delay(10);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// ESP-NOW 失败时,降级使用 UDP(可选)
|
||||||
|
//Serial.println("⚠️ ESP-NOW 发送失败,降级使用 UDP");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 默认:使用 UDP 发送
|
||||||
|
char content[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(content, sizeof(content) - 1, format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
char json[300];
|
||||||
|
snprintf(json, sizeof(json), "{\"%s\":\"%s\"}\r\n", key.c_str(), content);
|
||||||
|
|
||||||
|
return udp.beginPacket(target_ip, local_port) &&
|
||||||
|
udp.write((const uint8_t*)json, strlen(json)) &&
|
||||||
|
udp.endPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 积木友好重载
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const String& value) {
|
||||||
|
return sendTo(target_ip_str, key, "%s", value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 辅助方法
|
||||||
|
String lastValue() { return parsedValue; }
|
||||||
|
String lastMessage() { return receivedRaw; }
|
||||||
|
String getLocalIP() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized ?
|
||||||
|
(is_ap_mode ? WiFi.softAPIP().toString() : WiFi.localIP().toString()) :
|
||||||
|
"未配置";
|
||||||
|
}
|
||||||
|
bool isConnected() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized && ready;
|
||||||
|
}
|
||||||
|
bool isAPMode() { return is_ap_mode; }
|
||||||
|
|
||||||
|
// ✅ 新增:获取已注册从机数量(调试用)
|
||||||
|
int getRegisteredSlaveCount() { return slaveCount; }
|
||||||
|
};
|
||||||
|
|
||||||
|
extern UDPPeer peer;
|
||||||
579
jxbeye/src/main.cpp
Normal file
579
jxbeye/src/main.cpp
Normal file
@@ -0,0 +1,579 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include <AsyncUDP.h> // ✅【改动 1】替换头文件
|
||||||
|
#include "esp_camera.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <esp_now.h>
|
||||||
|
#include <esp_wifi.h>
|
||||||
|
|
||||||
|
// ===== 修正后的管脚配置(严格匹配ESP32-S3-WROOM-1-N16R8)=====
|
||||||
|
#define PWDN_GPIO_NUM -1
|
||||||
|
#define RESET_GPIO_NUM -1
|
||||||
|
#define XCLK_GPIO_NUM 9
|
||||||
|
#define SIOD_GPIO_NUM 14 // SCCB SDA
|
||||||
|
#define SIOC_GPIO_NUM 13 // SCCB SCL
|
||||||
|
#define Y9_GPIO_NUM 3 // CSI_D7
|
||||||
|
#define Y8_GPIO_NUM 10 // CSI_D6
|
||||||
|
#define Y7_GPIO_NUM 8 // CSI_D5
|
||||||
|
#define Y6_GPIO_NUM 17 // CSI_D4
|
||||||
|
#define Y5_GPIO_NUM 15 // CSI_D3
|
||||||
|
#define Y4_GPIO_NUM 6 // CSI_D2
|
||||||
|
#define Y3_GPIO_NUM 7 // CSI_D1
|
||||||
|
#define Y2_GPIO_NUM 16 // CSI_D0
|
||||||
|
#define VSYNC_GPIO_NUM 12 // CSI_VSYNC
|
||||||
|
#define HREF_GPIO_NUM 11 // CSI_HREF
|
||||||
|
#define PCLK_GPIO_NUM 18 // CSI_PCLK
|
||||||
|
|
||||||
|
// ===== SVGA 流传输配置 =====
|
||||||
|
const framesize_t STREAM_RESOLUTION = FRAMESIZE_XGA;
|
||||||
|
const uint32_t STREAM_XCLK_FREQ = 20000000;
|
||||||
|
const uint8_t STREAM_JPEG_QUALITY = 10;
|
||||||
|
|
||||||
|
// ===== 并行架构配置 =====
|
||||||
|
#define FRAME_QUEUE_SIZE 2
|
||||||
|
QueueHandle_t frame_queue = NULL;
|
||||||
|
volatile bool has_active_client = false;
|
||||||
|
|
||||||
|
// ===== 系统配置 =====
|
||||||
|
String ssid = "ESP32-S3-Camera";
|
||||||
|
String password = "12345678";
|
||||||
|
const uint8_t channel = 4;
|
||||||
|
const uint8_t max_clients = 10;
|
||||||
|
const uint16_t WEB_SERVER_PORT = 80;
|
||||||
|
|
||||||
|
// ✅【改动 2】修改 UDP 对象定义,移除缓冲区变量 (AsyncUDP 不需要)
|
||||||
|
const uint16_t UDP_PORT = 8888;
|
||||||
|
AsyncUDP udp;
|
||||||
|
// 以下变量不再需要,因为数据在回调中直接处理
|
||||||
|
// WiFiUDP udpServer;
|
||||||
|
// char udpPacketBuffer[256];
|
||||||
|
// String udpReceiveBuffer = "";
|
||||||
|
|
||||||
|
// 为了在回调中使用,我们需要一个全局的缓冲区字符串 (注意:回调中操作 String 需小心,但此处逻辑简单可行)
|
||||||
|
String udpReceiveBuffer = "";
|
||||||
|
|
||||||
|
WebServer server(WEB_SERVER_PORT);
|
||||||
|
|
||||||
|
// ===== 帧率统计 =====
|
||||||
|
volatile float capture_fps = 0.0;
|
||||||
|
volatile float transmit_fps = 0.0;
|
||||||
|
static uint32_t failed_frames = 0;
|
||||||
|
|
||||||
|
// ✅【改动 3】新增 UDP 回调函数 (原 loop 中的逻辑移至此)
|
||||||
|
// 此函数在网络数据到达时自动触发,不占用 loop 时间,不阻塞推流
|
||||||
|
void onUdpPacket(AsyncUDPPacket packet) {
|
||||||
|
if (packet.isBroadcast() || packet.isMulticast()) return;
|
||||||
|
|
||||||
|
String payload = "";
|
||||||
|
for (size_t i = 0; i < packet.length(); i++) {
|
||||||
|
payload += (char)packet.data()[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 激光控制
|
||||||
|
if (payload.endsWith("LASEROFF")) {
|
||||||
|
digitalWrite(2, HIGH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (payload.endsWith("LASERON")) {
|
||||||
|
digitalWrite(2, LOW);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 串口转发(截断长消息)
|
||||||
|
if(payload.endsWith("ZHY")||payload.endsWith("\n"))
|
||||||
|
{
|
||||||
|
Serial.println(payload);
|
||||||
|
}
|
||||||
|
else if(payload.length() > 128)
|
||||||
|
{
|
||||||
|
payload = payload.substring(0, 128);
|
||||||
|
Serial.println(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 捕获任务(全力捕获模式)
|
||||||
|
void capture_task(void *pv) {
|
||||||
|
static uint32_t cap_count = 0;
|
||||||
|
static uint32_t last_time = 0;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
if (!has_active_client) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(200));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
camera_fb_t *fb = esp_camera_fb_get();
|
||||||
|
|
||||||
|
if (fb && fb->len > 1024 && fb->buf[0] == 0xFF && fb->buf[1] == 0xD8) {
|
||||||
|
cap_count++;
|
||||||
|
|
||||||
|
if (xQueueSend(frame_queue, &fb, 0) != pdTRUE) {
|
||||||
|
esp_camera_fb_return(fb);
|
||||||
|
failed_frames++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fb) esp_camera_fb_return(fb);
|
||||||
|
failed_frames++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (millis() - last_time >= 1000) {
|
||||||
|
capture_fps = (float)cap_count;
|
||||||
|
cap_count = 0;
|
||||||
|
last_time = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 高效帧发送
|
||||||
|
void sendMjpegFrame(WiFiClient &client, camera_fb_t *fb) {
|
||||||
|
static char header[128];
|
||||||
|
int len = sprintf(header,
|
||||||
|
"--frame\r\n"
|
||||||
|
"Content-Type: image/jpeg\r\n"
|
||||||
|
"Content-Length: %u\r\n\r\n", fb->len);
|
||||||
|
client.write((uint8_t*)header, len);
|
||||||
|
client.write(fb->buf, fb->len);
|
||||||
|
client.write((uint8_t*)"\r\n", 2);
|
||||||
|
client.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 流处理(自适应传输帧率)- 保持阻塞模式
|
||||||
|
void handleStream() {
|
||||||
|
has_active_client = true;
|
||||||
|
Serial.println("✅ 客户端连接,开始推流 (AsyncUDP 后台运行)");
|
||||||
|
|
||||||
|
WiFiClient client = server.client();
|
||||||
|
if (!client || !client.connected()) {
|
||||||
|
Serial.println("⚠️ [STREAM] 客户端未连接");
|
||||||
|
has_active_client = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.setNoDelay(true);
|
||||||
|
|
||||||
|
client.print("HTTP/1.1 200 OK\r\n");
|
||||||
|
client.print("Content-Type: multipart/x-mixed-replace; boundary=frame\r\n");
|
||||||
|
client.print("Cache-Control: no-cache\r\n");
|
||||||
|
client.print("Pragma: no-cache\r\n");
|
||||||
|
client.print("Connection: close\r\n");
|
||||||
|
client.print("\r\n");
|
||||||
|
|
||||||
|
uint32_t tx_count = 0;
|
||||||
|
uint32_t last_fps_time = millis();
|
||||||
|
|
||||||
|
float target_fps = 25.0;
|
||||||
|
const float MIN_FPS = 15.0;
|
||||||
|
const float MAX_FPS = 25.0;
|
||||||
|
uint32_t next_frame_time = millis();
|
||||||
|
|
||||||
|
while (client.connected()) {
|
||||||
|
uint32_t now = millis();
|
||||||
|
if (now < next_frame_time) {
|
||||||
|
delayMicroseconds(300);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
camera_fb_t *fb = NULL;
|
||||||
|
if (xQueueReceive(frame_queue, &fb, 0) == pdTRUE) {
|
||||||
|
uint32_t send_start = millis();
|
||||||
|
sendMjpegFrame(client, fb);
|
||||||
|
esp_camera_fb_return(fb);
|
||||||
|
uint32_t send_duration = millis() - send_start;
|
||||||
|
|
||||||
|
tx_count++;
|
||||||
|
|
||||||
|
if (send_duration > 45) {
|
||||||
|
target_fps = max(MIN_FPS, target_fps);
|
||||||
|
} else if (send_duration < 30) {
|
||||||
|
target_fps = min(MAX_FPS, target_fps);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t target_interval = (uint32_t)(1000.0f / target_fps);
|
||||||
|
next_frame_time = now + target_interval;
|
||||||
|
if (next_frame_time < now) next_frame_time = now + 5;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
next_frame_time = now + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!client.connected()) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client.connected()) {
|
||||||
|
client.write((const uint8_t*)"--frame--\r\n", 11);
|
||||||
|
client.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
has_active_client = false;
|
||||||
|
Serial.println("⚠️ 客户端断开,暂停捕获");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool verifyPsram() {
|
||||||
|
Serial.println("\n=== PSRAM 验证 ===");
|
||||||
|
if (!psramFound()) {
|
||||||
|
Serial.println("❌ PSRAM 未检测到");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t psram_size = ESP.getPsramSize();
|
||||||
|
Serial.printf("✅ PSRAM 容量:%.2f MB\n", psram_size / 1024.0 / 1024.0);
|
||||||
|
|
||||||
|
uint8_t* test = (uint8_t*)ps_malloc(1024 * 1024);
|
||||||
|
if (!test) {
|
||||||
|
Serial.println("❌ 无法分配 1MB PSRAM 内存");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < 1024; i++) test[i] = i & 0xFF;
|
||||||
|
bool valid = true;
|
||||||
|
for (size_t i = 0; i < 1024; i++) {
|
||||||
|
if (test[i] != (i & 0xFF)) {
|
||||||
|
valid = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
heap_caps_free(test);
|
||||||
|
|
||||||
|
if (valid) {
|
||||||
|
Serial.println("✅ PSRAM 读写验证通过");
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Serial.println("❌ PSRAM 读写错误");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool initCamera() {
|
||||||
|
pinMode(21, OUTPUT);
|
||||||
|
pinMode(2, OUTPUT);
|
||||||
|
digitalWrite(21, LOW);
|
||||||
|
digitalWrite(2, HIGH);
|
||||||
|
|
||||||
|
camera_config_t config = {
|
||||||
|
.pin_pwdn = PWDN_GPIO_NUM,
|
||||||
|
.pin_reset = RESET_GPIO_NUM,
|
||||||
|
.pin_xclk = XCLK_GPIO_NUM,
|
||||||
|
.pin_sccb_sda = SIOD_GPIO_NUM,
|
||||||
|
.pin_sccb_scl = SIOC_GPIO_NUM,
|
||||||
|
.pin_d7 = Y9_GPIO_NUM,
|
||||||
|
.pin_d6 = Y8_GPIO_NUM,
|
||||||
|
.pin_d5 = Y7_GPIO_NUM,
|
||||||
|
.pin_d4 = Y6_GPIO_NUM,
|
||||||
|
.pin_d3 = Y5_GPIO_NUM,
|
||||||
|
.pin_d2 = Y4_GPIO_NUM,
|
||||||
|
.pin_d1 = Y3_GPIO_NUM,
|
||||||
|
.pin_d0 = Y2_GPIO_NUM,
|
||||||
|
.pin_vsync = VSYNC_GPIO_NUM,
|
||||||
|
.pin_href = HREF_GPIO_NUM,
|
||||||
|
.pin_pclk = PCLK_GPIO_NUM,
|
||||||
|
.xclk_freq_hz = STREAM_XCLK_FREQ,
|
||||||
|
.ledc_timer = LEDC_TIMER_0,
|
||||||
|
.ledc_channel = LEDC_CHANNEL_0,
|
||||||
|
.pixel_format = PIXFORMAT_JPEG,
|
||||||
|
.frame_size = STREAM_RESOLUTION,
|
||||||
|
.jpeg_quality = STREAM_JPEG_QUALITY,
|
||||||
|
.fb_count = 3,
|
||||||
|
.fb_location = CAMERA_FB_IN_PSRAM,
|
||||||
|
.grab_mode = CAMERA_GRAB_LATEST
|
||||||
|
};
|
||||||
|
|
||||||
|
esp_err_t err = esp_camera_init(&config);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
Serial.printf("❌ 摄像头初始化失败 (0x%X)\n", err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sensor_t *s = esp_camera_sensor_get();
|
||||||
|
if (s) {
|
||||||
|
s->set_hmirror(s, 0);
|
||||||
|
s->set_vflip(s, 1);
|
||||||
|
s->set_exposure_ctrl(s, 0);
|
||||||
|
s->set_aec2(s, 0);
|
||||||
|
s->set_gain_ctrl(s, 1);
|
||||||
|
s->set_whitebal(s, 1);
|
||||||
|
s->set_awb_gain(s, 0);
|
||||||
|
s->set_aec_value(s, 200);
|
||||||
|
s->set_wb_mode(s, 0);
|
||||||
|
s->set_brightness(s, 0);
|
||||||
|
s->set_contrast(s, 0);
|
||||||
|
s->set_saturation(s, 2);
|
||||||
|
s->set_framesize(s, STREAM_RESOLUTION);
|
||||||
|
s->set_quality(s, STREAM_JPEG_QUALITY);
|
||||||
|
}
|
||||||
|
Serial.println("✅ 摄像头初始化成功");
|
||||||
|
Serial.printf("⚡ XCLK: %d MHz | JPEG质量:%d | 缓冲区:3帧\n",
|
||||||
|
STREAM_XCLK_FREQ / 1000000, STREAM_JPEG_QUALITY);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(1000000);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin("wifi_cfg", false);
|
||||||
|
String saved_ssid = prefs.getString("ssid", "ESP32-S3-Camera");
|
||||||
|
String saved_pwd = prefs.getString("pwd", "12345678");
|
||||||
|
if (saved_ssid.length() > 0 && saved_ssid.length() <= 32) ssid = saved_ssid;
|
||||||
|
if (saved_pwd.length() >= 8 && saved_pwd.length() <= 64) password = saved_pwd;
|
||||||
|
prefs.end();
|
||||||
|
|
||||||
|
Serial.println("\n");
|
||||||
|
Serial.println("╔══════════════════════════════════════════════════════════════════════╗");
|
||||||
|
Serial.println("║ ESP32-S3 + OV2640 双核并行流系统 v4.0 (AsyncUDP 中断版) ║");
|
||||||
|
Serial.println("╚══════════════════════════════════════════════════════════════════════╝");
|
||||||
|
Serial.println("💡 串口指令: WIFI:名称:密码 (例如: WIFI:LabCam:Secure1234)");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
frame_queue = xQueueCreate(FRAME_QUEUE_SIZE, sizeof(camera_fb_t*));
|
||||||
|
if (!frame_queue) {
|
||||||
|
Serial.println("❌ 队列创建失败");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
Serial.printf("✅ 帧队列创建成功 (深度=%d)\n", FRAME_QUEUE_SIZE);
|
||||||
|
|
||||||
|
Serial.println("\n[1] PSRAM 验证...");
|
||||||
|
if (!verifyPsram()) {
|
||||||
|
Serial.println("❌ PSRAM 不可用,系统无法运行");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("\n[2] WiFi 初始化...");
|
||||||
|
WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
|
||||||
|
WiFi.softAP(ssid.c_str(), password.c_str(), channel, false, max_clients);
|
||||||
|
|
||||||
|
Serial.print("✅ 热点:"); Serial.println(ssid);
|
||||||
|
Serial.print("🔒 密码:"); Serial.println(password);
|
||||||
|
Serial.print("🌐 IP: "); Serial.println(WiFi.softAPIP());
|
||||||
|
Serial.printf("🌐 访问:http://%s\n", WiFi.softAPIP().toString().c_str());
|
||||||
|
|
||||||
|
Serial.println("\n[3] 摄像头初始化 (SVGA)...");
|
||||||
|
if (!initCamera()) {
|
||||||
|
Serial.println("❌ 摄像头初始化失败,系统退出");
|
||||||
|
while (1) delay(1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
xTaskCreatePinnedToCore(
|
||||||
|
capture_task, "cam_capture", 3072, NULL, 5, NULL, 0
|
||||||
|
);
|
||||||
|
Serial.println("✅ 捕获任务已启动 (Core 0)");
|
||||||
|
|
||||||
|
Serial.println("\n[4] WebServer 路由配置...");
|
||||||
|
|
||||||
|
server.on("/", []() {
|
||||||
|
String html =
|
||||||
|
"<!DOCTYPE html>"
|
||||||
|
"<html lang=\"zh-CN\">"
|
||||||
|
"<head>"
|
||||||
|
"<meta charset=\"UTF-8\">"
|
||||||
|
"<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
|
||||||
|
"<title>ESP32-S3 摄像头</title>"
|
||||||
|
"<style>"
|
||||||
|
"body{font-family:'Microsoft YaHei',Arial,sans-serif;padding:20px;background:#f5f5f5;margin:0}"
|
||||||
|
"header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:white;padding:30px;text-align:center;border-radius:0 0 20px 20px;margin-bottom:25px;box-shadow:0 4px 12px rgba(0,0,0,0.1)}"
|
||||||
|
"h1{margin:0;font-size:28px}"
|
||||||
|
".subtitle{margin-top:8px;opacity:0.9;font-size:16px}"
|
||||||
|
".camera-container{background:white;padding:20px;border-radius:12px;box-shadow:0 4px 12px rgba(0,0,0,0.1);margin-bottom:25px;text-align:center}"
|
||||||
|
".camera-container img{max-width:100%;height:auto;border-radius:8px}"
|
||||||
|
".footer{text-align:center;margin-top:25px;color:#666;font-size:14px;padding:15px;border-top:1px solid #eee}"
|
||||||
|
"</style>"
|
||||||
|
"</head>"
|
||||||
|
"<body>"
|
||||||
|
"<header>"
|
||||||
|
"<h1>📸 局域物联网-" + String(ssid) + "</h1>"
|
||||||
|
"<div class=\"subtitle\">ESP32-S3 双核并行摄像头 (AsyncUDP)</div>"
|
||||||
|
"</header>"
|
||||||
|
|
||||||
|
"<div class=\"camera-container\">"
|
||||||
|
"<img src=\"/stream\" id=\"camera-stream\" alt=\"实时画面\">"
|
||||||
|
"</div>"
|
||||||
|
|
||||||
|
"<div class=\"footer\">"
|
||||||
|
"<p>ESP32-S3 双核流系统 | 推流阻塞不影响 UDP 中断</p>"
|
||||||
|
"</div>"
|
||||||
|
|
||||||
|
"<script>"
|
||||||
|
"const img = document.getElementById('camera-stream');"
|
||||||
|
"img.onerror = () => {"
|
||||||
|
" console.log('Stream error, reconnecting...');"
|
||||||
|
" setTimeout(() => img.src = '/stream?' + Date.now(), 3000);"
|
||||||
|
"};"
|
||||||
|
"</script>"
|
||||||
|
"</body>"
|
||||||
|
"</html>";
|
||||||
|
|
||||||
|
server.send(200, "text/html; charset=utf-8", html);
|
||||||
|
Serial.println("✅ 首页响应已发送");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on("/stream", handleStream);
|
||||||
|
|
||||||
|
server.on("/status", []() {
|
||||||
|
char json_buffer[180];
|
||||||
|
snprintf(json_buffer, sizeof(json_buffer),
|
||||||
|
"{\"clients\":%d,\"capture_fps\":%.1f,\"transmit_fps\":%.1f,\"failed\":%lu,\"free_psram\":%lu,\"has_client\":%s}",
|
||||||
|
WiFi.softAPgetStationNum(),
|
||||||
|
capture_fps,
|
||||||
|
transmit_fps,
|
||||||
|
failed_frames,
|
||||||
|
ESP.getFreePsram() / 1024,
|
||||||
|
has_active_client ? "true" : "false"
|
||||||
|
);
|
||||||
|
server.send(200, "application/json", json_buffer);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.onNotFound([]() {
|
||||||
|
server.send(404, "text/plain; charset=utf-8", "404 - Not Found");
|
||||||
|
});
|
||||||
|
|
||||||
|
server.begin();
|
||||||
|
Serial.printf("\n✅ Web 服务器已启动(端口 %d)\n", WEB_SERVER_PORT);
|
||||||
|
|
||||||
|
// ✅【改动 4】AsyncUDP 初始化 (替换原来的 udpServer.begin)
|
||||||
|
if (udp.listen(UDP_PORT)) {
|
||||||
|
Serial.printf("✅ AsyncUDP 已监听端口 %d (中断回调模式)\n", UDP_PORT);
|
||||||
|
// 注册回调函数
|
||||||
|
udp.onPacket(onUdpPacket);
|
||||||
|
} else {
|
||||||
|
Serial.println("❌ AsyncUDP 监听失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.printf("✅ 系统就绪!访问 http://%s\n\n", WiFi.softAPIP().toString().c_str());
|
||||||
|
Serial.println("💡 提示:UDP 数据现在由中断处理,推流不会卡顿通讯。");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
server.handleClient();
|
||||||
|
|
||||||
|
// ✅【改动 5】删除了所有 udpServer.parsePacket() 相关代码
|
||||||
|
// 现在 UDP 处理完全在 onUdpPacket 回调中自动完成,loop 中无需任何操作
|
||||||
|
|
||||||
|
static uint32_t last_report = 0;
|
||||||
|
// 系统状态报告可根据需要开启
|
||||||
|
// if (millis() - last_report >= 20000) { ... }
|
||||||
|
|
||||||
|
if (Serial.available()) {
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
|
||||||
|
if (cmd.startsWith("WIFI:")) {
|
||||||
|
int firstColon = cmd.indexOf(':', 5);
|
||||||
|
if (firstColon != -1) {
|
||||||
|
String new_ssid = cmd.substring(5, firstColon);
|
||||||
|
String new_pwd = cmd.substring(firstColon + 1);
|
||||||
|
|
||||||
|
if (new_ssid.length() >= 1 && new_ssid.length() <= 32 &&
|
||||||
|
new_pwd.length() >= 8 && new_pwd.length() <= 64) {
|
||||||
|
|
||||||
|
Preferences prefs;
|
||||||
|
prefs.begin("wifi_cfg", false);
|
||||||
|
prefs.putString("ssid", new_ssid);
|
||||||
|
prefs.putString("pwd", new_pwd);
|
||||||
|
prefs.end();
|
||||||
|
|
||||||
|
Serial.println("\n✅ 配置已保存,2秒后自动重启...");
|
||||||
|
Serial.printf(" SSID: %s\n", new_ssid.c_str());
|
||||||
|
Serial.printf(" 密码: %s\n", new_pwd.c_str());
|
||||||
|
delay(2000);
|
||||||
|
ESP.restart();
|
||||||
|
} else {
|
||||||
|
Serial.println("❌ 错误:SSID(1-32字符) 密码(8-64字符)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("❌ 格式错误:请使用 'wifi:名称:密码' 格式");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// void onUdpPacket(AsyncUDPPacket packet) {
|
||||||
|
// if (packet.isBroadcast() || packet.isMulticast()) return;
|
||||||
|
|
||||||
|
// String payload = "";
|
||||||
|
// for (size_t i = 0; i < packet.length(); i++) {
|
||||||
|
// payload += (char)packet.data()[i];
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 激光控制
|
||||||
|
// if (payload.endsWith("LASEROFF")) {
|
||||||
|
// digitalWrite(2, HIGH);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// if (payload.endsWith("LASERON")) {
|
||||||
|
// digitalWrite(2, LOW);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 串口转发(截断长消息)
|
||||||
|
// if(payload.endsWith("ZHY")||payload.endsWith("\n"))
|
||||||
|
// {
|
||||||
|
// Serial.println(payload);
|
||||||
|
// }
|
||||||
|
// else if(payload.length() > 128)
|
||||||
|
// {
|
||||||
|
// payload = payload.substring(0, 128);
|
||||||
|
// Serial.println(payload);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// if (Serial.available()) {
|
||||||
|
// String cmd = Serial.readStringUntil('\n');
|
||||||
|
// cmd.trim();
|
||||||
|
|
||||||
|
// if (cmd.startsWith("WIFI:")) {
|
||||||
|
// int firstColon = cmd.indexOf(':', 5);
|
||||||
|
// if (firstColon != -1) {
|
||||||
|
// String new_ssid = cmd.substring(5, firstColon);
|
||||||
|
// String new_pwd = cmd.substring(firstColon + 1);
|
||||||
|
|
||||||
|
// if (new_ssid.length() >= 1 && new_ssid.length() <= 32 &&
|
||||||
|
// new_pwd.length() >= 8 && new_pwd.length() <= 64) {
|
||||||
|
|
||||||
|
// Preferences prefs;
|
||||||
|
// prefs.begin("wifi_cfg", false);
|
||||||
|
// prefs.putString("ssid", new_ssid);
|
||||||
|
// prefs.putString("pwd", new_pwd);
|
||||||
|
// prefs.end();
|
||||||
|
|
||||||
|
// Serial.println("\n✅ 配置已保存,2秒后自动重启...");
|
||||||
|
// Serial.printf(" SSID: %s\n", new_ssid.c_str());
|
||||||
|
// Serial.printf(" 密码: %s\n", new_pwd.c_str());
|
||||||
|
// delay(2000);
|
||||||
|
// ESP.restart();
|
||||||
|
// } else {
|
||||||
|
// Serial.println("❌ 错误:SSID(1-32字符) 密码(8-64字符)");
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// Serial.println("❌ 格式错误:请使用 'wifi:名称:密码' 格式");
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
672
jxbeye/src/普通连接版本
Normal file
672
jxbeye/src/普通连接版本
Normal file
@@ -0,0 +1,672 @@
|
|||||||
|
从机端
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WebServer.h>
|
||||||
|
#include "esp_camera.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include <freertos/queue.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
// ===== UDP通信类(is_initialized 改回普通成员 + 新增访问方法)=====
|
||||||
|
class UDPPeer {
|
||||||
|
private:
|
||||||
|
WiFiUDP udp;
|
||||||
|
bool ready = false;
|
||||||
|
uint16_t local_port = 8888;
|
||||||
|
|
||||||
|
// 双缓存
|
||||||
|
String receivedRaw;
|
||||||
|
String parsedValue;
|
||||||
|
|
||||||
|
// 连接参数
|
||||||
|
const char* ssid = nullptr;
|
||||||
|
const char* password = nullptr;
|
||||||
|
IPAddress local_ip;
|
||||||
|
IPAddress gateway;
|
||||||
|
IPAddress subnet;
|
||||||
|
|
||||||
|
// ✅ 监视器IP最后一位
|
||||||
|
uint8_t monitor_ip_last = 101;
|
||||||
|
|
||||||
|
// ✅ 改回普通成员变量(非静态)
|
||||||
|
bool is_initialized = false;
|
||||||
|
|
||||||
|
uint32_t last_maintenance = 0;
|
||||||
|
const uint32_t MAINTENANCE_INTERVAL = 1000;
|
||||||
|
|
||||||
|
void autoMaintain() {
|
||||||
|
if (!is_initialized) return;
|
||||||
|
|
||||||
|
if (millis() - last_maintenance < MAINTENANCE_INTERVAL) return;
|
||||||
|
last_maintenance = millis();
|
||||||
|
//Serial.printf("testA");
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
WiFi.config(local_ip, gateway, subnet);
|
||||||
|
if (!ready)
|
||||||
|
{
|
||||||
|
Serial.printf("正在启动网络连接!!!\n");
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.printf("网络中断,恢复中!!!\n");
|
||||||
|
WiFi.reconnect();
|
||||||
|
}
|
||||||
|
//last_maintenance = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//Serial.printf("testB");
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
if (ready) {
|
||||||
|
Serial.printf("✅ 网络就绪: IP=%s 端口=%d\n",
|
||||||
|
WiFi.localIP().toString().c_str(), local_port);
|
||||||
|
} else {
|
||||||
|
//last_maintenance = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//last_maintenance = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ✅ 新增:访问 is_initialized 的成员方法
|
||||||
|
bool isInitialized() {
|
||||||
|
return is_initialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 设置监视器IP
|
||||||
|
void setMonitorIPLast(uint8_t last_octet) {
|
||||||
|
monitor_ip_last = last_octet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 获取完整监视器IP
|
||||||
|
String getMonitorIP() {
|
||||||
|
return String(gateway[0]) + "." + String(gateway[1]) + "." +
|
||||||
|
String(gateway[2]) + "." + String(monitor_ip_last);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool connect(const char* _ssid, const char* _pwd,
|
||||||
|
uint8_t gw1, uint8_t gw2, uint8_t gw3, uint8_t gw4,
|
||||||
|
uint8_t local_ip_last, uint16_t port = 8888) {
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
last_maintenance = 0;
|
||||||
|
ready = false;
|
||||||
|
|
||||||
|
ssid = _ssid;
|
||||||
|
password = _pwd;
|
||||||
|
local_ip = IPAddress(gw1, gw2, gw3, local_ip_last);
|
||||||
|
gateway = IPAddress(gw1, gw2, gw3, gw4);
|
||||||
|
subnet = IPAddress(255, 255, 255, 0);
|
||||||
|
local_port = port;
|
||||||
|
is_initialized = true; // ✅ 普通成员赋值
|
||||||
|
|
||||||
|
Serial.printf("📶 wifi:%s, pwd:%s,配置网络: IP=%d.%d.%d.%d 网关=%d.%d.%d.%d 端口=%d\n",
|
||||||
|
ssid, password, gw1, gw2, gw3, local_ip_last, gw1, gw2, gw3, gw4, port);
|
||||||
|
Serial.println("💡 网络将在后台自动连接...");
|
||||||
|
|
||||||
|
autoMaintain();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasMessage() {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
receivedRaw = "";
|
||||||
|
parsedValue = "";
|
||||||
|
|
||||||
|
int len = udp.parsePacket();
|
||||||
|
if (len <= 0 || len > 512) return false;
|
||||||
|
|
||||||
|
char buf[513];
|
||||||
|
int read = udp.read((uint8_t*)buf, min(len, 512));
|
||||||
|
if (read > 0) {
|
||||||
|
buf[read] = '\0';
|
||||||
|
receivedRaw = String(buf);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool getMessageValue(const String& key) {
|
||||||
|
parsedValue = "";
|
||||||
|
if (receivedRaw.length() == 0) return false;
|
||||||
|
|
||||||
|
String keyPattern = "\"" + key + "\"";
|
||||||
|
int startIndex = receivedRaw.indexOf(keyPattern);
|
||||||
|
if (startIndex == -1) return false;
|
||||||
|
|
||||||
|
int colonIndex = receivedRaw.indexOf(":", startIndex);
|
||||||
|
if (colonIndex == -1) return false;
|
||||||
|
|
||||||
|
int valueStart = colonIndex + 1;
|
||||||
|
while (valueStart < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueStart) == ' ') valueStart++;
|
||||||
|
|
||||||
|
if (valueStart < receivedRaw.length() && receivedRaw.charAt(valueStart) == '"') {
|
||||||
|
int valueEnd = receivedRaw.indexOf("\"", valueStart + 1);
|
||||||
|
if (valueEnd != -1) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart + 1, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int valueEnd = valueStart;
|
||||||
|
while (valueEnd < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ',' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != '}' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ' ') valueEnd++;
|
||||||
|
while (valueEnd > valueStart && receivedRaw.charAt(valueEnd - 1) == ' ') valueEnd--;
|
||||||
|
if (valueEnd > valueStart) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const char* format, ...) {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
IPAddress target_ip;
|
||||||
|
if (!target_ip.fromString(target_ip_str)) return false;
|
||||||
|
|
||||||
|
char content[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(content, sizeof(content) - 1, format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
char json[300];
|
||||||
|
snprintf(json, sizeof(json), "{\"%s\":\"%s\"}\r\n", key.c_str(), content);
|
||||||
|
|
||||||
|
return udp.beginPacket(target_ip, local_port) &&
|
||||||
|
udp.write((const uint8_t*)json, strlen(json)) &&
|
||||||
|
udp.endPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const String& value) {
|
||||||
|
return sendTo(target_ip_str, key, "%s", value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastValue() { return parsedValue; }
|
||||||
|
String lastMessage() { return receivedRaw; }
|
||||||
|
String getLocalIP() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized ? WiFi.localIP().toString() : "未配置";
|
||||||
|
}
|
||||||
|
bool isConnected() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized && ready;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
extern UDPPeer peer;
|
||||||
|
UDPPeer peer;
|
||||||
|
|
||||||
|
// ===== 默认配置 =====
|
||||||
|
#define DEFAULT_SSID "ESP32-Camera"
|
||||||
|
#define DEFAULT_PWD "12345678"
|
||||||
|
#define DEFAULT_GW1 192
|
||||||
|
#define DEFAULT_GW2 168
|
||||||
|
#define DEFAULT_GW3 4
|
||||||
|
#define DEFAULT_GW4 1
|
||||||
|
#define DEFAULT_LOCAL_LAST 100
|
||||||
|
#define DEFAULT_MONITOR_LAST 101
|
||||||
|
|
||||||
|
// ===== Flash配置管理 =====
|
||||||
|
void loadConfigFromFlash() {
|
||||||
|
Preferences prefs;
|
||||||
|
if (!prefs.begin("cam_cfg", true)) { // 只读模式
|
||||||
|
Serial.println("⚠️ Flash配置不存在,使用默认值");
|
||||||
|
peer.connect(DEFAULT_SSID, DEFAULT_PWD,
|
||||||
|
DEFAULT_GW1, DEFAULT_GW2, DEFAULT_GW3, DEFAULT_GW4,
|
||||||
|
DEFAULT_LOCAL_LAST);
|
||||||
|
peer.setMonitorIPLast(DEFAULT_MONITOR_LAST);
|
||||||
|
prefs.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取配置
|
||||||
|
String ssid = prefs.getString("ssid", DEFAULT_SSID);
|
||||||
|
String pwd = prefs.getString("pwd", DEFAULT_PWD);
|
||||||
|
uint8_t gw1 = prefs.getUChar("gw1", DEFAULT_GW1);
|
||||||
|
uint8_t gw2 = prefs.getUChar("gw2", DEFAULT_GW2);
|
||||||
|
uint8_t gw3 = prefs.getUChar("gw3", DEFAULT_GW3);
|
||||||
|
uint8_t gw4 = prefs.getUChar("gw4", DEFAULT_GW4);
|
||||||
|
uint8_t local_last = prefs.getUChar("local", DEFAULT_LOCAL_LAST);
|
||||||
|
uint8_t monitor_last = prefs.getUChar("monitor", DEFAULT_MONITOR_LAST);
|
||||||
|
|
||||||
|
prefs.end();
|
||||||
|
|
||||||
|
// 应用配置
|
||||||
|
peer.connect(ssid.c_str(), pwd.c_str(), gw1, gw2, gw3, gw4, local_last);
|
||||||
|
peer.setMonitorIPLast(monitor_last);
|
||||||
|
|
||||||
|
Serial.println("✅ 从Flash加载配置成功");
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveConfigToFlash(const char* ssid, const char* pwd,
|
||||||
|
uint8_t gw1, uint8_t gw2, uint8_t gw3, uint8_t gw4,
|
||||||
|
uint8_t local_last, uint8_t monitor_last) {
|
||||||
|
Preferences prefs;
|
||||||
|
if (!prefs.begin("cam_cfg", false)) { // 读写模式
|
||||||
|
Serial.println("❌ Flash配置保存失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
prefs.putString("ssid", ssid);
|
||||||
|
prefs.putString("pwd", pwd);
|
||||||
|
prefs.putUChar("gw1", gw1);
|
||||||
|
prefs.putUChar("gw2", gw2);
|
||||||
|
prefs.putUChar("gw3", gw3);
|
||||||
|
prefs.putUChar("gw4", gw4);
|
||||||
|
prefs.putUChar("local", local_last);
|
||||||
|
prefs.putUChar("monitor", monitor_last);
|
||||||
|
|
||||||
|
prefs.end();
|
||||||
|
Serial.println("✅ 配置已保存到Flash");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== 串口指令解析 =====
|
||||||
|
void handleSerialCommand() {
|
||||||
|
if (!Serial.available()) return;
|
||||||
|
|
||||||
|
String cmd = Serial.readStringUntil('\n');
|
||||||
|
cmd.trim();
|
||||||
|
|
||||||
|
if (!cmd.startsWith("wificar:"))
|
||||||
|
{
|
||||||
|
if(cmd.endsWith("LZU")||cmd.endsWith("LZU\r"))
|
||||||
|
{
|
||||||
|
peer.sendTo(peer.getMonitorIP(), "f", cmd);
|
||||||
|
Serial.printf("📤 已发送到 %s: {\"f\":%s}\n", peer.getMonitorIP().c_str(), cmd);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 解析格式: wificar:ssid:pwd:gw1:gw2:gw3:gw4:local_last:monitor_last
|
||||||
|
// 示例: wificar:MyCam:secure123:192:168:43:1:100:101
|
||||||
|
int pos[7]; // 存储6个冒号位置
|
||||||
|
int count = 0;
|
||||||
|
for (int i = 8; i < cmd.length() && count < 7; i++) { // 从"wificar:"后开始
|
||||||
|
if (cmd.charAt(i) == ':') {
|
||||||
|
pos[count++] = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count < 7) {
|
||||||
|
Serial.println("❌ 指令格式错误,应为: wificar:ssid:pwd:gw1:gw2:gw3:gw4:local_last:monitor_last");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ssid = cmd.substring(8, pos[0]);
|
||||||
|
String pwd = cmd.substring(pos[0] + 1, pos[1]);
|
||||||
|
uint8_t gw1 = cmd.substring(pos[1] + 1, pos[2]).toInt();
|
||||||
|
uint8_t gw2 = cmd.substring(pos[2] + 1, pos[3]).toInt();
|
||||||
|
uint8_t gw3 = cmd.substring(pos[3] + 1, pos[4]).toInt();
|
||||||
|
uint8_t gw4 = cmd.substring(pos[4] + 1, pos[5]).toInt();
|
||||||
|
uint8_t local_last = cmd.substring(pos[5] + 1, pos[6]).toInt();
|
||||||
|
uint8_t monitor_last = cmd.substring(pos[6] + 1).toInt();
|
||||||
|
|
||||||
|
// 应用新配置
|
||||||
|
peer.connect(ssid.c_str(), pwd.c_str(), gw1, gw2, gw3, gw4, local_last);
|
||||||
|
peer.setMonitorIPLast(monitor_last);
|
||||||
|
|
||||||
|
// 保存到Flash
|
||||||
|
saveConfigToFlash(ssid.c_str(), pwd.c_str(), gw1, gw2, gw3, gw4, local_last, monitor_last);
|
||||||
|
|
||||||
|
Serial.println("✅ WiFi配置已更新并保存");
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(1000);
|
||||||
|
|
||||||
|
Serial.println("\n╔════════════════════════════════════════════════════════════════╗");
|
||||||
|
Serial.println("║ ESP32-S3 摄像头 UDP 通信系统 v1.0 ║");
|
||||||
|
Serial.println("║ 支持Flash配置存储 | 串口动态配置 | 自动重连 ║");
|
||||||
|
Serial.println("╚════════════════════════════════════════════════════════════════╝");
|
||||||
|
Serial.println("💡 串口指令:wificar:ssid:pwd:gw1:gw2:gw3:gw4:local_last:monitor_last");
|
||||||
|
Serial.println(" 示例: wificar:MyCam:secure123:192:168:43:1:100:101\n");
|
||||||
|
|
||||||
|
// 从Flash加载配置(自动回退到默认值)
|
||||||
|
loadConfigFromFlash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
// 处理串口配置指令
|
||||||
|
handleSerialCommand();
|
||||||
|
|
||||||
|
// ✅ 使用成员方法读取 is_initialized 状态
|
||||||
|
if (peer.isInitialized()) {
|
||||||
|
// 每2秒发送一次消息到监视器
|
||||||
|
// static uint32_t last_send = 0;
|
||||||
|
// if (millis() - last_send >= 2000) {
|
||||||
|
// last_send = millis();
|
||||||
|
// peer.sendTo(peer.getMonitorIP(), "f", "device");
|
||||||
|
// Serial.printf("📤 已发送到 %s: {\"f\":\"start\"}\n", peer.getMonitorIP().c_str());
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 每5ms检查一次消息
|
||||||
|
static uint32_t last_check = 0;
|
||||||
|
if (millis() - last_check >= 6) {
|
||||||
|
last_check = millis();
|
||||||
|
|
||||||
|
if (peer.hasMessage()) {
|
||||||
|
if (peer.getMessageValue("c")) {
|
||||||
|
Serial.printf("📥 收到消息: %sZHY\n", peer.lastValue().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(1); // 避免忙等待
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
|
||||||
|
主机端
|
||||||
|
#include <WiFi.h>
|
||||||
|
#include <WiFiUdp.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
|
||||||
|
class UDPPeer {
|
||||||
|
private:
|
||||||
|
WiFiUDP udp;
|
||||||
|
bool ready = false;
|
||||||
|
uint16_t local_port = 8888;
|
||||||
|
|
||||||
|
// 双缓存
|
||||||
|
String receivedRaw;
|
||||||
|
String parsedValue;
|
||||||
|
|
||||||
|
// 连接参数
|
||||||
|
const char* ssid = nullptr;
|
||||||
|
const char* password = nullptr;
|
||||||
|
IPAddress local_ip;
|
||||||
|
IPAddress gateway;
|
||||||
|
IPAddress subnet;
|
||||||
|
|
||||||
|
// ✅ 新增:AP模式标志(默认false=STA模式)
|
||||||
|
bool is_ap_mode = false;
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 新增:网络配置缓存(6行) ✅ ✅ ✅
|
||||||
|
String current_ssid = "";
|
||||||
|
String current_password = "";
|
||||||
|
uint8_t cached_gw1 = 0, cached_gw2 = 0, cached_gw3 = 0, cached_gw4 = 0;
|
||||||
|
|
||||||
|
// 状态标志
|
||||||
|
bool is_initialized = false;
|
||||||
|
uint32_t last_maintenance = 0;
|
||||||
|
const uint32_t MAINTENANCE_INTERVAL = 1000;
|
||||||
|
|
||||||
|
// ✅ 修正后的轻量维护(AP模式特殊处理)
|
||||||
|
void autoMaintain() {
|
||||||
|
if (!is_initialized) return;
|
||||||
|
|
||||||
|
if (millis() - last_maintenance < MAINTENANCE_INTERVAL) return;
|
||||||
|
last_maintenance = millis();
|
||||||
|
|
||||||
|
// ✅ AP模式:无需维护WiFi(始终在线),仅确保UDP就绪
|
||||||
|
if (is_ap_mode) {
|
||||||
|
if (!ready) {
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
if (ready) {
|
||||||
|
Serial.printf("✅ AP模式UDP就绪: 端口=%d\n", local_port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//last_maintenance = millis();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WiFi.status() != WL_CONNECTED) {
|
||||||
|
WiFi.config(local_ip, gateway, subnet);
|
||||||
|
if (!ready)
|
||||||
|
{
|
||||||
|
Serial.printf("start local net正在启动网络连接!!!\n");
|
||||||
|
WiFi.begin(ssid, password);
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Serial.printf("reconnect网络中断,恢复中!!!\n");
|
||||||
|
WiFi.reconnect();
|
||||||
|
}
|
||||||
|
//last_maintenance = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ready) {
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
if (ready) {
|
||||||
|
Serial.printf("✅ 网络就绪: IP=%s 端口=%d\n",
|
||||||
|
WiFi.localIP().toString().c_str(), local_port);
|
||||||
|
} else {
|
||||||
|
//last_maintenance = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//last_maintenance = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// ✅ STA模式连接(末尾添加配置缓存 - 6行)
|
||||||
|
bool connect(const char* _ssid, const char* _pwd,
|
||||||
|
uint8_t gw1, uint8_t gw2, uint8_t gw3, uint8_t gw4,
|
||||||
|
uint8_t local_ip_last, uint16_t port = 8888) {
|
||||||
|
// 切换到STA模式
|
||||||
|
WiFi.mode(WIFI_STA);
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
last_maintenance = 0;
|
||||||
|
ready = false;
|
||||||
|
ssid = _ssid;
|
||||||
|
password = _pwd;
|
||||||
|
local_ip = IPAddress(gw1, gw2, gw3, local_ip_last);
|
||||||
|
gateway = IPAddress(gw1, gw2, gw3, gw4);
|
||||||
|
subnet = IPAddress(255, 255, 255, 0);
|
||||||
|
local_port = port;
|
||||||
|
is_initialized = true;
|
||||||
|
is_ap_mode = false;
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 新增:缓存网络配置 ✅ ✅ ✅
|
||||||
|
current_ssid = String(_ssid);
|
||||||
|
current_password = String(_pwd);
|
||||||
|
cached_gw1 = gw1;
|
||||||
|
cached_gw2 = gw2;
|
||||||
|
cached_gw3 = gw3;
|
||||||
|
cached_gw4 = gw4;
|
||||||
|
|
||||||
|
Serial.printf("📶 配置STA网络: IP=%d.%d.%d.%d 网关=%d.%d.%d.%d 端口=%d\n",
|
||||||
|
gw1, gw2, gw3, local_ip_last, gw1, gw2, gw3, gw4, port);
|
||||||
|
Serial.println("💡 网络将在后台自动连接...");
|
||||||
|
|
||||||
|
autoMaintain();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ AP模式连接(末尾添加配置缓存 - 6行)✅ ✅ ✅
|
||||||
|
bool connectAP(const char* _ssid, const char* _pwd, uint16_t port = 8888) {
|
||||||
|
WiFi.disconnect(true);
|
||||||
|
WiFi.mode(WIFI_AP);
|
||||||
|
last_maintenance = 0;
|
||||||
|
ready = false;
|
||||||
|
WiFi.softAPConfig(IPAddress(192, 168, 4, 1),
|
||||||
|
IPAddress(192, 168, 4, 1),
|
||||||
|
IPAddress(255, 255, 255, 0));
|
||||||
|
|
||||||
|
if (!WiFi.softAP(_ssid, _pwd, 4, false, 4)) {
|
||||||
|
Serial.println("❌ AP启动失败");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_port = port;
|
||||||
|
ready = (udp.begin(local_port) == 1);
|
||||||
|
is_initialized = true;
|
||||||
|
is_ap_mode = true;
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 新增:缓存AP模式固定配置 ✅ ✅ ✅
|
||||||
|
current_ssid = String(_ssid);
|
||||||
|
current_password = String(_pwd);
|
||||||
|
cached_gw1 = 192;
|
||||||
|
cached_gw2 = 168;
|
||||||
|
cached_gw3 = 4;
|
||||||
|
cached_gw4 = 1;
|
||||||
|
|
||||||
|
Serial.printf("✅ AP模式启动: 热点=\"%s\" 密码=\"%s\"\n", _ssid, _pwd);
|
||||||
|
Serial.printf("🌐 本机IP: %s | 端口: %d | 客户端: %d/%d\n",
|
||||||
|
WiFi.softAPIP().toString().c_str(), port,
|
||||||
|
WiFi.softAPgetStationNum(), 4);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ ✅ ✅ 核心:向小车发送配置(仅需IP最后1段 - 20行)✅ ✅ ✅
|
||||||
|
void sendConfigToDevice(uint8_t car_last_octet, uint8_t monitor_last_octet) {
|
||||||
|
if (!is_initialized) {
|
||||||
|
Serial.println("❌ 手柄未配置网络,无法发送配置");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (car_last_octet == cached_gw4 || car_last_octet == 0 || car_last_octet == 255) {
|
||||||
|
Serial.println("❌ 无效的小车编号(需1~254,且不能与网关相同)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取本机IP最后1段(AP模式固定为1,STA模式取local_ip[3])
|
||||||
|
uint8_t local_last = is_ap_mode ? 1 : local_ip[3];
|
||||||
|
if(monitor_last_octet!=255) local_last =monitor_last_octet;
|
||||||
|
|
||||||
|
// 发送标准配置指令(与终端handleSerialCommand完全兼容)
|
||||||
|
Serial.printf("wificar:%s:%s:%d:%d:%d:%d:%d:%d\nWZHY",
|
||||||
|
current_ssid.c_str(),
|
||||||
|
current_password.c_str(),
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, cached_gw4,
|
||||||
|
car_last_octet, // 小车本机IP最后1段
|
||||||
|
local_last); // 监视器IP最后1段(指向本机)
|
||||||
|
|
||||||
|
Serial1.printf("wificar:%s:%s:%d:%d:%d:%d:%d:%d\nWZHY",
|
||||||
|
current_ssid.c_str(),
|
||||||
|
current_password.c_str(),
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, cached_gw4,
|
||||||
|
car_last_octet, // 小车本机IP最后1段
|
||||||
|
local_last); // 监视器IP最后1段(指向本机)
|
||||||
|
|
||||||
|
|
||||||
|
Serial.printf("✅ 已配置小车 #%d → IP=%d.%d.%d.%d | 监视器=%d.%d.%d.%d\n",
|
||||||
|
car_last_octet,
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, car_last_octet,
|
||||||
|
cached_gw1, cached_gw2, cached_gw3, local_last);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 接收:轻量维护 + 纯检查(自动适配AP/STA)
|
||||||
|
bool hasMessage() {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
receivedRaw = "";
|
||||||
|
parsedValue = "";
|
||||||
|
|
||||||
|
int len = udp.parsePacket();
|
||||||
|
if (len <= 0 || len > 512) return false;
|
||||||
|
|
||||||
|
char buf[513];
|
||||||
|
int read = udp.read((uint8_t*)buf, min(len, 512));
|
||||||
|
if (read > 0) {
|
||||||
|
buf[read] = '\0';
|
||||||
|
receivedRaw = String(buf);
|
||||||
|
// ✅【修复 1】补全右括号 + 简化 this->
|
||||||
|
// ✅【修复 2】类内调用直接写 lastValue()
|
||||||
|
if (getMessageValue("ESPNOW_REG")) {
|
||||||
|
Serial.println(lastValue());
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 解析字段(不变)
|
||||||
|
bool getMessageValue(const String& key) {
|
||||||
|
parsedValue = "";
|
||||||
|
if (receivedRaw.length() == 0) return false;
|
||||||
|
|
||||||
|
String keyPattern = "\"" + key + "\"";
|
||||||
|
int startIndex = receivedRaw.indexOf(keyPattern);
|
||||||
|
if (startIndex == -1) return false;
|
||||||
|
|
||||||
|
int colonIndex = receivedRaw.indexOf(":", startIndex);
|
||||||
|
if (colonIndex == -1) return false;
|
||||||
|
|
||||||
|
int valueStart = colonIndex + 1;
|
||||||
|
while (valueStart < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueStart) == ' ') valueStart++;
|
||||||
|
|
||||||
|
if (valueStart < receivedRaw.length() && receivedRaw.charAt(valueStart) == '"') {
|
||||||
|
int valueEnd = receivedRaw.indexOf("\"", valueStart + 1);
|
||||||
|
if (valueEnd != -1) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart + 1, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
int valueEnd = valueStart;
|
||||||
|
while (valueEnd < receivedRaw.length() &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ',' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != '}' &&
|
||||||
|
receivedRaw.charAt(valueEnd) != ' ') valueEnd++;
|
||||||
|
while (valueEnd > valueStart && receivedRaw.charAt(valueEnd - 1) == ' ') valueEnd--;
|
||||||
|
if (valueEnd > valueStart) {
|
||||||
|
parsedValue = receivedRaw.substring(valueStart, valueEnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 发送:轻量维护 + 直接发送(自动适配AP/STA)
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const char* format, ...) {
|
||||||
|
autoMaintain();
|
||||||
|
if (!is_initialized || !ready) return false;
|
||||||
|
|
||||||
|
IPAddress target_ip;
|
||||||
|
if (!target_ip.fromString(target_ip_str)) return false;
|
||||||
|
|
||||||
|
char content[256];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
vsnprintf(content, sizeof(content) - 1, format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
char json[300];
|
||||||
|
snprintf(json, sizeof(json), "{\"%s\":\"%s\"}\r\n", key.c_str(), content);
|
||||||
|
|
||||||
|
return udp.beginPacket(target_ip, local_port) &&
|
||||||
|
udp.write((const uint8_t*)json, strlen(json)) &&
|
||||||
|
udp.endPacket();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 积木友好重载
|
||||||
|
bool sendTo(const String& target_ip_str, const String& key, const String& value) {
|
||||||
|
return sendTo(target_ip_str, key, "%s", value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 辅助方法:自动返回正确IP(AP/STA自适应)
|
||||||
|
String lastValue() { return parsedValue; }
|
||||||
|
String lastMessage() { return receivedRaw; }
|
||||||
|
String getLocalIP() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized ?
|
||||||
|
(is_ap_mode ? WiFi.softAPIP().toString() : WiFi.localIP().toString()) :
|
||||||
|
"未配置";
|
||||||
|
}
|
||||||
|
bool isConnected() {
|
||||||
|
autoMaintain();
|
||||||
|
return is_initialized && ready;
|
||||||
|
}
|
||||||
|
bool isAPMode() { return is_ap_mode; }
|
||||||
|
};
|
||||||
|
extern UDPPeer peer;
|
||||||
11
jxbeye/test/README
Normal file
11
jxbeye/test/README
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
This directory is intended for PlatformIO Test Runner and project tests.
|
||||||
|
|
||||||
|
Unit Testing is a software testing method by which individual units of
|
||||||
|
source code, sets of one or more MCU program modules together with associated
|
||||||
|
control data, usage procedures, and operating procedures, are tested to
|
||||||
|
determine whether they are fit for use. Unit testing finds problems early
|
||||||
|
in the development cycle.
|
||||||
|
|
||||||
|
More information about PlatformIO Unit Testing:
|
||||||
|
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||||
Reference in New Issue
Block a user