Compare commits

..

16 Commits

Author SHA1 Message Date
zhangyisong
0b62356bd7 Add temperature sample rate display and log clear button 2026-08-18 22:56:37 +08:00
zhangyisong
3b0c0c321f Configure IR enable pin at initialization 2026-08-12 15:08:54 +08:00
zhangyisong
68bc52572a Add software architecture documentation for photomagnetic head 2026-08-06 22:11:23 +08:00
zhangyisong
214af410e4 Add UART sensor support with MS overlay and diagnostics
Group UART sensors into channel slots and add an MS board overlay
with per-port speed and sensor mode configuration. Also add optional
per-second channel diagnostic output controlled by APP_DEBUG_PRINT.
2026-08-06 20:10:08 +08:00
zhangyisong
0e5d15c814 Refactor app to use static classes and event-driven callbacks
Restructure Led as a template on the DT spec, add a generic
signal/slot mechanism for host status, and cache Infrared sensor
samples via trigger callbacks. Add new init modules for LED, temp,
and watchdog, and build them into the main app.
2026-08-03 22:12:31 +08:00
zhangyisong
a9e8885ec8 Refactor thermistor components and update board dependencies
- Remove heating and NTC functionality from communication module
- Convert Infrared from template to runtime array-based implementation
- Refactor Led to support multiple LEDs with compile-time name
  validation
- Simplify main to remove direct hardware initialization
- Update west manifest for dr2501a board and add ch9438 module
2026-08-02 21:59:50 +08:00
zhangyisong
e0908bc8f0 feat: Parameterize heating start with configurable duty cycle 2026-07-16 21:52:15 +08:00
zhangyisong
f425f0acab Add StartFullEnergy method to HeatingPad API 2026-07-15 11:18:21 +08:00
zhangyisong
1e6d6b9b30 Add support for heating state and NTC commands
Extend the UART protocol to support writing and reading the heating
state, and reading individual pole NTC sensors and the heating pad NTC.
Cache NTC readings for query on demand.  Move PID updates to a workqueue
to avoid blocking the timer IRQ context.
2026-07-13 21:57:54 +08:00
zhangyisong
2c42b67eca Add PWM heating pad binding 2026-07-09 12:42:14 +08:00
zhangyisong
2692282ba3 Remove unused pwm-servo binding, disable heading_pad module, and reduce
sensor polling rate
2026-07-09 12:42:04 +08:00
zhangyisong
aed11ecee8 Add debug logging and fix minor issues
Add printk debug statements to NTC temperature sensor reading and
IR temperature sensor reading. Change NTC loop to use index-based
iteration. Fix missing newline in boot message. Remove unused
watchdog and commented-out code.
2026-07-09 12:29:50 +08:00
zhangyisong
eaa5d0c28b Add NTC sample configuration and PID temperature control
- Add CONFIG_SAMPLE_NTC to CMakeLists.txt and Kconfig
- Implement PI temperature controller with PWM output in heating.hpp
- Convert NtcGroup from template to non-template class
- Add LED helper class with flash support
- Implement COM protocol handlers for device ID and running state
- Add SPI, sensor, and PWM configuration to prj.conf
- Remove indicator overlay and update main.cpp with temperature
  monitoring
- Add test script for serial protocol communication
2026-07-09 11:06:11 +08:00
zhangyisong
1afea0b932 feat: Add required Zephyr headers and return value from main 2026-07-07 19:48:39 +08:00
zhangyisong
5c122122f0 Refactor main.cpp by extracting logic into modular components
Remove CONFIG_STDOUT_CONSOLE from configuration
2026-07-07 19:47:48 +08:00
zhangyisong
e74fa5c804 feat: Add Kconfig and conditional build for hello world sample 2026-07-07 19:47:30 +08:00
25 changed files with 2304 additions and 118 deletions

View File

@ -3,7 +3,24 @@ cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(app_photomagnetic) project(app_photomagnetic)
target_include_directories(app PRIVATE include) target_include_directories(app PRIVATE include)
target_sources(app PRIVATE src/main.cpp)
if(CONFIG_SAMPLE_HELLOWORLD)
message("CONFIG_SAMPLE_HELLOWORLD is enabled")
target_sources(app PRIVATE src/sample_hello_world.cpp)
elseif(CONFIG_SAMPLE_NTC)
message("CONFIG_SAMPLE_NTC is enabled")
target_sources(app PRIVATE src/sample_ntc.cpp)
else()
message("build main app")
target_sources(app PRIVATE
src/main.cpp
src/led.cpp
src/temp.cpp
src/watdog.cpp
src/com.cpp
)
endif()

8
Kconfig Normal file
View File

@ -0,0 +1,8 @@
menu "app thermotherapy"
source "Kconfig.zephyr"
endmenu
config SAMPLE_HELLOWORLD
bool "Hello World sample"
config SAMPLE_NTC
bool "NTC sample"

View File

@ -1,18 +0,0 @@
&indicator {
led-strip = <&led_strip>;
compatible = "led-strip-indicator";
en-gpios = <&gpiob 13 GPIO_ACTIVE_HIGH>;
standby {
rgb = <0 0 255>; // Blue
};
running {
rgb = <0 255 0>; // Green
};
pause {
rgb = <254 254 0>;
// interval-ms = <500>;
};
error {
rgb = <255 0 0>; //Red
};
};

71
boards/use_ms.overlay Normal file
View File

@ -0,0 +1,71 @@
&ch9438_uart0 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor0 {
mode = "wrist";
};
&ch9438_uart1 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor1 {
mode = "wrist";
};
&ch9438_uart2 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor2 {
mode = "wrist";
};
&ch9438_uart3 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor3 {
mode = "wrist";
};
&ch9438_uart4 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor4 {
mode = "wrist";
};
&ch9438_uart5 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor5 {
mode = "wrist";
};
&ch9438_uart6 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor6 {
mode = "wrist";
};
&ch9438_uart7 {
status = "okay";
current-speed = <9600>;
};
&gd_sensor7 {
mode = "wrist";
};

View File

