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.
This commit is contained in:
zhangyisong 2026-07-13 21:57:54 +08:00
parent 2c42b67eca
commit 1e6d6b9b30
5 changed files with 281 additions and 189 deletions

View File

@ -1,7 +1,9 @@
#ifndef __THER_COM_HPP__ #ifndef __THER_COM_HPP__
#define __THER_COM_HPP__ #define __THER_COM_HPP__
#include "heating.hpp"
#include "led.hpp" #include "led.hpp"
#include "ntc.hpp"
#include <led_strip_indicator/led_strip_indicator.hpp> #include <led_strip_indicator/led_strip_indicator.hpp>
#include <uart_com/simple_protocal.hpp> #include <uart_com/simple_protocal.hpp>
#include <zephyr/drivers/hwinfo.h> #include <zephyr/drivers/hwinfo.h>
@ -23,7 +25,17 @@ public:
private: private:
enum HostStatus : uint8_t { STANDBY, RUNNING, PAUSE, ERROR }; enum HostStatus : uint8_t { STANDBY, RUNNING, PAUSE, ERROR };
enum Addr : uint8_t { TEMP = 0, GET_ID, RUNNING_STATE }; enum Addr : uint8_t {
TEMP = 0,
GET_ID,
RUNNING_STATE,
W_HEATING_STATE,
R_HEATING_STATE
};
enum DevAddr : uint8_t {
R_TEMP_POLE_NTC = 100,
R_TEMP_HETING_PAD_NTC = 101,
};
using CbTableValueType = using CbTableValueType =
std::pair<const uint8_t, void (*)(uart_com::DataType)>; std::pair<const uint8_t, void (*)(uart_com::DataType)>;
struct Cb { struct Cb {
@ -42,17 +54,65 @@ private:
} else { } else {
InfLed::Off(); InfLed::Off();
} }
s_led_strip_indicator->Status(data[0]); printk("set ledsrtip to %d", data[0]);
const uint8_t id = data[0];
s_led_strip_indicator->Status(id).on_error([](zpp::error_code err) {
printk("err code: %s", zpp::error_str(err));
});
}
static auto WHeatingState(uart_com::DataType data) -> void {
if (data.size() != 1) {
return;
}
const bool state = (data[0] > 0);
if (state) {
HeatingPad::Start(sensor_value{data[0], 0});
} else {
HeatingPad::Stop();
}
}
static auto RHeatingState(uart_com::DataType data) -> void {
if (data.size() != 0) {
return;
}
const sensor_value temp = HeatingPad::CurrentTemp();
const uint8_t state[] = {s_heating_state, (uint8_t)temp.val1,
(uint8_t)temp.val2};
s_proto->Send(R_HEATING_STATE, state);
}
/// Read one pole NTC by index (0~7).
static auto ReadPoleNtcTemperature(uart_com::DataType data) -> void {
if (data.size() != 1) {
return;
}
const sensor_value temp = NtcGroup::GetSensorValue(data[0]);
const uint8_t payload[] = {(uint8_t)temp.val1, (uint8_t)temp.val2};
s_proto->Send(R_TEMP_POLE_NTC, payload);
}
/// Read heating pad NTC (onboard ADC).
static auto ReadHeatingPadNtcTemperature(uart_com::DataType data) -> void {
const sensor_value temp = HeatingPad::CurrentTemp();
const uint8_t payload[] = {(uint8_t)temp.val1, (uint8_t)temp.val2};
s_proto->Send(R_TEMP_HETING_PAD_NTC, payload);
} }
}; };
constexpr static std::pair<const uint8_t, constexpr static std::pair<const uint8_t,
uart_com::SimpleProtocal::CallbackType> uart_com::SimpleProtocal::CallbackType>
kRxCallbackTable[] = {{GET_ID, Cb::GetId}, kRxCallbackTable[] = {
{RUNNING_STATE, Cb::RunningState}}; {GET_ID, Cb::GetId},
{RUNNING_STATE, Cb::RunningState},
{W_HEATING_STATE, Cb::WHeatingState},
{R_HEATING_STATE, Cb::RHeatingState},
// Pole NTC (MCP3208)
{R_TEMP_POLE_NTC, Cb::ReadPoleNtcTemperature},
// Heating pad NTC (ADC1)
{R_TEMP_HETING_PAD_NTC, Cb::ReadHeatingPadNtcTemperature},
};
inline static auto s_led_strip_indicator = inline static auto s_led_strip_indicator =
ZPP_DRV_GET_P(ledstrip::Indicator, DT_NODELABEL(indicator)); ZPP_DRV_GET_P(ledstrip::Indicator, DT_NODELABEL(indicator));
inline static auto s_proto = inline static auto s_proto =
(uart_com::SimpleProtocal *)(DEVICE_DT_GET(DT_NODELABEL(pm_protocal))); (uart_com::SimpleProtocal *)(DEVICE_DT_GET(DT_NODELABEL(pm_protocal)));
inline static auto s_heating_state = false;
using InfLed = ther::Led<LED_DT_SPEC_GET(DT_NODELABEL(inf_led))>; using InfLed = ther::Led<LED_DT_SPEC_GET(DT_NODELABEL(inf_led))>;
}; };
} // namespace ther } // namespace ther

