优化雷达信号处理,添加心率和呼吸率平滑处理和异常值检测

This commit is contained in:
Admin
2026-02-28 14:39:03 +08:00
parent f3b99428cf
commit 83b2f3ba07
19 changed files with 845 additions and 3594 deletions

View File

@@ -0,0 +1,58 @@
# 修改蓝牙发送雷达数据模式
## 1. 需求分析
- 修改蓝牙发送机制,实现基于数据变化的发送
- 当心率、呼吸、存在检测、体动状态、睡眠状态发生变化时才发送
- 根据小程序设定的时间间隔进行定时检测
- 如果检测到变化,立即更新数据
- 按设定间隔检测数据更新,有变化则发送全部数据
## 2. 实现方案
### 2.1 添加数据存储结构
-`radar_manager.h` 中添加 `LastSentData` 结构体,用于存储上一次发送的数据
- 包含需要检测变化的字段:心率、呼吸率、存在状态、运动状态、睡眠状态
### 2.2 修改全局变量
-`radar_manager.cpp` 中添加 `lastSentData` 全局变量
- 添加 `lastCheckTime` 变量用于记录上次检测时间
### 2.3 实现数据变化检测函数
- 添加 `isDataChanged()` 函数,比较当前数据与上次发送数据
- 实现阈值判断,避免微小波动导致频繁发送
### 2.4 重写 BLE 数据发送任务
- 修改 `bleSendTask` 任务,实现以下逻辑:
1. 定时检测数据变化(基于 `continuousSendInterval`
2. 检测到变化时立即发送数据
3. 发送后更新 `lastSentData` 为当前数据
4. 保持与现有命令处理的兼容性
### 2.5 保持命令处理逻辑
- 保留 `processStartContinuousSend``processStopContinuousSend` 函数
- 确保小程序可以正常控制发送模式和间隔
## 3. 代码修改点
### 3.1 radar_manager.h
- 添加 `LastSentData` 结构体定义
- 在全局变量声明中添加 `lastSentData` 和相关变量
### 3.2 radar_manager.cpp
- 初始化 `lastSentData` 变量
- 实现 `isDataChanged()` 函数
- 重写 `bleSendTask` 任务函数
- 确保数据发送格式与现有代码保持一致
## 4. 技术要点
- 使用阈值检测避免微小数据波动导致的频繁发送
- 保持定时检测机制,确保数据及时性
- 维持与现有 BLE API 的兼容性
- 按照现有代码的注释格式编写新代码
- 确保内存使用合理,避免内存泄漏
## 5. 预期效果
- 减少不必要的蓝牙数据传输
- 只在数据发生实际变化时发送
- 保持数据的实时性和准确性
- 与现有小程序控制逻辑完全兼容

View File

@@ -0,0 +1,37 @@
## 实现BLE API规范计划
### 步骤1: 分析现有实现
- 检查main_backup.cpp.bak中的BLE命令处理函数实现
- 确认它们是否符合BLE_API.md中定义的API规范
- 识别需要移植的函数
### 步骤2: 移植BLE命令处理函数
- 将以下函数从main_backup.cpp.bak移植到main.cpp中
- processWiFiConfigCommand() - 处理WiFi配置命令
- processScanWiFi() - 处理WiFi扫描命令
- processGetSavedNetworks() - 处理获取已保存网络命令
- processEchoRequest() - 处理回显测试命令
- processSetDeviceId() - 处理设置设备ID命令
- processQueryStatus() - 处理查询状态命令
- processQueryRadarData() - 处理查询雷达数据命令
- processStartContinuousSend() - 处理启动持续发送命令
- processStopContinuousSend() - 处理停止持续发送命令
### 步骤3: 更新processBLEConfig函数
- 更新main.cpp中的processBLEConfig()函数,添加命令处理逻辑
- 实现JSON解析和命令分发功能
- 确保按照API规范处理所有支持的命令
### 步骤4: 添加必要的辅助函数
- 添加sendRawEchoResponse()函数,处理回显测试响应
- 确保所有函数都按照API规范返回正确的响应格式
### 步骤5: 验证实现
- 编译项目,确保没有错误
- 检查所有函数是否符合API规范
- 确认响应格式是否与API文档一致
### 预期结果
- 项目能够成功编译
- BLE命令处理函数符合BLE_API.md中定义的API规范
- 设备能够正确处理所有支持的BLE命令并返回符合规范的响应

View File

@@ -0,0 +1,26 @@
# 修复编译错误计划
## 问题分析
编译时出现了多个错误,主要是因为删除了 `radar_manager.cpp``radar_manager.h` 文件后,`main.cpp` 中仍然引用了这些文件中定义的变量和函数。
## 修复步骤
### 1. 修复 SAMPLE_RATE 重复定义
- 删除 `main.cpp` 中的 `SAMPLE_RATE` 定义,使用 `radar_vitals.h` 中的定义
### 2. 添加缺失的 BLE 相关代码
- 添加 `MyServerCallbacks` 类定义
- 添加 `MyCallbacks` 类定义
- 添加 `processBLEConfig` 函数定义
- 添加 `sendStatusToBLE` 函数定义
### 3. 修复其他未声明的变量和函数
- 确保所有使用的变量和函数都有正确的声明
### 4. 测试编译
- 运行 PlatformIO 编译命令,确保所有错误都已修复
## 预期结果
- 编译成功,没有错误
- 系统能够正常启动和运行
- 新的雷达模块能够正常工作

View File

@@ -0,0 +1,49 @@
# 删除无用代码优化项目
## 目标
在保持API功能不变的情况下删除项目中没有用的代码部分减少Flash占用提高代码可读性。
## 分析结果
通过对代码库的分析,我发现了以下可以删除的无用部分:
### 1. main.cpp
- **I2S相关配置**第17-18行的I2S配置定义因为使用的是直接ADC读取方式
- **setup_i2s()函数**第127-131行只是一个空函数没有实际功能
- **重复的雷达处理调用**第166行的`radar_vitals_process(&sample, &vitals);`调用因为已经在第163行调用了`radar_process(I_buf, Q_buf, DATA_LEN);`
- **冗余的WiFi连接成功消息**第357行总是会执行即使WiFi连接失败
- **注释掉的代码**第361-364行已经被注释掉没有实际功能
### 2. radar_vitals.cpp
- **旧的信号处理函数**第29-41行的`bp_breath``bp_heart`函数,使用的是新的处理方法
- **重复的相位解缠函数**第44-50行的`unwrap`函数,在`radar_process`中已经实现
- **旧的BPM计算函数**第53-80行的`calc_bpm`函数,使用的是新的频率估计方法
- **旧的主处理函数**第203-252行的`radar_vitals_process`函数,使用的是新的`radar_process`函数
- **未使用的系统参数**第12-15行的实时系统参数定义
- **未使用的缓冲区大小**第18行的`RADAR_BUFFER_SIZE`定义
- **未使用的队列长度**第19行的`QUEUE_LENGTH`定义
- **未使用的变量**第14-17行的`phase_buf``buf_idx``I_mean``Q_mean``prev_phase`变量
### 3. ble_api.cpp
- **未使用的函数**第60-73行的`sendCustomJSONData`函数
- **未使用的函数**第194-197行的`sendRawEchoResponse`函数
- **无效的超时处理**第369-378行的超时处理代码因为相关变量没有被设置
### 4. io_flash.h
- **重复的常量定义**第40-50行的常量定义已经在`io_flash.cpp`中定义
### 5. io_flash.cpp
- **重复的局部变量**第15-23行的局部变量定义在函数内部已经有局部定义
## 实施步骤
1. **修改main.cpp**删除I2S相关配置、setup_i2s()函数、重复的雷达处理调用、冗余的WiFi连接成功消息和注释掉的代码
2. **修改radar_vitals.cpp**删除旧的信号处理函数、重复的相位解缠函数、旧的BPM计算函数、旧的主处理函数、未使用的系统参数和变量
3. **修改ble_api.cpp**:删除未使用的函数和无效的超时处理代码
4. **修改io_flash.h**:删除重复的常量定义
5. **修改io_flash.cpp**:删除重复的局部变量
6. **编译项目**:确保项目能够成功编译,没有错误
7. **验证功能**确保API功能保持不变
## 预期结果
- 减少Flash占用提高代码可读性
- 保持API功能不变确保系统正常运行
- 消除冗余代码,减少维护成本

View File

@@ -0,0 +1,22 @@
## 备份并删除radar_manager.h文件计划
### 步骤1: 确认文件引用情况
- 检查是否有其他文件引用了radar_manager.h
- 验证main.cpp中确实没有包含radar_manager.h
### 步骤2: 备份文件
- 将radar_manager.h文件重命名为radar_manager.h.bak作为备份
### 步骤3: 删除文件
- 删除radar_manager.h文件
### 步骤4: 验证编译
- 运行platformio编译命令确保项目能够成功编译
- 检查是否有任何编译错误
### 步骤5: 清理临时文件
- 确认项目编译成功后,可以选择删除备份文件(如果需要)
### 预期结果
- 项目能够成功编译,没有任何错误
- 证明radar_manager.h文件是冗余的可以安全删除

View File

BIN
README.md

Binary file not shown.

View File

@@ -15,3 +15,5 @@ framework = arduino
lib_deps = lib_deps =
bblanchon/ArduinoJson@^7.4.2 bblanchon/ArduinoJson@^7.4.2
emelianov/modbus-esp8266@^4.1.0 emelianov/modbus-esp8266@^4.1.0
build_flags = -Wno-redefinition
source_filter = +<*> -<radar_manager.cpp>

View File

View File

@@ -1,36 +0,0 @@
#ifndef RADAR_H
#define RADAR_H
// 在main.cpp顶部添加R60ABD1协议相关定义
#define FRAME_HEADER1 0x53 // 帧头字节1
#define FRAME_HEADER2 0x59 // 帧头字节2
#define FRAME_TAIL1 0x54 // 帧尾字节1
#define FRAME_TAIL2 0x43 // 帧尾字节2
// 控制字定义
#define CTRL_PRESENCE 0x80 // 人体存在检测
#define CTRL_BREATH 0x81 // 呼吸检测
#define CTRL_SLEEP 0x84 // 睡眠监测
#define CTRL_HEARTRATE 0x85 // 心率监测
// 命令字定义
#define CMD_REPORT 0x80 // 主动上报
#define CMD_QUERY 0x81 // 查询命令
#define CMD_SET 0x82 // 设置命令
// 定义R60ABD1数据结构
typedef struct {
uint8_t present; // 有人/无人状态 (DP1)
uint16_t distance; // 人体距离 (DP3) 单位cm
uint8_t heartRate; // 心率 (DP6) 单位BPM
uint8_t breathRate; // 呼吸率 (DP8) 单位次/分钟
uint8_t heartWave; // 心率波形 (DP7) 数值+128
uint8_t breathWave; // 呼吸波形 (DP10) 数值+128
uint8_t sleepState; // 睡眠状态 (DP12)
uint32_t sleepTime; // 睡眠时长 (DP13) 单位秒
uint8_t sleepScore; // 睡眠质量评分 (DP14)
uint8_t bedEntry; // 入床/离床状态 (DP11)
uint8_t abnormal; // 异常状态 (DP18)
} R60ABD1Data;
#endif

288
src/io_flash.cpp Normal file
View File

@@ -0,0 +1,288 @@
#include "io_flash.h"
#include <esp_task_wdt.h>
// 全局变量定义
Preferences preferences; // 声明preferences变量
uint16_t currentDeviceId; // 当前设备ID
bool clearConfigRequested = false; // 清除配置请求标志
bool forceLedOff = false; // 强制关闭LED标志
ConfigClearStatus currentConfigClearStatus = CONFIG_NORMAL; // 当前配置清除状态
// 常量定义
const int SLOW_BLINK_INTERVAL = 1000; // 慢闪间隔(毫秒)
const int FAST_BLINK_INTERVAL = 200; // 快闪间隔(毫秒)
const int BREATHE_INTERVAL = 40; // 呼吸灯更新间隔(毫秒)
const int BREATHE_MIN = 0; // 呼吸灯最小亮度值
const int BREATHE_MAX = 155; // 呼吸灯最大亮度值
const int BREATHE_STEP = 5; // 呼吸灯亮度步进值
const unsigned long CLEAR_CONFIG_DURATION = 3000; // 清除配置持续时间(毫秒)
/**
* @brief 检查Boot按钮状态
* 在启动时检查Boot按钮是否被按下如果按下则进入配置清除流程
*/
void checkBootButton() {
Serial.println("🔍 检查Boot按钮状态...");
pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP);
delay(10);
int buttonState = digitalRead(BOOT_BUTTON_PIN);
Serial.printf("📊 Boot按钮状态: %s\n", buttonState == LOW ? "按下" : "释放");
if (buttonState == LOW) {
Serial.println("⚠️ 检测到Boot按钮按下请释放按钮后继续启动");
Serial.println("⏰ 等待按钮释放...");
while (digitalRead(BOOT_BUTTON_PIN) == LOW) {
delay(100);
}
Serial.println("✅ Boot按钮已释放正常启动");
} else {
Serial.println("✅ Boot按钮未按下正常启动");
}
}
/**
* @brief 加载设备ID
* 从Flash中读取保存的设备ID
*/
void loadDeviceId() {
currentDeviceId = preferences.getUShort("deviceId", 1001);
Serial.printf("从Flash加载设备ID: %u\n", currentDeviceId);
}
/**
* @brief 保存设备ID
* 将设备ID保存到Flash中
*/
void saveDeviceId() {
preferences.putUShort("deviceId", currentDeviceId);
Serial.printf("设备ID已保存到Flash: %u\n", currentDeviceId);
}
/**
* @brief 清除存储的配置
* 清除Flash中保存的所有配置包括设备ID
*/
void clearStoredConfig() {
Serial.println("🧹 开始清除存储的配置...");
uint16_t oldDeviceId = preferences.getUShort("deviceId", 0);
preferences.remove("deviceId");
Serial.println("✅ 配置已清除完成");
Serial.printf("🗑️ 被清除的设备ID: %u\n", oldDeviceId);
currentDeviceId = 1001;
Serial.println("🔄 已清除Flash与内存中的配置");
}
/**
* @brief 配置清除LED控制任务
* 根据配置清除状态控制CONFIG_CLEAR_PIN引脚的LED显示
* @param parameter 任务参数(未使用)
*/
void configClearLedTask(void *parameter) {
unsigned long lastConfigBlinkTime = 0; // 上次配置清除LED闪烁时间
bool configLedState = false; // 配置清除LED状态
int configBreatheValue = 0; // 配置清除呼吸灯当前亮度值
bool configBreatheIncreasing = true; // 配置清除呼吸灯是否在增加亮度
while (1) {
switch (currentConfigClearStatus) {
case CONFIG_NORMAL:
analogWrite(CONFIG_CLEAR_PIN, 0);
break;
case CONFIG_PREPARING:
analogWrite(CONFIG_CLEAR_PIN, 255);
break;
case CONFIG_CLEARING:
if (millis() - lastConfigBlinkTime >= BREATHE_INTERVAL) {
analogWrite(CONFIG_CLEAR_PIN, configBreatheValue);
if (configBreatheIncreasing) {
configBreatheValue += 5;
if (configBreatheValue >= BREATHE_MAX) {
configBreatheValue = BREATHE_MAX;
configBreatheIncreasing = false;
}
} else {
configBreatheValue -= 5;
if (configBreatheValue <= BREATHE_MIN) {
configBreatheValue = BREATHE_MIN;
configBreatheIncreasing = true;
}
}
lastConfigBlinkTime = millis();
}
break;
case CONFIG_COMPLETED:
if (millis() - lastConfigBlinkTime >= FAST_BLINK_INTERVAL) {
configLedState = !configLedState;
digitalWrite(CONFIG_CLEAR_PIN, configLedState ? HIGH : LOW);
lastConfigBlinkTime = millis();
static int blinkCount = 0;
blinkCount++;
if (blinkCount >= 6) {
blinkCount = 0;
currentConfigClearStatus = CONFIG_NORMAL;
digitalWrite(CONFIG_CLEAR_PIN, LOW);
}
}
break;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
/**
* @brief BOOT按钮监控任务
* 持续监控BOOT按钮状态检测长按3秒事件并触发配置清除
* @param parameter 任务参数(未使用)
*/
void bootButtonMonitorTask(void *parameter) {
Serial.println("🔍 启动BOOT按钮监控任务...");
pinMode(BOOT_BUTTON_PIN, INPUT_PULLUP);
unsigned long buttonPressStartTime = 0;
bool buttonPressed = false;
while (1) {
int buttonState = digitalRead(BOOT_BUTTON_PIN);
if (buttonState == LOW && !buttonPressed) {
buttonPressed = true;
buttonPressStartTime = millis();
Serial.println("⚠️ 检测到BOOT按钮按下长按3秒将清除配置");
currentConfigClearStatus = CONFIG_PREPARING;
}
else if (buttonState == HIGH && buttonPressed) {
if (!clearConfigRequested) {
currentConfigClearStatus = CONFIG_NORMAL;
Serial.println("❌ 按钮释放,取消清除操作");
}
buttonPressed = false;
}
if (buttonPressed && (millis() - buttonPressStartTime >= CLEAR_CONFIG_DURATION)) {
if (!clearConfigRequested) {
clearConfigRequested = true;
forceLedOff = true;
ledcWrite(0, 0);
Serial.println("✅ 长按3秒确认将清除配置");
Serial.println("💡 网络LED已强制熄灭");
clearStoredConfig();
Serial.println("🔄 配置清除完成LED将闪烁3次表示完成...");
analogWrite(CONFIG_CLEAR_PIN, 0);
Serial.println("🔄 系统即将重启...");
vTaskDelay(1000 / portTICK_PERIOD_MS);
ESP.restart();
}
}
vTaskDelay(50 / portTICK_PERIOD_MS);
esp_task_wdt_reset();
}
}
/**
* @brief LED控制任务
* 控制NETWORK_LED_PIN引脚的LED显示保持常亮
* @param parameter 任务参数(未使用)
*/
void ledControlTask(void *parameter) {
while (1) {
if (forceLedOff) {
ledcWrite(0, 0);
} else {
ledcWrite(0, 255); // 保持LED常亮
}
vTaskDelay(100 / portTICK_PERIOD_MS);
}
}
/**
* @brief IO和Flash初始化函数
* 初始化IO引脚、Flash存储和LED控制
*/
void io_flash_init() {
// 初始化IO引脚
pinMode(BOOT_BUTTON_PIN, INPUT);
pinMode(NETWORK_LED_PIN, OUTPUT);
pinMode(CONFIG_CLEAR_PIN, OUTPUT);
pinMode(GPIO8, OUTPUT);
pinMode(GPIO9, OUTPUT);
pinMode(Radar_Start, OUTPUT); // 初始化雷达启动引脚
// 设置初始状态
digitalWrite(CONFIG_CLEAR_PIN, LOW);
digitalWrite(GPIO8, LOW);
digitalWrite(GPIO9, LOW);
digitalWrite(NETWORK_LED_PIN, LOW);
digitalWrite(CONFIG_CLEAR_PIN, LOW);
digitalWrite(Radar_Start, HIGH); // 拉高雷达启动引脚,启动雷达
// 初始化LED控制
ledcSetup(0, 5000, 8);
ledcSetup(1, 5000, 8);
ledcAttachPin(NETWORK_LED_PIN, 0);
ledcAttachPin(CONFIG_CLEAR_PIN, 1);
// 初始化Flash存储
preferences.begin("radar_data", false);
// 加载设备配置
Serial.println("💾 加载设备配置...");
loadDeviceId();
// 初始化任务
xTaskCreate(
configClearLedTask,
"Config Clear LED Task",
2048,
NULL,
1,
NULL
);
xTaskCreate(
bootButtonMonitorTask,
"Boot Button Monitor Task",
2048,
NULL,
1,
NULL
);
xTaskCreate(
ledControlTask,
"LED Control Task",
2048,
NULL,
1,
NULL
);
}

42
src/io_flash.h Normal file
View File

@@ -0,0 +1,42 @@
#ifndef IO_FLASH_H
#define IO_FLASH_H
#include <Arduino.h>
#include <Preferences.h>
// 引脚定义
#define BOOT_BUTTON_PIN 0 // Boot按钮引脚
#define NETWORK_LED_PIN 35 // 网络状态LED指示灯开发板48引脚雷达板35引脚
#define CONFIG_CLEAR_PIN 4 // 配置清除指示灯
#define Radar_Start 36 // 定义雷达启动引脚拉高启动
#define GPIO8 8 // 自定义GPIO8
#define GPIO9 9 // 自定义GPIO9
// 配置清除指示灯状态枚举
enum ConfigClearStatus {
CONFIG_NORMAL, // 正常运行 - LOW
CONFIG_PREPARING, // 准备清除 - HIGH
CONFIG_CLEARING, // 清除过程中 - 呼吸灯
CONFIG_COMPLETED // 清除完成 - 快速闪烁3次
};
// 全局变量声明
extern Preferences preferences;
extern uint16_t currentDeviceId; // 当前设备ID
extern bool clearConfigRequested; // 清除配置请求标志
extern bool forceLedOff; // 强制关闭LED标志
extern ConfigClearStatus currentConfigClearStatus; // 当前配置清除状态
// 函数声明
void checkBootButton();
void loadDeviceId();
void saveDeviceId();
void clearStoredConfig();
void io_flash_init();
// 任务声明
void configClearLedTask(void *parameter);
void bootButtonMonitorTask(void *parameter);
void ledControlTask(void *parameter);
#endif // IO_FLASH_H

File diff suppressed because it is too large Load Diff

200
src/radar_vitals.cpp Normal file
View File

@@ -0,0 +1,200 @@
#include "radar_vitals.h"
#include <math.h>
#include <driver/adc.h>
/******** 参数 ********/
#define ADC_MAX 4095.0f
#define VREF 3.3f
#define TWO_PI 6.2831853f
#define RADAR_LAMBDA 0.0125f // 24GHz 波长(米)
/******** 内部状态 ********/
static float fs = 20.0f;
// 状态变量
static float phase[DATA_LEN];
static float unwrap_phase[DATA_LEN];
static human_state_t human_state = NO_PERSON;
static float resp_bpm = 0;
static float heart_bpm = 0;
// 平滑处理相关变量
static float resp_bpm_history[10] = {0};
static float heart_bpm_history[10] = {0};
static int history_index = 0;
/******** 新的雷达处理函数 ********/
void radar_process(float *I, float *Q, int len)
{
/* 1. 去直流 (DC Removal)
* I'(n) = I(n) - mean(I)
* Q'(n) = Q(n) - mean(Q)
*/
float meanI = 0, meanQ = 0;
for(int i=0;i<len;i++){ meanI += I[i]; meanQ += Q[i]; }
meanI /= len; meanQ /= len;
for(int i=0;i<len;i++){ I[i] -= meanI; Q[i] -= meanQ; }
/* 2. IQ → 相位
* φ(n) = atan2(Q, I)
*/
for(int i=0;i<len;i++){
phase[i] = atan2f(Q[i], I[i]);
}
/* 3. 相位展开 (Unwrap)
* Δφ > π → Δφ -= 2π
*/
unwrap_phase[0] = phase[0];
for(int i=1;i<len;i++){
float d = phase[i] - phase[i-1];
if(d > M_PI) d -= 2*M_PI;
if(d < -M_PI) d += 2*M_PI;
unwrap_phase[i] = unwrap_phase[i-1] + d;
}
/* 4. 空人判断:相位方差
* Var(φ) = E[(φ-μ)^2]
*/
float mean = 0, var = 0;
for(int i=0;i<len;i++) mean += unwrap_phase[i];
mean /= len;
for(int i=0;i<len;i++){
float d = unwrap_phase[i] - mean;
var += d * d;
}
var /= len;
if(var < 1e-4){
human_state = NO_PERSON;
return;
}
/* 5. 活动检测:相位差分能量
* E = mean[(φ(n)-φ(n-1))^2]
*/
float energy = 0;
for(int i=1;i<len;i++){
float d = unwrap_phase[i] - unwrap_phase[i-1];
energy += d * d;
}
energy /= len;
if(energy > 0.05){
human_state = MOTION;
} else {
human_state = STATIC_HUMAN;
}
/* 6. 呼吸 / 心率频率估计
* 方法:频段能量最大值搜索(简化 DFT
* Fs_eff = SAMPLE_RATE / decimation
*/
const float Fs_eff = 200.0f; // 4kHz / 20
// ---- 呼吸0.1 ~ 0.5 Hz ----
float max_resp_mag = 0; float resp_freq = 0;
for(float f=0.1f; f<=0.5f; f+=0.02f){
float re=0, im=0;
for(int n=0;n<len;n++){
float ang = 2*M_PI*f*n/Fs_eff;
re += unwrap_phase[n]*cosf(ang);
im -= unwrap_phase[n]*sinf(ang);
}
float mag = re*re + im*im;
if(mag > max_resp_mag){ max_resp_mag = mag; resp_freq = f; }
}
float new_resp_bpm = resp_freq * 60.0f;
// ---- 心率0.8 ~ 2.5 Hz ----
float max_hr_mag = 0; float hr_freq = 0;
for(float f=0.8f; f<=2.5f; f+=0.05f){
float re=0, im=0;
for(int n=0;n<len;n++){
float ang = 2*M_PI*f*n/Fs_eff;
re += unwrap_phase[n]*cosf(ang);
im -= unwrap_phase[n]*sinf(ang);
}
float mag = re*re + im*im;
if(mag > max_hr_mag){ max_hr_mag = mag; hr_freq = f; }
}
float new_heart_bpm = hr_freq * 60.0f;
// 异常值检测和过滤
if(new_heart_bpm < 40 || new_heart_bpm > 180){
new_heart_bpm = heart_bpm; // 使用上次值
}
if(new_resp_bpm < 4 || new_resp_bpm > 40){
new_resp_bpm = resp_bpm; // 使用上次值
}
// 移动平均滤波
heart_bpm_history[history_index] = new_heart_bpm;
resp_bpm_history[history_index] = new_resp_bpm;
history_index = (history_index + 1) % 10;
// 计算平均值
float sum_hr = 0, sum_resp = 0;
for(int i=0; i<10; i++){
sum_hr += heart_bpm_history[i];
sum_resp += resp_bpm_history[i];
}
heart_bpm = sum_hr / 10.0f;
resp_bpm = sum_resp / 10.0f;
// 四舍五入到最接近的整数
heart_bpm = roundf(heart_bpm);
resp_bpm = roundf(resp_bpm);
}
/******** 获取人体状态 ********/
human_state_t radar_get_state(){ return human_state; }
/******** 获取呼吸率 ********/
float radar_get_resp_bpm(){ return resp_bpm; }
/******** 获取心率 ********/
float radar_get_heart_bpm(){ return heart_bpm; }
/******** 初始化 ********/
void radar_vitals_init(float fs_hz)
{
fs = fs_hz;
human_state = NO_PERSON;
resp_bpm = 0;
heart_bpm = 0;
// 初始化历史数据数组
for(int i=0; i<10; i++){
resp_bpm_history[i] = 0;
heart_bpm_history[i] = 0;
}
history_index = 0;
}
/**
* @brief 雷达初始化函数
* 初始化雷达相关的ADC和引脚
*/
void radar_init() {
// 配置雷达I/Q引脚为ADC输入
pinMode(PIN_I, INPUT);
pinMode(PIN_Q, INPUT);
// 初始化ADC
adc1_config_width(ADC_WIDTH_BIT_12);
adc1_config_channel_atten(ADC1_CHANNEL_0, ADC_ATTEN_DB_12); // PIN_I (GPIO1) 对应 ADC1_CHANNEL_0
adc1_config_channel_atten(ADC1_CHANNEL_1, ADC_ATTEN_DB_12); // PIN_Q (GPIO2) 对应 ADC1_CHANNEL_1
Serial.println("🏗️ 初始化雷达管理器...");
radar_vitals_init(SAMPLE_RATE);
}

51
src/radar_vitals.h Normal file
View File

@@ -0,0 +1,51 @@
#ifndef RADAR_VITALS_H
#define RADAR_VITALS_H
#include <stdint.h>
#include <Arduino.h>
#define PIN_I 9
#define PIN_Q 10
#define ADC_WIDTH ADC_WIDTH_BIT_12 // 配置ADC宽度为12位
#define ADC_ATTEN ADC_ATTEN_DB_12 // 配置ADC衰减为12dB
#define SAMPLE_RATE 4000 // 基础采样率(Hz)
#define DATA_LEN 256 // 数据长度
// 人体状态枚举
typedef enum {
NO_PERSON = 0,
STATIC_HUMAN,
MOTION
} human_state_t;
/******** 雷达采样 ********/
typedef struct {
uint16_t i_value; // ADC 原始值
uint16_t q_value; // ADC 原始值
uint32_t timestamp; // ms
} radar_sample_t;
/******** 输出结果 ********/
typedef struct {
float heart_bpm;
float breath_bpm;
uint8_t valid; // 1: 有效
human_state_t state; // 人体状态
} radar_vitals_t;
/******** 接口 ********/
void radar_vitals_init(float fs_hz);
void radar_vitals_process(
const radar_sample_t *sample,
radar_vitals_t *out
);
void radar_process(float *I, float *Q, int len);
human_state_t radar_get_state();
float radar_get_resp_bpm();
float radar_get_heart_bpm();
void radar_init();
#endif

View File

@@ -1,330 +0,0 @@
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 = 1003
# 分配的设备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:
# 根据协议ID确定字段名
field_mapping = {
1: "heartRate",
2: "breathingRate",
13: "personDetected",
14: "humanActivity",
15: "humanDistance", # 人体距离 (cm)
16: "humanPosition", # 人体方位 (cm)
17: "sleepState" # 睡眠状态
}
if protocol_id in field_mapping:
field_name = field_mapping[protocol_id]
# 创建数据点 - 使用 "daily_data" 作为测量值名称
data_point = {
"measurement": "daily_data", # 改为 daily_data 以区分日常数据
"tags": {
"deviceId": assigned_device_id,
"dataType": "daily" # 标识这是日常数据
},
"time": datetime.utcnow().isoformat() + "Z",
"fields": {}
}
# 对特定字段进行数值处理
if protocol_id in [1, 2]: # 心率和呼吸频率需要除以10
data_point["fields"][field_name] = float(data_value) / 10.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]}")
elif protocol_id == 15: # 人体距离范围0-65535
data_point["fields"][field_name] = int(data_value)
elif protocol_id == 16: # 人体方位,可以是正负值
data_point["fields"][field_name] = int(data_value)
elif protocol_id == 17: # 睡眠状态,使用预定义值
data_point["fields"][field_name] = int(data_value)
else:
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, 15, 16, 17]:
# 对于人检/活动/距离/位置/睡眠状态数据,使用整数格式
line_protocol = f"daily_data,deviceId={assigned_device_id},dataType=daily {field_name}={int(data_point['fields'][field_name])}i"
else:
# 对于其他数据,使用浮点数格式
line_protocol = f"daily_data,deviceId={assigned_device_id},dataType=daily {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 save_sleep_data_to_influxdb(sleep_data):
"""保存睡眠数据到InfluxDB"""
try:
# 构造睡眠数据的行协议格式 - 使用 "sleep_data" 作为测量值名称
line_protocol = f"sleep_data,deviceId={assigned_device_id},dataType=sleep "
# 按顺序添加各个字段
fields = []
fields.append(f"sleepQualityScore={int(sleep_data['sleepQualityScore'])}i") # 1B 睡眠质量评分 (0~100)
fields.append(f"totalSleepDuration={int(sleep_data['totalSleepDuration'])}i") # 2B 睡眠总时长 (0~65535 分钟)
fields.append(f"awakeDurationRatio={int(sleep_data['awakeDurationRatio'])}i") # 1B 清醒时长占比 (0~100)
fields.append(f"lightSleepRatio={int(sleep_data['lightSleepRatio'])}i") # 1B 浅睡时长占比 (0~100)
fields.append(f"deepSleepRatio={int(sleep_data['deepSleepRatio'])}i") # 1B 深睡时长占比 (0~100)
fields.append(f"outOfBedDuration={int(sleep_data['outOfBedDuration'])}i") # 1B 离床时长 (0~255)
fields.append(f"outOfBedCount={int(sleep_data['outOfBedCount'])}i") # 1B 离床次数 (0~255)
fields.append(f"turnCount={int(sleep_data['turnCount'])}i") # 1B 翻身次数 (0~255)
fields.append(f"avgBreathingRate={int(sleep_data['avgBreathingRate'])}i") # 1B 平均呼吸 (0~25)
fields.append(f"avgHeartRate={int(sleep_data['avgHeartRate'])}i") # 1B 平均心跳 (0~100)
fields.append(f"apneaCount={int(sleep_data['apneaCount'])}i") # 1B 呼吸暂停次数 (0~10)
line_protocol += ",".join(fields)
# 发送数据到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"
}
response = requests.post(url, headers=headers, data=line_protocol)
if response.status_code == 204:
print(f"✅ 睡眠数据已保存到InfluxDB设备{assigned_device_id}")
else:
print(f"❌ 保存睡眠数据到InfluxDB失败: {response.status_code} - {response.text}")
except Exception as e:
print(f"❌ 保存睡眠数据到InfluxDB时出错: {e}")
import traceback
traceback.print_exc()
def generate_random_sleep_data():
"""生成随机睡眠数据用于测试"""
sleep_data = {
"sleepQualityScore": random.randint(0, 100), # 1B 睡眠质量评分 (0~100)
"totalSleepDuration": random.randint(0, 65535), # 2B 睡眠总时长 (0~65535 分钟)
"awakeDurationRatio": random.randint(0, 100), # 1B 清醒时长占比 (0~100)
"lightSleepRatio": random.randint(0, 100), # 1B 浅睡时长占比 (0~100)
"deepSleepRatio": random.randint(0, 100), # 1B 深睡时长占比 (0~100)
"outOfBedDuration": random.randint(0, 255), # 1B 离床时长 (0~255)
"outOfBedCount": random.randint(0, 255), # 1B 离床次数 (0~255)
"turnCount": random.randint(0, 255), # 1B 翻身次数 (0~255)
"avgBreathingRate": random.randint(0, 25), # 1B 平均呼吸 (0~25)
"avgHeartRate": random.randint(0, 100), # 1B 平均心跳 (0~100)
"apneaCount": random.randint(0, 10), # 1B 呼吸暂停次数 (0~10)
}
# 确保比例字段总和为100
total_ratio = sleep_data["awakeDurationRatio"] + sleep_data["lightSleepRatio"] + sleep_data["deepSleepRatio"]
if total_ratio != 100:
# 调整浅睡时长占比以确保总和为100
adjustment = 100 - total_ratio
sleep_data["lightSleepRatio"] = max(0, min(100, sleep_data["lightSleepRatio"] + adjustment))
# 再次检查总和
total_ratio = sleep_data["awakeDurationRatio"] + sleep_data["lightSleepRatio"] + sleep_data["deepSleepRatio"]
if total_ratio != 100:
# 如果仍然不是100则最后一次调整浅睡占比
sleep_data["lightSleepRatio"] += (100 - total_ratio)
sleep_data["lightSleepRatio"] = max(0, min(100, sleep_data["lightSleepRatio"]))
return sleep_data
def main():
global assigned_device_id
try:
# 第一步注册设备获取设备ID
if not register_device():
print("❌ 设备注册失败,程序退出")
return
print(f"🎯 开始使用设备ID {assigned_device_id} 发送数据...")
# 启动时立即发送一次睡眠数据
print("⏰ 启动时生成并发送睡眠数据...")
initial_sleep_data = generate_random_sleep_data()
save_sleep_data_to_influxdb(initial_sleep_data)
# 记录上次发送睡眠数据的时间
last_sleep_data_time = time.time()
# 第二步:开始发送数据
while True:
# 初始化数据值
data_value = 0
# 发送其他数据
protocol_id = random.choice([1, 2, 13, 14, 15, 16, 17]) # 1=心跳, 2=呼吸, 13=检测到人, 14=人体活动, 15=人体距离, 16=人体方位, 17=睡眠状态
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([1])
elif protocol_id == 14: # 人体活动1活动0静止
data_value = random.choice([0])
elif protocol_id == 15: # 人体距离 (cm)范围0-65535
data_value = random.randint(0, 65535)
elif protocol_id == 16: # 人体方位 (cm),可以有正负值
data_value = random.randint(-32768, 32767)
elif protocol_id == 17: # 睡眠状态 (0x00=深睡, 0x01=浅睡, 0x02=清醒, 0x03=无)
data_value = random.choice([0, 1, 2, 3])
# 直接发送数据到InfluxDB
save_data_to_influxdb(protocol_id, data_value)
# 每隔一段时间例如30分钟发送一次睡眠数据
current_time = time.time()
if current_time - last_sleep_data_time >= 1800: # 30分钟 = 1800秒
print("⏰ 生成并发送睡眠数据...")
sleep_data = generate_random_sleep_data()
save_sleep_data_to_influxdb(sleep_data)
last_sleep_data_time = current_time
# 设置发送间隔
time.sleep(0.4) # 每0.4秒发送一次数据
except KeyboardInterrupt:
print(f"\n🛑 设备 {assigned_device_id} 发送端已停止")
except Exception as e:
print(f"❌ 发生错误: {e}")
if __name__ == "__main__":
main()

View File

@@ -1,72 +0,0 @@
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()

View File

@@ -1,82 +0,0 @@
// 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("=== 测试完成 ===");

View File

@@ -1,394 +0,0 @@
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