@ -0,0 +1,317 @@
# 光磁头软件架构文档
> 本文档为光磁头(小板 / 温度传感端)固件的**设计规范**,源码实现应遵循本文档的模块划分、数据流与设计约束。
> 配套协议:[`通讯协议.md`](./通讯协议.md)
> 硬件平台STM32G0B0板级 `dr2501a_g0b0ce`
---
## 1. 系统定位
光磁头是光磁热疗系统的**温度传感端**,通过一根 UART 线与主板(控制端)通信,向主板提供 9 路皮肤温度反馈,供主板 PID 温控闭环使用。
| 职责 | 说明 |
|---|---|
| 温度采集 | 9 路 godtek 温度传感器8 路经 CH9438 扩展 + 1 路片内直连) |
| 温度上报 | 每 20ms 主动上报 9 路**最大值**(温度闭环反馈) |
| 命令响应 | 响应主板查询ID / 温度)与运行状态设置 |
| 状态指示 | 按主板运行状态点亮红外灯与灯带 |
```mermaid
flowchart LR
subgraph 光磁头[光磁头 传感端]
S[9 路 godtek 传感器] -->|UART 触发| C[采集聚合 infrared]
C -->|20ms 周期| T[温度上报 temp]
T --> P[协议 com<br/>usart1 0x7EE7]
P -->|运行状态| L[LED 指示 led]
end
P <-->|UART 115200| M[主板 控制端]
```
---
## 2. 硬件资源
| 外设 | 用途 | 关键配置 |
|---|---|---|
| `spi2` + CH9438INT# PB13 | SPI→8×UART 扩展,传感器 ch0~7 | SPI 500kHz8 口均 9600 8N1overlay `use_ms.overlay` |
| `usart2`PA2/PA3 | 片内第 9 路传感器ch8 | 9600 8N1 |
| `usart1`PA9/PA10 | 与主板通信协议口 | 115200帧头 0x7EE7 + CRC16 |
| `usart3`PA5/PB0 | 调试 console | 115200 |
| `spi1` + WS281240 灯) | 灯带指示standby/running/pause/error | SPI 4MHz |
| `led_inf`PB1 | 红外加热指示 LED | 运行中点亮 |
| `led_mcu_state`PA15 | 心跳灯 | 1s 周期闪烁 |
| IWDG | 系统看门狗 | 20ms 喂狗 |
```mermaid
graph TD
subgraph MCU[STM32G0B0]
U1[usart1 协议口<br/>115200] -->|0x7EE7 帧| MB[主板]
U2[usart2 第9路<br/>9600] --> G8[godtek ch8]
U3[usart3 console]
SP2[spi2 500kHz] --> CH[CH9438<br/>INT# PB13]
CH --> G0[godtek ch0]
CH --> G1[godtek ch1]
CH --> G7[godtek ch7]
SP1[spi1 4MHz] --> WS[WS2812 灯带 ×40]
LED1[led_inf PB1]
LED2[led_mcu_state PA15]
WDT[IWDG]
end
```
> 传感器工作模式8 路外置为 `wrist`0xAC 测量命令),片内第 9 路默认 `surface`0xAA
---
## 3. 软件架构
```mermaid
graph TD
subgraph 应用层
MAIN[main 主线程]
TEMP[temp 采集调度<br/>20ms 上报]
COM[com 协议处理<br/>回调表/上电发ID]
LED[led 状态指示]
WDG[watdog 喂狗]
end
subgraph 聚合层
IR[infrared 9路聚合<br/>触发注册/缓存/最大值]
end
subgraph 驱动层
UC[uart_com 帧协议<br/>0x7EE7+CRC16]
GD[godtek_temp 传感器驱动<br/>组帧/校验/触发]
CH8[ch9438 SPI→8×UART]
IND[led_strip_indicator 灯带]
end
TEMP --> IR
TEMP --> COM
COM --> UC
IR --> GD
GD --> CH8
GD --> U2[usart2]
LED --> COM
LED --> IND
MAIN -.后台驻留.-> TEMP
```
### 模块职责
| 模块 | 职责 | 关键约束 |
|---|---|---|
| `infrared`(聚合层) | 9 路传感器统一抽象:设备列表、触发注册、通道缓存、取最大值 | 缓存须 ISR 安全;通道编号固定 ch0~7=CH9438、ch8=片内 |
| `temp`(应用层) | 采集触发注册 + 20ms 周期上报最大值 | 触发注册必须在传感器驱动初始化之后 |
| `com`(应用层) | 协议帧收发、命令回调、运行状态信号广播 | 回调在系统 workqueue 线程执行;上电主动发 ID |
| `led`(应用层) | 运行状态 → 红外灯 + 灯带;心跳灯 | 状态变化由 com 信号驱动,不轮询 |
| `watdog`(应用层) | IWDG 喂狗 | 20ms 周期;配置失败不得阻塞启动 |
| `main`(应用层) | 入口 + 后台驻留 | 各模块由 SYS_INIT 初始化 |
---
## 4. 数据流
### 4.1 温度采集链(触发驱动,无轮询)
9 路传感器**全部由 UART 数据触发驱动**,不占用 CPU 轮询:
```mermaid
flowchart LR
S[godtek 传感器自发上报] --> CH[CH9438 INT# 中断<br/>或 usart2 RX 中断]
CH --> WQ[系统 workqueue<br/>或 ISR]
WQ --> GD[godtek 驱动<br/>组帧 + 校验]
GD -->|有效帧| TRIG[触发回调<br/>更新第 I 路缓存]
TRIG --> CACHE[volatile 通道缓存]
```
- **帧校验**`+`/`-` 符号 + 6 位十进制数字 + 温度范围(-40.0 ~ +125.0°C坏帧直接丢弃不上报
- **上下文**CH9438 端口回调在系统 workqueueSPI 安全);片内 usart2 回调在 ISR —— 回调内不得做任何阻塞操作
### 4.2 温度上报链20ms 周期)
```mermaid
flowchart LR
T[temp 周期任务<br/>20ms] --> M[取 9 路缓存最大值]
M --> C[组帧上报]
C -->|0x7EE7 帧 00 02 整数 小数 CRC| U[usart1 → 主板]
```
温度值格式×10 整数(如 `345` = 34.5°C上报数据 2 字节 `[整数][小数]`
### 4.3 命令响应链RX ISR → workqueue
```mermaid
flowchart LR
RX[usart1 RX ISR<br/>逐字节帧状态机] -->|完整帧| P[拷贝到 pending 缓冲<br/>提交 work]
P --> WQ2[系统 workqueue<br/>执行命令回调]
WQ2 --> H[ID / 温度查询 / 运行状态]
H --> LED2[运行状态 → LED 指示]
H --> R[ID / 温度查询 → 回帧]
```
> 命令回调必须在 workqueue 线程上下文执行ISR 只做轻量拷贝),可安全使用 `printk` / `uart_poll_out`,避免 ISR 内阻塞导致温度上报停止。
---
## 5. 通信协议
帧结构(详见 `通讯协议.md`
```
| 7E | E7 | CMD | LEN | DATA[0..LEN-1] | CRC_L | CRC_H |
|----- 帧头 -----| | |-- CRC16 Modbus (lsb-msb) --|
```
| 指令 | 方向 | 内容 | 响应行为 |
|---|---|---|---|
| `0x00` 温度 | 小板→主板(主动) | 每 20ms 上报最大值2 字节 `[整数][小数]` | — |
| `0x00` 温度 | 主板请求→小板回复 | 请求时回复当前最大值 | 查询回复 |
| `0x01` ID | 小板→主板(主动) | **上电主动发一次**3 字节hwinfo 前 3 字节) | — |
| `0x01` ID | 主板请求→小板回复 | 请求时回复 | 查询回复 |
| `0x02` 运行状态 | 主板→小板 | 1 字节0x00 待机 / 0x01 运行 / 0x02 暂停 / 0x03 故障 | 更新状态并驱动 LED |
```mermaid
sequenceDiagram
participant S as 小板 光磁头
participant M as 主板 控制端
Note over S: 系统上电
S->>M: 【主动】ID 帧 (0x01)
loop 每 20ms
S->>M: 【主动】温度最大值帧 (0x00)
end
Note over M: 运行状态变化
M->>S: 运行状态帧 (0x02) → LED 指示
opt 主板查询
M->>S: 请求 ID (0x01)
S-->>M: 回复 ID
M->>S: 请求温度 (0x00)
S-->>M: 回复温度最大值
end
```
---
## 6. 初始化顺序
各模块通过 SYS_INIT 按以下顺序初始化(同优先级内不得存在跨模块依赖):
| 阶段 | 优先级 | 模块 | 说明 |
|---|---|---|---|
| POST_KERNEL | 驱动默认 | ch9438 | SPI 初始化、**波特率配置9600含 FCR 时序处理)**、中断使能 |
| POST_KERNEL | 传感器默认 | godtek | UART 回调注册、500ms 周期测量命令 |
| APPLICATION | 40 | led | GPIO / 灯带初始化 + 注册状态槽 |
| APPLICATION | 45 | temp | **触发注册**(必须在 godtek 之后)+ 启动 20ms 上报 |
| APPLICATION | 50 | com | 协议回调表注册 + **上电主动发 ID** |
| APPLICATION | 60 | watdog | IWDG 安装 + 启动喂狗 |
| APPLICATION | 90 | ch9438 flush | POR 残留 FIFO 清空(边沿中断补偿) |
| — | — | main | 主循环(后台线程) |
```mermaid
sequenceDiagram
participant B as 启动
participant D as 驱动层
participant A as 应用层
B->>D: ch9438 初始化(波特率 9600 时序)
B->>D: godtek 初始化(回调 + 500ms 测量命令)
B->>A: led 初始化APPLICATION 40
B->>A: temp 初始化APPLICATION 45触发注册
B->>A: com 初始化APPLICATION 50上电发 ID
B->>A: watdog 初始化APPLICATION 60
B->>A: ch9438 flushAPPLICATION 90
A->>D: 传感器数据触发 → 缓存 → 20ms 上报
```
---
## 7. 关键设计决策
### 7.1 波特率 POR 时序(重点坑)
**现象**烧录后波特率正常9600断电重启后回退为 115200部分端口只收到 `0x00`
**根因**CH9438 的 FCR 复位位RFIFORST/TFIFORST会**异步重置波特率分频器**。POR 冷启动时 FCR 复位持续较久,把紧随其后的 9600 配置LCR/DLL/DLM清回芯片默认值 115200烧录时芯片未断电、复位瞬间完成因此正常。
**修复**(初始化顺序):
```
写 LCR(DLAB=1) → 写 DLL/DLM(9600) → 验证
→ 引脚配置 → FCR=0x07复位+使能,触发发生器重锁存)→ 1ms 延时
→ 重写 LCR(DLAB=1) → 重写 DLL/DLM(9600) → 回读验证(连 LCR 一起检查)→ LCR(DLAB=0)
```
**回读验证假象**DLAB 未锁存时,读 "DLL" 实际读到 RBRuart7 短接回环的 0x20、"DLM" 读到 IER0x4E——自洽的谎言。**必须连 LCR 一起回读比对**,确认 DLAB 真正置位;再加 8 次 ×20ms 重试兜底POR 后 UART 模块可能晚就绪,写入被静默丢弃)。
### 7.2 触发驱动采集
9 路全部由 UART 数据触发更新缓存,无轮询,响应实时;发送侧固定 20ms 周期取缓存最大值,采集与上报解耦。
### 7.3 缓存线程安全
volatile 字段 + 编译期通道索引触发回调ISR / workqueue 双上下文)与查询无锁,避免死锁与中断延迟。
### 7.4 回调上下文
协议命令回调全部在系统 workqueue 线程执行ISR 只做数据拷贝 —— 避免 ISR 内 `uart_poll_out` / `printk` 阻塞导致温度上报停止。
### 7.5 通道编号稳定
`DT_FOREACH` 按设备树节点顺序展开,若 usart2 排在 spi2 前会导致通道错位ch0 变成片内传感器)。通过按父节点 compatible 分两段构建设备列表,保证 ch0~7 = CH9438、ch8 = 片内。
### 7.6 坏帧丢弃
帧校验含符号、6 位数字与温度范围三重检查,字节丢失/错位产生的坏帧直接丢弃,不污染缓存。
---
## 8. 调试与诊断
### 8.1 打印规范
所有诊断打印由 `APP_DEBUG_PRINT` 宏控制(各模块默认 0默认只保留启动确认与错误打印
| 宏 | 位置 | 打印内容 |
|---|---|---|
| `APP_DEBUG_PRINT=1` | ch9438 | 波特率回读、work 计数、FIFO 扫描、运行期诊断 |
| `APP_DEBUG_PRINT=1` | godtek | 原始字节 dump、命令追踪 |
| `APP_DEBUG_PRINT=1` | temp | 每秒 9 路通道状态(含有效掩码) |
| `APP_DEBUG_PRINT=1` | com | 协议错误统计 |
启动确认打印(应保留的最小集):
```
[ch9438] v1.0 port=0 ok ← CH9438 8 端口初始化成功
[godtek] mode = 0xac: baudrate = 9600 ← 传感器模式与波特率确认
[app_led] Init: ... ← LED 初始化
[app_temp]temp init ← 温度模块初始化
[infra] ch0 ready=1 ... ch8 ready=1 ← 9 路设备就绪
[com] init: rx table=3, id sent ← 协议就绪 + 上电 ID
[watchdog] init ← 看门狗就绪
```
### 8.2 常见问题排查
| 现象 | 排查方向 |
|---|---|
| 重启后波特率异常0x00 / 乱码) | 确认 ch9438 初始化含 FCR 复位后重写波特率 + 回读验证§7.1 |
| 温度恒为 0.0 / 无数据 | 检查传感器 UART 波特率9600、测量命令周期500ms、帧校验是否丢帧 |
| 灯带无指示 | 检查 `led_strip_indicator` 节点、WS2812 SPI 时序 |
| 主板收不到上报 | 检查 usart1 115200、帧头 0x7EE7、CRC16 lsb-msb 与主板一致 |
### 8.3 回环自测
短接 CH9438 UART7 的 TX/RXPIN25 ↔ PIN26验证中断接收链路ISR → workqueue → 回调 → FIFO 读取),用于驱动回归测试。
---
## 9. 构建与烧录
```bash
# 构建board 定义在 boards/dr2501aoverlay 配置传感器波特率/模式)
west build -p auto -b dr2501a_g0b0ce/stm32g0b0xx app/app_photomagnetic \
-d build -DOVERLAY_CONFIG=boards/use_ms.overlay
# 烧录
west flash -d build
```
> 板级传感器配置(波特率、模式)统一在 `app/app_photomagnetic/boards/use_ms.overlay` 中,不修改板级 dts。

View File

@ -0,0 +1,14 @@
description: |
Heating pad using PWM
compatible: "heating-pad-pwm"
include: base.yaml
properties:
pwms:
type: phandle-array
description: PWM device to use for heating pad
required: true
temp-sensor:
type: phandle
description: Temperature sensor to use for heating pad
required: true

66
include/com.hpp Normal file
View File