View File

@ -1,6 +1,7 @@
#ifndef __THER_HEATING_HPP__ #ifndef __THER_HEATING_HPP__
#define __THER_HEATING_HPP__ #define __THER_HEATING_HPP__
#include <algorithm> #include <algorithm>
#include <cmath>
#include <zephyr/device.h> #include <zephyr/device.h>
#include <zephyr/drivers/pwm.h> #include <zephyr/drivers/pwm.h>
#include <zephyr/drivers/sensor.h> #include <zephyr/drivers/sensor.h>
@ -11,69 +12,41 @@ namespace ther {
class HeatingPad { class HeatingPad {
public: public:
/// Start PID temperature control toward the given target.
static auto Start(sensor_value target_temp) -> void { static auto Start(sensor_value target_temp) -> void {
s_target = sensor_value_to_double(&target_temp); s_target = sensor_value_to_double(&target_temp);
s_integral = 0.0; s_integral = 0.0;
s_last_error = 0.0; s_last_error = 0.0;
s_last_time = k_uptime_get(); s_last_time = k_uptime_get();
printk("[heat] start target=%.1f°C\n", s_target); printk("[heat] start target=%.1f C\n", s_target);
// Check PWM device
if (!device_is_ready(s_pwm_spec.dev)) { if (!device_is_ready(s_pwm_spec.dev)) {
printk("[heat] ERR: PWM device not ready\n"); printk("[heat] ERR: PWM not ready\n");
return; return;
} }
printk("[heat] PWM dev=%s ch=%d period=%u ns\n", s_pwm_spec.dev->name, printk("[heat] PWM OK\n");
s_pwm_spec.channel, s_pwm_spec.period);
// Check sensor k_work_init(&s_work, WorkHandler);
if (!device_is_ready(s_temp_sensor)) { k_timer_init(&s_timer, TimerTick, nullptr);
printk("[heat] ERR: temp sensor not ready\n");
return;
}
printk("[heat] temp sensor OK\n");
// Test: set 50% duty immediately to verify PWM works
const uint32_t pulse_50 = s_pwm_spec.period / 2;
int ret = pwm_set_pulse_dt(&s_pwm_spec, pulse_50);
printk("[heat] test 50%% duty pulse=%u ret=%d\n", pulse_50, ret);
k_timer_init(&s_timer, Tick, nullptr);
k_timer_start(&s_timer, K_NO_WAIT, K_MSEC(kUpdatePeriodMs)); k_timer_start(&s_timer, K_NO_WAIT, K_MSEC(kUpdatePeriodMs));
s_active = true; s_active = true;
} }
static auto Start() {
const uint32_t pulse_50 = s_pwm_spec.period / 2;
int ret = pwm_set_pulse_dt(&s_pwm_spec, pulse_50);
return ret;
}
/// Stop PID control and turn off heating.
static auto Stop() -> void { static auto Stop() -> void {
printk("[heat] stop\n");
s_active = false; s_active = false;
k_timer_stop(&s_timer); k_timer_stop(&s_timer);
pwm_set_pulse_dt(&s_pwm_spec, 0); pwm_set_pulse_dt(&s_pwm_spec, 0);
} }
/// Change the target temperature while keeping PID running. static auto CurrentTemp() -> sensor_value { return s_current_temp; }
static auto SetTarget(sensor_value target_temp) -> void {
s_target = sensor_value_to_double(&target_temp);
printk("[heat] new target=%.1f°C\n", s_target);
}
private: private:
static constexpr uint32_t kPeriodNs = PWM_KHZ(5); static constexpr uint32_t kPeriodNs = PWM_KHZ(5);
static constexpr uint32_t kUpdatePeriodMs = 100; static constexpr uint32_t kUpdatePeriodMs = 100;
static constexpr double kOutMin = 0.0; static constexpr double kOutMin = 0.0;
static constexpr double kOutMax = 1.0; static constexpr double kOutMax = 1.0;
static constexpr double kKp = 2.0;
// ── PI gains (tune these) ───────────────────────── static constexpr double kKi = 0.02;
static constexpr double kKp = 2.0; // proportional
static constexpr double kKi = 0.02; // integral
static constexpr double kKd = 0.0; // derivative (not needed for heating-only)
inline static bool s_active{false}; inline static bool s_active{false};
inline static double s_target{0.0}; inline static double s_target{0.0};
@ -92,45 +65,44 @@ private:
DEVICE_DT_GET(DT_NODELABEL(heating_pad_ntc)); DEVICE_DT_GET(DT_NODELABEL(heating_pad_ntc));
inline static k_timer s_timer; inline static k_timer s_timer;
inline static k_work s_work;
inline static bool s_work_pending{false};
/// Called periodically by the timer. /// Timer fires → submit work. Skips if previous work hasn't finished.
static auto Tick(k_timer * /*timer*/) -> void { 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; double current_temp;
if (ReadTemperature(current_temp) != 0) { if (ReadTemperature(current_temp) != 0) {
printk("[heat] ERR: read temp failed\n"); s_work_pending = false;
return; return;
} }
printk("[heat] tick: cur=%.1f°C target=%.1f°C\n", current_temp, s_target); printk("[heat] cur=%.1f C target=%.1f C\n", current_temp, s_target);
// PI computation
const int64_t now = k_uptime_get(); const int64_t now = k_uptime_get();
const double dt = static_cast<double>(now - s_last_time) / 1000.0; const double dt = static_cast<double>(now - s_last_time) / 1000.0;
s_last_time = now; s_last_time = now;
const double error = s_target - current_temp; const double error = s_target - current_temp;
// Proportional
const double p = kKp * error;
// Integral with anti-windup
s_integral += kKi * error * dt; s_integral += kKi * error * dt;
s_integral = std::clamp(s_integral, kOutMin, kOutMax); s_integral = std::clamp(s_integral, kOutMin, kOutMax);
s_last_error = error; double output = kKp * error + s_integral;
// Compute output, clamp to [0, 1]
double output = p + s_integral;
output = std::clamp(output, kOutMin, kOutMax); output = std::clamp(output, kOutMin, kOutMax);
// Apply PWM duty cycle
const uint32_t pulse = static_cast<uint32_t>(output * kPeriodNs); const uint32_t pulse = static_cast<uint32_t>(output * kPeriodNs);
printk("[heat] PID out=%.2f%% pulse=%u / %u\n", output * 100.0, pulse,
s_pwm_spec.period);
pwm_set_pulse_dt(&s_pwm_spec, pulse); pwm_set_pulse_dt(&s_pwm_spec, pulse);
s_work_pending = false;
} }
/// Read temperature from the NTC sensor into `out_temp` (in °C).
static auto ReadTemperature(double &out_temp) -> int { static auto ReadTemperature(double &out_temp) -> int {
if (0 != sensor_sample_fetch(s_temp_sensor)) { if (0 != sensor_sample_fetch(s_temp_sensor)) {
printk("[heat] ERR: sensor fetch failed\n"); printk("[heat] ERR: sensor fetch failed\n");
@ -145,8 +117,10 @@ private:
} }
out_temp = sensor_value_to_double(&val); out_temp = sensor_value_to_double(&val);
s_current_temp = val;
return 0; return 0;
} }
inline static sensor_value s_current_temp{};
}; };
} // namespace ther } // namespace ther

