commit 5e2dfc8ac887de3c38eafd539d209b53230f29fd Author: userName Date: Mon Dec 15 09:19:47 2025 +0800 Radar diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -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" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..01b09ad --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.associations": { + "cmath": "cpp", + "array": "cpp", + "string": "cpp", + "string_view": "cpp" + }, + "C_Cpp.errorSquiggles": "disabled" +} \ No newline at end of file diff --git a/LVEDO.cpp b/LVEDO.cpp new file mode 100644 index 0000000..e69de29 diff --git a/data_formats_explanation.md b/data_formats_explanation.md new file mode 100644 index 0000000..e875a8c --- /dev/null +++ b/data_formats_explanation.md @@ -0,0 +1,92 @@ +# 数据格式说明 + +## 1. 雷达数据格式 (HBR01) + +### 格式特征 +- 以 `HBR01:` 开头 +- 后跟8个用逗号分隔的数值字段 +- 每行一条数据记录 + +### 数据字段说明 +``` +HBR01:presence,heart_rate,breath_rate,motion,rssi,heartbeat_waveform,breathing_waveform,raw_signal +``` + +### 字段含义 +1. `presence` - 人员存在状态 (0=无人, 1=有人) +2. `heart_rate` - 心率 (bpm) +3. `breath_rate` - 呼吸率 (bpm) +4. `motion` - 运动状态 (0=静止, 1=运动) +5. `rssi` - 信号强度 +6. `heartbeat_waveform` - 心跳波形数据 +7. `breathing_waveform` - 呼吸波形数据 +8. `raw_signal` - 原始信号数据 + +### 示例数据 +``` +HBR01:1,72,16,0,-45,120,45,-25 +HBR01:0,0,0,0,-50,0,0,10 +``` + +### 处理方式 +在ESP32代码中通过 `parseSensorLine()` 函数处理,按行读取并解析逗号分隔的数值。 + +## 2. JSON数据格式 + +### 格式特征 +- 以花括号 `{` 开始,`}` 结束 +- 符合标准JSON格式 +- 可能因长度超过BLE MTU而需要分包传输 + +### 示例数据 +```json +{ + "type": "radarData", + "deviceId": 1001, + "timestamp": 1640995200000, + "presence": 1, + "heartRate": 72.5, + "breathRate": 16.2, + "motion": 0, + "rssi": -45, + "heartbeatWaveform": 120, + "breathingWaveform": 45, + "rawSignal": -25 +} +``` + +### 分包传输特点 +- 每包最大20字节(BLE限制) +- 需要通过匹配花括号来重组完整JSON +- 可能出现一个完整JSON被分成多个BLE包的情况 + +### 接收处理方式 +使用JavaScript代码中的 `processChunk()` 函数处理: +1. 将接收到的数据片段追加到缓冲区 +2. 通过计数花括号来识别完整的JSON对象 +3. 提取完整JSON并解析 +4. 清理缓冲区中已处理的部分 + +## 3. 两种格式的主要区别 + +| 特性 | 雷达数据 (HBR01) | JSON数据 | +|------|------------------|----------| +| 格式标识 | HBR01:开头 | 花括号{}包围 | +| 数据结构 | 固定8个数值字段 | 灵活的键值对 | +| 分隔符 | 逗号分隔 | JSON标准格式 | +| 分包处理 | 按行处理 | 按花括号匹配 | +| 解析方式 | 数值转换 | JSON解析 | + +## 4. 处理建议 + +### 雷达数据处理 +- 按行读取处理 +- 验证以"HBR01:"开头 +- 按逗号分割提取8个字段 +- 进行数值有效性检查 + +### JSON数据处理 +- 使用缓冲区累积数据 +- 通过花括号匹配识别完整对象 +- 使用JSON解析库处理 +- 注意缓冲区大小限制和清理 \ No newline at end of file diff --git a/generate_radar_data.py b/generate_radar_data.py new file mode 100644 index 0000000..c1d2328 --- /dev/null +++ b/generate_radar_data.py @@ -0,0 +1,44 @@ +import random +import time + +def generate_radar_data_lines(count=100): + """生成模拟雷达数据行""" + lines = [] + + for i in range(count): + # 生成8个随机数值字段 + # 字段含义参考代码: presence, heart_rate, breath_rate, motion, rssi, heartbeat_waveform, breathing_waveform, raw_signal + presence = random.choice([0, 1]) # 0=无人, 1=有人 + heart_rate = random.uniform(0, 200) if presence else 0 # 心率 (bpm) + breath_rate = random.uniform(0, 60) if presence else 0 # 呼吸率 (bpm) + motion = random.choice([0, 1]) if presence else 0 # 运动状态 + rssi = random.randint(-100, -30) # 信号强度 + heartbeat_waveform = random.randint(-1000, 1000) # 心跳波形 + breathing_waveform = random.randint(-1000, 1000) # 呼吸波形 + raw_signal = random.randint(-100, 100) # 原始信号 + + # 格式化为HBR01数据行 + line = f"HBR01:{presence},{heart_rate:.0f},{breath_rate:.0f},{motion},{rssi},{heartbeat_waveform},{breathing_waveform},{raw_signal}" + lines.append(line) + + return lines + +def save_to_file(lines, filename="generated_sensor_data.txt"): + """保存数据到文件""" + with open(filename, "w", encoding="utf-8") as f: + for line in lines: + f.write(line + "\n") + print(f"已生成 {len(lines)} 行雷达数据并保存到 {filename}") + +def main(): + # 生成1000行测试数据 + lines = generate_radar_data_lines(1000) + save_to_file(lines, "generated_sensor_data.txt") + + # 显示前10行作为示例 + print("\n前10行数据示例:") + for i, line in enumerate(lines[:10]): + print(f"{i+1:2d}: {line}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/include/README b/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/include/README @@ -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 diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/lib/README @@ -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 +#include + +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 diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..83d4f5f --- /dev/null +++ b/platformio.ini @@ -0,0 +1,17 @@ +; 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:freenove_esp32_s3_wroom] +platform = espressif32 +board = freenove_esp32_s3_wroom +framework = arduino +lib_deps = + bblanchon/ArduinoJson@^7.4.2 + emelianov/modbus-esp8266@^4.1.0 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..327cec7 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,1316 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include // 添加这个头文件以支持memset函数 + +#define BUFFER_SIZE 2000 // 固定存储2000个数据点 +// 环形缓冲区结构 +struct CircularBuffer { + float data[BUFFER_SIZE]; + unsigned long timestamps[BUFFER_SIZE]; // 存储时间戳 + int head; // 最新数据位置 + int tail; // 最旧数据位置 + int count; // 当前有效数据数量 + bool isFull; // 缓冲区是否已满 + float sum; // 当前数据总和 +}; + +CircularBuffer heartRateBuffer = {{0}, {0}, 0, 0, 0, false, 0}; +CircularBuffer breathRateBuffer = {{0}, {0}, 0, 0, 0, false, 0}; + +Preferences preferences; + +// 设备ID变量 +uint16_t currentDeviceId = 0000; // 默认设备ID为0000 +const uint16_t MIN_DEVICE_ID = 1000; // 设备ID最小值 +const uint16_t MAX_DEVICE_ID = 1999; // 设备ID最大值 + +const uint32_t PHASE_SEND_INTERVAL = 1;// 每1毫秒发送一次相位数据 +const uint32_t VITAL_SEND_INTERVAL = 10;// 每10毫秒发送一次生命体征数据 + +#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" + +BLEServer* pServer = NULL; +BLECharacteristic* pCharacteristic = NULL; +bool deviceConnected = false; +bool oldDeviceConnected = false; +String receivedData = ""; +String completeData = ""; +unsigned long lastReceiveTime = 0; + +WiFiClient client; +char ssid[32] = "13-205"; +char password[64] = "12345678"; +bool wifiConfigured = false; + +const char* influxDBHost = "8.134.11.76"; +const int influxDBPort = 8086; +const char* influxDBToken = "KuTa5ZsqoHIhi2IglOO06zExUYw1_mJ6K0mcA9X1y6O6CJDog3_Cgr8mUw1SwpuCCKRElqxa6wAhrrhsYPytkg=="; +const char* influxDBOrg = "gzlg"; +const char* influxDBBucket = "gzlg"; + +const unsigned long SENSOR_TIMEOUT = 40000; +static uint32_t packetCounter = 0; +static bool shouldSendOtherData = false; + +unsigned long lastSensorUpdate = 0; + +typedef struct { + float breath_rate; + float heart_rate; + int rssi; + uint8_t breath_valid; + uint8_t heart_valid; + uint8_t presence; + uint8_t motion; + int heartbeat_waveform; + int breathing_waveform; + int raw_signal; +} SensorData; + +SensorData sensorData = {0}; + +HardwareSerial mySerial2(2); +const int BAUD_RATE = 576000; +const int UART2_RX = 16; +const int UART2_TX = 17; +String rxBuffer = ""; + +// FreeRTOS任务和队列定义 +QueueHandle_t phaseDataQueue; +QueueHandle_t vitalDataQueue; +TaskHandle_t bleSendTaskHandle = NULL; +TaskHandle_t vitalSendTaskHandle = NULL; +// 注意:bleSendTaskHandle现在用于蓝牙发送任务 + +#define QUEUE_SIZE 50 +#define TASK_STACK_SIZE 8192 +#define TASK_PRIORITY 1 +#define UART_RX_QUEUE_SIZE 100 // 串口接收队列大小 + +typedef struct { + int heartbeat_waveform; + int breathing_waveform; +} PhaseData; + +typedef struct { + float heart_rate; + float breath_rate; + uint8_t presence; + uint8_t motion; + int rssi; +} VitalData; + +// 添加流量控制类 +class BLEFlowController { +private: + size_t maxBytesPerSecond; + size_t bytesSent; + unsigned long lastResetTime; + unsigned long lastSendTime; + +public: + BLEFlowController(size_t maxBps) : maxBytesPerSecond(maxBps), bytesSent(0) { + lastResetTime = millis(); + lastSendTime = 0; + } + + bool canSend(size_t dataSize) { + unsigned long currentTime = millis(); + + // 每秒重置计数器 + if(currentTime - lastResetTime >= 1000) { + bytesSent = 0; + lastResetTime = currentTime; + } + + // 检查速率限制(放宽限制) + if((bytesSent + dataSize) > maxBytesPerSecond) { + return false; + } + + // 最小发送间隔控制(重要!) + if(currentTime - lastSendTime < 5) { // 进一步减小最小间隔到5ms + return false; + } + + return true; + } + + bool check() { + // 这是一个简化版本的检查方法,只检查最小发送间隔 + unsigned long currentTime = millis(); + if(currentTime - lastSendTime < 5) { // 最小间隔5ms + return false; + } + return true; + } + + void recordSend(size_t dataSize) { + bytesSent += dataSize; + lastSendTime = millis(); + } + + void reset() { + bytesSent = 0; + lastResetTime = millis(); + lastSendTime = 0; + } +}; + +// 全局变量用于控制持续发送 +bool continuousSendEnabled = false; +unsigned long continuousSendInterval = 1000; // 默认1秒发送一次 +BLEFlowController bleFlow(500); // 提高到500 B/s限制,进一步提高数据传输速率 + +void processBLEConfig(); // 处理BLE配置命令 +void saveWiFiConfig(); // 保存WiFi配置 +void loadWiFiConfig(); // 加载WiFi配置 +bool connectWiFi(); // 连接WiFi +bool processWiFiConfigCommand(JsonDocument& doc); // 处理WiFi配置命令 +void processRadarData(); // 处理雷达数据 +bool parseSensorLine(String line); // 解析传感器数据行 +// void sendRadarCommand(String cmd); // 发送雷达命令 +void updateStatusFlags(); // 更新状态标志 +void loadDeviceId(); // 加载设备ID +void saveDeviceId(); // 保存设备ID +bool processSetDeviceId(JsonDocument& doc); // 处理设置设备ID命令 +void sendStatusToBLE(); // 发送状态信息到BLE +bool processQueryStatus(JsonDocument& doc); // 处理查询状态命令 +bool processQueryRadarData(JsonDocument& doc); // 处理查询雷达数据命令 +bool processStartContinuousSend(JsonDocument& doc); // 处理开始持续发送命令 +bool processStopContinuousSend(JsonDocument& doc); // 处理停止持续发送命令 +void sendRadarDataToBLE(); // 发送雷达数据到BLE +void sendDataInChunks(const String& data); // 分包发送函数 +void sendJSONDataToBLE(const String& jsonData); // 发送JSON数据到BLE(使用纯数据分包发送,不带包头) +bool sendCustomJSONData(const String& jsonType, const String& jsonString); // 发送自定义JSON数据到BLE + +float addDataAndCalculateAverage(CircularBuffer* buffer, float newData); // 计算平均值并添加新数据到缓冲区 + +// FreeRTOS任务函数声明 +void bleSendTask(void *parameter); +void vitalSendTask(void *parameter); +void radarDataTask(void *parameter); + +class MyServerCallbacks: public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + deviceConnected = true; + Serial.println("BLE客户端已连接"); + + // 发送连接状态信息 + sendStatusToBLE(); + }; + + void onDisconnect(BLEServer* pServer) { + deviceConnected = false; + Serial.println("BLE客户端已断开"); + + // 重置持续发送状态 + continuousSendEnabled = false; + Serial.println("重置持续发送状态"); + } +}; + +class MyCallbacks: public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic) { + std::string value = pCharacteristic->getValue(); + Serial.printf("🔵 收到BLE写入数据,长度: %d 字节\n", value.length()); + + if (value.length() > 0) { + String fragment = ""; + for (int i = 0; i < value.length(); i++) + fragment += value[i]; + + Serial.printf("📄 接收数据片段: %s\n", fragment.c_str()); + + completeData += fragment; + lastReceiveTime = millis(); + + // 检查是否收到完整的JSON数据 + int openBrace = completeData.indexOf('{'); + int closeBrace = completeData.lastIndexOf('}'); + + if (openBrace >= 0 && closeBrace > openBrace) { + String jsonData = completeData.substring(openBrace, closeBrace + 1); + completeData = completeData.substring(closeBrace + 1); + + Serial.printf("📥 完整JSON数据: %s\n", jsonData.c_str()); + + // 将JSON数据存储到receivedData变量中供后续处理 + receivedData = jsonData; + } + } + } +}; + +void setup() { + Serial.begin(115200); + //delay(10000); // 关键:等待雷达完全启动 + sensorData.breath_rate = 0; + sensorData.heart_rate = 0; + sensorData.rssi = 0; + sensorData.breath_valid = 0; + sensorData.heart_valid = 0; + + esp_task_wdt_init(30, true); + esp_task_wdt_add(NULL); + + // 增加串口缓冲区大小到4096字节以处理大量雷达数据 + mySerial2.setRxBufferSize(4096); + mySerial2.begin(BAUD_RATE, SERIAL_8N1, UART2_RX, UART2_TX); + Serial.println("UART2配置完成,缓冲区大小: 4096字节"); + + delay(1000); + + // String startCmd = "{\"cmd\":\"setMonitor\",\"para\":1}\n"; + // mySerial2.print(startCmd); + // delay(1000); + // Serial.println("启动雷达数据传输"); + + preferences.begin("radar_data", false); + + // 加载设备ID和WiFi配置 + loadDeviceId(); + loadWiFiConfig(); + + // 创建FreeRTOS队列 + phaseDataQueue = xQueueCreate(QUEUE_SIZE, sizeof(PhaseData)); + vitalDataQueue = xQueueCreate(QUEUE_SIZE, sizeof(VitalData)); + + if (phaseDataQueue == NULL || vitalDataQueue == NULL) { + Serial.println("❌ 队列创建失败"); + } else { + Serial.println("✅ FreeRTOS队列创建成功"); + } + + // 创建FreeRTOS任务 + xTaskCreatePinnedToCore( + bleSendTask, + "BleSendTask", + TASK_STACK_SIZE, + NULL, + 3, // 提高优先级到3 + &bleSendTaskHandle, + 1 + ); + + xTaskCreatePinnedToCore( + vitalSendTask, + "VitalSendTask", + TASK_STACK_SIZE, + NULL, + 2, // 中等优先级 + &vitalSendTaskHandle, + 1 + ); + + // 创建专门的雷达数据处理任务(最高优先级) + xTaskCreatePinnedToCore( + radarDataTask, + "RadarProcessTask", + 4096, + NULL, + 4, // 最高优先级 + NULL, + 1 + ); + + + Serial.println("✅ FreeRTOS任务创建成功"); + + BLEDevice::init("ESP32-Radar"); + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + + BLEService *pService = pServer->createService(SERVICE_UUID); + pCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_WRITE | + BLECharacteristic::PROPERTY_NOTIFY + ); + pCharacteristic->setCallbacks(new MyCallbacks()); + pCharacteristic->addDescriptor(new BLE2902()); + + pService->start(); + BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->setMinPreferred(0x06); + pAdvertising->setMinPreferred(0x12); + BLEDevice::startAdvertising(); + + Serial.println("BLE已启动,设备名称: ESP32-Radar"); + + if (wifiConfigured) { + Serial.println("检测到已保存的WiFi配置,尝试连接..."); + if (connectWiFi()) { + Serial.println("WiFi连接成功!"); + } else { + Serial.println("WiFi连接失败,请通过BLE重新配置"); + wifiConfigured = false; + } + } +} + +void loop() { + esp_task_wdt_reset(); + + if (!deviceConnected && oldDeviceConnected) { + delay(500); + pServer->startAdvertising(); + Serial.println("开始BLE广播"); + oldDeviceConnected = deviceConnected; + } + if (deviceConnected && !oldDeviceConnected) { + oldDeviceConnected = deviceConnected; + } + + processBLEConfig(); + + // 移除WiFi连接检查,使雷达数据处理不依赖WiFi连接 + esp_task_wdt_reset(); + // 雷达数据处理已移至专用任务 radarDataTask + // processRadarData(); + esp_task_wdt_reset(); + updateStatusFlags(); + + // 注意:持续发送雷达数据的逻辑已移至FreeRTOS任务处理 + // 主循环不再直接处理蓝牙数据发送,避免影响雷达数据接收和解析 + + delay(1); +} + +// 处理查询状态命令 +bool processQueryStatus(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "queryStatus") == 0) { + // 发送设备状态信息 + if (deviceConnected) { + String statusMsg = String("{\"type\":\"deviceStatus\",\"success\":true,\"deviceId\":") + + String(currentDeviceId) + + String(",\"wifiConfigured\":") + + String(wifiConfigured ? "true" : "false") + + String(",\"wifiConnected\":") + + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + + String(",\"ipAddress\":\"") + + (WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "") + + String("\"}"); + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(statusMsg); + Serial.println("已发送设备状态信息"); + } + return true; + } + return false; +} + +// 处理查询雷达数据命令 +bool processQueryRadarData(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "queryRadarData") == 0) { + Serial.println("收到查询雷达数据命令"); + + // 发送最新的雷达数据 + if (deviceConnected) { + String radarDataMsg = String("{\"type\":\"radarData\",\"success\":true") + + String(",\"deviceId\":") + String(currentDeviceId) + + String(",\"timestamp\":") + String(millis()) + + String(",\"presence\":") + String(sensorData.presence) + + String(",\"heartRate\":") + String(sensorData.heart_rate, 1) + + String(",\"breathRate\":") + String(sensorData.breath_rate, 1) + + String(",\"motion\":") + String(sensorData.motion) + + String(",\"rssi\":") + String(sensorData.rssi) + + String(",\"heartbeatWaveform\":") + String(sensorData.heartbeat_waveform) + + String(",\"breathingWaveform\":") + String(sensorData.breathing_waveform) + + String(",\"rawSignal\":") + String(sensorData.raw_signal) + + String("}"); + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(radarDataMsg); + Serial.println("已发送雷达数据"); + Serial.printf("发送的数据: %s\n", radarDataMsg.c_str()); + } else { + Serial.println("BLE未连接,无法发送雷达数据"); + } + return true; + } + return false; +} + +// 处理开始持续发送命令 +bool processStartContinuousSend(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "startContinuousSend") == 0) { + // 获取发送间隔参数(可选,默认1000ms) + if (doc["interval"].is()) { + continuousSendInterval = doc["interval"].as(); + // 限制最小间隔为100ms,最大间隔为10000ms + if (continuousSendInterval < 100) continuousSendInterval = 100; + if (continuousSendInterval > 10000) continuousSendInterval = 10000; + } + + continuousSendEnabled = true; + bleFlow.reset(); // 重置流量控制器 + + Serial.printf("⚙️ 启动持续发送模式,间隔: %lu ms\n", continuousSendInterval); + + // 发送JSON格式的确认消息 + if (deviceConnected) { + String confirmMsg = String("{\"type\":\"startContinuousSendResult\",\"success\":true,\"message\":\"已启动持续发送模式\",\"interval\":") + + String(continuousSendInterval) + "}"; + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(confirmMsg); + Serial.println("✅ 启动确认消息发送成功"); + + delay(5); // 减少延迟以提高实时性 + Serial.println("🚀 已启动持续发送模式"); + } else { + Serial.println("❌ BLE未连接,无法发送确认消息"); + } + return true; + } + return false; +} + +// 处理停止持续发送命令 +bool processStopContinuousSend(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "stopContinuousSend") == 0) { + continuousSendEnabled = false; + + Serial.println("🛑 停止持续发送模式"); + + // 发送JSON格式的确认消息 + if (deviceConnected) { + String confirmMsg = String("{\"type\":\"stopContinuousSendResult\",\"success\":true,\"message\":\"已停止持续发送模式\"}"); + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(confirmMsg); + Serial.println("✅ 停止确认消息发送成功"); + + delay(5); // 减少延迟以提高实时性 + Serial.println("⏹️ 已停止持续发送模式"); + } else { + Serial.println("❌ BLE未连接,无法发送确认消息"); + } + return true; + } + return false; +} + +// 分包发送函数 - 优化版本,解决分包混乱和数据截断问题 +void sendDataInChunks(const String& data) { + const int MAX_PACKET_SIZE = 20; // BLE最大包大小 + const int HEADER_SIZE = 6; // 包头大小 "[N/M]" + const int CHUNK_SIZE = MAX_PACKET_SIZE - HEADER_SIZE; // 实际数据大小 + + int totalLength = data.length(); + int numChunks = (totalLength + CHUNK_SIZE - 1) / CHUNK_SIZE; + + // 只有在数据较长需要分包时才显示分包信息 + Serial.printf("📦 开始分包发送,总长度: %d, 分包数: %d\n", totalLength, numChunks); + + for(int i = 0; i < numChunks; i++) { + int start = i * CHUNK_SIZE; + int chunkLength = min(CHUNK_SIZE, totalLength - start); + String chunk = data.substring(start, start + chunkLength); + + // 构造简单包头: "[序号/总数]",确保总长度不超过6字符 + // 例如: "[1/5]" = 4字符 + String packetHeader = String("[") + String(i+1) + String("/") + String(numChunks) + String("]"); + + // 确保总包大小不超过20字节 + int maxDataLength = MAX_PACKET_SIZE - packetHeader.length(); + if (chunk.length() > maxDataLength) { + chunk = chunk.substring(0, maxDataLength); + } + + String packet = packetHeader + chunk; + + Serial.printf("📤 发送分包 %s: %s\n", packetHeader.c_str(), chunk.c_str()); + + // 检查BLE连接状态 + if (!deviceConnected) { + Serial.println("❌ BLE未连接,无法发送数据"); + return; + } + + // 发送数据包 + pCharacteristic->setValue(packet.c_str()); + pCharacteristic->notify(); + Serial.println("✅ 分包发送成功"); + + // 等待一段时间确保接收端处理完成,避免数据丢失 + if (i < numChunks - 1) { + Serial.printf("⏳ 等待接收端处理第%d个包...\n", i+1); + delay(20); // 进一步减少等待时间以提高实时性 + } + } + + Serial.println("📦 分包发送完成"); +} + +// 新增:发送JSON数据到BLE(使用纯数据分包发送,不带包头) +void sendJSONDataToBLE(const String& jsonData) { + Serial.printf("📤 准备发送JSON数据: %s\n", jsonData.c_str()); + + // 检查BLE连接状态 + if (!deviceConnected) { + Serial.println("❌ BLE未连接,无法发送JSON数据"); + return; + } + + // 直接发送JSON数据,使用纯数据分包(不带包头) + const int MAX_PACKET_SIZE = 20; // BLE最大包大小 + int totalLength = jsonData.length(); + int numChunks = (totalLength + MAX_PACKET_SIZE - 1) / MAX_PACKET_SIZE; + + Serial.printf("📦 开始分包发送JSON,总长度: %d, 分包数: %d\n", totalLength, numChunks); + + for(int i = 0; i < numChunks; i++) { + int start = i * MAX_PACKET_SIZE; + int chunkLength = min(MAX_PACKET_SIZE, totalLength - start); + String chunk = jsonData.substring(start, start + chunkLength); + + Serial.printf("📤 发送JSON分包 %d/%d: %s\n", i+1, numChunks, chunk.c_str()); + + // 发送数据包 + pCharacteristic->setValue(chunk.c_str()); + pCharacteristic->notify(); + Serial.println("✅ JSON分包发送成功"); + + // 等待一段时间确保接收端处理完成,避免数据丢失 + if (i < numChunks - 1) { + Serial.printf("⏳ 等待接收端处理第%d个包...\n", i+1); + delay(10); // 减少等待时间以提高实时性 + } + } + + Serial.println("📦 JSON分包发送完成"); +} + +// 新增:发送自定义JSON数据到BLE +bool sendCustomJSONData(const String& jsonType, const String& jsonString) { + if (!deviceConnected) { + Serial.println("❌ BLE未连接,无法发送自定义JSON数据"); + return false; + } + + // 构造完整的JSON数据 + String fullJSON = String("{\"type\":\"") + jsonType + String("\",") + jsonString + String("}"); + + Serial.printf("📤 发送自定义JSON数据类型 '%s': %s\n", jsonType.c_str(), fullJSON.c_str()); + + // 使用纯数据分包发送函数发送JSON数据 + sendJSONDataToBLE(fullJSON); + + return true; +} + +// 发送雷达数据到BLE(已移至FreeRTOS任务处理) +void sendRadarDataToBLE() { + // 此函数已废弃,雷达数据发送现在由FreeRTOS任务处理 + // 保留此函数以防其他代码引用 + Serial.println("ℹ️ 雷达数据发送已移至FreeRTOS任务处理"); +} + +// 处理BLE配置和命令 +void processBLEConfig() { + // 增强超时处理机制 + if (completeData.length() > 0 && (millis() - lastReceiveTime > 3000)) { + Serial.println("⏰ [超时] 数据接收超时3秒,清空缓冲区"); + completeData = ""; + } + + if (receivedData.length() > 0) { + String bleData = receivedData; + receivedData = ""; + bleData.trim(); + + Serial.printf("⚙️ [处理] 准备解析JSON: %s\n", bleData.c_str()); + + if (bleData.startsWith("{") && bleData.endsWith("}")) { + JsonDocument doc; + DeserializationError error = deserializeJson(doc, bleData); + + if (error) { + String errorMsg = String("[错误] JSON解析失败: ") + String(error.c_str()); + Serial.println(errorMsg); + if (deviceConnected) { + String responseMsg = String("{\"type\":\"error\",\"message\":\"配置格式错误,请使用JSON格式\"}"); + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(responseMsg); + } + } else { + Serial.println("✅ JSON解析成功"); + + // 处理各种命令 + bool processed = false; + + // 处理设置设备ID命令 + if (!processed) processed = processSetDeviceId(doc); + + // 处理WiFi配置命令 + if (!processed) processed = processWiFiConfigCommand(doc); + + // 处理查询状态命令 + if (!processed) processed = processQueryStatus(doc); + + // 处理查询雷达数据命令 + if (!processed) processed = processQueryRadarData(doc); + + // 处理开始持续发送命令 + if (!processed) processed = processStartContinuousSend(doc); + + // 处理停止持续发送命令 + if (!processed) processed = processStopContinuousSend(doc); + + // 如果没有处理任何命令,发送错误响应 + if (!processed) { + Serial.println("❓ 未知命令"); + if (deviceConnected) { + String responseMsg = String("{\"type\":\"error\",\"message\":\"未知命令\"}"); + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(responseMsg); + } + } + } + } else { + Serial.println("📥 接收到非JSON数据"); + } + } +} + + + + +// FreeRTOS任务:蓝牙数据发送(精简格式以节省空间) +void bleSendTask(void *parameter) { + Serial.println("🔁 蓝牙数据发送任务启动"); + + while (1) { + PhaseData phaseData; + + // 从队列接收相位数据用于蓝牙发送 + if (xQueueReceive(phaseDataQueue, &phaseData, portMAX_DELAY) == pdTRUE) { + esp_task_wdt_reset(); + + // 蓝牙传输 - 构造并发送完整的雷达数据(包含所有7个字段,无标识符以节省空间) + // 修改逻辑:无论是否检测到人都发送蓝牙数据 + if (deviceConnected && continuousSendEnabled) { + String radarDataCore; + + // 检查是否检测到人 + if (sensorData.presence > 0) { + // 检测到人时,发送实际数据 + radarDataCore = String(sensorData.heart_rate, 1) + String("|") + + String(sensorData.breath_rate, 1) + String("|") + + String(phaseData.heartbeat_waveform) + String("|") + + String(phaseData.breathing_waveform) + String("|") + + String(sensorData.presence) + String("|") + + String(sensorData.motion) + String("|") + + String(sensorData.rssi); + } else { + // 未检测到人时,发送零数据 + radarDataCore = String("0.0") + String("|") + + String("0.0") + String("|") + + String("0") + String("|") + + String("0") + String("|") + + String("0") + String("|") + + String("0") + String("|") + + String(sensorData.rssi); + } + + // 计算CRC校验和 + unsigned int crc = 0xFFFF; + for (int i = 0; i < radarDataCore.length(); i++) { + crc ^= (unsigned int)radarDataCore.charAt(i); + for (int j = 0; j < 8; j++) { + if (crc & 0x0001) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + + // 添加校验和到数据末尾 + String radarDataMsg = radarDataCore + String("|") + String(crc, HEX); + + Serial.printf("📤 通过蓝牙发送完整雷达数据: %s\n", radarDataMsg.c_str()); + + // 检查数据长度决定发送方式 + const int MAX_BLE_PACKET_SIZE = 20; // BLE最大包大小 + if (radarDataMsg.length() <= MAX_BLE_PACKET_SIZE) { + // 数据较短,直接发送 + pCharacteristic->setValue(radarDataMsg.c_str()); + pCharacteristic->notify(); + Serial.println("✅ 完整雷达数据蓝牙发送成功"); + } else { + // 数据较长,使用分包发送 + Serial.println("🔄 雷达数据较长,使用分包发送"); + sendDataInChunks(radarDataMsg); + } + } + + esp_task_wdt_reset(); + } + + vTaskDelay(1 / portTICK_PERIOD_MS); // 减少延迟以提高实时性 + } +} + +// FreeRTOS任务:发送生命体征数据 +void vitalSendTask(void *parameter) { + Serial.println("🔁 生命体征数据发送任务启动(WiFi数据库传输)"); + + while (1) { + VitalData vitalData; + + // 从队列接收生命体征数据用于WiFi数据库发送 + if (xQueueReceive(vitalDataQueue, &vitalData, portMAX_DELAY) == pdTRUE) { + esp_task_wdt_reset(); + + // WiFi数据库传输 - 一直发送生命体征数据(不通过蓝牙) + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.setTimeout(2000); + + String url = String("http://") + String(influxDBHost) + ":" + String(influxDBPort) + "/api/v2/write?org=" + String(influxDBOrg) + "&bucket=" + String(influxDBBucket) + "&precision=ns"; + + http.begin(url); + http.addHeader("Authorization", String("Token ") + String(influxDBToken)); + http.addHeader("Content-Type", "text/plain"); + http.setReuse(true); + + // 使用可变的设备ID + String deviceId = String(currentDeviceId); + + String vitalDataStr = ""; + bool firstField = true; + + // 修改发送逻辑: + // 当检测到人体存在时(presence > 0),发送完整的雷达数据(不包含相位数据) + // 当检测到人体不存在时(presence == 0),只发送检测是否有人的数据 + if (vitalData.presence > 0) { + // 人体存在时,发送完整数据 + + // 心率数据 - 使用浮点数格式 + if (vitalData.heart_rate > 0) { + if (!firstField) vitalDataStr += ","; + vitalDataStr += "heartRate="; + vitalDataStr += String(vitalData.heart_rate / 1.0, 1); + firstField = false; + } + + // 呼吸率数据 - 使用浮点数格式 + if (vitalData.breath_rate > 0) { + if (!firstField) vitalDataStr += ","; + vitalDataStr += "breathingRate="; + vitalDataStr += String(vitalData.breath_rate / 1.0, 1); + firstField = false; + } + + // 人检数据 - 确保值为0或1,使用整数格式 + if (!firstField) vitalDataStr += ","; + vitalDataStr += "personDetected="; + // 确保值为0或1 + int presenceValue = (vitalData.presence > 0) ? 1 : 0; + vitalDataStr += String(presenceValue) + "i"; // 整数格式 + firstField = false; + + // 人体活动数据 - 确保值为0或1,使用整数格式 + if (!firstField) vitalDataStr += ","; + vitalDataStr += "humanActivity="; + // 确保值为0或1 + int motionValue = (vitalData.motion > 0) ? 1 : 0; + vitalDataStr += String(motionValue) + "i"; // 整数格式 + firstField = false; + + // RSSI数据 - 使用整数格式 + if (vitalData.rssi != 0) { + if (!firstField) vitalDataStr += ","; + vitalDataStr += "rssi="; + vitalDataStr += String(vitalData.rssi) + "i"; // 整数格式 + firstField = false; + } + } else { + // 人体不存在时,只发送人检数据以减少芯片压力 + vitalDataStr += "personDetected="; + // 确保值为0 + vitalDataStr += String(0) + "i"; // 整数格式 + firstField = false; + } + + // 如果有数据要发送,则构造完整的行协议字符串 + if (vitalDataStr.length() > 0) { + vitalDataStr = "device_data,deviceId=" + deviceId + " " + vitalDataStr; + Serial.println("📤 发送生命体征数据到数据库: " + vitalDataStr); + + int httpResponseCode = http.POST(vitalDataStr); + + if (httpResponseCode == 204) { + Serial.println("✅ 生命体征数据发送到数据库成功"); + } else { + String errorMsg = String("❌ 生命体征数据发送到数据库失败: ") + String(httpResponseCode); + Serial.println(errorMsg); + } + } + + http.end(); + } else { + Serial.println("❌ WiFi未连接,无法发送生命体征数据到数据库"); + } + + esp_task_wdt_reset(); + } + + vTaskDelay(5 / portTICK_PERIOD_MS); + } +} + +// 雷达数据处理任务 - 高优先级 +void radarDataTask(void *parameter) { + Serial.println("🔁 雷达数据处理任务启动(最高优先级)"); + + while (1) { + // 处理雷达数据 + processRadarData(); + + // 短暂延迟以允许其他任务运行 + vTaskDelay(1 / portTICK_PERIOD_MS); + } +} + +void updateStatusFlags() { + static unsigned long lastStatusUpdate = 0; + static int lastPresence = -1; // 记录上一次的presence状态 + + // 检查presence状态是否发生变化 + if (lastPresence != -1 && lastPresence != sensorData.presence) { + // 状态发生变化时重置滤波器缓冲区 + memset(&heartRateBuffer, 0, sizeof(CircularBuffer)); + memset(&breathRateBuffer, 0, sizeof(CircularBuffer)); + // Serial.println("🔄 检测状态变化,重置滤波器缓冲区"); + } + lastPresence = sensorData.presence; + + if (millis() - lastStatusUpdate > 5000) { + // Serial.printf("系统状态: 心率=%.1f, 呼吸=%.1f, 有人=%d, 运动=%d, RSSI=%d\n", + // sensorData.heart_rate, sensorData.breath_rate, + // sensorData.presence, sensorData.motion, sensorData.rssi); + lastStatusUpdate = millis(); + } +} + +bool parseSensorLine(String line) { + // if (!line.startsWith("HBR01")) { + // Serial.println("❌ 数据行不以HBR01开头"); + // return false; + // } + + int colonIndex = line.indexOf(':'); + // if (colonIndex == -1) { + // Serial.println("❌ 数据行缺少冒号分隔符"); + // return false; + // } + + String dataPart = line.substring(colonIndex + 1); + dataPart.trim(); + + // Serial.printf("🔍 解析数据部分: %s\n", dataPart.c_str()); + + int values[8]; + int startIndex = 0; + int commaIndex; + int fieldCount = 0; + + for (int i = 0; i < 8; i++) { + commaIndex = dataPart.indexOf(',', startIndex); + + if (commaIndex == -1 && i == 7) { + String valueStr = dataPart.substring(startIndex); + valueStr.trim(); + values[i] = valueStr.toInt(); + fieldCount++; + // Serial.printf(" 字段%d: %s -> %d\n", i+1, valueStr.c_str(), values[i]); + } else if (commaIndex != -1) { + String valueStr = dataPart.substring(startIndex, commaIndex); + valueStr.trim(); + values[i] = valueStr.toInt(); + startIndex = commaIndex + 1; + fieldCount++; + // Serial.printf(" 字段%d: %s -> %d\n", i+1, valueStr.c_str(), values[i]); + } else { + // Serial.printf("❌ 解析错误: 字段%d缺失,总共找到%d个字段\n", i+1, fieldCount); + return false; + } + } + + sensorData.presence = values[0]; + + // 当检测到人时才进行滤波,否则使用原始数据 + if (sensorData.presence > 0) { + // 应用滤波器到心率和呼吸速率数据 + sensorData.heart_rate = addDataAndCalculateAverage(&heartRateBuffer, (float)values[1]); + sensorData.breath_rate = addDataAndCalculateAverage(&breathRateBuffer, (float)values[2]); + // Serial.println("📈 使用滤波数据"); + } else { + // 无人时使用原始数据 + sensorData.heart_rate = (float)values[1]; + sensorData.breath_rate = (float)values[2]; + // Serial.println("📉 使用原始数据(无人状态)"); + } + + sensorData.motion = values[3]; + sensorData.rssi = values[4]; + sensorData.heartbeat_waveform = values[5]; + sensorData.breathing_waveform = values[6]; + sensorData.raw_signal = values[7]; + + // 如果雷达强度信号小于625,强制将检测结果设为无人 + if (sensorData.rssi < 625) { + sensorData.presence = 0; + } + + sensorData.heart_valid = (sensorData.heart_rate > 0 && sensorData.heart_rate < 200); + sensorData.breath_valid = (sensorData.breath_rate >= 0.1f && sensorData.breath_rate <= 60.0f); + + lastSensorUpdate = millis(); + + // 添加您要求保留的打印语句 + Serial.printf("Heart_Rate_RAW:%.2f\n", sensorData.heart_rate); + Serial.printf("Breath_Rate_RAW:%.2f\n", sensorData.breath_rate); + + // Serial.printf("✅ 解析成功: 有人=%d, 心率=%.1f, 呼吸=%.1f, 运动=%d, 波形=%d/%d\n", + // sensorData.presence, sensorData.heart_rate, + // sensorData.breath_rate, sensorData.motion, + // sensorData.heartbeat_waveform, sensorData.breathing_waveform); + + return true; +} + +void saveWiFiConfig() { + preferences.putString("ssid", ssid); + preferences.putString("password", password); + preferences.putBool("configured", true); + Serial.println("WiFi配置已保存到Flash"); +} + +void loadWiFiConfig() { + wifiConfigured = preferences.getBool("configured", false); + if (wifiConfigured) { + preferences.getString("ssid", ssid, sizeof(ssid)); + preferences.getString("password", password, sizeof(password)); + Serial.printf("从Flash加载WiFi配置 - SSID: %s\n", ssid); + } +} + +bool connectWiFi() { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + Serial.println("[WiFi] 正在尝试连接..."); + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 20) { + delay(500); + Serial.printf("[WiFi] 尝试连接中,当前状态: %d\n", WiFi.status()); + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println("[WiFi] 连接成功!"); + Serial.printf("[WiFi] 分配的IP地址: %s\n", WiFi.localIP().toString().c_str()); + return true; + } else { + Serial.println("[WiFi] 连接超时,未能成功连接到WiFi。"); + Serial.printf("[WiFi] 最终状态码: %d\n", WiFi.status()); + return false; + } +} + +// void sendRadarCommand(String cmd) { +// if (!cmd.endsWith("\n")) { +// cmd += "\n"; +// } +// mySerial2.print(cmd); +// Serial.println("📤 [雷达命令] " + cmd); +// } + +// 处理设置设备ID命令 +bool processSetDeviceId(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "setDeviceId") == 0) { + // 处理设置设备ID的命令 + uint16_t newDeviceId = doc["newDeviceId"]; + + //验证设备ID范围 + if (newDeviceId < MIN_DEVICE_ID || newDeviceId > MAX_DEVICE_ID){ + Serial.printf("[错误] 设备ID超出范围,有效范围: %d-%d\n", MIN_DEVICE_ID, MAX_DEVICE_ID); + if (deviceConnected) { + String errorMsg = String("{\"type\":\"error\",\"message\":\"设备ID超出范围,有效范围: ") + + String(MIN_DEVICE_ID) + "-" + String(MAX_DEVICE_ID) + "\"}"; + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(errorMsg); + } + return true; // 已处理该命令 + } + + currentDeviceId = newDeviceId; + + Serial.printf("[设备ID] 已设置新的设备ID: %u\n", currentDeviceId); + + // 保存设备ID到Preferences + saveDeviceId(); + + // 发送确认消息 + if (deviceConnected) { + String confirmMsg = String("{\"type\":\"setDeviceIdResult\",\"success\":true,\"message\":\"设备ID设置成功\",\"newDeviceId\":") + + String(newDeviceId) + "}"; + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(confirmMsg); + + // 发送更新后的状态信息 + sendStatusToBLE(); + } + return true; + } + return false; +} + +// 加载设备ID +void loadDeviceId() { + currentDeviceId = preferences.getUShort("deviceId", 1001); + Serial.printf("从Flash加载设备ID: %u\n", currentDeviceId); +} + +// 保存设备ID +void saveDeviceId() { + preferences.putUShort("deviceId", currentDeviceId); + Serial.printf("设备ID已保存到Flash: %u\n", currentDeviceId); +} + +// 发送状态信息到BLE +void sendStatusToBLE() { + if (deviceConnected) { + String statusMsg = String("{\"type\":\"status\",\"wifiConfigured\":") + + String(wifiConfigured ? "true" : "false") + + String(",\"wifiConnected\":") + + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + + String(",\"ipAddress\":\"") + + (WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "") + "\"" + + String(",\"deviceId\":") + + String(currentDeviceId) + "}"; + + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(statusMsg); + Serial.println("已发送连接状态信息"); + } +} + +// 处理WiFi配置命令 +bool processWiFiConfigCommand(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "setWiFiConfig") == 0) { + // 获取WiFi配置参数 + const char* newSSID = doc["ssid"]; + const char* newPassword = doc["password"]; + + if (newSSID != nullptr && newPassword != nullptr) { + // 更新WiFi配置 + strncpy(ssid, newSSID, sizeof(ssid)-1); + ssid[sizeof(ssid)-1] = '\0'; // 确保字符串结束符 + strncpy(password, newPassword, sizeof(password)-1); + password[sizeof(password)-1] = '\0'; // 确保字符串结束符 + + // 保存WiFi配置到Flash + saveWiFiConfig(); + + // 尝试连接WiFi + bool connected = connectWiFi(); + + // 发送配置结果 + if (deviceConnected) { + String resultMsg = String("{\"type\":\"wifiConfigResult\",\"success\":") + + String(connected ? "true" : "false") + + String(",\"message\":\"") + + String(connected ? "WiFi配置成功" : "WiFi配置失败") + + String("\"}"); + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(resultMsg); + Serial.println("已发送WiFi配置结果"); + } + + return true; // 已处理该命令 + } else { + // 参数不完整 + if (deviceConnected) { + String errorMsg = String("{\"type\":\"error\",\"message\":\"WiFi配置参数不完整,需要ssid和password字段\"}"); + // 使用新的分包发送函数发送JSON数据 + sendJSONDataToBLE(errorMsg); + Serial.println("WiFi配置参数不完整"); + } + return true; // 已处理该命令(虽然失败了) + } + } + return false; // 未处理该命令 +} + + + + + +/** + * 添加新数据并计算一分钟内的平均值 + * @param buffer 环形缓冲区指针 + * @param newData 新输入的数据 + * @return 一分钟内数据的平均值,如果没有数据返回0 + */ +float addDataAndCalculateAverage(CircularBuffer* buffer, float newData) { + + // 只有在检测到人的情况下才进行滤波处理 + if (sensorData.presence == 0) { + return newData; // 没有检测到人时,直接返回原始数据 + } + + unsigned long currentTime = millis(); + unsigned long oneMinuteAgo = currentTime - 60000; // 一分钟前的时间 + + // 移除过期的旧数据(一分钟前的数据) + while (buffer->count > 0 && buffer->timestamps[buffer->tail] < oneMinuteAgo) { + buffer->sum -= buffer->data[buffer->tail]; + buffer->tail = (buffer->tail + 1) % BUFFER_SIZE; + buffer->count--; + buffer->isFull = (buffer->count == BUFFER_SIZE); + } + + // 添加新数据 + if (buffer->count < BUFFER_SIZE) { + // 缓冲区未满,直接添加 + buffer->head = (buffer->head + 1) % BUFFER_SIZE; + if (buffer->count == 0) buffer->tail = buffer->head; + buffer->data[buffer->head] = newData; + buffer->timestamps[buffer->head] = currentTime; + buffer->sum += newData; + buffer->count++; + buffer->isFull = (buffer->count == BUFFER_SIZE); + } else { + // 缓冲区已满,替换最旧数据 + buffer->sum -= buffer->data[buffer->tail]; // 移除最旧数据 + buffer->tail = (buffer->tail + 1) % BUFFER_SIZE; // 移动尾指针 + buffer->head = (buffer->head + 1) % BUFFER_SIZE; // 移动头指针 + buffer->data[buffer->head] = newData; // 存储新数据 + buffer->timestamps[buffer->head] = currentTime; + buffer->sum += newData; // 添加新数据到总和 + } + + // 计算并返回平均值 + if (buffer->count > 0) { + return buffer->sum / buffer->count; + } else { + return 0; + } +} + + + +// 修改后的processRadarData函数 - 直接从串口读取数据 +void processRadarData() { + static unsigned long lastDataTime = 0; + static unsigned long noDataWarningTime = 0; + static uint32_t totalLinesReceived = 0; + static uint32_t validLinesReceived = 0; + static uint32_t phasePacketCounter = 0; + static uint32_t vitalPacketCounter = 0; + + // 直接从串口读取数据 + while (mySerial2.available() > 0) { + char c = mySerial2.read(); + lastDataTime = millis(); + + rxBuffer += c; + + // 增加缓冲区大小检查,防止溢出 + if (rxBuffer.length() > 1000) { + rxBuffer = ""; + } + + if (c == '\n' || c == '\r') { + if (rxBuffer.length() > 0) { + totalLinesReceived++; + + bool parseResult = parseSensorLine(rxBuffer); + if (parseResult) { + validLinesReceived++; + + // 更新全局传感器数据 + lastSensorUpdate = millis(); + + // 使用FreeRTOS队列发送数据到任务 + // 只有在检测到人的情况下才将数据放入队列 + if (sensorData.presence > 0) { + phasePacketCounter++; + if (phasePacketCounter >= PHASE_SEND_INTERVAL) { + PhaseData phaseData; + phaseData.heartbeat_waveform = sensorData.heartbeat_waveform; + phaseData.breathing_waveform = sensorData.breathing_waveform; + + if (xQueueSend(phaseDataQueue, &phaseData, 0) == pdTRUE) { + // Serial.println("📤 相位数据已加入发送队列"); + } else { + // Serial.println("❌ 相位数据队列已满,数据丢失"); + } + phasePacketCounter = 0; + } + + vitalPacketCounter++; + if (vitalPacketCounter >= VITAL_SEND_INTERVAL) { + VitalData vitalData; + vitalData.heart_rate = sensorData.heart_rate; + vitalData.breath_rate = sensorData.breath_rate; + vitalData.presence = sensorData.presence; + vitalData.motion = sensorData.motion; + vitalData.rssi = sensorData.rssi; + + if (xQueueSend(vitalDataQueue, &vitalData, 0) == pdTRUE) { + Serial.println("📤 生命体征数据已加入发送队列"); + } else { + Serial.println("❌ 生命体征数据队列已满,数据丢失"); + } + vitalPacketCounter = 0; + } + } + } + + rxBuffer = ""; + } + } + } + + // 检查数据超时 + if (millis() - lastDataTime > 5000 && millis() - noDataWarningTime > 5000) { + Serial.println("⚠️ [警告] 超过5秒未收到串口数据!"); + Serial.printf(" 检查项: 1) 雷达是否通电 2) 串口连接(RX=%d,TX=%d) 3) 波特率=%d\n", + UART2_RX, UART2_TX, BAUD_RATE); + noDataWarningTime = millis(); + } +} diff --git a/tcp-output(5).py b/tcp-output(5).py new file mode 100644 index 0000000..518f737 --- /dev/null +++ b/tcp-output(5).py @@ -0,0 +1,243 @@ +import time +import random +import requests +from datetime import datetime + +# 添加InfluxDB配置 +INFLUXDB_URL = 'http://8.134.11.76:8086' +INFLUXDB_TOKEN = 'KuTa5ZsqoHIhi2IglOO06zExUYw1_mJ6K0mcA9X1y6O6CJDog3_Cgr8mUw1SwpuCCKRElqxa6wAhrrhsYPytkg==' +INFLUXDB_ORG = 'gzlg' +INFLUXDB_BUCKET = 'gzlg' + +# 配置设备ID:设置为0则自动注册,设置为1000~1999则固定使用该ID +CONFIG_DEVICE_ID = 1002 + +# 分配的设备ID(初始为None,注册后更新) +assigned_device_id = None +# 相位计数器 +phase_counter = 0 + +def get_next_device_id(): + """从InfluxDB查询已注册的设备ID,并返回下一个可用的ID""" + try: + # 查询InfluxDB中已存在的设备ID + query = ''' + from(bucket: "{}") + |> range(start: -365d) + |> filter(fn: (r) => r["_measurement"] == "device_data") + |> keep(columns: ["deviceId"]) + |> distinct(column: "deviceId") + |> sort() + '''.format(INFLUXDB_BUCKET) + + url = f"{INFLUXDB_URL}/api/v2/query?org={INFLUXDB_ORG}" + headers = { + "Authorization": f"Token {INFLUXDB_TOKEN}", + "Content-Type": "application/json" + } + data = { + "query": query + } + + response = requests.post(url, headers=headers, json=data) + print(f"🔍 InfluxDB查询响应状态: {response.status_code}") + if response.status_code == 200: + print(f"🔍 InfluxDB查询响应内容: {response.text}") + # 解析响应数据 + device_ids = [] + lines = response.text.strip().split('\n') + for line in lines: + if line and not line.startswith('#') and 'deviceId' not in line: + try: + # 解析CSV格式的响应数据 + # 格式类似于: ,_result,0,1001,1001 + parts = line.split(',') + if len(parts) >= 5: + # deviceId在第4个位置(索引3) + device_id_str = parts[3].strip() + if device_id_str: + device_id = int(device_id_str) + if 1000 <= device_id <= 1999: # 只考虑1000-1999范围内的设备ID + device_ids.append(device_id) + print(f"📱 发现设备ID: {device_id}") + except (ValueError, IndexError) as e: + # 忽略解析错误的行 + print(f"⚠️ 忽略无法解析的行: {line}") + continue + + # 对设备ID进行排序 + device_ids.sort() + + # 找到下一个可用的ID + next_id = 1001 + for device_id in device_ids: + if device_id == next_id: + next_id += 1 + elif device_id > next_id: + break + + # 确保ID在有效范围内 + if next_id > 1999: + next_id = 1001 # 如果超出范围,从头开始 + + print(f"📊 查询到已注册设备: {device_ids}") + print(f"🆕 分配新设备ID: {next_id}") + return next_id + else: + print(f"❌ 查询InfluxDB失败: {response.status_code} - {response.text}") + # 如果查询失败,返回默认ID + return 1001 + except Exception as e: + print(f"❌ 查询设备ID时出错: {e}") + import traceback + traceback.print_exc() + # 如果查询失败,返回默认ID + return 1001 + +def register_device(): + """注册设备并获取设备ID""" + global assigned_device_id + + # 检查配置的设备ID + if CONFIG_DEVICE_ID == 0: + # 自动注册模式 + try: + # 获取下一个可用的设备ID + next_device_id = get_next_device_id() + assigned_device_id = next_device_id + + print(f"✅ 设备注册成功! 设备ID: {assigned_device_id} (0x{assigned_device_id:04X})") + return True + except Exception as e: + print(f"❌ 注册过程中发生错误: {e}") + return False + elif 1000 <= CONFIG_DEVICE_ID <= 1999: + # 固定设备ID模式 + assigned_device_id = CONFIG_DEVICE_ID + print(f"✅ 使用固定设备ID: {assigned_device_id} (0x{assigned_device_id:04X})") + return True + else: + # 配置的设备ID无效 + print(f"❌ 配置的设备ID {CONFIG_DEVICE_ID} 无效,请设置为0(自动注册)或1000~1999之间的值") + return False + +def save_data_to_influxdb(protocol_id, data_value): + """保存数据到InfluxDB""" + try: + # 创建数据点 + data_point = { + "measurement": "device_data", + "tags": { + "deviceId": assigned_device_id + }, + "time": datetime.utcnow().isoformat() + "Z", + "fields": {} + } + + # 根据协议ID确定字段名 + field_mapping = { + 1: "heartRate", + 2: "breathingRate", + 3: "heartPhase", + 4: "breathingPhase", + 13: "personDetected", + 14: "humanActivity" + } + + if protocol_id in field_mapping: + field_name = field_mapping[protocol_id] + + # 对特定字段进行数值处理 + if protocol_id in [1, 2]: # 心率和呼吸频率需要除以10 + data_point["fields"][field_name] = float(data_value) / 10.0 + elif protocol_id in [3, 4]: # 相位数据需要除以1000 + data_point["fields"][field_name] = float(data_value) / 1000.0 + elif protocol_id in [13, 14]: # 人检/活动数据,确保是整数0或1 + # 强制转换为整数,确保只有0或1 + data_point["fields"][field_name] = int(data_value) + if data_point["fields"][field_name] not in [0, 1]: + print(f"⚠️ 警告: 人检/活动数据值异常: {data_value}, 强制转换为: {data_point['fields'][field_name]}") + else: + data_point["fields"][field_name] = data_value + + print(f"📊 准备保存数据: 协议ID={protocol_id}, 字段={field_name}, 值={data_point['fields'][field_name]}, 原始值={data_value}") + + # 发送数据到InfluxDB + url = f"{INFLUXDB_URL}/api/v2/write?org={INFLUXDB_ORG}&bucket={INFLUXDB_BUCKET}" + headers = { + "Authorization": f"Token {INFLUXDB_TOKEN}", + "Content-Type": "text/plain; charset=utf-8" + } + + # 构造行协议格式的数据,明确指定数据类型 + if protocol_id in [13, 14]: + # 对于人检/活动数据,使用整数格式 + line_protocol = f"device_data,deviceId={assigned_device_id} {field_name}={int(data_point['fields'][field_name])}i" + else: + # 对于其他数据,使用浮点数格式 + line_protocol = f"device_data,deviceId={assigned_device_id} {field_name}={data_point['fields'][field_name]}" + + response = requests.post(url, headers=headers, data=line_protocol) + if response.status_code == 204: + print(f"✅ 数据已保存到InfluxDB设备{assigned_device_id}上: {field_name}={data_point['fields'][field_name]}") + else: + print(f"❌ 保存数据到InfluxDB失败: {response.status_code} - {response.text}") + else: + print(f"⚠️ 未知的协议ID: {protocol_id}") + + except Exception as e: + print(f"❌ 保存数据到InfluxDB时出错: {e}") + import traceback + traceback.print_exc() + +def main(): + global assigned_device_id, phase_counter + + try: + # 第一步:注册设备获取设备ID + if not register_device(): + print("❌ 设备注册失败,程序退出") + return + + print(f"🎯 开始使用设备ID {assigned_device_id} 发送数据...") + + # 第二步:开始发送数据 + while True: + # 初始化数据值 + data_value = 0 + + # 每发送100个相位数据后发送其他数据 + if phase_counter < 2: + # 发送相位数据 (心率相位或呼吸相位) + protocol_id = random.choice([3, 4]) # 3=心率相位, 4=呼吸相位 + data_value = random.randint(-10000, 10000) + phase_counter += 1 + else: + # 重置计数器 + phase_counter = 0 + + # 发送其他数据 + protocol_id = random.choice([1, 2, 13, 14]) # 1=心跳, 2=呼吸, 13=检测到人, 14=人体活动 + + if protocol_id == 1: # 心跳 + data_value = random.randint(600, 1000) + elif protocol_id == 2: # 呼吸 + data_value = random.randint(120, 200) + elif protocol_id == 13: # 检测到人(1检测到,0未检测到) + data_value = random.choice([0, 1]) + elif protocol_id == 14: # 人体活动(1活动,0静止) + data_value = random.choice([0, 1]) + + # 直接发送数据到InfluxDB + save_data_to_influxdb(protocol_id, data_value) + + # 设置发送间隔 + time.sleep(0.1) # 每100ms发送一次数据 + + except KeyboardInterrupt: + print(f"\n🛑 设备 {assigned_device_id} 发送端已停止") + except Exception as e: + print(f"❌ 发生错误: {e}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -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 diff --git a/test/app.cpp b/test/app.cpp new file mode 100644 index 0000000..df570ad --- /dev/null +++ b/test/app.cpp @@ -0,0 +1,1177 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define BUFFER_SIZE 2000 // 固定存储2000个数据点 + +// 环形缓冲区结构 +struct CircularBuffer { + float data[BUFFER_SIZE]; + unsigned long timestamps[BUFFER_SIZE]; // 存储时间戳 + int head; // 最新数据位置 + int tail; // 最旧数据位置 + int count; // 当前有效数据数量 + bool isFull; // 缓冲区是否已满 + float sum; // 当前数据总和 +}; + +CircularBuffer heartRateBuffer = {{0}, {0}, 0, 0, 0, false, 0}; + +// 滤波器定义 +#define FILTER_SIZE 5 // 滤波器窗口大小 + +Preferences preferences; + +// 设备ID变量 +uint16_t currentDeviceId = 1001; // 默认设备ID为1001 +const uint16_t MIN_DEVICE_ID = 1000; // 设备ID最小值 +const uint16_t MAX_DEVICE_ID = 1999; // 设备ID最大值 + +const uint32_t PHASE_SEND_INTERVAL = 1; +const uint32_t VITAL_SEND_INTERVAL = 10; + +#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b" +#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8" + +BLEServer* pServer = NULL; +BLECharacteristic* pCharacteristic = NULL; +bool deviceConnected = false; +bool oldDeviceConnected = false; +String receivedData = ""; +String completeData = ""; +unsigned long lastReceiveTime = 0; + +WiFiClient client; +char ssid[32] = "13-205"; +char password[64] = "12345678"; +bool wifiConfigured = false; + +const char* influxDBHost = "8.134.11.76"; +const int influxDBPort = 8086; +const char* influxDBToken = "KuTa5ZsqoHIhi2IglOO06zExUYw1_mJ6K0mcA9X1y6O6CJDog3_Cgr8mUw1SwpuCCKRElqxa6wAhrrhsYPytkg=="; +const char* influxDBOrg = "gzlg"; +const char* influxDBBucket = "gzlg"; + +const unsigned long SENSOR_TIMEOUT = 40000; +static uint32_t packetCounter = 0; +static bool shouldSendOtherData = false; + +unsigned long lastSensorUpdate = 0; + +typedef struct { + float breath_rate; + float heart_rate; + int rssi; + uint8_t breath_valid; + uint8_t heart_valid; + uint8_t presence; + uint8_t motion; + int heartbeat_waveform; + int breathing_waveform; + int raw_signal; +} SensorData; + +SensorData sensorData = {0}; + +// 呼吸率滤波相关变量定义 +// 添加滤波器数组和索引(仅用于呼吸率) +float breathRateFilter[FILTER_SIZE] = {0}; +int filterIndex = 0; +bool filterInitialized = false; + + +HardwareSerial mySerial2(2); +const int BAUD_RATE = 576000; +const int UART2_RX = 16; +const int UART2_TX = 17; +String rxBuffer = ""; + +// FreeRTOS任务和队列定义 +QueueHandle_t phaseDataQueue; +QueueHandle_t vitalDataQueue; +TaskHandle_t bleSendTaskHandle = NULL; +TaskHandle_t vitalSendTaskHandle = NULL; +// 注意:bleSendTaskHandle现在用于蓝牙发送任务 + +#define QUEUE_SIZE 50 +#define TASK_STACK_SIZE 8192 +#define TASK_PRIORITY 1 + +typedef struct { + int heartbeat_waveform; + int breathing_waveform; +} PhaseData; + +typedef struct { + float heart_rate; + float breath_rate; + uint8_t presence; + uint8_t motion; + int rssi; +} VitalData; + +// 添加流量控制类 +class BLEFlowController { +private: + size_t maxBytesPerSecond; + size_t bytesSent; + unsigned long lastResetTime; + unsigned long lastSendTime; + +public: + BLEFlowController(size_t maxBps) : maxBytesPerSecond(maxBps), bytesSent(0) { + lastResetTime = millis(); + lastSendTime = 0; + } + + bool canSend(size_t dataSize) { + unsigned long currentTime = millis(); + + // 每秒重置计数器 + if(currentTime - lastResetTime >= 1000) { + bytesSent = 0; + lastResetTime = currentTime; + } + + // 检查速率限制(放宽限制) + if((bytesSent + dataSize) > maxBytesPerSecond) { + return false; + } + + // 最小发送间隔控制(重要!) + if(currentTime - lastSendTime < 5) { // 进一步减小最小间隔到5ms + return false; + } + + return true; + } + + bool check() { + // 这是一个简化版本的检查方法,只检查最小发送间隔 + unsigned long currentTime = millis(); + if(currentTime - lastSendTime < 5) { // 最小间隔5ms + return false; + } + return true; + } + + void recordSend(size_t dataSize) { + bytesSent += dataSize; + lastSendTime = millis(); + } + + void reset() { + bytesSent = 0; + lastResetTime = millis(); + lastSendTime = 0; + } +}; + +// 全局变量用于控制持续发送 +bool continuousSendEnabled = false; +unsigned long continuousSendInterval = 1000; // 默认1秒发送一次 +BLEFlowController bleFlow(500); // 提高到500 B/s限制,进一步提高数据传输速率 + +void processBLEConfig(); +void saveWiFiConfig(); +void loadWiFiConfig(); +bool connectWiFi(); +void processRadarData(); +bool parseSensorLine(String line); +void sendRadarCommand(String cmd); +void updateStatusFlags(); +void loadDeviceId(); // 加载设备ID +void saveDeviceId(); // 保存设备ID +bool processSetDeviceId(JsonDocument& doc); // 处理设置设备ID命令 +void sendStatusToBLE(); // 发送状态信息到BLE +bool processQueryStatus(JsonDocument& doc); // 处理查询状态命令 +bool processQueryRadarData(JsonDocument& doc); // 处理查询雷达数据命令 +bool processStartContinuousSend(JsonDocument& doc); // 处理开始持续发送命令 +bool processStopContinuousSend(JsonDocument& doc); // 处理停止持续发送命令 +void sendRadarDataToBLE(); // 发送雷达数据到BLE +void sendDataInChunks(const String& data); // 分包发送函数 +float addDataAndCalculateAverage(float newData); // 计算平均值并添加新数据 + +// FreeRTOS任务函数声明 +void bleSendTask(void *parameter); +void vitalSendTask(void *parameter); + +class MyServerCallbacks: public BLEServerCallbacks { + void onConnect(BLEServer* pServer) { + deviceConnected = true; + // 注释掉这行以减少串口输出 + // Serial.println("BLE客户端已连接"); + + // 发送连接状态信息 + sendStatusToBLE(); + }; + + void onDisconnect(BLEServer* pServer) { + deviceConnected = false; + // 注释掉这行以减少串口输出 + // Serial.println("BLE客户端已断开"); + + // 重置持续发送状态 + continuousSendEnabled = false; + // 注释掉这行以减少串口输出 + // Serial.println("重置持续发送状态"); + } +}; + +class MyCallbacks: public BLECharacteristicCallbacks { + void onWrite(BLECharacteristic *pCharacteristic) { + std::string value = pCharacteristic->getValue(); + // 注释掉这行以减少串口输出 + // Serial.printf("🔵 收到BLE写入数据,长度: %d 字节\n", value.length()); + + if (value.length() > 0) { + String fragment = ""; + for (int i = 0; i < value.length(); i++) + fragment += value[i]; + + // 注释掉这行以减少串口输出 + // Serial.printf("📄 接收数据片段: %s\n", fragment.c_str()); + + completeData += fragment; + lastReceiveTime = millis(); + + // 检查是否收到完整的JSON数据 + int openBrace = completeData.indexOf('{'); + int closeBrace = completeData.lastIndexOf('}'); + + if (openBrace >= 0 && closeBrace > openBrace) { + String jsonData = completeData.substring(openBrace, closeBrace + 1); + completeData = completeData.substring(closeBrace + 1); + + // 注释掉这行以减少串口输出 + // Serial.printf("📥 完整JSON数据: %s\n", jsonData.c_str()); + + // 将JSON数据存储到receivedData变量中供后续处理 + receivedData = jsonData; + } + } + } +}; + +void setup() { + // 启用串口通信以便观察滤波后的数据 + Serial.begin(115200); + + sensorData.breath_rate = 0; + sensorData.heart_rate = 0; + sensorData.rssi = 0; + sensorData.breath_valid = 0; + sensorData.heart_valid = 0; + + esp_task_wdt_init(30, true); + esp_task_wdt_add(NULL); + + // 增加串口缓冲区大小到4096字节以处理大量雷达数据 + mySerial2.setRxBufferSize(4096); + mySerial2.begin(BAUD_RATE, SERIAL_8N1, UART2_RX, UART2_TX); + // 注释掉这行以减少串口输出 + // Serial.println("UART2配置完成,缓冲区大小: 4096字节"); + + delay(1000); + + String startCmd = "{\"cmd\":\"setMonitor\",\"para\":1}\n"; + mySerial2.print(startCmd); + delay(1000); + // 注释掉这行以减少串口输出 + // Serial.println("启动雷达数据传输"); + + preferences.begin("radar_data", false); + + // 加载设备ID和WiFi配置 + loadDeviceId(); + loadWiFiConfig(); + + // 创建FreeRTOS队列 + phaseDataQueue = xQueueCreate(QUEUE_SIZE, sizeof(PhaseData)); + vitalDataQueue = xQueueCreate(QUEUE_SIZE, sizeof(VitalData)); + // bleDataQueue = xQueueCreate(QUEUE_SIZE, sizeof(BLEData)); // 移除:不再需要创建蓝牙数据队列 + + // 注释掉这行以减少串口输出 + /* + if (phaseDataQueue == NULL || vitalDataQueue == NULL) { // 移除:不再检查蓝牙数据队列 + Serial.println("❌ 队列创建失败"); + } else { + Serial.println("✅ FreeRTOS队列创建成功"); + } + */ + + // 创建FreeRTOS任务 + xTaskCreatePinnedToCore( + bleSendTask, + "BleSendTask", + TASK_STACK_SIZE, + NULL, + TASK_PRIORITY, + &bleSendTaskHandle, + 1 + ); + + xTaskCreatePinnedToCore( + vitalSendTask, + "VitalSendTask", + TASK_STACK_SIZE, + NULL, + TASK_PRIORITY, + &vitalSendTaskHandle, + 1 + ); + + + // 注释掉这行以减少串口输出 + // Serial.println("✅ FreeRTOS任务创建成功"); + + BLEDevice::init("ESP32-Radar"); + pServer = BLEDevice::createServer(); + pServer->setCallbacks(new MyServerCallbacks()); + + BLEService *pService = pServer->createService(SERVICE_UUID); + pCharacteristic = pService->createCharacteristic( + CHARACTERISTIC_UUID, + BLECharacteristic::PROPERTY_READ | + BLECharacteristic::PROPERTY_WRITE | + BLECharacteristic::PROPERTY_NOTIFY + ); + pCharacteristic->setCallbacks(new MyCallbacks()); + pCharacteristic->addDescriptor(new BLE2902()); + + pService->start(); + BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); + pAdvertising->addServiceUUID(SERVICE_UUID); + pAdvertising->setScanResponse(true); + pAdvertising->setMinPreferred(0x06); + pAdvertising->setMinPreferred(0x12); + BLEDevice::startAdvertising(); + + // 注释掉这行以减少串口输出 + // Serial.println("BLE已启动,设备名称: ESP32-Radar"); + + if (wifiConfigured) { + // 注释掉这行以减少串口输出 + // Serial.println("检测到已保存的WiFi配置,尝试连接..."); + if (connectWiFi()) { + // 注释掉这行以减少串口输出 + // Serial.println("WiFi连接成功!"); + } else { + // 注释掉这行以减少串口输出 + // Serial.println("WiFi连接失败,请通过BLE重新配置"); + wifiConfigured = false; + } + } +} + +void loop() { + esp_task_wdt_reset(); + + if (!deviceConnected && oldDeviceConnected) { + delay(500); + pServer->startAdvertising(); + // 注释掉这行以减少串口输出 + // Serial.println("开始BLE广播"); + oldDeviceConnected = deviceConnected; + } + if (deviceConnected && !oldDeviceConnected) { + oldDeviceConnected = deviceConnected; + } + + processBLEConfig(); + + // 移除WiFi连接检查,使雷达数据处理不依赖WiFi连接 + esp_task_wdt_reset(); + processRadarData(); + esp_task_wdt_reset(); + updateStatusFlags(); + + // 注意:持续发送雷达数据的逻辑已移至FreeRTOS任务处理 + // 主循环不再直接处理蓝牙数据发送,避免影响雷达数据接收和解析 + + delay(1); +} + +// 处理查询状态命令 +bool processQueryStatus(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "queryStatus") == 0) { + // 发送设备状态信息 + if (deviceConnected) { + String statusMsg = String("{\"type\":\"deviceStatus\",\"success\":true,\"deviceId\":") + + String(currentDeviceId) + + String(",\"wifiConfigured\":") + + String(wifiConfigured ? "true" : "false") + + String(",\"wifiConnected\":") + + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + + String(",\"ipAddress\":\"") + + (WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "") + + String("\"}"); + + // 直接发送状态查询响应数据(不使用队列,因为这是即时响应) + pCharacteristic->setValue(statusMsg.c_str()); + pCharacteristic->notify(); + Serial.println("已发送设备状态信息"); + } + return true; + } + return false; +} + +// 处理查询雷达数据命令 +bool processQueryRadarData(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "queryRadarData") == 0) { + Serial.println("收到查询雷达数据命令"); + + // 发送最新的雷达数据 + if (deviceConnected) { + String radarDataMsg = String("{\"type\":\"radarData\",\"success\":true") + + String(",\"deviceId\":") + String(currentDeviceId) + + String(",\"timestamp\":") + String(millis()) + + String(",\"presence\":") + String(sensorData.presence) + + String(",\"heartRate\":") + String(sensorData.heart_rate, 1) + + String(",\"breathRate\":") + String(sensorData.breath_rate, 1) + + String(",\"motion\":") + String(sensorData.motion) + + String(",\"rssi\":") + String(sensorData.rssi) + + String(",\"heartbeatWaveform\":") + String(sensorData.heartbeat_waveform) + + String(",\"breathingWaveform\":") + String(sensorData.breathing_waveform) + + String(",\"rawSignal\":") + String(sensorData.raw_signal) + + String("}"); + + // 直接发送查询响应数据(不使用队列,因为这是即时响应) + pCharacteristic->setValue(radarDataMsg.c_str()); + pCharacteristic->notify(); + Serial.println("已发送雷达数据"); + Serial.printf("发送的数据: %s\n", radarDataMsg.c_str()); + } else { + Serial.println("BLE未连接,无法发送雷达数据"); + } + return true; + } + return false; +} + +// 处理开始持续发送命令 +bool processStartContinuousSend(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "startContinuousSend") == 0) { + // 获取发送间隔参数(可选,默认1000ms) + if (doc.containsKey("interval")) { + continuousSendInterval = doc["interval"]; + // 限制最小间隔为100ms,最大间隔为10000ms + if (continuousSendInterval < 100) continuousSendInterval = 100; + if (continuousSendInterval > 10000) continuousSendInterval = 10000; + } + + continuousSendEnabled = true; + bleFlow.reset(); // 重置流量控制器 + + Serial.printf("⚙️ 启动持续发送模式,间隔: %lu ms\n", continuousSendInterval); + + // 发送清晰格式的确认消息 + if (deviceConnected) { + String confirmMsg = String("[START]Interval:") + String(continuousSendInterval); + + // 直接发送确认消息 + pCharacteristic->setValue(confirmMsg.c_str()); + pCharacteristic->notify(); + Serial.println("✅ 启动确认消息发送成功"); + + delay(5); // 减少延迟以提高实时性 + Serial.println("🚀 已启动持续发送模式"); + } else { + Serial.println("❌ BLE未连接,无法发送确认消息"); + } + return true; + } + return false; +} + +// 处理停止持续发送命令 +bool processStopContinuousSend(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "stopContinuousSend") == 0) { + continuousSendEnabled = false; + + Serial.println("🛑 停止持续发送模式"); + + // 发送清晰格式的确认消息 + if (deviceConnected) { + String confirmMsg = String("[STOP]ContinuousSend"); + + // 直接发送确认消息 + pCharacteristic->setValue(confirmMsg.c_str()); + pCharacteristic->notify(); + Serial.println("✅ 停止确认消息发送成功"); + + delay(5); // 减少延迟以提高实时性 + Serial.println("⏹️ 已停止持续发送模式"); + } else { + Serial.println("❌ BLE未连接,无法发送确认消息"); + } + return true; + } + return false; +} + +// 分包发送函数 - 优化版本,解决分包混乱和数据截断问题 +void sendDataInChunks(const String& data) { + const int MAX_PACKET_SIZE = 20; // BLE最大包大小 + const int HEADER_SIZE = 6; // 包头大小 "[N/M]" + const int CHUNK_SIZE = MAX_PACKET_SIZE - HEADER_SIZE; // 实际数据大小 + + int totalLength = data.length(); + int numChunks = (totalLength + CHUNK_SIZE - 1) / CHUNK_SIZE; + + // 只有在数据较长需要分包时才显示分包信息 + // 注释掉这行以减少串口输出 + // Serial.printf("📦 开始分包发送,总长度: %d, 分包数: %d\n", totalLength, numChunks); + + for(int i = 0; i < numChunks; i++) { + int start = i * CHUNK_SIZE; + int chunkLength = min(CHUNK_SIZE, totalLength - start); + String chunk = data.substring(start, start + chunkLength); + + // 构造简单包头: "[序号/总数]",确保总长度不超过6字符 + // 例如: "[1/5]" = 4字符 + String packetHeader = String("[") + String(i+1) + String("/") + String(numChunks) + String("]"); + + // 确保总包大小不超过20字节 + int maxDataLength = MAX_PACKET_SIZE - packetHeader.length(); + if (chunk.length() > maxDataLength) { + chunk = chunk.substring(0, maxDataLength); + } + + String packet = packetHeader + chunk; + + // 注释掉这行以减少串口输出 + // Serial.printf("📤 发送分包 %s: %s\n", packetHeader.c_str(), chunk.c_str()); + + // 检查BLE连接状态 + if (!deviceConnected) { + // 注释掉这行以减少串口输出 + // Serial.println("❌ BLE未连接,无法发送数据"); + return; + } + + // 发送数据包 + pCharacteristic->setValue(packet.c_str()); + pCharacteristic->notify(); + // 注释掉这行以减少串口输出 + // Serial.println("✅ 分包发送成功"); + + // 等待一段时间确保接收端处理完成,避免数据丢失 + if (i < numChunks - 1) { + // 注释掉这行以减少串口输出 + // Serial.printf("⏳ 等待接收端处理第%d个包...\n", i+1); + delay(20); // 进一步减少等待时间以提高实时性 + } + } + + // 注释掉这行以减少串口输出 + // Serial.println("📦 分包发送完成"); +} + +// 发送雷达数据到BLE(已移至FreeRTOS任务处理) +void sendRadarDataToBLE() { + // 此函数已废弃,雷达数据发送现在由FreeRTOS任务处理 + // 保留此函数以防其他代码引用 + Serial.println("ℹ️ 雷达数据发送已移至FreeRTOS任务处理"); +} + +// 处理BLE配置和命令 +void processBLEConfig() { + // 增强超时处理机制 + if (completeData.length() > 0 && (millis() - lastReceiveTime > 3000)) { + completeData = ""; + } + + if (receivedData.length() > 0) { + String bleData = receivedData; + receivedData = ""; + bleData.trim(); + + if (bleData.startsWith("{") && bleData.endsWith("}")) { + JsonDocument doc; + DeserializationError error = deserializeJson(doc, bleData); + + if (error) { + if (deviceConnected) { + String responseMsg = String("{\"type\":\"error\",\"message\":\"配置格式错误,请使用JSON格式\"}"); + + // 使用分包发送函数发送错误消息 + sendDataInChunks(responseMsg); + } + } else { + // 处理各种命令 + bool processed = false; + + // 处理设置设备ID命令 + if (!processed) processed = processSetDeviceId(doc); + + // 处理查询状态命令 + if (!processed) processed = processQueryStatus(doc); + + // 处理开始持续发送命令 + if (!processed) processed = processStartContinuousSend(doc); + + // 处理停止持续发送命令 + if (!processed) processed = processStopContinuousSend(doc); + + // 如果没有处理任何命令,发送错误响应 + if (!processed) { + if (deviceConnected) { + String responseMsg = String("{\"type\":\"error\",\"message\":\"未知命令\"}"); + + // 使用分包发送函数发送错误消息 + sendDataInChunks(responseMsg); + } + } + } + } else { + } + } +} + +void processRadarData() { + static unsigned long lastDataTime = 0; + static unsigned long noDataWarningTime = 0; + static uint32_t totalLinesReceived = 0; + static uint32_t validLinesReceived = 0; + static uint32_t phasePacketCounter = 0; + static uint32_t vitalPacketCounter = 0; + static bool processingData = false; + + if (processingData) { + return; + } + + int available = mySerial2.available(); + if (available > 0) { + processingData = true; + lastDataTime = millis(); + + bool lineProcessed = false; + while (mySerial2.available() > 0 && !lineProcessed) { + char c = mySerial2.read(); + rxBuffer += c; + + // 增加缓冲区大小检查,防止溢出 + if (rxBuffer.length() > 1000) { + rxBuffer = ""; + } + + if (c == '\n' || c == '\r') { + if (rxBuffer.length() > 0) { + totalLinesReceived++; + bool parseResult = parseSensorLine(rxBuffer); + if (parseResult) { + validLinesReceived++; + + // 更新全局传感器数据 + lastSensorUpdate = millis(); + + // 使用FreeRTOS队列发送数据到任务 + phasePacketCounter++; + if (phasePacketCounter >= PHASE_SEND_INTERVAL) { + PhaseData phaseData; + phaseData.heartbeat_waveform = sensorData.heartbeat_waveform; + phaseData.breathing_waveform = sensorData.breathing_waveform; + + if (xQueueSend(phaseDataQueue, &phaseData, 0) == pdTRUE) { + } else { + } + phasePacketCounter = 0; + } + + vitalPacketCounter++; + if (vitalPacketCounter >= VITAL_SEND_INTERVAL) { + VitalData vitalData; + vitalData.heart_rate = sensorData.heart_rate; + vitalData.breath_rate = sensorData.breath_rate; + vitalData.presence = sensorData.presence; + vitalData.motion = sensorData.motion; + vitalData.rssi = sensorData.rssi; + + if (xQueueSend(vitalDataQueue, &vitalData, 0) == pdTRUE) { + } else { + } + vitalPacketCounter = 0; + } + } else { + } + + rxBuffer = ""; + lineProcessed = true; + } + } + + esp_task_wdt_reset(); + } + + if (!lineProcessed && available > 0) { + } + + processingData = false; + } else { + if (millis() - lastDataTime > 5000 && millis() - noDataWarningTime > 5000) { + noDataWarningTime = millis(); + } + } +} + + +// FreeRTOS任务:蓝牙数据发送(精简格式以节省空间) +void bleSendTask(void *parameter) { + while (1) { + PhaseData phaseData; + + // 从队列接收相位数据用于蓝牙发送 + if (xQueueReceive(phaseDataQueue, &phaseData, portMAX_DELAY) == pdTRUE) { + esp_task_wdt_reset(); + + // 蓝牙传输 - 构造并发送完整的雷达数据(包含所有7个字段,无标识符以节省空间) + if (deviceConnected && continuousSendEnabled) { + // 构造完整雷达数据格式(去掉"radar"标识以节省空间) + // 格式: "heartRate|breathRate|heartbeatWaveform|breathingWaveform|presence|motion|rssi|checksum" + String radarDataCore = String(sensorData.heart_rate, 1) + String("|") + + String(sensorData.breath_rate, 1) + String("|") + + String(phaseData.heartbeat_waveform) + String("|") + + String(phaseData.breathing_waveform) + String("|") + + String(sensorData.presence) + String("|") + + String(sensorData.motion) + String("|") + + String(sensorData.rssi); + + // 计算CRC校验和 + unsigned int crc = 0xFFFF; + for (int i = 0; i < radarDataCore.length(); i++) { + crc ^= (unsigned int)radarDataCore.charAt(i); + for (int j = 0; j < 8; j++) { + if (crc & 0x0001) { + crc >>= 1; + crc ^= 0xA001; + } else { + crc >>= 1; + } + } + } + + // 添加校验和到数据末尾 + String radarDataMsg = radarDataCore + String("|") + String(crc, HEX); + + // 注释掉这行以减少串口输出 + // Serial.printf("📤 通过蓝牙发送完整雷达数据: %s\n", radarDataMsg.c_str()); + + // 检查数据长度决定发送方式 + const int MAX_BLE_PACKET_SIZE = 20; // BLE最大包大小 + if (radarDataMsg.length() <= MAX_BLE_PACKET_SIZE) { + // 数据较短,直接发送 + pCharacteristic->setValue(radarDataMsg.c_str()); + pCharacteristic->notify(); + // 注释掉这行以减少串口输出 + // Serial.println("✅ 完整雷达数据蓝牙发送成功"); + } else { + // 数据较长,使用分包发送 + // 注释掉这行以减少串口输出 + // Serial.println("🔄 雷达数据较长,使用分包发送"); + sendDataInChunks(radarDataMsg); + } + } + + esp_task_wdt_reset(); + } + + vTaskDelay(1 / portTICK_PERIOD_MS); // 减少延迟以提高实时性 + } +} + +// FreeRTOS任务:发送生命体征数据 +void vitalSendTask(void *parameter) { + while (1) { + VitalData vitalData; + + // 从队列接收生命体征数据用于WiFi数据库发送 + if (xQueueReceive(vitalDataQueue, &vitalData, portMAX_DELAY) == pdTRUE) { + esp_task_wdt_reset(); + + // WiFi数据库传输 - 一直发送生命体征数据(不通过蓝牙) + if (WiFi.status() == WL_CONNECTED) { + HTTPClient http; + http.setTimeout(2000); + + String url = String("http://") + String(influxDBHost) + ":" + String(influxDBPort) + "/api/v2/write?org=" + String(influxDBOrg) + "&bucket=" + String(influxDBBucket) + "&precision=ns"; + + http.begin(url); + http.addHeader("Authorization", String("Token ") + String(influxDBToken)); + http.addHeader("Content-Type", "text/plain"); + http.setReuse(true); + + // 使用可变的设备ID + String deviceId = String(currentDeviceId); + + String vitalDataStr = ""; + bool firstField = true; + + // 心率数据 - 使用浮点数格式 + if (vitalData.heart_rate > 0) { + if (!firstField) vitalDataStr += ","; + vitalDataStr += "heartRate="; + vitalDataStr += String(vitalData.heart_rate / 1.0, 1); + firstField = false; + } + + // 呼吸率数据 - 使用浮点数格式 + if (vitalData.breath_rate > 0) { + if (!firstField) vitalDataStr += ","; + vitalDataStr += "breathingRate="; + vitalDataStr += String(vitalData.breath_rate / 1.0, 1); + firstField = false; + } + + // 人检数据 - 确保值为0或1,使用整数格式 + if (true) { // 总是发送人检数据 + if (!firstField) vitalDataStr += ","; + vitalDataStr += "personDetected="; + // 确保值为0或1 + int presenceValue = (vitalData.presence > 0) ? 1 : 0; + vitalDataStr += String(presenceValue) + "i"; // 整数格式 + firstField = false; + } + + // 人体活动数据 - 确保值为0或1,使用整数格式 + if (true) { // 总是发送人体活动数据 + if (!firstField) vitalDataStr += ","; + vitalDataStr += "humanActivity="; + // 确保值为0或1 + int motionValue = (vitalData.motion > 0) ? 1 : 0; + vitalDataStr += String(motionValue) + "i"; // 整数格式 + firstField = false; + } + + // RSSI数据 - 使用整数格式 + if (vitalData.rssi != 0) { + if (!firstField) vitalDataStr += ","; + vitalDataStr += "rssi="; + vitalDataStr += String(vitalData.rssi) + "i"; // 整数格式 + firstField = false; + } + + // 如果有数据要发送,则构造完整的行协议字符串 + if (vitalDataStr.length() > 0) { + vitalDataStr = "device_data,deviceId=" + deviceId + " " + vitalDataStr; + //Serial.println("📤 发送生命体征数据到数据库: " + vitalDataStr); + + int httpResponseCode = http.POST(vitalDataStr); + + if (httpResponseCode == 204) { + //Serial.println("✅ 生命体征数据发送到数据库成功"); + } else { + String errorMsg = String("❌ 生命体征数据发送到数据库失败: ") + String(httpResponseCode); + Serial.println(errorMsg); + } + } + + http.end(); + } else { + Serial.println("❌ WiFi未连接,无法发送生命体征数据到数据库"); + } + + esp_task_wdt_reset(); + } + + vTaskDelay(5 / portTICK_PERIOD_MS); + } +} + +void updateStatusFlags() { + static unsigned long lastStatusUpdate = 0; +} + +bool parseSensorLine(String line) { + if (!line.startsWith("HBR01")) { + // 注释掉这行以减少串口输出 + // Serial.println("❌ 数据行不以HBR01开头"); + return false; + } + + int colonIndex = line.indexOf(':'); + if (colonIndex == -1) { + // 注释掉这行以减少串口输出 + // Serial.println("❌ 数据行缺少冒号分隔符"); + return false; + } + + String dataPart = line.substring(colonIndex + 1); + dataPart.trim(); + + // 注释掉这行以减少串口输出 + // Serial.printf("🔍 解析数据部分: %s\n", dataPart.c_str()); + + int values[8]; + int startIndex = 0; + int commaIndex; + int fieldCount = 0; + + for (int i = 0; i < 8; i++) { + commaIndex = dataPart.indexOf(',', startIndex); + + if (commaIndex == -1 && i == 7) { + String valueStr = dataPart.substring(startIndex); + valueStr.trim(); + values[i] = valueStr.toInt(); + fieldCount++; + // 注释掉这行以减少串口输出 + // Serial.printf(" 字段%d: %s -> %d\n", i+1, valueStr.c_str(), values[i]); + } else if (commaIndex != -1) { + String valueStr = dataPart.substring(startIndex, commaIndex); + valueStr.trim(); + values[i] = valueStr.toInt(); + startIndex = commaIndex + 1; + fieldCount++; + // 注释掉这行以减少串口输出 + // Serial.printf(" 字段%d: %s -> %d\n", i+1, valueStr.c_str(), values[i]); + } else { + // 注释掉这行以减少串口输出 + // Serial.printf("❌ 解析错误: 字段%d缺失,总共找到%d个字段\n", i+1, fieldCount); + return false; + } + } + + sensorData.presence = values[0]; + sensorData.heart_rate = (float)values[1]; + sensorData.breath_rate = (float)values[2]; + sensorData.motion = values[3]; + sensorData.rssi = values[4]; + sensorData.heartbeat_waveform = values[5]; + sensorData.breathing_waveform = values[6]; + sensorData.raw_signal = values[7]; + + sensorData.heart_valid = (sensorData.heart_rate > 0 && sensorData.heart_rate < 200); + sensorData.breath_valid = (sensorData.breath_rate >= 0.1f && sensorData.breath_rate <= 60.0f); + + // 只有在检测到人的情况下才进行心率滤波处理 + if (sensorData.presence > 0) { + sensorData.heart_rate = addDataAndCalculateAverage(sensorData.heart_rate); + } + + // 只有在检测到人的情况下才进行呼吸率滤波处理 + if (sensorData.presence > 0) { + // 对呼吸率继续使用原有的移动平均滤波器 + breathRateFilter[filterIndex] = sensorData.breath_rate; + + // 计算滤波后的呼吸率值(移动平均) + float filteredBreathRate = 0; + int count = filterInitialized ? FILTER_SIZE : (filterIndex + 1); + + for (int i = 0; i < count; i++) { + filteredBreathRate += breathRateFilter[i]; + } + + filteredBreathRate /= count; + + // 更新滤波器索引 + filterIndex = (filterIndex + 1) % FILTER_SIZE; + if (filterIndex == 0) { + filterInitialized = true; + } + + // 使用滤波后的呼吸率值 + sensorData.breath_rate = filteredBreathRate; + } + + lastSensorUpdate = millis(); + + // 按照指定格式输出心率和呼吸率的原始数据 + Serial.printf("Heart_Rate_RAW:%.2f\n", sensorData.heart_rate); + Serial.printf("Breath_Rate_RAW:%.2f\n", sensorData.breath_rate); + + /* + // 注释掉原来的详细输出 + Serial.printf("✅ 解析成功: 有人=%d, 心率=%.1f, 呼吸=%.1f, 运动=%d, 波形=%d/%d\n", + sensorData.presence, sensorData.heart_rate, + sensorData.breath_rate, sensorData.motion, + sensorData.heartbeat_waveform, sensorData.breathing_waveform); + */ + + return true; +} + +void saveWiFiConfig() { + preferences.putString("ssid", ssid); + preferences.putString("password", password); + preferences.putBool("configured", true); + Serial.println("WiFi配置已保存到Flash"); +} + +void loadWiFiConfig() { + wifiConfigured = preferences.getBool("configured", false); + if (wifiConfigured) { + preferences.getString("ssid", ssid, sizeof(ssid)); + preferences.getString("password", password, sizeof(password)); + Serial.printf("从Flash加载WiFi配置 - SSID: %s\n", ssid); + } +} + +bool connectWiFi() { + WiFi.mode(WIFI_STA); + WiFi.begin(ssid, password); + + Serial.println("[WiFi] 正在尝试连接..."); + int attempts = 0; + while (WiFi.status() != WL_CONNECTED && attempts < 20) { + delay(500); + Serial.printf("[WiFi] 尝试连接中,当前状态: %d\n", WiFi.status()); + attempts++; + } + + if (WiFi.status() == WL_CONNECTED) { + Serial.println("[WiFi] 连接成功!"); + Serial.printf("[WiFi] 分配的IP地址: %s\n", WiFi.localIP().toString().c_str()); + return true; + } else { + Serial.println("[WiFi] 连接超时,未能成功连接到WiFi。"); + Serial.printf("[WiFi] 最终状态码: %d\n", WiFi.status()); + return false; + } +} + +void sendRadarCommand(String cmd) { + if (!cmd.endsWith("\n")) { + cmd += "\n"; + } + mySerial2.print(cmd); + Serial.println("📤 [雷达命令] " + cmd); +} + +// 处理设置设备ID命令 +bool processSetDeviceId(JsonDocument& doc) { + const char* command = doc["command"]; + if (command != nullptr && strcmp(command, "setDeviceId") == 0) { + // 处理设置设备ID的命令 + uint16_t newDeviceId = doc["newDeviceId"]; + + // 验证设备ID范围 + if (newDeviceId < MIN_DEVICE_ID || newDeviceId > MAX_DEVICE_ID) { + Serial.printf("[错误] 设备ID超出范围,有效范围: %d-%d\n", MIN_DEVICE_ID, MAX_DEVICE_ID); + if (deviceConnected) { + String errorMsg = String("{\"type\":\"error\",\"message\":\"设备ID超出范围,有效范围: ") + + String(MIN_DEVICE_ID) + "-" + String(MAX_DEVICE_ID) + "\"}"; + + // 直接发送错误响应数据 + pCharacteristic->setValue(errorMsg.c_str()); + pCharacteristic->notify(); + } + return true; // 已处理该命令 + } + + currentDeviceId = newDeviceId; + + Serial.printf("[设备ID] 已设置新的设备ID: %u\n", currentDeviceId); + + // 保存设备ID到Preferences + saveDeviceId(); + + // 发送确认消息 + if (deviceConnected) { + String confirmMsg = String("{\"type\":\"setDeviceIdResult\",\"success\":true,\"message\":\"设备ID设置成功\",\"newDeviceId\":") + + String(newDeviceId) + "}"; + + // 直接发送设置设备ID响应数据 + pCharacteristic->setValue(confirmMsg.c_str()); + pCharacteristic->notify(); + + // 发送更新后的状态信息 + sendStatusToBLE(); + } + return true; + } + return false; +} + +// 加载设备ID +void loadDeviceId() { + currentDeviceId = preferences.getUShort("deviceId", 1001); + Serial.printf("从Flash加载设备ID: %u\n", currentDeviceId); +} + +// 保存设备ID +void saveDeviceId() { + preferences.putUShort("deviceId", currentDeviceId); + Serial.printf("设备ID已保存到Flash: %u\n", currentDeviceId); +} + +// 发送状态信息到BLE +void sendStatusToBLE() { + if (deviceConnected) { + String statusMsg = String("{\"type\":\"status\",\"wifiConfigured\":") + + String(wifiConfigured ? "true" : "false") + + String(",\"wifiConnected\":") + + String(WiFi.status() == WL_CONNECTED ? "true" : "false") + + String(",\"ipAddress\":\"") + + (WiFi.status() == WL_CONNECTED ? WiFi.localIP().toString() : "") + "\"" + + String(",\"deviceId\":") + + String(currentDeviceId) + "}"; + + // 直接发送状态信息数据 + pCharacteristic->setValue(statusMsg.c_str()); + pCharacteristic->notify(); + Serial.println("已发送连接状态信息"); + } +} + + +float addDataAndCalculateAverage(float newData) { + // 只有在检测到人的情况下才进行滤波处理 + if (sensorData.presence == 0) { + return newData; // 没有检测到人时,直接返回原始数据 + } + + unsigned long currentTime = millis(); + unsigned long oneMinuteAgo = currentTime - 60000; // 一分钟前的时间 + + // 移除过期的旧数据(一分钟前的数据) + while (heartRateBuffer.count > 0 && heartRateBuffer.timestamps[heartRateBuffer.tail] < oneMinuteAgo) { + heartRateBuffer.sum -= heartRateBuffer.data[heartRateBuffer.tail]; + heartRateBuffer.tail = (heartRateBuffer.tail + 1) % BUFFER_SIZE; + heartRateBuffer.count--; + heartRateBuffer.isFull = (heartRateBuffer.count == BUFFER_SIZE); + } + + // 添加新数据 + if (heartRateBuffer.count < BUFFER_SIZE) { + // 缓冲区未满,直接添加 + heartRateBuffer.head = (heartRateBuffer.head + 1) % BUFFER_SIZE; + if (heartRateBuffer.count == 0) heartRateBuffer.tail = heartRateBuffer.head; + heartRateBuffer.data[heartRateBuffer.head] = newData; + heartRateBuffer.timestamps[heartRateBuffer.head] = currentTime; + heartRateBuffer.sum += newData; + heartRateBuffer.count++; + heartRateBuffer.isFull = (heartRateBuffer.count == BUFFER_SIZE); + } else { + // 缓冲区已满,替换最旧数据 + heartRateBuffer.sum -= heartRateBuffer.data[heartRateBuffer.tail]; // 移除最旧数据 + heartRateBuffer.tail = (heartRateBuffer.tail + 1) % BUFFER_SIZE; // 移动尾指针 + heartRateBuffer.head = (heartRateBuffer.head + 1) % BUFFER_SIZE; // 移动头指针 + heartRateBuffer.data[heartRateBuffer.head] = newData; // 存储新数据 + heartRateBuffer.timestamps[heartRateBuffer.head] = currentTime; + heartRateBuffer.sum += newData; // 添加新数据到总和 + } + + // 计算并返回平均值 + if (heartRateBuffer.count > 0) { + return heartRateBuffer.sum / heartRateBuffer.count; + } else { + return 0; + } +} diff --git a/test_json_chunk_sender.py b/test_json_chunk_sender.py new file mode 100644 index 0000000..8f181a1 --- /dev/null +++ b/test_json_chunk_sender.py @@ -0,0 +1,72 @@ +import json +import time + +def simulate_json_chunk_sending(data, chunk_size=15): + """ + 模拟JSON数据分包发送 + """ + json_string = json.dumps(data) + print(f"原始JSON数据: {json_string}") + print(f"数据总长度: {len(json_string)} 字符") + + chunks = [] + # 分包 + for i in range(0, len(json_string), chunk_size): + chunk = json_string[i:i+chunk_size] + chunks.append(chunk) + + print(f"\n分包结果 ({len(chunks)} 个包):") + for i, chunk in enumerate(chunks): + print(f" 包 {i+1}: {chunk}") + + return chunks + +def create_sample_json_data(): + """ + 创建示例JSON数据 + """ + return { + "type": "radarData", + "deviceId": 1001, + "timestamp": int(time.time() * 1000), + "presence": 1, + "heartRate": 72.5, + "breathRate": 16.2, + "motion": 0, + "rssi": -45, + "heartbeatWaveform": 120, + "breathingWaveform": 45, + "rawSignal": -25 + } + +def create_status_json_data(): + """ + 创建状态JSON数据 + """ + return { + "type": "status", + "wifiConfigured": True, + "wifiConnected": True, + "ipAddress": "192.168.1.100", + "deviceId": 1001 + } + +def main(): + print("=== JSON分包发送模拟测试 ===\n") + + # 测试雷达数据 + print("1. 雷达数据分包测试:") + radar_data = create_sample_json_data() + simulate_json_chunk_sending(radar_data, 15) + + print("\n" + "="*50 + "\n") + + # 测试状态数据 + print("2. 状态数据分包测试:") + status_data = create_status_json_data() + simulate_json_chunk_sending(status_data, 15) + + print("\n=== 测试完成 ===") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test_json_parser.js b/test_json_parser.js new file mode 100644 index 0000000..7e518f3 --- /dev/null +++ b/test_json_parser.js @@ -0,0 +1,82 @@ +// JSON分包接收处理函数 +let streamBuffer = "" + +function addLog(message) { + console.log(message); +} + +function handleParsedJson(obj) { + console.log("处理解析后的JSON对象:", obj); +} + +function processChunk(fragment){ + addLog('进入processChunk') + streamBuffer += fragment + let braceCount = 0 + let jsonStart = -1 + for(let i = 0; i < streamBuffer.length; i++){ + const ch = streamBuffer[i] + + if(ch === "{"){ + if(braceCount === 0){ + jsonStart = i + } + braceCount++ + }else if(ch === "}"){ + braceCount-- + if(braceCount === 0 && jsonStart !== -1){ + const jsonStr = streamBuffer.substring(jsonStart, i + 1) + addLog("收到完整JSON:" + jsonStr) + try{ + const obj = JSON.parse(jsonStr) + handleParsedJson(obj) + }catch(e){ + addLog("JSON解析失败:" + e) + } + streamBuffer = streamBuffer.substring(i + 1) + i = -1 + jsonStart = -1 + } + } + } + //缓冲区过大时清理防越界 + if (streamBuffer.length > 20000) { + addLog("⚠ 清空超大缓冲区(异常保护)") + streamBuffer = "" + braceCount = 0 + jsonStart = -1 + } +} + +// 模拟分包发送JSON数据 +function simulateJSONSending() { + const jsonData = { + type: "radarData", + deviceId: 1001, + timestamp: Date.now(), + presence: 1, + heartRate: 72.5, + breathRate: 16.2, + motion: 0, + rssi: -45, + heartbeatWaveform: 120, + breathingWaveform: 45, + rawSignal: -25 + }; + + const jsonString = JSON.stringify(jsonData); + console.log("原始JSON数据:", jsonString); + + // 模拟分包发送 + const packetSize = 15; // 每包15个字符 + for (let i = 0; i < jsonString.length; i += packetSize) { + const packet = jsonString.substring(i, Math.min(i + packetSize, jsonString.length)); + console.log(`发送分包 ${Math.floor(i/packetSize)+1}:`, packet); + processChunk(packet); + } +} + +// 测试函数 +console.log("=== 开始测试JSON分包接收处理 ==="); +simulateJSONSending(); +console.log("=== 测试完成 ==="); \ No newline at end of file diff --git a/传感器数据.txt b/传感器数据.txt new file mode 100644 index 0000000..ec75a0a --- /dev/null +++ b/传感器数据.txt @@ -0,0 +1,394 @@ +HBR01:0,0,0,0,9,0,0,-30 +HBR01:0,0,0,0,9,0,0,-20 +HBR01:0,0,0,0,9,0,0,-21 +HBR01:0,0,0,0,9,0,0,-19 +HBR01:0,0,0,0,9,0,0,-22 +HBR01:0,0,0,0,9,0,0,-36 +HBR01:0,0,0,0,9,0,0,-35 +HBR01:0,0,0,0,9,0,0,-39 +HBR01:0,0,0,0,9,0,0,-40 +HBR01:0,0,0,0,9,0,0,-40 +HBR01:0,0,0,0,9,0,0,-40 +HBR01:0,0,0,0,9,0,0,-40 +HBR01:0,0,0,0,9,0,0,-23 +HBR01:0,0,0,0,9,0,0,0 +HBR01:0,0,0,0,9,0,0,0 +HBR01:0,0,0,0,9,0,0,-11 +HBR01:0,0,0,0,9,0,0,-19 +HBR01:0,0,0,0,9,0,0,-29 +HBR01:0,0,0,0,9,0,0,-34 +HBR01:0,0,0,0,9,0,0,-19 +HBR01:0,0,0,0,9,0,0,-22 +HBR01:0,0,0,0,9,0,0,-10 +HBR01:0,0,0,0,9,0,0,-9 +HBR01:0,0,0,0,9,0,0,-12 +HBR01:0,0,0,0,9,0,0,-13 +HBR01:0,0,0,0,9,0,0,-19 +HBR01:0,0,0,0,9,0,0,-23 +HBR01:0,0,0,0,9,0,0,-33 +HBR01:0,0,0,0,9,0,0,-33 +HBR01:0,0,0,0,9,0,0,-19 +HBR01:0,0,0,0,9,0,0,-3 +HBR01:0,0,0,0,9,0,0,-22 +HBR01:0,0,0,0,9,0,0,-13 +HBR01:0,0,0,0,9,0,0,-18 +HBR01:0,0,0,0,9,0,0,-12 +HBR01:0,0,0,0,9,0,0,0 +HBR01:0,0,0,0,9,0,0,-16 +HBR01:0,0,0,0,9,0,0,-27 +HBR01:0,0,0,0,9,0,0,-14 +HBR01:0,0,0,0,9,0,0,-23 +HBR01:0,0,0,0,9,0,0,-16 +HBR01:0,0,0,0,10,0,0,-40 +HBR01:0,0,0,0,10,0,0,-42 +HBR01:0,0,0,0,10,0,0,-38 +HBR01:0,0,0,0,10,0,0,-40 +HBR01:0,0,0,0,10,0,0,-43 +HBR01:0,0,0,0,10,0,0,0 +HBR01:0,0,0,0,10,0,0,0 +HBR01:0,0,0,0,10,0,0,0 +HBR01:0,0,0,0,10,0,0,1 +HBR01:0,0,0,0,10,0,0,0 +HBR01:0,0,0,0,10,0,0,0 +HBR01:0,0,0,0,10,0,0,0 +HBR01:0,0,0,0,10,0,0,-4 +HBR01:0,0,0,0,10,0,0,-22 +HBR01:0,0,0,0,10,0,0,-40 +HBR01:0,0,0,0,11,0,0,-57 +HBR01:0,0,0,0,11,0,0,-29 +HBR01:0,0,0,0,11,0,0,-22 +HBR01:0,0,0,0,11,0,0,-10 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-10 +HBR01:0,0,0,0,11,0,0,4 +HBR01:0,0,0,0,11,0,0,-16 +HBR01:0,0,0,0,11,0,0,-59 +HBR01:0,0,0,0,11,0,0,-9 +HBR01:0,0,0,0,11,0,0,-15 +HBR01:0,0,0,0,11,0,0,-21 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-28 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-24 +HBR01:0,0,0,0,11,0,0,-24 +HBR01:0,0,0,0,11,0,0,-14 +HBR01:0,0,0,0,11,0,0,-28 +HBR01:0,0,0,0,11,0,0,-30 +HBR01:0,0,0,0,11,0,0,-9 +HBR01:0,0,0,0,11,0,0,-25 +HBR01:0,0,0,0,11,0,0,-29 +HBR01:0,0,0,0,11,0,0,-24 +HBR01:0,0,0,0,11,0,0,-23 +HBR01:0,0,0,0,11,0,0,-31 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-25 +HBR01:0,0,0,0,11,0,0,-21 +HBR01:0,0,0,0,11,0,0,-36 +HBR01:0,0,0,0,11,0,0,-25 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-39 +HBR01:0,0,0,0,11,0,0,-27 +HBR01:0,0,0,0,11,0,0,-24 +HBR01:0,0,0,0,11,0,0,-24 +HBR01:0,0,0,0,11,0,0,-27 +HBR01:0,0,0,0,11,0,0,-27 +HBR01:0,0,0,0,11,0,0,-26 +HBR01:0,0,0,0,11,0,0,-36 +HBR01:0,0,0,0,11,0,0,-40 +HBR01:0,0,0,0,11,0,0,-40 +HBR01:0,0,0,0,11,0,0,-40 +HBR01:0,0,0,0,11,0,0,-40 +HBR01:0,0,0,0,11,0,0,-40 +HBR01:0,0,0,0,11,0,0,-59 +HBR01:0,0,0,0,11,0,0,-41 +HBR01:0,0,0,0,11,0,0,-52 +HBR01:0,0,0,0,11,0,0,-39 +HBR01:0,0,0,0,11,0,0,-35 +HBR01:0,0,0,0,11,0,0,-43 +HBR01:0,0,0,0,11,0,0,-30 +HBR01:0,0,0,0,11,0,0,-17 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-20 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-16 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-12 +HBR01:0,0,0,0,11,0,0,-7 +HBR01:0,0,0,0,11,0,0,-9 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-10 +HBR01:0,0,0,0,11,0,0,-14 +HBR01:0,0,0,0,11,0,0,-24 +HBR01:0,0,0,0,11,0,0,-15 +HBR01:0,0,0,0,11,0,0,-20 +HBR01:0,0,0,0,11,0,0,-16 +HBR01:0,0,0,0,11,0,0,-11 +HBR01:0,0,0,0,11,0,0,-12 +HBR01:0,0,0,0,12,0,0,9 +HBR01:0,0,0,0,12,0,0,-11 +HBR01:0,0,0,0,12,0,0,-11 +HBR01:0,0,0,0,12,0,0,-16 +HBR01:0,0,0,0,12,0,0,11 +HBR01:0,0,0,0,12,0,0,11 +HBR01:0,0,0,0,12,0,0,1 +HBR01:0,0,0,0,12,0,0,-4 +HBR01:0,0,0,0,12,0,0,-13 +HBR01:0,0,0,0,12,0,0,-3 +HBR01:0,0,0,0,12,0,0,-6 +HBR01:0,0,0,0,12,0,0,3 +HBR01:0,0,0,0,12,0,0,-12 +HBR01:0,0,0,0,12,0,0,-23 +HBR01:0,0,0,0,12,0,0,-40 +HBR01:0,0,0,0,12,0,0,-22 +HBR01:0,0,0,0,12,0,0,-40 +HBR01:0,0,0,0,12,0,0,-20 +HBR01:0,0,0,0,12,0,0,-21 +HBR01:0,0,0,0,12,0,0,-42 +HBR01:0,0,0,0,12,0,0,-40 +HBR01:0,0,0,0,13,0,0,-51 +HBR01:0,0,0,0,13,0,0,-40 +HBR01:0,0,0,0,13,0,0,-40 +HBR01:0,0,0,0,13,0,0,-40 +HBR01:0,0,0,0,13,0,0,-45 +HBR01:0,0,0,0,13,0,0,-48 +HBR01:0,0,0,0,13,0,0,-35 +HBR01:0,0,0,0,13,0,0,-38 +HBR01:0,0,0,0,13,0,0,-48 +HBR01:0,0,0,0,13,0,0,-19 +HBR01:0,0,0,0,13,0,0,-12 +HBR01:0,0,0,0,13,0,0,-26 +HBR01:0,0,0,0,13,0,0,-23 +HBR01:0,0,0,0,13,0,0,-9 +HBR01:0,0,0,0,13,0,0,4 +HBR01:0,0,0,0,13,0,0,-1 +HBR01:0,0,0,0,13,0,0,0 +HBR01:0,0,0,0,13,0,0,0 +HBR01:0,0,0,0,13,0,0,-1 +HBR01:0,0,0,0,13,0,0,1 +HBR01:0,0,0,0,13,0,0,0 +HBR01:0,0,0,0,13,0,0,-1 +HBR01:0,0,0,0,13,0,0,0 +HBR01:0,0,0,0,13,0,0,21 +HBR01:0,0,0,0,13,0,0,15 +HBR01:0,0,0,0,13,0,0,0 +HBR01:0,0,0,0,13,0,0,21 +HBR01:0,0,0,0,14,0,0,18 +HBR01:0,0,0,0,14,0,0,0 +HBR01:0,0,0,0,14,0,0,2 +HBR01:0,0,0,0,14,0,0,4 +HBR01:0,0,0,0,14,0,0,-3 +HBR01:0,0,0,0,14,0,0,5 +HBR01:0,0,0,0,14,0,0,-1 +HBR01:0,0,0,0,14,0,0,-22 +HBR01:0,0,0,0,14,0,0,4 +HBR01:0,0,0,0,14,0,0,3 +HBR01:0,0,0,0,14,0,0,-18 +HBR01:0,0,0,0,14,0,0,-16 +HBR01:0,0,0,0,14,0,0,-6 +HBR01:0,0,0,0,14,0,0,-23 +HBR01:0,0,0,0,14,0,0,-28 +HBR01:0,0,0,0,14,0,0,-24 +HBR01:0,0,0,0,14,0,0,-24 +HBR01:0,0,0,0,14,0,0,-39 +HBR01:0,0,0,0,14,0,0,-24 +HBR01:0,0,0,0,14,0,0,-38 +HBR01:0,0,0,0,14,0,0,-40 +HBR01:0,0,0,0,14,0,0,-37 +HBR01:0,0,0,0,14,0,0,-40 +HBR01:0,0,0,0,14,0,0,-40 +HBR01:0,0,0,0,14,0,0,-40 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-45 +HBR01:0,0,0,0,15,0,0,-36 +HBR01:0,0,0,0,15,0,0,-29 +HBR01:0,0,0,0,15,0,0,-10 +HBR01:0,0,0,0,15,0,0,-43 +HBR01:0,0,0,0,15,0,0,-12 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-33 +HBR01:0,0,0,0,15,0,0,-37 +HBR01:0,0,0,0,15,0,0,-30 +HBR01:0,0,0,0,15,0,0,-35 +HBR01:0,0,0,0,15,0,0,-26 +HBR01:0,0,0,0,15,0,0,2 +HBR01:0,0,0,0,15,0,0,-30 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-22 +HBR01:0,0,0,0,15,0,0,-23 +HBR01:0,0,0,0,15,0,0,-30 +HBR01:0,0,0,0,15,0,0,-33 +HBR01:0,0,0,0,15,0,0,-15 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-12 +HBR01:0,0,0,0,15,0,0,-20 +HBR01:0,0,0,0,15,0,0,-20 +HBR01:0,0,0,0,15,0,0,-10 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,15,0,0,-35 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-62 +HBR01:0,0,0,0,15,0,0,-33 +HBR01:0,0,0,0,15,0,0,-24 +HBR01:0,0,0,0,15,0,0,-30 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-45 +HBR01:0,0,0,0,15,0,0,-29 +HBR01:0,0,0,0,15,0,0,-31 +HBR01:0,0,0,0,15,0,0,-37 +HBR01:0,0,0,0,15,0,0,-43 +HBR01:0,0,0,0,15,0,0,-23 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-7 +HBR01:0,0,0,0,15,0,0,-21 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,16,0,0,-69 +HBR01:0,0,0,0,16,0,0,-30 +HBR01:0,0,0,0,16,0,0,-34 +HBR01:0,0,0,0,16,0,0,-1 +HBR01:0,0,0,0,16,0,0,-1 +HBR01:0,0,0,0,16,0,0,-8 +HBR01:0,0,0,0,16,0,0,-26 +HBR01:0,0,0,0,16,0,0,-15 +HBR01:0,0,0,0,16,0,0,-16 +HBR01:0,0,0,0,16,0,0,-17 +HBR01:0,0,0,0,16,0,0,-26 +HBR01:0,0,0,0,16,0,0,-14 +HBR01:0,0,0,0,16,0,0,-16 +HBR01:0,0,0,0,16,0,0,-18 +HBR01:0,0,0,0,16,0,0,-14 +HBR01:0,0,0,0,16,0,0,-9 +HBR01:0,0,0,0,16,0,0,-9 +HBR01:0,0,0,0,16,0,0,-14 +HBR01:0,0,0,0,16,0,0,-16 +HBR01:0,0,0,0,16,0,0,-30 +HBR01:0,0,0,0,16,0,0,-40 +HBR01:0,0,0,0,15,0,0,-23 +HBR01:0,0,0,0,15,0,0,-27 +HBR01:0,0,0,0,15,0,0,-23 +HBR01:0,0,0,0,15,0,0,-11 +HBR01:0,0,0,0,15,0,0,-21 +HBR01:0,0,0,0,15,0,0,-22 +HBR01:0,0,0,0,15,0,0,-12 +HBR01:0,0,0,0,15,0,0,-16 +HBR01:0,0,0,0,15,0,0,-35 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,16,0,0,-1 +HBR01:0,0,0,0,16,0,0,0 +HBR01:0,0,0,0,16,0,0,0 +HBR01:0,0,0,0,16,0,0,-24 +HBR01:0,0,0,0,16,0,0,-16 +HBR01:0,0,0,0,16,0,0,-24 +HBR01:0,0,0,0,16,0,0,-22 +HBR01:0,0,0,0,16,0,0,-38 +HBR01:0,0,0,0,16,0,0,-32 +HBR01:0,0,0,0,16,0,0,-20 +HBR01:0,0,0,0,16,0,0,-21 +HBR01:0,0,0,0,16,0,0,-9 +HBR01:0,0,0,0,16,0,0,-19 +HBR01:0,0,0,0,16,0,0,-31 +HBR01:0,0,0,0,16,0,0,-34 +HBR01:0,0,0,0,15,0,0,-22 +HBR01:0,0,0,0,15,0,0,-34 +HBR01:0,0,0,0,15,0,0,-21 +HBR01:0,0,0,0,15,0,0,-26 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-46 +HBR01:0,0,0,0,15,0,0,-7 +HBR01:0,0,0,0,15,0,0,-4 +HBR01:0,0,0,0,15,0,0,-26 +HBR01:0,0,0,0,15,0,0,-30 +HBR01:0,0,0,0,15,0,0,-14 +HBR01:0,0,0,0,15,0,0,-19 +HBR01:0,0,0,0,15,0,0,-28 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-9 +HBR01:0,0,0,0,15,0,0,-13 +HBR01:0,0,0,0,15,0,0,-27 +HBR01:0,0,0,0,15,0,0,-13 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-29 +HBR01:0,0,0,0,15,0,0,-13 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,15,0,0,-32 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-14 +HBR01:0,0,0,0,15,0,0,-17 +HBR01:0,0,0,0,15,0,0,-29 +HBR01:0,0,0,0,15,0,0,-31 +HBR01:0,0,0,0,15,0,0,-39 +HBR01:0,0,0,0,15,0,0,-11 +HBR01:0,0,0,0,15,0,0,-24 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-33 +HBR01:0,0,0,0,15,0,0,-41 +HBR01:0,0,0,0,15,0,0,-32 +HBR01:0,0,0,0,15,0,0,-32 +HBR01:0,0,0,0,15,0,0,-32 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-28 +HBR01:0,0,0,0,15,0,0,-21 +HBR01:0,0,0,0,15,0,0,-18 +HBR01:0,0,0,0,15,0,0,-7 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-14 +HBR01:0,0,0,0,15,0,0,-11 +HBR01:0,0,0,0,15,0,0,-23 +HBR01:0,0,0,0,15,0,0,-22 +HBR01:0,0,0,0,15,0,0,-14 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,15,0,0,-15 +HBR01:0,0,0,0,15,0,0,-28 +HBR01:0,0,0,0,15,0,0,1 +HBR01:0,0,0,0,15,0,0,1 +HBR01:0,0,0,0,15,0,0,-20 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,0 +HBR01:0,0,0,0,15,0,0,-28 +HBR01:0,0,0,0,15,0,0,-20 +HBR01:0,0,0,0,15,0,0,-10 +HBR01:0,0,0,0,15,0,0,-16 +HBR01:0,0,0,0,15,0,0,-12 +HBR01:0,0,0,0,15,0,0,-26 +HBR01:0,0,0,0,15,0,0,-33 +HBR01:0,0,0,0,15,0,0,-21 +HBR01:0,0,0,0,15,0,0,-22 +HBR01:0,0,0,0,15,0,0,-29 +HBR01:0,0,0,0,15,0,0,-40 +HBR01:0,0,0,0,15,0,0,-33 +HBR01:0,0,0,0,15,0,0,-28 +HBR01:0,0,0,0,15,0,0,-19 +HBR01:0,0,0,0,15,0,0,-26 +HBR01:0,0,0,0,15,0,0,-30 +HBR01:0,0,0,0,15,0,0,-20 +HBR01:0,0,0,0,15,0,0,-29 +HBR01:0,0,0,0,15,0,0,-35 +HBR01:0,0,0,0,15,0,0,-25 +HBR01:0,0,0,0,15,0,0,-27 +HBR01:0,0,0,0,15,0,0,-38 +HBR01:0,0,0,0,15,0,0,-27 +HBR01:0,0,0,0,15,0,0,-27 +HBR01:0,0,0,0,15,0,0,-48 +HBR01:0,0,0,0,15,0,0,-31 +HBR01:0,0,0,0,15,0,0,-26 +HBR01:0,0,0,0,15,0,0,-16 +HBR01:0,0,0,0,15,0,0,-6 +HBR01:0,0,0,0,15,0,0,-13 +HBR01:0,0,0,0,15,0,0,-11 +HBR01:0,0,0,0,14,0,0,-20 +HBR01:0,0,0,0,14,0,0,-58 +HBR01:0,0,0,0,14,0,0,-25 +HBR01:0,0,0,0,14,0,0,-11