@ -0,0 +1,66 @@
#ifndef __THER_COM_HPP__
#define __THER_COM_HPP__
#include "led.hpp"
#include <etl/delegate.h>
#include <etl/signal.h>
#include <led_strip_indicator/led_strip_indicator.hpp>
#include <uart_com/simple_protocal.hpp>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/drivers/sensor.h>
#include <zpp/driver.hpp>
#include <zpp/error.hpp>
namespace ther {
class Com {
public:
enum HostStatus : uint8_t { STANDBY, RUNNING, PAUSE, ERROR };
using RunningStateSignal = etl::signal<void(HostStatus), 10>;
static auto Init() -> zpp::error {
s_proto->SetRxCallbackTable(kRxCallbackTable);
auto size = hwinfo_get_device_id(buff, sizeof(buff));
s_id_buff = etl::span<uint8_t>(buff, size);
return zpp::ok();
}
static auto SendTemp(sensor_value val) -> void {
const uint8_t temp_data[] = {(uint8_t)val.val1, (uint8_t)val.val2};
s_proto->Send(R_TEMP, temp_data);
}
static auto AddRunningState(RunningStateSignal::slot_type slot) -> bool {
return s_running_state_sig.connect(slot);
}
private:
enum Addr : uint8_t {
R_TEMP = 0,
R_GET_ID,
W_RUNNING_STATE,
};
inline static RunningStateSignal s_running_state_sig{};
static auto RunningState(uart_com::DataType data) -> void {
if (data.size() != 1) {
return;
}
s_running_state_sig(HostStatus(data[0]));
}
static auto GetId(uart_com::DataType data) -> void {
printk("handle get id");
s_proto->Send(R_GET_ID, uart_com::DataType(s_id_buff));
};
constexpr static std::pair<const uint8_t,
uart_com::SimpleProtocal::CallbackType>
kRxCallbackTable[] = {
{R_GET_ID, GetId},
{W_RUNNING_STATE, RunningState},
};
inline static auto s_proto = (uart_com::SimpleProtocal *)DEVICE_DT_GET(
DT_COMPAT_GET_ANY_STATUS_OKAY(uart_com_simple_protocal));
inline static uint8_t buff[20];
inline static auto s_heating_state = false;
inline static etl::span<uint8_t> s_id_buff;
};
} // namespace ther
#endif

144
include/heating.hpp Normal file
View File

@ -0,0 +1,144 @@
#ifndef __THER_HEATING_HPP__
#define __THER_HEATING_HPP__
#include <algorithm>
#include <cmath>
#include <zephyr/device.h>
#include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
namespace ther {
class HeatingPad {
public:
static auto Start(sensor_value target_temp) -> void {
s_target = sensor_value_to_double(&target_temp);
s_integral = 0.0;
s_last_error = 0.0;
s_last_time = k_uptime_get();
printk("[heat] start target=%.1f C\n", s_target);
if (!device_is_ready(s_pwm_spec.dev)) {
printk("[heat] ERR: PWM not ready\n");
return;
}
printk("[heat] PWM OK\n");
k_work_init(&s_work, WorkHandler);
k_timer_init(&s_timer, TimerTick, nullptr);
k_timer_start(&s_timer, K_NO_WAIT, K_MSEC(kUpdatePeriodMs));
s_active = true;
}
/// Start PWM with raw duty cycle (0.0 ~ 1.0).
static auto Start(float duty_cycle) -> void {
duty_cycle = std::clamp(duty_cycle, 0.0f, 1.0f);
const uint32_t pulse =
static_cast<uint32_t>(s_pwm_spec.period * duty_cycle);
pwm_set_pulse_dt(&s_pwm_spec, pulse);
s_active = true;
}
static auto Stop() -> void {
s_active = false;
k_timer_stop(&s_timer);
pwm_set_pulse_dt(&s_pwm_spec, 0);
}
static auto CurrentTemp() -> sensor_value {
if (sensor_sample_fetch(s_temp_sensor) == 0) {
sensor_value temp;
sensor_channel_get(s_temp_sensor, SENSOR_CHAN_AMBIENT_TEMP, &temp);
s_current_temp = temp;
}
return s_current_temp;
}
private:
static constexpr uint32_t kPeriodNs = PWM_KHZ(5);
static constexpr uint32_t kUpdatePeriodMs = 100;
static constexpr double kOutMin = 0.0;
static constexpr double kOutMax = 1.0;
static constexpr double kKp = 2.0;
static constexpr double kKi = 0.02;
inline static bool s_active{false};
inline static double s_target{0.0};
inline static double s_integral{0.0};
inline static double s_last_error{0.0};
inline static int64_t s_last_time{0};
inline static pwm_dt_spec s_pwm_spec = {
.dev = DEVICE_DT_GET(DT_NODELABEL(pwm1)),
.channel = 1,
.period = PWM_KHZ(5),
.flags = PWM_POLARITY_NORMAL,
};
inline static const device *s_temp_sensor =
DEVICE_DT_GET(DT_NODELABEL(heating_pad_ntc));
inline static k_timer s_timer;
inline static k_work s_work;
inline static bool s_work_pending{false};
/// Timer fires → submit work. Skips if previous work hasn't finished.
static auto TimerTick(k_timer * /*timer*/) -> void {
if (s_work_pending) {
return;
}
s_work_pending = true;
k_work_submit(&s_work);
}
/// PID update running in system workqueue context.
static auto WorkHandler(k_work * /*work*/) -> void {
double current_temp;
if (ReadTemperature(current_temp) != 0) {
s_work_pending = false;
return;
}
printk("[heat] cur=%.1f C target=%.1f C\n", current_temp, s_target);
const int64_t now = k_uptime_get();
const double dt = static_cast<double>(now - s_last_time) / 1000.0;
s_last_time = now;
const double error = s_target - current_temp;
s_integral += kKi * error * dt;
s_integral = std::clamp(s_integral, kOutMin, kOutMax);
double output = kKp * error + s_integral;
output = std::clamp(output, kOutMin, kOutMax);
const uint32_t pulse = static_cast<uint32_t>(output * kPeriodNs);
pwm_set_pulse_dt(&s_pwm_spec, pulse);
s_work_pending = false;
}
static auto ReadTemperature(double &out_temp) -> int {
if (0 != sensor_sample_fetch(s_temp_sensor)) {
printk("[heat] ERR: sensor fetch failed\n");
return -EIO;
}
sensor_value val{};
if (0 !=
sensor_channel_get(s_temp_sensor, SENSOR_CHAN_AMBIENT_TEMP, &val)) {
printk("[heat] ERR: channel_get failed\n");
return -EIO;
}
out_temp = sensor_value_to_double(&val);
s_current_temp = val;
return 0;
}
inline static sensor_value s_current_temp{};
};
} // namespace ther
#endif

162
include/infrared.hpp Normal file
View File

@ -0,0 +1,162 @@
#ifndef __THER_INFRARED_HPP__
#define __THER_INFRARED_HPP__
#include <etl/algorithm.h>
#include <etl/array.h>
#include <etl/delegate.h>
#include <etl/tuple.h>
#include <utility>
#include <zephyr/device.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/sensor.h>
#include <zpp/result.hpp>
#include <zpp/value.hpp>
#include <zpp/work_queue.hpp>
namespace ther {
class Infrared {
public:
static auto Init() -> zpp::error {
return InitImpl(std::make_index_sequence<s_dev.size()>{});
}
/* Register a callback invoked every scan period (20 ms) with the
* hottest sensor value. Runs in the system workqueue thread context -
* keep the callback lightweight (no blocking, no SPI). */
static auto
AddCallbackWhenSensorValueReady(etl::delegate<void(sensor_value)> cb)
-> zpp::error {
if (s_cb.is_valid()) {
return zpp::error_code::k_busy;
}
s_cb = cb;
return zpp::ok();
}
/* Hottest sensor among the latest cached samples (no polling: the
* trigger callback keeps the cache fresh). */
static auto GetMaxSensorValue() -> zpp::result<std::pair<int, sensor_value>> {
bool found = false;
int max_id = 0;
sensor_value max_val{};
for (size_t id = 0; id < s_dev.size(); ++id) {
if (!s_latest_valid[id]) {
continue;
}
const sensor_value val{s_latest_val1[id], s_latest_val2[id]};
if (!found || val.val1 > max_val.val1 ||
(val.val1 == max_val.val1 && val.val2 > max_val.val2)) {
found = true;
max_id = static_cast<int>(id);
max_val = val;
}
}
if (!found) {
return zpp::error_code::k_nodata;
}
return std::make_pair(max_id, max_val);
}
/* Snapshot of all cached sensor values (pure memory reads, safe in
* trigger context). Channels without a sample yet read as 0.
* auto return: the body is parsed in complete-class context where the
* trailing members (s_dev, caches) are visible. */
static auto GetAllSensorValues() {
etl::array<sensor_value, s_dev.size()> out{};
for (size_t id = 0; id < s_dev.size(); ++id) {
if (s_latest_valid[id]) {
out[id] = sensor_value{s_latest_val1[id], s_latest_val2[id]};
}
}
return out;
}
/* Bitmask of channels that have received at least one sample (diag). */
static auto GetChannelValidMask() -> uint32_t {
uint32_t mask = 0;
for (size_t id = 0; id < s_dev.size(); ++id) {
if (s_latest_valid[id]) {
mask |= (1u << id);
}
}
return mask;
}
private:
/* Per-sensor trigger handler; the channel index is a compile-time
* constant. Runs in the UART trigger context (workqueue thread for
* CH9438 ports, ISR context for on-chip UARTs) - keep it lock-free. */
template <size_t I>
static void OnSensorDataReady(const struct device *dev,
const struct sensor_trigger *trig) {
sensor_value val;
if (0 != sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &val)) {
printk("[infra] ch%u channel_get failed\n", (unsigned)I);
return;
}
s_latest_val1[I] = val.val1;
s_latest_val2[I] = val.val2;
s_latest_valid[I] = true;
if (s_cb.is_valid()) {
s_cb(MaxCachedValue()); /* notify with the hottest sensor value */
}
}
/* Latest cached max value; pure memory reads, safe in trigger context. */
static auto MaxCachedValue() -> sensor_value {
bool found = false;
sensor_value max_val{};
for (size_t id = 0; id < s_dev.size(); ++id) {
if (!s_latest_valid[id]) {
continue;
}
const sensor_value val{s_latest_val1[id], s_latest_val2[id]};
if (!found || val.val1 > max_val.val1 ||
(val.val1 == max_val.val1 && val.val2 > max_val.val2)) {
found = true;
max_val = val;
}
}
return max_val;
}
template <size_t... Is>
static auto InitImpl(std::index_sequence<Is...>) -> zpp::error {
static sensor_trigger tri{.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_AMBIENT_TEMP};
int ret = 0;
((printk("[infra] ch%u ready=%d\n", (unsigned)Is,
device_is_ready(s_dev[Is])),
ret |= sensor_trigger_set(s_dev[Is], &tri, &OnSensorDataReady<Is>)),
...);
return ret != 0 ? zpp::error{-ENODEV} : zpp::ok();
}
/* Channel order: ch0~7 = CH9438 SPI-UART ports, ch8 = on-chip usart2 inner
* sensor. DT_FOREACH follows .dtsi soc node order (usart2 before spi2), so
* split by parent compatible instead of relying on node order. */
#define INFRARED_DEV(node) \
COND_CODE_1(DT_NODE_HAS_COMPAT(DT_PARENT(node), wch_ch9438_uart), \
(DEVICE_DT_GET(node), ), ())
#define INFRARED_INNER_DEV(node) \
COND_CODE_1(DT_NODE_HAS_COMPAT(DT_PARENT(node), st_stm32_usart), \
(DEVICE_DT_GET(node), ), ())
inline static etl::array s_dev{
DT_FOREACH_STATUS_OKAY(godtek_temp_uart, INFRARED_DEV)
DT_FOREACH_STATUS_OKAY(godtek_temp_uart, INFRARED_INNER_DEV)};
#undef INFRARED_DEV
#undef INFRARED_INNER_DEV
/* Field-level volatile caches: written by the trigger handlers, read by
* GetMaxSensorValue. sensor_value itself cannot be volatile (no volatile
* copy/assign), so the two 32-bit fields are cached separately. A torn
* cross-field read is possible but negligible for slowly changing temps. */
inline static volatile int32_t s_latest_val1[s_dev.size()]{};
inline static volatile int32_t s_latest_val2[s_dev.size()]{};
inline static volatile bool s_latest_valid[s_dev.size()]{};
inline static etl::delegate<void(sensor_value)> s_cb;
};
} // namespace ther
#endif