View File

@ -1,5 +1,6 @@
#ifndef THER_NTC_HPP #ifndef THER_NTC_HPP
#define THER_NTC_HPP #define THER_NTC_HPP
#include <array>
#include <cmath> #include <cmath>
#include <zephyr/device.h> #include <zephyr/device.h>
#include <zephyr/drivers/sensor.h> #include <zephyr/drivers/sensor.h>
@ -30,7 +31,8 @@ public:
continue; continue;
} }
printk("[ntc]#%zu = %d.%d°C\n", i + 1, val.val1, val.val2); // Always cache the latest reading for individual query
s_current_temp[i] = val;
if (!found || val.val1 > max_val.val1 || if (!found || val.val1 > max_val.val1 ||
(val.val1 == max_val.val1 && val.val2 > max_val.val2)) { (val.val1 == max_val.val1 && val.val2 > max_val.val2)) {
@ -46,9 +48,17 @@ public:
return zpp::error_code::k_io; 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: private:
static constexpr size_t NTC_COUNT = 8; static constexpr size_t NTC_COUNT = 8;
inline static std::array<sensor_value, NTC_COUNT> s_current_temp{};
inline static const device *s_devices[] = { inline static const device *s_devices[] = {
DEVICE_DT_GET(DT_NODELABEL(pole_ntc1)), DEVICE_DT_GET(DT_NODELABEL(pole_ntc1)),
DEVICE_DT_GET(DT_NODELABEL(pole_ntc2)), DEVICE_DT_GET(DT_NODELABEL(pole_ntc2)),

View File

@ -6,15 +6,23 @@ Frame format (SimpleProtocal, per DTS config):
H0(0x7E) H1(0xE7) CMD(1B) LEN(1B) DATA[0..N] CRC_LO CRC_HI 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 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: Usage:
python test.py /dev/ttyUSB0 --get-id (request device ID) python test.py /dev/ttyUSB0 --monitor
python test.py /dev/ttyUSB0 --state 1 (set running state) python test.py /dev/ttyUSB0 --heating 42
python test.py /dev/ttyUSB0 --monitor (listen for temp data) python test.py /dev/ttyUSB0 --read-pole-ntc 3
python test.py /dev/ttyUSB0 --baud 115200 --loop (poll ID in a loop) python test.py /dev/ttyUSB0 --read-heating-ntc
""" """
import argparse import argparse
import struct
import sys import sys
import time import time
@ -25,27 +33,22 @@ except ImportError:
sys.exit(1) sys.exit(1)
# ── Protocol constants ──────────────────────────────────────── # ── Protocol constants ────────────────────────────────────────
HEADER = b"\x7e\xe7" # 2-byte header (DTS: header = <0x7EE7>) HEADER = b"\x7e\xe7"
HEADER_SIZE = len(HEADER) HEADER_SIZE = len(HEADER)
CMD_TEMP = 0x00 CMD_TEMP = 0x00
CMD_GET_ID = 0x01 CMD_GET_ID = 0x01
CMD_RUNNING_STATE = 0x02 CMD_RUNNING_STATE = 0x02
CMD_W_HEATING = 0x03
CMD_R_HEATING = 0x04
CMD_READ_POLE_NTC = 0x64
CMD_READ_HEATING_NTC = 0x65
STATE_NAMES = { HEATING_STATE_NAMES = {0: "OFF", 1: "ON"}
0x00: "Standby",
0x01: "Running",
0x02: "Pause",
0x03: "Fault",
0x04: "Upgrade",
0x05: "EStop",
0x06: "Exception",
}
# ── CRC-16 Modbus ───────────────────────────────────────────── # ── CRC-16 Modbus ─────────────────────────────────────────────
def crc16_modbus(data: bytes) -> int: def crc16_modbus(data: bytes) -> int:
"""CRC-16 Modbus (polynomial 0xA001, init 0xFFFF)."""
crc = 0xFFFF crc = 0xFFFF
for byte in data: for byte in data:
crc ^= byte crc ^= byte
@ -59,128 +62,165 @@ def crc16_modbus(data: bytes) -> int:
# ── Frame helpers ───────────────────────────────────────────── # ── Frame helpers ─────────────────────────────────────────────
def build_frame(cmd: int, payload: bytes = b"") -> bytes: def build_frame(cmd: int, payload: bytes = b"") -> bytes:
"""Build a SimpleProtocal frame with LSB-MSB CRC.
Frame: H0 H1 CMD LEN DATA[0..N] CRC_LO CRC_HI"""
body = bytes([cmd, len(payload)]) + payload body = bytes([cmd, len(payload)]) + payload
crc = crc16_modbus(body) crc = crc16_modbus(body)
crc_lo = crc & 0xFF return HEADER + body + bytes([crc & 0xFF, (crc >> 8) & 0xFF])
crc_hi = (crc >> 8) & 0xFF
return HEADER + body + bytes([crc_lo, crc_hi])
def parse_frame(frame: bytes) -> tuple[int, bytes] | None: def parse_frame(frame: bytes) -> tuple[int, bytes] | None:
"""Parse a SimpleProtocal frame with 2-byte header + LSB-MSB CRC. if len(frame) < HEADER_SIZE + 2 + 2:
Returns (cmd, data) or None on error."""
min_len = HEADER_SIZE + 2 + 2 # HDR(2) + CMD(1) + LEN(1) + CRC(2) = 6
if len(frame) < min_len:
return None return None
if frame[:HEADER_SIZE] != HEADER: if frame[:HEADER_SIZE] != HEADER:
return None return None
cmd = frame[HEADER_SIZE] cmd = frame[HEADER_SIZE]
length = frame[HEADER_SIZE + 1] length = frame[HEADER_SIZE + 1]
expected = HEADER_SIZE + 2 + length + 2 # HDR + CMD+LEN + DATA + CRC expected = HEADER_SIZE + 2 + length + 2
if len(frame) != expected: if len(frame) != expected:
return None return None
data = frame[HEADER_SIZE + 2 : HEADER_SIZE + 2 + length] data = frame[HEADER_SIZE + 2 : HEADER_SIZE + 2 + length]
# CRC covers CMD + LEN + DATA (LSB-MSB order on wire)
crc_body = frame[HEADER_SIZE : HEADER_SIZE + 2 + length] crc_body = frame[HEADER_SIZE : HEADER_SIZE + 2 + length]
actual_crc = crc16_modbus(crc_body) actual_crc = crc16_modbus(crc_body)
crc_lo = frame[HEADER_SIZE + 2 + length] wire_crc = (frame[expected - 1] << 8) | frame[expected - 2]
crc_hi = frame[HEADER_SIZE + 2 + length + 1]
wire_crc = (crc_hi << 8) | crc_lo
if actual_crc != wire_crc: if actual_crc != wire_crc:
return None return None
return cmd, data return cmd, data
# ── Serial reader ───────────────────────────────────────────── # ── Serial reader ─────────────────────────────────────────────
class ProtocolReader: class ProtocolReader:
"""State-machine frame reader for 2-byte header protocol."""
def __init__(self, ser: serial.Serial): def __init__(self, ser: serial.Serial):
self._ser = ser self._ser = ser
self._buf = bytearray() self._buf = bytearray()
def read_frame(self, timeout: float = 1.0) -> bytes | None: def read_frame(self, timeout: float = 1.0) -> bytes | None:
"""Read one complete frame. Returns raw frame bytes or None on timeout."""
min_frame = HEADER_SIZE + 2 + 2 # HDR + CMD + LEN + CRC = 6
deadline = time.time() + timeout deadline = time.time() + timeout
while time.time() < deadline: while time.time() < deadline:
# Look for header
if len(self._buf) >= 2: if len(self._buf) >= 2:
idx = self._buf.find(HEADER) idx = self._buf.find(HEADER)
if idx > 0: if idx > 0:
del self._buf[:idx] # discard bytes before header del self._buf[:idx]
elif idx < 0 and len(self._buf) > 0: elif idx < 0:
self._buf.clear() self._buf.clear()
# Have a header → try to parse frame length if len(self._buf) >= HEADER_SIZE and self._buf[:HEADER_SIZE] == HEADER:
if len(self._buf) >= HEADER_SIZE: if len(self._buf) >= HEADER_SIZE + 2 + 2:
if self._buf[:HEADER_SIZE] == HEADER: length = self._buf[HEADER_SIZE + 1]
if len(self._buf) >= min_frame: total = HEADER_SIZE + 2 + length + 2
length = self._buf[HEADER_SIZE + 1] if len(self._buf) >= total:
total = HEADER_SIZE + 2 + length + 2 frame = bytes(self._buf[:total])
if len(self._buf) >= total: del self._buf[:total]
frame = bytes(self._buf[:total]) return frame
del self._buf[:total] elif len(self._buf) >= HEADER_SIZE:
return frame del self._buf[0]
else: continue
# Wrong header, discard first byte and retry
del self._buf[0]
continue
# Read more bytes
try: try:
chunk = self._ser.read(self._ser.in_waiting or 1) chunk = self._ser.read(self._ser.in_waiting or 1)
if chunk: if chunk:
self._buf.extend(chunk) self._buf.extend(chunk)
except (serial.SerialTimeoutException, serial.SerialException): except (serial.SerialTimeoutException, serial.SerialException):
pass pass
return None return None
# ── Command handlers ────────────────────────────────────────── def _read_reply(ser: serial.Serial, timeout: float = 2.0) -> tuple[int, bytes] | None:
def cmd_get_id(ser: serial.Serial, timeout: float = 2.0):
"""Send GET_ID request and print response."""
print("→ Sending GET_ID...")
ser.write(build_frame(CMD_GET_ID))
reader = ProtocolReader(ser) reader = ProtocolReader(ser)
frame = reader.read_frame(timeout) frame = reader.read_frame(timeout)
if frame is None: if frame is None:
print("✗ No response (timeout)") print("✗ No response (timeout)")
return return None
result = parse_frame(frame) result = parse_frame(frame)
if result is None: if result is None:
print(f"✗ Invalid frame: {frame.hex()}") print(f"✗ Invalid frame: {frame.hex()}")
return return None
return result
cmd, data = 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: if cmd != CMD_GET_ID:
print(f"✗ Unexpected cmd=0x{cmd:02X} (expected 0x{CMD_GET_ID:02X})") print(f"✗ Unexpected cmd=0x{cmd:02X}")
return return
print(f"✓ Device ID ({len(data)} bytes): {data.hex(' ')}") print(f"✓ Device ID ({len(data)} bytes): {data.hex(' ')}")
def cmd_set_state(ser: serial.Serial, state: int): def cmd_set_state(ser: serial.Serial, state: int):
"""Send RUNNING_STATE command.""" names = {0: "Standby", 1: "Running", 2: "Pause", 3: "Error"}
name = STATE_NAMES.get(state, "Unknown") name = names.get(state, "Unknown")
print(f"→ Setting state: {name} (0x{state:02X})") print(f"→ Setting state: {name} (0x{state:02X})")
ser.write(build_frame(CMD_RUNNING_STATE, bytes([state]))) ser.write(build_frame(CMD_RUNNING_STATE, bytes([state])))
print(f"✓ Sent") 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): def cmd_monitor(ser: serial.Serial):
"""Listen for incoming frames (TEMP data) and print them."""
print("Monitoring... (Ctrl+C to stop)") print("Monitoring... (Ctrl+C to stop)")
reader = ProtocolReader(ser) reader = ProtocolReader(ser)
try: try:
@ -188,7 +228,6 @@ def cmd_monitor(ser: serial.Serial):
frame = reader.read_frame(timeout=0.5) frame = reader.read_frame(timeout=0.5)
if frame is None: if frame is None:
continue continue
result = parse_frame(frame) result = parse_frame(frame)
if result is None: if result is None:
print(f"⚠ Bad frame: {frame.hex()}") print(f"⚠ Bad frame: {frame.hex()}")
@ -196,28 +235,34 @@ def cmd_monitor(ser: serial.Serial):
cmd, data = result cmd, data = result
if cmd == CMD_TEMP: if cmd == CMD_TEMP:
if len(data) >= 2: t = _temp_from_data(data)
# sensor_value: val1 (int8), val2 (int8) print(f"🌡 Temp: {t:.2f}°C (raw: {data.hex(' ')})")
t = data[0]
frac = data[1] / 100.0
temp = t + frac
print(f"🌡 Temp: {temp:.2f} °C (raw: {data.hex(' ')})")
else:
print(f"⚠ TEMP with invalid data: {data.hex()}")
elif cmd == CMD_GET_ID: elif cmd == CMD_GET_ID:
print(f"🆔 Device ID response: {data.hex(' ')}") print(f"🆔 Device ID: {data.hex(' ')}")
elif cmd == CMD_RUNNING_STATE: elif cmd == CMD_RUNNING_STATE:
print(f"🔁 Running state echo: 0x{data.hex()}") 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: else:
print(f"📦 Unknown cmd=0x{cmd:02X} data={data.hex()}") print(f"📦 cmd=0x{cmd:02X} data={data.hex()}")
except KeyboardInterrupt: except KeyboardInterrupt:
print("\nDone.") print("\nDone.")
def cmd_loop(ser: serial.Serial, count: int = 0): def cmd_loop(ser: serial.Serial, count: int = 0):
"""Send GET_ID in a loop."""
i = 0 i = 0
print(f"Looping GET_ID... (Ctrl+C to stop)") print("Looping GET_ID... (Ctrl+C to stop)")
try: try:
while count == 0 or i < count: while count == 0 or i < count:
cmd_get_id(ser, timeout=1.0) cmd_get_id(ser, timeout=1.0)
@ -230,46 +275,37 @@ def cmd_loop(ser: serial.Serial, count: int = 0):
# ── Main ────────────────────────────────────────────────────── # ── Main ──────────────────────────────────────────────────────
def main(): def main():
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Test script for app_photomagnetic communication protocol" description="Test script for app_photomagnetic comm protocol"
) )
parser.add_argument("port", help="Serial port (e.g. /dev/ttyUSB0, COM3)") 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( parser.add_argument(
"--baud", type=int, default=115200, help="Baud rate (default: 115200)" "--state", type=lambda x: int(x, 0), metavar="N",
) help="Set running state")
parser.add_argument( parser.add_argument(
"--timeout", type=float, default=0.1, help="Serial timeout in seconds" "--heating", type=int, metavar="TEMP",
) help="Write heating target (0=stop)")
parser.add_argument( parser.add_argument(
"--get-id", action="store_true", help="Send GET_ID and print response" "--read-heating", action="store_true",
) help="Read heating state + temp")
parser.add_argument( parser.add_argument(
"--state", "--read-pole-ntc", type=int, metavar="IDX",
type=lambda x: int(x, 0), help="Read pole NTC by index (0..7)")
metavar="N",
help="Set running state (0=Standby, 1=Running, 2=Pause, ...)",
)
parser.add_argument( parser.add_argument(
"--monitor", action="store_true", help="Listen for incoming frames" "--read-heating-ntc", action="store_true",
) help="Read heating pad NTC temperature")
parser.add_argument("--monitor", action="store_true")
parser.add_argument( parser.add_argument(
"--loop", "--loop", type=int, nargs="?", const=0, metavar="N",
type=int, help="Loop GET_ID N times")
nargs="?",
const=0,
metavar="N",
help="Send GET_ID in a loop (N times, or infinite if omitted)",
)
parser.add_argument( parser.add_argument(
"--raw", "--raw", type=lambda x: bytes.fromhex(x), metavar="HEX",
type=lambda x: bytes.fromhex(x), help="Send raw CMD+DATA")
metavar="HEX",
help="Send raw payload (CMD DATA..., CRC auto-appended)",
)
parser.add_argument("--debug", action="store_true", help="Show raw frame hex")
args = parser.parse_args() args = parser.parse_args()
ser = serial.Serial(args.port, args.baud, timeout=args.timeout) ser = serial.Serial(args.port, args.baud, timeout=0.1)
print(f"Connected to {args.port} @ {args.baud} baud") print(f"Connected to {args.port} @ {args.baud} baud")
try: try:
@ -277,20 +313,28 @@ def main():
cmd_get_id(ser) cmd_get_id(ser)
elif args.state is not None: elif args.state is not None:
cmd_set_state(ser, args.state) 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: elif args.monitor:
cmd_monitor(ser) cmd_monitor(ser)
elif args.loop is not None: elif args.loop is not None:
cmd_loop(ser, args.loop) cmd_loop(ser, args.loop)
elif args.raw is not None: elif args.raw is not None:
payload = args.raw payload = args.raw
cmd, data = payload[0], payload[1:] if len(payload) > 1 else b"" c = payload[0]
print(f"→ Sending CMD=0x{cmd:02X} data={data.hex()}") d = payload[1:] if len(payload) > 1 else b""
frame = build_frame(cmd, data) print(f"→ Sending CMD=0x{c:02X} data={d.hex()}")
print(f" Frame: {frame.hex()}") ser.write(build_frame(c, d))
ser.write(frame) print("✓ Sent")
print(f"✓ Sent")
else: else:
print("No action. Use --get-id, --state N, --monitor, --loop, or --raw") print("Use --get-id, --state, --heating, --read-heating, "
"--read-pole-ntc, --read-heating-ntc, --monitor, --loop, --raw")
finally: finally:
ser.close() ser.close()

View File

@ -20,17 +20,20 @@ auto GetmaxTemp() -> std::optional<sensor_value> {
max_val = *ntc_result; max_val = *ntc_result;
found = true; found = true;
} }
// 红外数据就绪时参与比较 // 红外数据就绪时参与比较
sensor_value ir_val{}; sensor_value ir_val{};
if (0 == sensor_sample_fetch_chan(infrared, SENSOR_CHAN_AMBIENT_TEMP)) { if (0 == sensor_sample_fetch_chan(infrared, SENSOR_CHAN_AMBIENT_TEMP)) {
if (0 == if (0 ==
sensor_channel_get(infrared, SENSOR_CHAN_AMBIENT_TEMP, &ir_val)) { sensor_channel_get(infrared, SENSOR_CHAN_AMBIENT_TEMP, &ir_val)) {
printk("[ir] = %d.%d°C\n", ir_val.val1, ir_val.val2); double ir_t = sensor_value_to_double(&ir_val);
// printk("[ir] = %.2f°C\n", ir_t);
if (!found || ir_val.val1 > max_val.val1 || if (!found || ir_val.val1 > max_val.val1 ||
(ir_val.val1 == max_val.val1 && ir_val.val2 > max_val.val2)) { (ir_val.val1 == max_val.val1 && ir_val.val2 > max_val.val2)) {
max_val = ir_val; max_val = ir_val;
} }
max_val = ir_val;
// printk("[out] sent = %.2f°C\n", sensor_value_to_double(&max_val));
is_infrared_ready = false; is_infrared_ready = false;
} }
} }
@ -45,7 +48,7 @@ auto main(void) -> int {
// auto wdt = app::WatchDogConfig{}; // auto wdt = app::WatchDogConfig{};
StatusLed::Flash(1s); StatusLed::Flash(1s);
ther::Com::Init(); ther::Com::Init();
ther::HeatingPad::Start(sensor_value{42, 0}); // ther::HeatingPad::Start(sensor_value{42, 0});
sensor_trigger tri{.type = SENSOR_TRIG_DATA_READY, sensor_trigger tri{.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_AMBIENT_TEMP}; .chan = SENSOR_CHAN_AMBIENT_TEMP};
sensor_trigger_set(infrared, &tri, sensor_trigger_set(infrared, &tri,
@ -56,7 +59,8 @@ auto main(void) -> int {
if (auto v = GetmaxTemp(); v.has_value()) { if (auto v = GetmaxTemp(); v.has_value()) {
ther::Com::Send(v.value()); ther::Com::Send(v.value());
} }
k_sleep(K_MSEC(50));
k_sleep(K_MSEC(20));
} }
return 0; return 0;
} }