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:
2026-05-14 16:39:01 +08:00
commit aa7ed7cf51
13 changed files with 3185 additions and 0 deletions

34
jxbeye/.clangd Normal file
View 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
View 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
View 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"
]
}

View 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"
}

File diff suppressed because one or more lines are too long

37
jxbeye/include/README Normal file
View 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
View 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
View 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 模式必须为 qioQuad 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
View 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
View 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
View 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:名称:密码' 格式");
// }
// }
// }

View 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模式固定为1STA模式取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());
}
// ✅ 辅助方法自动返回正确IPAP/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
View 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