33
include/led.hpp Normal file
View File

@ -0,0 +1,33 @@
#ifndef __THER_LED_HPP__
#define __THER_LED_HPP__
#include <etl/algorithm.h>
#include <etl/array.h>
#include <zephyr/drivers/led.h>
#include <zpp/ct_string.hpp>
#include <zpp/work_queue.hpp>
namespace ther {
template <led_dt_spec kSpec> class Led {
public:
static auto On() -> zpp::error { return led_on_dt(&kSpec); }
static auto Off() -> zpp::error { return led_off_dt(&kSpec); }
template <typename TRep, typename TPeriod>
static auto Flash(std::chrono::duration<TRep, TPeriod> period) {
s_work.submit(period);
}
private:
static auto Flash() -> void {
static bool flag = false;
if (flag) {
led_off_dt(&kSpec);
} else {
led_on_dt(&kSpec);
}
flag = !flag;
}
static inline zpp::periodic_work<> s_work{Flash};
};
} // namespace ther
#endif

76
include/ntc.hpp Normal file
View File

@ -0,0 +1,76 @@
#ifndef THER_NTC_HPP
#define THER_NTC_HPP
#include <array>
#include <cmath>
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/sys/printk.h>
#include <zpp/result.hpp>
namespace ther {
class NtcGroup {
public:
/// Read all NTC sensors, return the maximum temperature value.
/// Returns error if every sensor read fails.
static auto GetSensorValue() -> zpp::result<sensor_value> {
bool found = false;
sensor_value max_val{};
for (size_t i = 0; i < NTC_COUNT; ++i) {
const auto *dev = s_devices[i];
if (0 != sensor_sample_fetch(dev)) {
printk("[ntc] #%zu ERR fetch\n", i + 1);
continue;
}
sensor_value val{};
if (0 != sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &val)) {
printk("[ntc] #%zu ERR channel_get\n", i + 1);
continue;
}
// Always cache the latest reading for individual query
s_current_temp[i] = val;
if (!found || val.val1 > max_val.val1 ||
(val.val1 == max_val.val1 && val.val2 > max_val.val2)) {
max_val = val;
found = true;
}
}
if (found) {
return max_val;
}
return zpp::error_code::k_io;
}
/// Return the cached value for a single NTC sensor.
static auto GetSensorValue(uint8_t id) -> sensor_value {
if (id >= NTC_COUNT) {
return sensor_value{};
}
return s_current_temp[id];
}
private:
static constexpr size_t NTC_COUNT = 8;
inline static std::array<sensor_value, NTC_COUNT> s_current_temp{};
inline static const device *s_devices[] = {
DEVICE_DT_GET(DT_NODELABEL(pole_ntc1)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc2)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc3)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc4)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc5)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc6)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc7)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc8)),
};
};
} // namespace ther
#endif // THER_NTC_HPP

View File

@ -3,7 +3,7 @@
#include <zephyr/drivers/watchdog.h> #include <zephyr/drivers/watchdog.h>
#include <zpp/assert.hpp> #include <zpp/assert.hpp>
#include <zpp/timer.hpp> #include <zpp/timer.hpp>
namespace app { namespace ther {
class WatchDogConfig { class WatchDogConfig {
public: public:
WatchDogConfig() { WatchDogConfig() {
@ -28,6 +28,6 @@ private:
int chan_id; int chan_id;
}; };
} // namespace app } // namespace ther
#endif #endif

View File

@ -1,14 +1,30 @@
CONFIG_STDOUT_CONSOLE=y
CONFIG_CBPRINTF_FP_SUPPORT=y CONFIG_CBPRINTF_FP_SUPPORT=y
CONFIG_STD_CPP20=y CONFIG_STD_CPP23=y
CONFIG_CPP=y CONFIG_CPP=y
CONFIG_REQUIRES_FULL_LIBCPP=y CONFIG_REQUIRES_FULL_LIBCPP=y
CONFIG_CONSOLE=y CONFIG_CONSOLE=y
CONFIG_SERIAL=y CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_PMC_COM=y
CONFIG_HWINFO=y CONFIG_HWINFO=y
CONFIG_LED=y
# CONFIG_PRINTK=n
CONFIG_ADC=y
CONFIG_SPI=y
CONFIG_SPI_STM32=y
# SPI 中断模式:传输带 1s 超时,多路高频 SPI 下不会永久挂死(与 ch9438 测试对齐)
CONFIG_SPI_STM32_INTERRUPT=y
CONFIG_SENSOR=y
CONFIG_ADC_MCP320X_ACQUISITION_THREAD_STACK_SIZE=2048
CONFIG_REBOOT=y CONFIG_REBOOT=y
CONFIG_WATCHDOG=y CONFIG_WATCHDOG=y
CONFIG_PWM=y
# # Debug logging
# CONFIG_LOG=y
# CONFIG_LOG_MODE_IMMEDIATE=y
# CONFIG_ADC_LOG_LEVEL_DBG=y
# CONFIG_SENSOR_LOG_LEVEL_DBG=y
# CONFIG_SPI_LOG_LEVEL_DBG=y

343
scripts/test.py Normal file
View File

@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
Test script for app_photomagnetic communication protocol.
Frame format (SimpleProtocal, per DTS config):
H0(0x7E) H1(0xE7) CMD(1B) LEN(1B) DATA[0..N] CRC_LO CRC_HI
CRC = CRC-16 Modbus over CMD+LEN+DATA, LSB-MSB order
Commands:
0x00 TEMP DH (2 bytes: val1 val2)
0x01 GET_ID HD / DH (20 bytes HWID)
0x02 RUNNING_STATE HD (1 byte: 0=Standby 1=Running)
0x03 W_HEATING_STATE HD (1 byte: 0=stop, >0=target °C)
0x04 R_HEATING_STATE HD / DH (3 bytes: state val1 val2)
0x64 R_TEMP_POLE_NTC HD (1 byte index) / DH (2 bytes: val1 val2)
0x65 R_HEATING_PAD_NTC HD (0 bytes) / DH (2 bytes: val1 val2)
Usage:
python test.py /dev/ttyUSB0 --monitor
python test.py /dev/ttyUSB0 --heating 42
python test.py /dev/ttyUSB0 --read-pole-ntc 3
python test.py /dev/ttyUSB0 --read-heating-ntc
"""
import argparse
import sys
import time
try:
import serial
except ImportError:
print("Please install pyserial: pip install pyserial", file=sys.stderr)
sys.exit(1)
# ── Protocol constants ────────────────────────────────────────
HEADER = b"\x7e\xe7"
HEADER_SIZE = len(HEADER)
CMD_TEMP = 0x00
CMD_GET_ID = 0x01
CMD_RUNNING_STATE = 0x02
CMD_W_HEATING = 0x03
CMD_R_HEATING = 0x04
CMD_READ_POLE_NTC = 0x64
CMD_READ_HEATING_NTC = 0x65
HEATING_STATE_NAMES = {0: "OFF", 1: "ON"}
# ── CRC-16 Modbus ─────────────────────────────────────────────
def crc16_modbus(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc >>= 1
return crc & 0xFFFF
# ── Frame helpers ─────────────────────────────────────────────
def build_frame(cmd: int, payload: bytes = b"") -> bytes:
body = bytes([cmd, len(payload)]) + payload
crc = crc16_modbus(body)
return HEADER + body + bytes([crc & 0xFF, (crc >> 8) & 0xFF])
def parse_frame(frame: bytes) -> tuple[int, bytes] | None:
if len(frame) < HEADER_SIZE + 2 + 2:
return None
if frame[:HEADER_SIZE] != HEADER:
return None
cmd = frame[HEADER_SIZE]
length = frame[HEADER_SIZE + 1]
expected = HEADER_SIZE + 2 + length + 2
if len(frame) != expected:
return None
data = frame[HEADER_SIZE + 2 : HEADER_SIZE + 2 + length]
crc_body = frame[HEADER_SIZE : HEADER_SIZE + 2 + length]
actual_crc = crc16_modbus(crc_body)
wire_crc = (frame[expected - 1] << 8) | frame[expected - 2]
if actual_crc != wire_crc:
return None
return cmd, data
# ── Serial reader ─────────────────────────────────────────────
class ProtocolReader:
def __init__(self, ser: serial.Serial):
self._ser = ser
self._buf = bytearray()
def read_frame(self, timeout: float = 1.0) -> bytes | None:
deadline = time.time() + timeout
while time.time() < deadline:
if len(self._buf) >= 2:
idx = self._buf.find(HEADER)
if idx > 0:
del self._buf[:idx]
elif idx < 0:
self._buf.clear()
if len(self._buf) >= HEADER_SIZE and self._buf[:HEADER_SIZE] == HEADER:
if len(self._buf) >= HEADER_SIZE + 2 + 2:
length = self._buf[HEADER_SIZE + 1]
total = HEADER_SIZE + 2 + length + 2
if len(self._buf) >= total:
frame = bytes(self._buf[:total])
del self._buf[:total]
return frame
elif len(self._buf) >= HEADER_SIZE:
del self._buf[0]
continue
try:
chunk = self._ser.read(self._ser.in_waiting or 1)
if chunk:
self._buf.extend(chunk)
except (serial.SerialTimeoutException, serial.SerialException):
pass
return None
def _read_reply(ser: serial.Serial, timeout: float = 2.0) -> tuple[int, bytes] | None:
reader = ProtocolReader(ser)
frame = reader.read_frame(timeout)
if frame is None:
print("✗ No response (timeout)")
return None
result = parse_frame(frame)
if result is None:
print(f"✗ Invalid frame: {frame.hex()}")
return None
return result
def _temp_from_data(data: bytes) -> float:
return data[0] + data[1] / 100.0 if len(data) >= 2 else 0.0
# ── Command handlers ──────────────────────────────────────────
def cmd_get_id(ser: serial.Serial, timeout: float = 2.0):
print("→ Sending GET_ID...")
ser.write(build_frame(CMD_GET_ID))
r = _read_reply(ser, timeout)
if r is None:
return
cmd, data = r
if cmd != CMD_GET_ID:
print(f"✗ Unexpected cmd=0x{cmd:02X}")
return
print(f"✓ Device ID ({len(data)} bytes): {data.hex(' ')}")
def cmd_set_state(ser: serial.Serial, state: int):
names = {0: "Standby", 1: "Running", 2: "Pause", 3: "Error"}
name = names.get(state, "Unknown")
print(f"→ Setting state: {name} (0x{state:02X})")
ser.write(build_frame(CMD_RUNNING_STATE, bytes([state])))
print("✓ Sent")
def cmd_write_heating(ser: serial.Serial, target: int):
if target == 0:
print("→ Stopping heating")
else:
print(f"→ Starting heating to {target}°C")
ser.write(build_frame(CMD_W_HEATING, bytes([target])))
print("✓ Sent")
def cmd_read_heating(ser: serial.Serial, timeout: float = 2.0):
print("→ Querying heating state...")
ser.write(build_frame(CMD_R_HEATING))
r = _read_reply(ser, timeout)
if r is None:
return
cmd, data = r
if cmd != CMD_R_HEATING:
print(f"✗ Unexpected cmd=0x{cmd:02X}")
return
if len(data) < 3:
print(f"✗ Short data: {data.hex()}")
return
state = HEATING_STATE_NAMES.get(data[0], f"Unknown({data[0]})")
print(f"✓ Heating: {state}, temp={_temp_from_data(data[1:3]):.2f}°C")
def cmd_read_pole_ntc(ser: serial.Serial, index: int, timeout: float = 2.0):
print(f"→ Reading pole NTC #{index}...")
ser.write(build_frame(CMD_READ_POLE_NTC, bytes([index])))
r = _read_reply(ser, timeout)
if r is None:
return
cmd, data = r
if cmd != CMD_READ_POLE_NTC:
print(f"✗ Unexpected cmd=0x{cmd:02X}")
return
if len(data) < 2:
print(f"✗ Short data: {data.hex()}")
return
print(f"✓ Pole NTC #{index}: {_temp_from_data(data):.2f}°C")
def cmd_read_heating_ntc(ser: serial.Serial, timeout: float = 2.0):
print("→ Reading heating pad NTC...")
ser.write(build_frame(CMD_READ_HEATING_NTC))
r = _read_reply(ser, timeout)
if r is None:
return
cmd, data = r
if cmd != CMD_READ_HEATING_NTC:
print(f"✗ Unexpected cmd=0x{cmd:02X}")
return
if len(data) < 2:
print(f"✗ Short data: {data.hex()}")
return
print(f"✓ Heating pad NTC: {_temp_from_data(data):.2f}°C")
def cmd_monitor(ser: serial.Serial):
print("Monitoring... (Ctrl+C to stop)")
reader = ProtocolReader(ser)
try:
while True:
frame = reader.read_frame(timeout=0.5)
if frame is None:
continue
result = parse_frame(frame)
if result is None:
print(f"⚠ Bad frame: {frame.hex()}")
continue
cmd, data = result
if cmd == CMD_TEMP:
t = _temp_from_data(data)
print(f"🌡 Temp: {t:.2f}°C (raw: {data.hex(' ')})")
elif cmd == CMD_GET_ID:
print(f"🆔 Device ID: {data.hex(' ')}")
elif cmd == CMD_RUNNING_STATE:
print(f"🔁 Running state: 0x{data.hex()}")
elif cmd == CMD_R_HEATING:
if len(data) >= 3:
state = HEATING_STATE_NAMES.get(data[0], f"?{data[0]}")
t = _temp_from_data(data[1:3])
print(f"🔥 Heating: {state}, temp={t:.2f}°C")
else:
print(f"🔥 Heating: {data.hex()}")
elif cmd == CMD_READ_POLE_NTC:
t = _temp_from_data(data)
print(f"📡 Pole NTC reply: {t:.2f}°C")
elif cmd == CMD_READ_HEATING_NTC:
t = _temp_from_data(data)
print(f"🔥 Heating pad NTC reply: {t:.2f}°C")
else:
print(f"📦 cmd=0x{cmd:02X} data={data.hex()}")
except KeyboardInterrupt:
print("\nDone.")
def cmd_loop(ser: serial.Serial, count: int = 0):
i = 0
print("Looping GET_ID... (Ctrl+C to stop)")
try:
while count == 0 or i < count:
cmd_get_id(ser, timeout=1.0)
i += 1
time.sleep(0.5)
except KeyboardInterrupt:
print("\nDone.")
# ── Main ──────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Test script for app_photomagnetic comm protocol"
)
parser.add_argument("port", help="Serial port")
parser.add_argument("--baud", type=int, default=115200)
parser.add_argument("--get-id", action="store_true")
parser.add_argument(
"--state", type=lambda x: int(x, 0), metavar="N",
help="Set running state")
parser.add_argument(
"--heating", type=int, metavar="TEMP",
help="Write heating target (0=stop)")
parser.add_argument(
"--read-heating", action="store_true",
help="Read heating state + temp")
parser.add_argument(
"--read-pole-ntc", type=int, metavar="IDX",
help="Read pole NTC by index (0..7)")
parser.add_argument(
"--read-heating-ntc", action="store_true",
help="Read heating pad NTC temperature")
parser.add_argument("--monitor", action="store_true")
parser.add_argument(
"--loop", type=int, nargs="?", const=0, metavar="N",
help="Loop GET_ID N times")
parser.add_argument(
"--raw", type=lambda x: bytes.fromhex(x), metavar="HEX",
help="Send raw CMD+DATA")
args = parser.parse_args()
ser = serial.Serial(args.port, args.baud, timeout=0.1)
print(f"Connected to {args.port} @ {args.baud} baud")
try:
if args.get_id:
cmd_get_id(ser)
elif args.state is not None:
cmd_set_state(ser, args.state)
elif args.heating is not None:
cmd_write_heating(ser, args.heating)
elif args.read_heating:
cmd_read_heating(ser)
elif args.read_pole_ntc is not None:
cmd_read_pole_ntc(ser, args.read_pole_ntc)
elif args.read_heating_ntc:
cmd_read_heating_ntc(ser)
elif args.monitor:
cmd_monitor(ser)
elif args.loop is not None:
cmd_loop(ser, args.loop)
elif args.raw is not None:
payload = args.raw
c = payload[0]
d = payload[1:] if len(payload) > 1 else b""
print(f"→ Sending CMD=0x{c:02X} data={d.hex()}")
ser.write(build_frame(c, d))
print("✓ Sent")
else:
print("Use --get-id, --state, --heating, --read-heating, "
"--read-pole-ntc, --read-heating-ntc, --monitor, --loop, --raw")
finally:
ser.close()
if __name__ == "__main__":
main()

606
scripts/test_gui.py Normal file
View File

@ -0,0 +1,606 @@
#!/usr/bin/env python3
"""
GUI test tool for app_photomagnetic communication protocol.
Built with Python native tkinter + threading.
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
import time
import sys
from enum import IntEnum
try:
import serial
import serial.tools.list_ports
except ImportError:
print("Please install pyserial: pip install pyserial", file=sys.stderr)
sys.exit(1)
# ── Protocol constants ────────────────────────────────────────
HEADER = b"\x7e\xe7"
HEADER_SIZE = len(HEADER)
class Cmd(IntEnum):
TEMP = 0x00
GET_ID = 0x01
RUNNING_STATE = 0x02
W_HEATING = 0x03
R_HEATING = 0x04
READ_POLE_NTC = 0x64
READ_HEATING_NTC = 0x65
HEATING_STATE_NAMES = {0: "OFF", 1: "ON"}
STATE_NAMES = {0: "Standby", 1: "Running", 2: "Pause", 3: "Error"}
# ── Protocol helpers (from test.py) ───────────────────────────
def crc16_modbus(data: bytes) -> int:
crc = 0xFFFF
for byte in data:
crc ^= byte
for _ in range(8):
crc = (crc >> 1) ^ 0xA001 if crc & 1 else crc >> 1
return crc & 0xFFFF
def build_frame(cmd: int, payload: bytes = b"") -> bytes:
body = bytes([cmd, len(payload)]) + payload
crc = crc16_modbus(body)
return HEADER + body + bytes([crc & 0xFF, (crc >> 8) & 0xFF])
def parse_frame(frame: bytes) -> tuple[int, bytes] | None:
if len(frame) < HEADER_SIZE + 2 + 2:
return None
if frame[:HEADER_SIZE] != HEADER:
return None
cmd = frame[HEADER_SIZE]
length = frame[HEADER_SIZE + 1]
expected = HEADER_SIZE + 2 + length + 2
if len(frame) != expected:
return None
data = frame[HEADER_SIZE + 2: HEADER_SIZE + 2 + length]
crc_body = frame[HEADER_SIZE: HEADER_SIZE + 2 + length]
actual_crc = crc16_modbus(crc_body)
wire_crc = (frame[expected - 1] << 8) | frame[expected - 2]
if actual_crc != wire_crc:
return None
return cmd, data
def temp_from_data(data: bytes) -> float:
return data[0] + data[1] / 100.0 if len(data) >= 2 else 0.0
# ── Serial I/O thread ─────────────────────────────────────────
class SerialWorker:
def __init__(self, gui_callback):
self.ser: serial.Serial | None = None
self._lock = threading.Lock()
self._running = False
self._monitoring = False
self._reader_thread: threading.Thread | None = None
self._gui = gui_callback # on_frame(cmd, data) called from reader thread
@property
def is_open(self) -> bool:
return self.ser is not None and self.ser.is_open
def open(self, port: str, baud: int) -> str | None:
"""Open serial port. Returns error string or None on success."""
try:
ser = serial.Serial(port, baud, timeout=0.05)
with self._lock:
self.ser = ser
self._running = True
self._reader_thread = threading.Thread(target=self._reader_loop, daemon=True)
self._reader_thread.start()
return None
except serial.SerialException as e:
return str(e)
def close(self):
with self._lock:
self._running = False
self._monitoring = False
if self.ser:
try:
self.ser.close()
except Exception:
pass
self.ser = None
if self._reader_thread:
self._reader_thread.join(timeout=2)
self._reader_thread = None
def send(self, cmd: int, payload: bytes = b"") -> bool:
"""Send a frame. Returns True if sent."""
with self._lock:
if not self.ser or not self.ser.is_open:
return False
try:
self.ser.write(build_frame(cmd, payload))
return True
except serial.SerialException:
return False
def send_raw(self, data: bytes) -> bool:
"""Send raw bytes directly."""
with self._lock:
if not self.ser or not self.ser.is_open:
return False
try:
self.ser.write(data)
return True
except serial.SerialException:
return False
@property
def monitoring(self) -> bool:
return self._monitoring
@monitoring.setter
def monitoring(self, value: bool):
self._monitoring = value
def _reader_loop(self):
buf = bytearray()
while self._running:
# Read data
with self._lock:
if not self.ser or not self.ser.is_open:
time.sleep(0.05)
continue
try:
chunk = self.ser.read(self.ser.in_waiting or 1)
except serial.SerialException:
chunk = b""
if not chunk:
time.sleep(0.01)
continue
buf.extend(chunk)
# Try to extract frames
while len(buf) >= HEADER_SIZE + 2 + 2:
# Find header
idx = buf.find(HEADER)
if idx > 0:
del buf[:idx]
continue
if idx < 0:
buf.clear()
break
length = buf[HEADER_SIZE + 1]
total = HEADER_SIZE + 2 + length + 2
if len(buf) < total:
break # wait for more
frame = bytes(buf[:total])
del buf[:total]
result = parse_frame(frame)
if result:
self._gui.on_frame_received(result[0], result[1])
else:
self._gui.on_log(f"⚠ Bad frame: {frame.hex()}")
# ── GUI Application ───────────────────────────────────────────
class PhotomagneticGUI:
def __init__(self):
self.root = tk.Tk()
self.root.title("Photomagnetic Communication Tool")
self.root.geometry("850x700")
self.root.minsize(700, 550)
self.worker = SerialWorker(self)
self._monitor_after_id = None
self._last_temp_time = 0.0
self._temp_interval = 0.0
self._build_ui()
# ── UI Build ───────────────────────────────────────────
def _build_ui(self):
# ── Top: Serial connection ──
conn_frame = ttk.LabelFrame(self.root, text="Serial Connection", padding=8)
conn_frame.pack(fill=tk.X, padx=8, pady=4)
ttk.Label(conn_frame, text="Port:").grid(row=0, column=0, sticky=tk.W)
self._port_var = tk.StringVar()
self._port_combo = ttk.Combobox(conn_frame, textvariable=self._port_var, width=25)
self._port_combo.grid(row=0, column=1, padx=4, sticky=tk.W)
ttk.Button(conn_frame, text="", width=3,
command=self._scan_ports).grid(row=0, column=2, padx=2)
ttk.Label(conn_frame, text="Baud:").grid(row=0, column=3, padx=(12, 0), sticky=tk.W)
self._baud_var = tk.StringVar(value="115200")
baud_combo = ttk.Combobox(conn_frame, textvariable=self._baud_var,
values=("9600", "19200", "38400", "57600",
"115200", "230400", "460800"),
width=10)
baud_combo.grid(row=0, column=4, padx=4, sticky=tk.W)
self._connect_btn = ttk.Button(conn_frame, text="Connect", command=self._toggle_connect)
self._connect_btn.grid(row=0, column=5, padx=(12, 0))
self._status_lbl = ttk.Label(conn_frame, text="Disconnected", foreground="gray")
self._status_lbl.grid(row=0, column=6, padx=8, sticky=tk.W)
conn_frame.columnconfigure(6, weight=1)
# ── Main content: left (controls) + right (display) ──
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=8, pady=2)
left = ttk.Frame(main_frame)
left.pack(side=tk.LEFT, fill=tk.BOTH, expand=False)
right = ttk.Frame(main_frame)
right.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0))
# ── Left: Command panel ──
cmd_frame = ttk.LabelFrame(left, text="Commands", padding=8)
cmd_frame.pack(fill=tk.X)
# Row 1: GET_ID + State
r1 = ttk.Frame(cmd_frame)
r1.pack(fill=tk.X, pady=2)
ttk.Button(r1, text="Get Device ID", width=14,
command=self._cmd_get_id).pack(side=tk.LEFT)
ttk.Label(r1, text="State:").pack(side=tk.LEFT, padx=(10, 2))
self._state_combo = ttk.Combobox(r1, values=["Standby", "Running", "Pause", "Error"],
state="readonly", width=10)
self._state_combo.current(0)
self._state_combo.pack(side=tk.LEFT)
ttk.Button(r1, text="Set", width=4,
command=self._cmd_set_state).pack(side=tk.LEFT, padx=4)
# Row 2: Heating
r2 = ttk.Frame(cmd_frame)
r2.pack(fill=tk.X, pady=2)
ttk.Label(r2, text="Heating:").pack(side=tk.LEFT)
self._heating_var = tk.IntVar(value=0)
self._heating_spin = tk.Spinbox(r2, from_=0, to=60, textvariable=self._heating_var,
width=5, state="readonly")
self._heating_spin.pack(side=tk.LEFT, padx=2)
ttk.Label(r2, text="°C").pack(side=tk.LEFT)
ttk.Button(r2, text="Set / Stop", width=10,
command=self._cmd_set_heating).pack(side=tk.LEFT, padx=6)
ttk.Button(r2, text="Read State", width=10,
command=self._cmd_read_heating).pack(side=tk.LEFT)
# Row 3: NTC
r3 = ttk.Frame(cmd_frame)
r3.pack(fill=tk.X, pady=2)
ttk.Label(r3, text="Pole NTC #").pack(side=tk.LEFT)
self._ntc_idx_var = tk.IntVar(value=0)
ntc_spin = tk.Spinbox(r3, from_=0, to=7, textvariable=self._ntc_idx_var,
width=3, state="readonly")
ntc_spin.pack(side=tk.LEFT, padx=2)
ttk.Button(r3, text="Read Pole NTC", width=12,
command=self._cmd_read_pole_ntc).pack(side=tk.LEFT, padx=6)
ttk.Button(r3, text="Read Pad NTC", width=12,
command=self._cmd_read_heating_ntc).pack(side=tk.LEFT)
# Row 4: Monitor + Raw
r4 = ttk.Frame(cmd_frame)
r4.pack(fill=tk.X, pady=2)
self._monitor_btn = ttk.Button(r4, text="▶ Monitor", width=12,
command=self._toggle_monitor)
self._monitor_btn.pack(side=tk.LEFT)
ttk.Label(r4, text="Raw Hex:").pack(side=tk.LEFT, padx=(10, 2))
self._raw_var = tk.StringVar()
ttk.Entry(r4, textvariable=self._raw_var, width=18).pack(side=tk.LEFT, padx=2)
ttk.Button(r4, text="Send", width=5,
command=self._cmd_raw).pack(side=tk.LEFT, padx=2)
# Row 5: Loop
r5 = ttk.Frame(cmd_frame)
r5.pack(fill=tk.X, pady=2)
self._loop_btn = ttk.Button(r5, text="▶ Loop GET_ID", width=14,
command=self._toggle_loop)
self._loop_btn.pack(side=tk.LEFT)
# ── Right top: Live display ──
disp_frame = ttk.LabelFrame(right, text="Live Values", padding=6)
disp_frame.pack(fill=tk.X)
self._disp_labels = {}
entries = [
("Temperature", "--- °C", "temp"),
("Sample Rate", "---", "sample_rate"),
("Heating State", "---", "heat_state"),
("Heating Temp.", "--- °C", "heat_temp"),
("Device ID", "---", "dev_id"),
("Running State", "---", "run_state"),
]
for i, (label, default, key) in enumerate(entries):
ttk.Label(disp_frame, text=label + ":").grid(row=i, column=0, sticky=tk.W, padx=4)
lbl = ttk.Label(disp_frame, text=default, font=("Consolas", 11, "bold"),
foreground="#333")
lbl.grid(row=i, column=1, sticky=tk.W, padx=4)
self._disp_labels[key] = lbl
# Pole NTC sub-frame
self._ntc_labels = []
ntc_header = ttk.Label(disp_frame, text="Pole NTCs:")
ntc_header.grid(row=len(entries), column=0, sticky=tk.W, padx=4, pady=(4, 0))
ntc_row = ttk.Frame(disp_frame)
ntc_row.grid(row=len(entries), column=1, sticky=tk.W, padx=4, pady=(4, 0))
for i in range(8):
lbl = ttk.Label(ntc_row, text=f"{i}:---", font=("Consolas", 10), width=8)
lbl.pack(side=tk.LEFT)
self._ntc_labels.append(lbl)
disp_frame.columnconfigure(1, weight=1)
# ── Bottom: Log ──
log_frame = ttk.LabelFrame(right, text="Log", padding=4)
log_frame.pack(fill=tk.BOTH, expand=True, pady=(4, 0))
log_toolbar = ttk.Frame(log_frame)
log_toolbar.pack(fill=tk.X, pady=(0, 2))
ttk.Button(log_toolbar, text="Clear", width=6,
command=self._clear_log).pack(side=tk.RIGHT)
self._log_text = scrolledtext.ScrolledText(log_frame, height=12, font=("Consolas", 9),
wrap=tk.WORD, state=tk.DISABLED)
self._log_text.pack(fill=tk.BOTH, expand=True)
self._scan_ports()
# ── Serial connection ────────────────────────────────
def _scan_ports(self):
ports = [p.device for p in serial.tools.list_ports.comports()]
self._port_combo["values"] = ports
if ports and not self._port_var.get():
self._port_var.set(ports[0])
def _toggle_connect(self):
if self.worker.is_open:
self._disconnect()
else:
self._connect()
def _connect(self):
port = self._port_var.get().strip()
if not port:
messagebox.showerror("Error", "Select a serial port")
return
try:
baud = int(self._baud_var.get())
except ValueError:
messagebox.showerror("Error", "Invalid baud rate")
return
err = self.worker.open(port, baud)
if err:
messagebox.showerror("Connection Error", err)
return
self._connect_btn.configure(text="Disconnect")
self._status_lbl.configure(text=f"Connected @ {baud}", foreground="green")
self.on_log(f"Connected to {port} @ {baud} baud")
def _disconnect(self):
self._stop_monitor()
self._stop_loop()
self.worker.close()
self._connect_btn.configure(text="Connect")
self._status_lbl.configure(text="Disconnected", foreground="gray")
self.on_log("Disconnected")
def _check_connected(self) -> bool:
if not self.worker.is_open:
self.on_log("✗ Not connected")
return False
return True
# ── Commands ─────────────────────────────────────────
def _cmd_get_id(self):
if not self._check_connected():
return
self.worker.send(Cmd.GET_ID)
self.on_log("→ GET_ID sent")
def _cmd_set_state(self):
if not self._check_connected():
return
idx = self._state_combo.current()
self.worker.send(Cmd.RUNNING_STATE, bytes([idx]))
self.on_log(f"→ Set state: {self._state_combo.get()} ({idx})")
def _cmd_set_heating(self):
if not self._check_connected():
return
target = self._heating_var.get()
self.worker.send(Cmd.W_HEATING, bytes([target]))
if target == 0:
self.on_log("→ Stop heating")
else:
self.on_log(f"→ Set heating to {target}°C")
def _cmd_read_heating(self):
if not self._check_connected():
return
self.worker.send(Cmd.R_HEATING)
self.on_log("→ Read heating state")
def _cmd_read_pole_ntc(self):
if not self._check_connected():
return
idx = self._ntc_idx_var.get()
self.worker.send(Cmd.READ_POLE_NTC, bytes([idx]))
self.on_log(f"→ Read pole NTC #{idx}")
def _cmd_read_heating_ntc(self):
if not self._check_connected():
return
self.worker.send(Cmd.READ_HEATING_NTC)
self.on_log("→ Read heating pad NTC")
def _cmd_raw(self):
if not self._check_connected():
return
hex_str = self._raw_var.get().strip()
if not hex_str:
return
try:
payload = bytes.fromhex(hex_str.replace(" ", ""))
except ValueError:
self.on_log(f"✗ Invalid hex: {hex_str}")
return
cmd = payload[0]
data = payload[1:] if len(payload) > 1 else b""
self.worker.send(cmd, data)
self.on_log(f"→ Raw: CMD=0x{cmd:02X} data={data.hex()}")
self._raw_var.set("")
# ── Monitor ──────────────────────────────────────────
def _toggle_monitor(self):
if self.worker.monitoring:
self._stop_monitor()
else:
self._start_monitor()
def _start_monitor(self):
if not self._check_connected():
return
self.worker.monitoring = True
self._monitor_btn.configure(text="■ Stop Monitor")
self.on_log("▶ Monitoring started")
def _stop_monitor(self):
self.worker.monitoring = False
self._monitor_btn.configure(text="▶ Monitor")
if self._monitor_after_id:
self.root.after_cancel(self._monitor_after_id)
self._monitor_after_id = None
# ── Loop ─────────────────────────────────────────────
def _toggle_loop(self):
if self._loop_running:
self._stop_loop()
else:
self._start_loop()
def _start_loop(self):
if not self._check_connected():
return
self._loop_running = True
self._loop_btn.configure(text="■ Stop Loop")
self.on_log("▶ Loop GET_ID started")
self._loop_tick()
def _stop_loop(self):
self._loop_running = False
self._loop_btn.configure(text="▶ Loop GET_ID")
if hasattr(self, '_loop_after_id') and self._loop_after_id:
self.root.after_cancel(self._loop_after_id)
self._loop_after_id = None
def _loop_tick(self):
if not self._loop_running or not self.worker.is_open:
self._stop_loop()
return
self.worker.send(Cmd.GET_ID)
self._loop_after_id = self.root.after(1000, self._loop_tick)
# ── Callbacks from SerialWorker (called in reader thread) ──
def on_frame_received(self, cmd: int, data: bytes):
"""Called from reader thread. Schedule UI update."""
self.root.after(0, self._handle_frame, cmd, data)
def on_log(self, msg: str):
"""Thread-safe log write."""
self.root.after(0, self._append_log, msg)
# ── Frame handling (runs in main thread via after) ───
def _handle_frame(self, cmd: int, data: bytes):
raw_hex = data.hex(" ")
# ── 0x00 TEMP: 设备主动推送, 只更新 live value, 不输出 log ──
if cmd == Cmd.TEMP:
t = temp_from_data(data)
now = time.time()
if self._last_temp_time > 0:
self._temp_interval = now - self._last_temp_time
hz = 1.0 / self._temp_interval if self._temp_interval > 0 else 0
self._disp_labels["sample_rate"].configure(
text=f"{self._temp_interval*1000:.0f}ms ({hz:.1f}Hz)")
self._last_temp_time = now
self._disp_labels["temp"].configure(text=f"{t:.2f} °C")
elif cmd == Cmd.GET_ID:
id_str = data.hex(" ")
id_show = id_str[:47] + "..." if len(id_str) > 50 else id_str
self._disp_labels["dev_id"].configure(text=id_show)
self._append_log(f"🆔 Device ID ({len(data)}B): {raw_hex}")
elif cmd == Cmd.RUNNING_STATE:
state = data[0] if data else -1
name = STATE_NAMES.get(state, f"Unknown({state})")
self._disp_labels["run_state"].configure(text=name)
self._append_log(f"🔁 State: {name}")
elif cmd == Cmd.W_HEATING:
target = data[0] if data else 0
if target == 0:
self._append_log(f"✓ Heating stopped (ACK)")
else:
self._append_log(f"✓ Heating set to {target}°C (ACK)")
elif cmd == Cmd.R_HEATING:
if len(data) >= 3:
s = HEATING_STATE_NAMES.get(data[0], f"?{data[0]}")
t = temp_from_data(data[1:3])
self._disp_labels["heat_state"].configure(text=s)
self._disp_labels["heat_temp"].configure(text=f"{t:.2f} °C")
self._append_log(f"🔥 Heating: {s}, temp={t:.2f}°C")
else:
self._append_log(f"🔥 Heating: {raw_hex}")
elif cmd == Cmd.READ_POLE_NTC:
if len(data) >= 2:
t = temp_from_data(data)
self._append_log(f"📡 Pole NTC reply: {t:.2f}°C ({raw_hex})")
else:
self._append_log(f"📡 Pole NTC: {raw_hex}")
elif cmd == Cmd.READ_HEATING_NTC:
t = temp_from_data(data)
self._append_log(f"🔥 Pad NTC: {t:.2f}°C ({raw_hex})")
else:
self._append_log(f"📦 CMD=0x{cmd:02X} data={raw_hex}")
def _set_ntc_display(self, idx: int, temp: float):
if 0 <= idx < len(self._ntc_labels):
self._ntc_labels[idx].configure(text=f"{idx}:{temp:.1f}")
def _clear_log(self):
self._log_text.configure(state=tk.NORMAL)
self._log_text.delete(1.0, tk.END)
self._log_text.configure(state=tk.DISABLED)
def _append_log(self, msg: str):
self._log_text.configure(state=tk.NORMAL)
self._log_text.insert(tk.END, msg + "\n")
self._log_text.see(tk.END)
self._log_text.configure(state=tk.DISABLED)
# ── Run ──────────────────────────────────────────────
def run(self):
self.root.mainloop()
self._disconnect()
if __name__ == "__main__":
app = PhotomagneticGUI()
app.run()

6
src/com.cpp Normal file
View File

@ -0,0 +1,6 @@
#include <com.hpp>
#include <zephyr/init.h>
static auto Init() -> int { return static_cast<int>(ther::Com::Init()); }
inline SYS_INIT(Init, APPLICATION, 50);

29
src/led.cpp Normal file
View File

@ -0,0 +1,29 @@
#include <com.hpp>
#include <etl/utility.h>
#include <led.hpp>
#include <led_strip_indicator/led_strip_indicator.hpp>
#include <zephyr/init.h>
using namespace ther;
namespace {
using McuState = Led<LED_DT_SPEC_GET(DT_NODELABEL(led_mcu_state))>;
using Inf = Led<LED_DT_SPEC_GET(DT_NODELABEL(led_inf))>;
auto indicator = ZPP_DRV_GET_P(ledstrip::Indicator, DT_NODELABEL(indicator));
constexpr const char *MODULE = "app_led";
} // namespace
static auto Init() -> int {
Com::AddRunningState([](Com::HostStatus state) {
indicator->Status(static_cast<uint8_t>(state));
if (state == Com::HostStatus::RUNNING) {
Inf::On();
} else {
Inf::Off();
}
});
using namespace std::chrono_literals;
McuState::Flash(1s);
printk("[%s] Init: MCU state LED flashing at 1s interval\n", MODULE);
return 0;
}
SYS_INIT(Init, APPLICATION, 50);

View File

@ -1,97 +1,10 @@
#include <ctime>
#include <etl/vector.h>
#include <zephyr/drivers/hwinfo.h>
#include <zephyr/drivers/sensor.h>
#include <led_strip_indicator/led_strip_indicator.hpp>
#include <uart_com/protocal.hpp>
#include <zpp/device.hpp>
#include "watchdog.hpp"
#include "zephyr/drivers/gpio.h"
#include "zephyr/kernel.h"
#include "zpp/fmt.hpp"
#include "zpp/timer.hpp"
#include <zephyr/drivers/led_strip.h>
#include <zephyr/sys/printk.h>
namespace {
auto sensor = DEVICE_DT_GET(DT_NODELABEL(godtek));
auto &pmc = ZPP_DRV_GET(uart_com::Protocal, DT_NODELABEL(pm_protocal));
auto &led_strip = ZPP_DRV_GET(ledstrip::Indicator, DT_NODELABEL(indicator));
enum Command { kTemp = 0x00, kGetId, kRunningState };
enum RunningState {};
constexpr auto kDeviceIdSize = 20;
enum StatusId : uint8_t { kStandby, kRunning, kPause, kError};
volatile bool flag{false};
gpio_dt_spec led_g = GPIO_DT_SPEC_GET(DT_NODELABEL(led_g), gpios);
k_timer led_timer;
} // namespace
const led_rgb red{255, 0, 0};
const led_rgb green{0, 255, 0};
const led_rgb blue{0, 0, 255};
auto rgb_init(void)->int{
led_strip.TurnOn(red);
k_msleep(10);
led_strip.TurnOff();
led_strip.TurnOn(green);
k_msleep(10);
led_strip.TurnOff();
led_strip.TurnOn(blue); // 常亮
// k_msleep(10);
// led_strip.TurnOff();
return 0;
}
#include <zephyr/device.h>
#include <zephyr/kernel.h>
auto main(void) -> int { auto main(void) -> int {
gpio_pin_configure_dt(&led_g, GPIO_OUTPUT_ACTIVE); printk("[main] init on %s\n", CONFIG_BOARD);
k_timer_init(&led_timer, [](k_timer* tim) {
gpio_pin_toggle_dt(&led_g);
}, [](k_timer* tim) {});
k_timer_start(&led_timer, K_NO_WAIT, K_SECONDS(1));
rgb_init();
pmc.SetRxCallback(kGetId, [](uart_com::DataType data) -> void {
uint8_t buffer[kDeviceIdSize];
auto size = hwinfo_get_device_id(buffer, sizeof(buffer));
pmc.Send(kGetId, uart_com::DataType(buffer, size));
});
pmc.SetRxCallback(kRunningState, [](uart_com::DataType data) -> void {
auto state = static_cast<StatusId>(data[0]);
pmc.Send(
kRunningState,
uart_com::DataType(reinterpret_cast<uint8_t *>(&state), sizeof(state)));
led_strip.Status(state).on_error([](zpp::error_code code) {});
});
auto wdt = app::WatchDogConfig{};
sensor_trigger tri{.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_AMBIENT_TEMP};
if (auto r = sensor_trigger_set(
sensor, &tri,
[](const device *dev, const sensor_trigger *trig) { flag = true; });
r != 0) {
return -ENODEV;
};
while (1) { while (1) {
if (flag) { k_sleep(K_SECONDS(1));
sensor_value val{0, 0};
if (sensor_sample_fetch_chan(sensor, SENSOR_CHAN_AMBIENT_TEMP) == 0) {
sensor_channel_get(sensor, SENSOR_CHAN_AMBIENT_TEMP, &val);
const uint8_t data[2] = {static_cast<uint8_t>(val.val1),
static_cast<uint8_t>(val.val2)};
pmc.Send("temp", data);
}
flag = false;
}
wdt.Feed();
} }
return 0; return 0;
} }

View File

@ -0,0 +1,18 @@
#include <zephyr/drivers/led.h>
#include <zephyr/kernel.h>
led_dt_spec status = LED_DT_SPEC_GET(DT_NODELABEL(status_led));
auto main() -> int {
printk("hello world %s\n", CONFIG_BOARD);
printk("hello world %s\n", CONFIG_BOARD);
printk("hello world %s\n", CONFIG_BOARD);
printk("hello world %s\n", CONFIG_BOARD);
while (true) {
printk("hello world\n");
led_on_dt(&status);
k_sleep(K_MSEC(1000));
led_off_dt(&status);
k_sleep(K_MSEC(1000));
}
return 0;
}

18
src/sample_ntc.cpp Normal file
View File

@ -0,0 +1,18 @@
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/kernel.h>
#define NTC_INS(node) DEVICE_DT_GET(node),
const device *ntc = DEVICE_DT_GET(DT_NODELABEL(pole_ntc1));
auto main() -> int {
sensor_value v;
while (1) {
if (0 == sensor_sample_fetch(ntc)) {
sensor_channel_get(ntc, SENSOR_CHAN_AMBIENT_TEMP, &v);
printk("temp: %d.%d\n", v.val1, v.val2);
}
k_sleep(K_SECONDS(1));
}
return 0;
}

93
src/temp.cpp Normal file
View File

@ -0,0 +1,93 @@
#include <com.hpp>
#include <etl/utility.h>
#include <infrared.hpp>
#include <zephyr/drivers/gpio.h>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
/* 1 = enable verbose diagnostics (per-second channel dump). */
#ifndef APP_DEBUG_PRINT
#define APP_DEBUG_PRINT 0
#endif
using namespace ther;
/* 调试打印开关:
* 1 = 9 +
* 0 / =
*/
#ifndef APP_TEMP_PRINT_ALL_CHANNELS
#define APP_TEMP_PRINT_ALL_CHANNELS 0
#endif
namespace {
using namespace std::string_literals;
constexpr const char *MODULE = "app_temp";
constexpr auto kPeriodSend = std::chrono::milliseconds(20);
/* 触发回调:只做调试打印(采集/缓存由 Infrared 内部完成),
* 20ms OnSendTick */
constexpr auto OnSensorValueReady =
etl::delegate<void(sensor_value)>::create(+[](sensor_value val) {
#if APP_TEMP_PRINT_ALL_CHANNELS
const auto all = Infrared::GetAllSensorValues();
for (size_t i = 0; i < all.size(); ++i) {
printk("[%s]ch%u: %d.%d\n", MODULE, (unsigned)i, all[i].val1,
all[i].val2);
}
printk("[%s]max: %d.%d (%lld ms)\n", MODULE, val.val1, val.val2,
(long long)k_uptime_get());
#endif
});
static void OnSendTick(struct k_work *work);
/* 每 kPeriodSend(20ms) 发送一次最新缓存的最大值。 */
static K_WORK_DELAYABLE_DEFINE(s_send_work, OnSendTick);
static void OnSendTick(struct k_work *work) {
if (auto r = Infrared::GetMaxSensorValue(); r.ok()) {
Com::SendTemp(r.value().second);
}
#if APP_DEBUG_PRINT
static uint32_t s_send_tick; /* diag: send counter, ~1s cadence */
if (++s_send_tick % 50 == 0) { /* every ~1 s */
const auto all = Infrared::GetAllSensorValues();
printk("[%s]diag tick=%u mask=0x%02X val:", MODULE, (unsigned)s_send_tick,
(unsigned)Infrared::GetChannelValidMask());
for (size_t i = 0; i < all.size(); ++i) {
printk(" %u=%d.%d", (unsigned)i, all[i].val1, all[i].val2);
}
printk("\n");
}
#endif /* APP_DEBUG_PRINT */
k_work_schedule(&s_send_work, K_MSEC(kPeriodSend.count()));
}
} // namespace
#define ZEPHYR_USER_NODE DT_PATH(zephyr_user)
auto InitEnblePins() {
const gpio_dt_spec en_pin = GPIO_DT_SPEC_GET(ZEPHYR_USER_NODE, en_ir_gpios);
gpio_pin_configure_dt(&en_pin, GPIO_OUTPUT_ACTIVE);
}
static auto Init() -> int {
InitEnblePins();
printk("[%s]temp init\n", MODULE);
if (auto r = Infrared::Init(); r.code_id() != zpp::error_code::k_ok) {
printk("[%s] Infrared::Init failed: %d\n", MODULE,
static_cast<int>(r.code_id()));
return 0;
}
if (auto r = Infrared::AddCallbackWhenSensorValueReady(OnSensorValueReady);
r.code_id() != zpp::error_code::k_ok) {
printk("[%s] AddCallback failed: %d\n", MODULE,
static_cast<int>(r.code_id()));
}
/* 启动 20ms 周期发送 */
k_work_schedule(&s_send_work, K_MSEC(kPeriodSend.count()));
return 0;
}
/* Register AFTER the godtek/ch9438 drivers (POST_KERNEL): registering the
* sensor trigger earlier gets wiped by the sensor driver's own Init,
* which resets data_ready_handler to nullptr. */
SYS_INIT(Init, APPLICATION, 50);

16
src/watdog.cpp Normal file
View File

@ -0,0 +1,16 @@
#include <watchdog.hpp>
#include <zpp/work_queue.hpp>
using namespace ther;
namespace {
WatchDogConfig wdt;
} // namespace
static auto Init() -> int {
printk("[watchdog] init\n");
using namespace std::chrono_literals;
static zpp::periodic_work<> work{[]() { wdt.Feed(); }};
work.submit(50ms);
return 0;
}
SYS_INIT(Init, POST_KERNEL, 50);

View File

@ -0,0 +1,15 @@
common:
platform:
- native_sim/native/64
- nucleo_f429zi
tests:
sample.helloworld:
extra_configs:
- CONFIG_SAMPLE_HELLOWORLD=y
sample.ntc:
extra_configs:
- CONFIG_SAMPLE_NTC=y
sample.ms:
extra_args:
- DTC_OVERLAY_FILE=boards/use_ms.overlay
extra_configs:

51
west.yml Normal file
View File

@ -0,0 +1,51 @@
manifest:
projects:
- import:
name-allowlist:
- cmsis_6
- hal_stm32
- hal_ti
path-prefix: extern
name: zephyr
path: zephyr
remote: zephyr
revision: v4.4.0
- name: etl
path: modules/etl
remote: robotstorm
revision: master
- name: uart_com
path: modules/uart_com
remote: robotstorm
revision: main
- name: zpp
path: modules/zpp
remote: robotstorm
revision: dev
- name: dr2501a
path: boards/dr2501a
remote: robotstorm
revision: main
- name: led_strip_indicator
path: modules/led_strip_indicator
remote: robotstorm
revision: main
- name: godtek_temp
path: modules/godtek_temp
remote: robotstorm
revision: main
- name: ch9438
path: modules/ch9438
remote: robotstorm
revision: main
# - name: heading_pad
# path: modules/heading_pad
# revision: main
# url: undefined
remotes:
- name: zephyr
url-base: https://gitcode.com/gh_mirrors/ze
- name: robotstorm
url-base: https://git.robotstorm.tech/EmbeddedTeam
self:
west-commands: scripts/west-commands.yml

172
zbuild.py Executable file
View File

@ -0,0 +1,172 @@
#!/usr/bin/env python3
"""
Zephyr build helper for app_i_core_link.
Usage:
python zbuild.py dr2501a_g070rb
python zbuild.py dr2501a_g070rb -p auto
python zbuild.py -p always (auto-detect board from west.yml)
python zbuild.py dr2501a_g070rb -T sample.helloworld
Options:
board Board name (optional, auto-detected from west.yml if omitted)
-p PURSE Pristine option: auto / always / never
-t TARGET CMake target to run after build, e.g. test / flash
-T TEST Test name/sample identifier (e.g. sample.helloworld)
Steps:
1. west topdir find Zephyr workspace root
2. west config --local manifest.file <this_script_dir>/west.yml
3. west build [-p PURSE] -b BOARD [-t TARGET] [-T TEST] <this_script_dir>
"""
import argparse
import os
import re
import subprocess
import sys
def get_script_dir() -> str:
"""Absolute path to the directory containing this script."""
return os.path.dirname(os.path.abspath(__file__))
def run(cmd: list[str], cwd: str | None = None) -> str:
"""Run a command and return stdout. Exit on failure."""
proc = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
if proc.returncode != 0:
print(f"{' '.join(cmd)}", file=sys.stderr)
print(f" {proc.stderr.strip()}", file=sys.stderr)
sys.exit(proc.returncode)
return proc.stdout.strip()
def discover_boards(manifest_path: str) -> list[str]:
"""Parse west.yml and return board names whose path starts with 'boards/'."""
if not os.path.exists(manifest_path):
return []
with open(manifest_path) as f:
content = f.read()
# Match entries like:
# - name: <board_name>
# path: boards/<something>
boards = re.findall(r"name:\s*(\S+)\s*\n\s+path:\s*boards/\S+", content)
return boards
def main():
parser = argparse.ArgumentParser(
description="Zephyr build helper for app_i_core_link"
)
parser.add_argument(
"board",
nargs="?",
default=None,
help="Board name (optional, auto-detected from west.yml if omitted)",
)
parser.add_argument(
"-p",
"--pristine",
default=None,
choices=["auto", "always", "never"],
help="Pristine build option",
)
parser.add_argument(
"-t",
"--target",
default=None,
help="CMake target to run after build (e.g. test, flash)",
)
parser.add_argument(
"-T",
"--test",
default=None,
dest="test_id",
help="Test name / sample identifier (e.g. sample.helloworld)",
)
parser.add_argument(
"--update",
action="store_true",
help="Run west update after setting manifest",
)
args = parser.parse_args()
# ── 0. Auto-detect board from west.yml when not specified ───
if args.board is None:
script_dir = get_script_dir()
manifest = os.path.join(script_dir, "west.yml")
boards = discover_boards(manifest)
if len(boards) == 0:
print(
" ✗ No board specified and no board found in west.yml "
"(no entry with path: boards/...)",
file=sys.stderr,
)
sys.exit(1)
elif len(boards) == 1:
args.board = boards[0]
print(f" → Auto-detected board: {args.board}")
else:
print(
" ✗ No board specified. Multiple boards found in west.yml:",
", ".join(boards),
"\n Please specify one explicitly.",
file=sys.stderr,
)
sys.exit(1)
script_dir = get_script_dir()
manifest = os.path.join(script_dir, "west.yml")
print(f" script dir : {script_dir}")
print(f" board : {args.board}")
print(f" pristine : {args.pristine or '(none)'}")
print(f" target : {args.target or '(none)'}")
print(f" test : {args.test_id or '(none)'}")
print(f" update : {'yes' if args.update else 'no'}")
# ── 1. Find workspace root ───────────────────────────────
print("\n [1/3] Finding west workspace root...")
topdir = run(["west", "topdir"])
print(f"{topdir}")
# ── 2. Set local manifest ────────────────────────────────
print("\n [2/3] Setting local manifest...")
if not os.path.exists(manifest):
print(f" ✗ manifest not found: {manifest}", file=sys.stderr)
sys.exit(1)
run(["west", "config", "--local", "manifest.file", manifest], cwd=topdir)
print(f" → manifest.file = {manifest}")
# ── 2.5. Run west update (optional) ───────────────────────
if args.update:
print("\n [2.5/3] Running west update...")
run(["west", "update", "--fetch", "smart"], cwd=topdir)
print(" → update done")
# ── 3. Run west build ─────────────────────────────────────
print("\n [3/3] Running west build...")
build_cmd = ["west", "build"]
if args.pristine:
build_cmd += ["-p", args.pristine]
build_cmd += ["-b", args.board]
if args.target:
build_cmd += ["-t", args.target]
if args.test_id:
build_cmd += ["-T", args.test_id]
build_cmd += [script_dir]
print(f" $ {' '.join(build_cmd)}")
proc = subprocess.run(build_cmd, cwd=topdir)
if proc.returncode != 0:
print(f"\n ✗ Build failed (exit code {proc.returncode})")
sys.exit(proc.returncode)
print("\n ✓ Build succeeded")
if __name__ == "__main__":
main()