forked from EmbeddedTeam/app_photomagnetic
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
This commit is contained in:
parent
1afea0b932
commit
eaa5d0c28b
@ -10,6 +10,10 @@ target_include_directories(app PRIVATE include)
|
|||||||
if(CONFIG_SAMPLE_HELLOWORLD)
|
if(CONFIG_SAMPLE_HELLOWORLD)
|
||||||
message("CONFIG_SAMPLE_HELLOWORLD is enabled")
|
message("CONFIG_SAMPLE_HELLOWORLD is enabled")
|
||||||
target_sources(app PRIVATE src/sample_hello_world.cpp)
|
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()
|
else()
|
||||||
|
message("build main app")
|
||||||
target_sources(app PRIVATE src/main.cpp)
|
target_sources(app PRIVATE src/main.cpp)
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
2
Kconfig
2
Kconfig
@ -4,3 +4,5 @@ endmenu
|
|||||||
|
|
||||||
config SAMPLE_HELLOWORLD
|
config SAMPLE_HELLOWORLD
|
||||||
bool "Hello World sample"
|
bool "Hello World sample"
|
||||||
|
config SAMPLE_NTC
|
||||||
|
bool "NTC sample"
|
||||||
|
|||||||
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,34 +1,59 @@
|
|||||||
#ifndef __THER_COM_HPP__
|
#ifndef __THER_COM_HPP__
|
||||||
#define __THER_COM_HPP__
|
#define __THER_COM_HPP__
|
||||||
|
|
||||||
|
#include "led.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/sensor.h>
|
||||||
#include <zpp/driver.hpp>
|
#include <zpp/driver.hpp>
|
||||||
#include <zpp/error.hpp>
|
#include <zpp/error.hpp>
|
||||||
namespace ther {
|
namespace ther {
|
||||||
|
|
||||||
template <device *kDev> class Com {
|
class Com {
|
||||||
public:
|
public:
|
||||||
enum Addr : uint8_t { TEMP = 0, GET_ID, RUNNING_STATE };
|
|
||||||
using CbTableValueType =
|
|
||||||
std::pair<const uint8_t, void (*)(uart_com::DataType)>;
|
|
||||||
static auto Init() -> zpp::error {
|
static auto Init() -> zpp::error {
|
||||||
s_proto->SetRxCallbackTable({GET_ID, Cb::GetId},
|
s_proto->SetRxCallbackTable(kRxCallbackTable);
|
||||||
{RUNNING_STATE, Cb::RunningState});
|
|
||||||
return zpp::ok();
|
return zpp::ok();
|
||||||
}
|
}
|
||||||
|
static auto Send(sensor_value val) -> void {
|
||||||
|
const uint8_t temp_data[] = {(uint8_t)val.val1, (uint8_t)val.val2};
|
||||||
|
s_proto->Send(TEMP, temp_data);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
enum HostStatus : uint8_t { STANDBY, RUNNING, PAUSE, ERROR };
|
||||||
|
enum Addr : uint8_t { TEMP = 0, GET_ID, RUNNING_STATE };
|
||||||
|
using CbTableValueType =
|
||||||
|
std::pair<const uint8_t, void (*)(uart_com::DataType)>;
|
||||||
struct Cb {
|
struct Cb {
|
||||||
static auto GetId(uart_com::DataType data) -> void {
|
static auto GetId(uart_com::DataType data) -> void {
|
||||||
// TODO
|
printk("handle get id");
|
||||||
|
uint8_t buff[20];
|
||||||
|
auto size = hwinfo_get_device_id(buff, sizeof(buff));
|
||||||
|
s_proto->Send(GET_ID, uart_com::DataType(buff, size));
|
||||||
}
|
}
|
||||||
static auto RunningState(uart_com::DataType data) -> void {
|
static auto RunningState(uart_com::DataType data) -> void {
|
||||||
// TODO
|
if (data.size() != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data[0] == RUNNING) {
|
||||||
|
InfLed::On();
|
||||||
|
} else {
|
||||||
|
InfLed::Off();
|
||||||
|
}
|
||||||
|
s_led_strip_indicator->Status(data[0]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
constexpr static uart_com::SimpleProtocal *s_proto =
|
constexpr static std::pair<const uint8_t,
|
||||||
(uart_com::SimpleProtocal *)(kDev);
|
uart_com::SimpleProtocal::CallbackType>
|
||||||
|
kRxCallbackTable[] = {{GET_ID, Cb::GetId},
|
||||||
|
{RUNNING_STATE, Cb::RunningState}};
|
||||||
|
inline static auto s_led_strip_indicator =
|
||||||
|
ZPP_DRV_GET_P(ledstrip::Indicator, DT_NODELABEL(indicator));
|
||||||
|
inline static auto s_proto =
|
||||||
|
(uart_com::SimpleProtocal *)(DEVICE_DT_GET(DT_NODELABEL(pm_protocal)));
|
||||||
|
using InfLed = ther::Led<LED_DT_SPEC_GET(DT_NODELABEL(inf_led))>;
|
||||||
};
|
};
|
||||||
} // namespace ther
|
} // namespace ther
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,154 @@
|
|||||||
#ifndef __THER_HEATING_HPP__
|
#ifndef __THER_HEATING_HPP__
|
||||||
#define __THER_HEATING_HPP__
|
#define __THER_HEATING_HPP__
|
||||||
|
#include <algorithm>
|
||||||
#include <zephyr/device.h>
|
#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 {
|
namespace ther {
|
||||||
template <const device *kDev> class HeatingPad {
|
|
||||||
|
class HeatingPad {
|
||||||
public:
|
public:
|
||||||
|
/// Start PID temperature control toward the given target.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Check PWM device
|
||||||
|
if (!device_is_ready(s_pwm_spec.dev)) {
|
||||||
|
printk("[heat] ERR: PWM device not ready\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
printk("[heat] PWM dev=%s ch=%d period=%u ns\n", s_pwm_spec.dev->name,
|
||||||
|
s_pwm_spec.channel, s_pwm_spec.period);
|
||||||
|
|
||||||
|
// Check sensor
|
||||||
|
if (!device_is_ready(s_temp_sensor)) {
|
||||||
|
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));
|
||||||
|
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 {
|
||||||
|
printk("[heat] stop\n");
|
||||||
|
s_active = false;
|
||||||
|
k_timer_stop(&s_timer);
|
||||||
|
pwm_set_pulse_dt(&s_pwm_spec, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the target temperature while keeping PID running.
|
||||||
|
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:
|
||||||
|
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;
|
||||||
|
|
||||||
|
// ── PI gains (tune these) ─────────────────────────
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
/// Called periodically by the timer.
|
||||||
|
static auto Tick(k_timer * /*timer*/) -> void {
|
||||||
|
double current_temp;
|
||||||
|
if (ReadTemperature(current_temp) != 0) {
|
||||||
|
printk("[heat] ERR: read temp failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printk("[heat] tick: cur=%.1f°C target=%.1f°C\n", current_temp, s_target);
|
||||||
|
|
||||||
|
// PI computation
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Proportional
|
||||||
|
const double p = kKp * error;
|
||||||
|
|
||||||
|
// Integral with anti-windup
|
||||||
|
s_integral += kKi * error * dt;
|
||||||
|
s_integral = std::clamp(s_integral, kOutMin, kOutMax);
|
||||||
|
|
||||||
|
s_last_error = error;
|
||||||
|
|
||||||
|
// Compute output, clamp to [0, 1]
|
||||||
|
double output = p + s_integral;
|
||||||
|
output = std::clamp(output, kOutMin, kOutMax);
|
||||||
|
|
||||||
|
// Apply PWM duty cycle
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read temperature from the NTC sensor into `out_temp` (in °C).
|
||||||
|
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);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace ther
|
} // namespace ther
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
29
include/led.hpp
Normal file
29
include/led.hpp
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#ifndef __THER_LED_HPP__
|
||||||
|
#define __THER_LED_HPP__
|
||||||
|
#include <zephyr/drivers/led.h>
|
||||||
|
#include <zpp/work_queue.hpp>
|
||||||
|
namespace ther {
|
||||||
|
|
||||||
|
template <led_dt_spec kSpec> class Led {
|
||||||
|
public:
|
||||||
|
static auto On() { led_on_dt(&kSpec); }
|
||||||
|
static auto Off() { led_off_dt(&kSpec); }
|
||||||
|
template <typename TRep, typename TPeriod>
|
||||||
|
static auto Flash(std::chrono::duration<TRep, TPeriod> period) {
|
||||||
|
s_work.submit(period);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
inline static zpp::periodic_work<> s_work{[]() {
|
||||||
|
static bool is_on = false;
|
||||||
|
if (is_on) {
|
||||||
|
Off();
|
||||||
|
} else {
|
||||||
|
On();
|
||||||
|
}
|
||||||
|
is_on = !is_on;
|
||||||
|
}};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace ther
|
||||||
|
#endif
|
||||||
@ -1,43 +1,55 @@
|
|||||||
#ifndef THER_NTC_HPP
|
#ifndef THER_NTC_HPP
|
||||||
#define THER_NTC_HPP
|
#define THER_NTC_HPP
|
||||||
|
#include <cmath>
|
||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
#include <zephyr/drivers/sensor.h>
|
#include <zephyr/drivers/sensor.h>
|
||||||
#include <zpp/result.hpp>
|
#include <zpp/result.hpp>
|
||||||
#include <zpp/value.hpp>
|
|
||||||
|
|
||||||
namespace ther {
|
namespace ther {
|
||||||
|
|
||||||
template <device *...kNtcDev> class Ntc {
|
class NtcGroup {
|
||||||
public:
|
public:
|
||||||
/// Read all NTC sensors, return the maximum temperature value.
|
/// Read all NTC sensors, return the maximum temperature value.
|
||||||
/// Returns error if every sensor read fails.
|
/// Returns error if every sensor read fails.
|
||||||
static auto GetSensorValue()
|
static auto GetSensorValue() -> zpp::result<sensor_value> {
|
||||||
-> zpp::result<zpp::value<zpp::value_type::AMBIENT_TEMP>> {
|
|
||||||
bool found = false;
|
bool found = false;
|
||||||
zpp::value<zpp::value_type::AMBIENT_TEMP> max_val;
|
sensor_value max_val{};
|
||||||
|
|
||||||
for (const auto *dev : s_devices) {
|
for (const auto *dev : s_devices) {
|
||||||
if (0 != sensor_sample_fetch(dev))
|
if (0 != sensor_sample_fetch(dev)) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
zpp::value<zpp::value_type::AMBIENT_TEMP> val;
|
sensor_value val{};
|
||||||
if (0 != sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &val))
|
if (0 != sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &val)) {
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!found || val.to_double() > max_val.to_double()) {
|
if (!found || val.val1 > max_val.val1 ||
|
||||||
|
(val.val1 == max_val.val1 && val.val2 > max_val.val2)) {
|
||||||
max_val = val;
|
max_val = val;
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (found)
|
if (found) {
|
||||||
return max_val;
|
return max_val;
|
||||||
|
}
|
||||||
|
|
||||||
return zpp::error_code::k_io;
|
return zpp::error_code::k_io;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr device *s_devices[] = {kNtcDev...};
|
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
|
} // namespace ther
|
||||||
|
|||||||
11
prj.conf
11
prj.conf
@ -9,5 +9,16 @@ CONFIG_UART_INTERRUPT_DRIVEN=y
|
|||||||
CONFIG_HWINFO=y
|
CONFIG_HWINFO=y
|
||||||
|
|
||||||
CONFIG_ADC=y
|
CONFIG_ADC=y
|
||||||
|
CONFIG_SPI=y
|
||||||
|
CONFIG_SPI_STM32=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
|
||||||
|
|||||||
299
scripts/test.py
Normal file
299
scripts/test.py
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python test.py /dev/ttyUSB0 --get-id (request device ID)
|
||||||
|
python test.py /dev/ttyUSB0 --state 1 (set running state)
|
||||||
|
python test.py /dev/ttyUSB0 --monitor (listen for temp data)
|
||||||
|
python test.py /dev/ttyUSB0 --baud 115200 --loop (poll ID in a loop)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import struct
|
||||||
|
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" # 2-byte header (DTS: header = <0x7EE7>)
|
||||||
|
HEADER_SIZE = len(HEADER)
|
||||||
|
|
||||||
|
CMD_TEMP = 0x00
|
||||||
|
CMD_GET_ID = 0x01
|
||||||
|
CMD_RUNNING_STATE = 0x02
|
||||||
|
|
||||||
|
STATE_NAMES = {
|
||||||
|
0x00: "Standby",
|
||||||
|
0x01: "Running",
|
||||||
|
0x02: "Pause",
|
||||||
|
0x03: "Fault",
|
||||||
|
0x04: "Upgrade",
|
||||||
|
0x05: "EStop",
|
||||||
|
0x06: "Exception",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── CRC-16 Modbus ─────────────────────────────────────────────
|
||||||
|
def crc16_modbus(data: bytes) -> int:
|
||||||
|
"""CRC-16 Modbus (polynomial 0xA001, init 0xFFFF)."""
|
||||||
|
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:
|
||||||
|
"""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
|
||||||
|
crc = crc16_modbus(body)
|
||||||
|
crc_lo = crc & 0xFF
|
||||||
|
crc_hi = (crc >> 8) & 0xFF
|
||||||
|
return HEADER + body + bytes([crc_lo, crc_hi])
|
||||||
|
|
||||||
|
|
||||||
|
def parse_frame(frame: bytes) -> tuple[int, bytes] | None:
|
||||||
|
"""Parse a SimpleProtocal frame with 2-byte header + LSB-MSB CRC.
|
||||||
|
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
|
||||||
|
if frame[:HEADER_SIZE] != HEADER:
|
||||||
|
return None
|
||||||
|
|
||||||
|
cmd = frame[HEADER_SIZE]
|
||||||
|
length = frame[HEADER_SIZE + 1]
|
||||||
|
expected = HEADER_SIZE + 2 + length + 2 # HDR + CMD+LEN + DATA + CRC
|
||||||
|
if len(frame) != expected:
|
||||||
|
return None
|
||||||
|
|
||||||
|
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]
|
||||||
|
actual_crc = crc16_modbus(crc_body)
|
||||||
|
crc_lo = frame[HEADER_SIZE + 2 + length]
|
||||||
|
crc_hi = frame[HEADER_SIZE + 2 + length + 1]
|
||||||
|
wire_crc = (crc_hi << 8) | crc_lo
|
||||||
|
|
||||||
|
if actual_crc != wire_crc:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return cmd, data
|
||||||
|
|
||||||
|
|
||||||
|
# ── Serial reader ─────────────────────────────────────────────
|
||||||
|
class ProtocolReader:
|
||||||
|
"""State-machine frame reader for 2-byte header protocol."""
|
||||||
|
|
||||||
|
def __init__(self, ser: serial.Serial):
|
||||||
|
self._ser = ser
|
||||||
|
self._buf = bytearray()
|
||||||
|
|
||||||
|
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
|
||||||
|
while time.time() < deadline:
|
||||||
|
# Look for header
|
||||||
|
if len(self._buf) >= 2:
|
||||||
|
idx = self._buf.find(HEADER)
|
||||||
|
if idx > 0:
|
||||||
|
del self._buf[:idx] # discard bytes before header
|
||||||
|
elif idx < 0 and len(self._buf) > 0:
|
||||||
|
self._buf.clear()
|
||||||
|
|
||||||
|
# Have a header → try to parse frame length
|
||||||
|
if len(self._buf) >= HEADER_SIZE:
|
||||||
|
if self._buf[:HEADER_SIZE] == HEADER:
|
||||||
|
if len(self._buf) >= min_frame:
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
# Wrong header, discard first byte and retry
|
||||||
|
del self._buf[0]
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Read more bytes
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# ── Command handlers ──────────────────────────────────────────
|
||||||
|
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)
|
||||||
|
frame = reader.read_frame(timeout)
|
||||||
|
if frame is None:
|
||||||
|
print("✗ No response (timeout)")
|
||||||
|
return
|
||||||
|
|
||||||
|
result = parse_frame(frame)
|
||||||
|
if result is None:
|
||||||
|
print(f"✗ Invalid frame: {frame.hex()}")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd, data = result
|
||||||
|
if cmd != CMD_GET_ID:
|
||||||
|
print(f"✗ Unexpected cmd=0x{cmd:02X} (expected 0x{CMD_GET_ID:02X})")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"✓ Device ID ({len(data)} bytes): {data.hex(' ')}")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_set_state(ser: serial.Serial, state: int):
|
||||||
|
"""Send RUNNING_STATE command."""
|
||||||
|
name = STATE_NAMES.get(state, "Unknown")
|
||||||
|
print(f"→ Setting state: {name} (0x{state:02X})")
|
||||||
|
ser.write(build_frame(CMD_RUNNING_STATE, bytes([state])))
|
||||||
|
print(f"✓ Sent")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_monitor(ser: serial.Serial):
|
||||||
|
"""Listen for incoming frames (TEMP data) and print them."""
|
||||||
|
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:
|
||||||
|
if len(data) >= 2:
|
||||||
|
# sensor_value: val1 (int8), val2 (int8)
|
||||||
|
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:
|
||||||
|
print(f"🆔 Device ID response: {data.hex(' ')}")
|
||||||
|
elif cmd == CMD_RUNNING_STATE:
|
||||||
|
print(f"🔁 Running state echo: 0x{data.hex()}")
|
||||||
|
else:
|
||||||
|
print(f"📦 Unknown cmd=0x{cmd:02X} data={data.hex()}")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_loop(ser: serial.Serial, count: int = 0):
|
||||||
|
"""Send GET_ID in a loop."""
|
||||||
|
i = 0
|
||||||
|
print(f"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 communication protocol"
|
||||||
|
)
|
||||||
|
parser.add_argument("port", help="Serial port (e.g. /dev/ttyUSB0, COM3)")
|
||||||
|
parser.add_argument(
|
||||||
|
"--baud", type=int, default=115200, help="Baud rate (default: 115200)"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout", type=float, default=0.1, help="Serial timeout in seconds"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--get-id", action="store_true", help="Send GET_ID and print response"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--state",
|
||||||
|
type=lambda x: int(x, 0),
|
||||||
|
metavar="N",
|
||||||
|
help="Set running state (0=Standby, 1=Running, 2=Pause, ...)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--monitor", action="store_true", help="Listen for incoming frames"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--loop",
|
||||||
|
type=int,
|
||||||
|
nargs="?",
|
||||||
|
const=0,
|
||||||
|
metavar="N",
|
||||||
|
help="Send GET_ID in a loop (N times, or infinite if omitted)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--raw",
|
||||||
|
type=lambda x: bytes.fromhex(x),
|
||||||
|
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()
|
||||||
|
|
||||||
|
ser = serial.Serial(args.port, args.baud, timeout=args.timeout)
|
||||||
|
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.monitor:
|
||||||
|
cmd_monitor(ser)
|
||||||
|
elif args.loop is not None:
|
||||||
|
cmd_loop(ser, args.loop)
|
||||||
|
elif args.raw is not None:
|
||||||
|
payload = args.raw
|
||||||
|
cmd, data = payload[0], payload[1:] if len(payload) > 1 else b""
|
||||||
|
print(f"→ Sending CMD=0x{cmd:02X} data={data.hex()}")
|
||||||
|
frame = build_frame(cmd, data)
|
||||||
|
print(f" Frame: {frame.hex()}")
|
||||||
|
ser.write(frame)
|
||||||
|
print(f"✓ Sent")
|
||||||
|
else:
|
||||||
|
print("No action. Use --get-id, --state N, --monitor, --loop, or --raw")
|
||||||
|
finally:
|
||||||
|
ser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
61
src/main.cpp
61
src/main.cpp
@ -1,16 +1,63 @@
|
|||||||
#include <com.hpp>
|
#include <com.hpp>
|
||||||
#include <heating.hpp>
|
#include <heating.hpp>
|
||||||
#include <infrared.hpp>
|
#include <infrared.hpp>
|
||||||
|
#include <led.hpp>
|
||||||
#include <ntc.hpp>
|
#include <ntc.hpp>
|
||||||
|
#include <optional>
|
||||||
#include <watchdog.hpp>
|
#include <watchdog.hpp>
|
||||||
namespace {} // namespace
|
namespace {
|
||||||
|
using StatusLed = ther::Led<LED_DT_SPEC_GET(DT_NODELABEL(status_led))>;
|
||||||
|
bool is_infrared_ready = false;
|
||||||
|
auto infrared = DEVICE_DT_GET(DT_NODELABEL(godtek));
|
||||||
|
auto GetmaxTemp() -> std::optional<sensor_value> {
|
||||||
|
if (is_infrared_ready) {
|
||||||
|
sensor_value max_val{};
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
auto main(void) -> int {
|
// NTC 8 路最大值
|
||||||
auto wdt = app::WatchDogConfig{};
|
auto ntc_result = ther::NtcGroup::GetSensorValue();
|
||||||
|
if (ntc_result) {
|
||||||
while (1) {
|
max_val = *ntc_result;
|
||||||
|
found = true;
|
||||||
wdt.Feed();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 红外数据就绪时参与比较
|
||||||
|
sensor_value ir_val{};
|
||||||
|
if (0 == sensor_sample_fetch_chan(infrared, SENSOR_CHAN_AMBIENT_TEMP)) {
|
||||||
|
if (0 ==
|
||||||
|
sensor_channel_get(infrared, SENSOR_CHAN_AMBIENT_TEMP, &ir_val)) {
|
||||||
|
if (!found || ir_val.val1 > max_val.val1 ||
|
||||||
|
(ir_val.val1 == max_val.val1 && ir_val.val2 > max_val.val2)) {
|
||||||
|
max_val = ir_val;
|
||||||
|
}
|
||||||
|
is_infrared_ready = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return max_val;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
auto main(void) -> int {
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
printk("app on board %s", CONFIG_BOARD);
|
||||||
|
// auto wdt = app::WatchDogConfig{};
|
||||||
|
StatusLed::Flash(1s);
|
||||||
|
ther::Com::Init();
|
||||||
|
// ther::HeatingPad::Start();
|
||||||
|
ther::HeatingPad::Start(sensor_value{42, 0});
|
||||||
|
sensor_trigger tri{.type = SENSOR_TRIG_DATA_READY,
|
||||||
|
.chan = SENSOR_CHAN_AMBIENT_TEMP};
|
||||||
|
sensor_trigger_set(infrared, &tri,
|
||||||
|
[](const device *dev, const sensor_trigger *trig) {
|
||||||
|
is_infrared_ready = true;
|
||||||
|
});
|
||||||
|
while (1) {
|
||||||
|
if (auto v = GetmaxTemp(); v.has_value()) {
|
||||||
|
ther::Com::Send(v.value());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// wdt.Feed();
|
||||||
|
k_sleep(K_MSEC(20));
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,18 @@
|
|||||||
#include <zephyr/device.h>
|
#include <zephyr/device.h>
|
||||||
|
#include <zephyr/drivers/sensor.h>
|
||||||
#include <zephyr/kernel.h>
|
#include <zephyr/kernel.h>
|
||||||
|
|
||||||
auto main() -> int { return 0; }
|
#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;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,3 +6,6 @@ tests:
|
|||||||
sample.helloworld:
|
sample.helloworld:
|
||||||
extra_configs:
|
extra_configs:
|
||||||
- CONFIG_SAMPLE_HELLOWORLD=y
|
- CONFIG_SAMPLE_HELLOWORLD=y
|
||||||
|
sample.ntc:
|
||||||
|
extra_configs:
|
||||||
|
- CONFIG_SAMPLE_NTC=y
|
||||||
|
|||||||
14
zbuild.py
14
zbuild.py
@ -6,16 +6,18 @@ Usage:
|
|||||||
python zbuild.py dr2501a_g070rb
|
python zbuild.py dr2501a_g070rb
|
||||||
python zbuild.py dr2501a_g070rb -p auto
|
python zbuild.py dr2501a_g070rb -p auto
|
||||||
python zbuild.py -p always (auto-detect board from west.yml)
|
python zbuild.py -p always (auto-detect board from west.yml)
|
||||||
|
python zbuild.py dr2501a_g070rb -T sample.helloworld
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
board Board name (optional, auto-detected from west.yml if omitted)
|
board Board name (optional, auto-detected from west.yml if omitted)
|
||||||
-p PURSE Pristine option: auto / always / never
|
-p PURSE Pristine option: auto / always / never
|
||||||
-t TARGET CMake target to run after build, e.g. test / flash
|
-t TARGET CMake target to run after build, e.g. test / flash
|
||||||
|
-T TEST Test name/sample identifier (e.g. sample.helloworld)
|
||||||
|
|
||||||
Steps:
|
Steps:
|
||||||
1. west topdir → find Zephyr workspace root
|
1. west topdir → find Zephyr workspace root
|
||||||
2. west config --local manifest.file <this_script_dir>/west.yml
|
2. west config --local manifest.file <this_script_dir>/west.yml
|
||||||
3. west build [-p PURSE] -b BOARD [-t TARGET] <this_script_dir>
|
3. west build [-p PURSE] -b BOARD [-t TARGET] [-T TEST] <this_script_dir>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@ -78,6 +80,13 @@ def main():
|
|||||||
default=None,
|
default=None,
|
||||||
help="CMake target to run after build (e.g. test, flash)",
|
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(
|
parser.add_argument(
|
||||||
"--update",
|
"--update",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
@ -117,6 +126,7 @@ def main():
|
|||||||
print(f" board : {args.board}")
|
print(f" board : {args.board}")
|
||||||
print(f" pristine : {args.pristine or '(none)'}")
|
print(f" pristine : {args.pristine or '(none)'}")
|
||||||
print(f" target : {args.target 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'}")
|
print(f" update : {'yes' if args.update else 'no'}")
|
||||||
|
|
||||||
# ── 1. Find workspace root ───────────────────────────────
|
# ── 1. Find workspace root ───────────────────────────────
|
||||||
@ -146,6 +156,8 @@ def main():
|
|||||||
build_cmd += ["-b", args.board]
|
build_cmd += ["-b", args.board]
|
||||||
if args.target:
|
if args.target:
|
||||||
build_cmd += ["-t", args.target]
|
build_cmd += ["-t", args.target]
|
||||||
|
if args.test_id:
|
||||||
|
build_cmd += ["-T", args.test_id]
|
||||||
build_cmd += [script_dir]
|
build_cmd += [script_dir]
|
||||||
|
|
||||||
print(f" $ {' '.join(build_cmd)}")
|
print(f" $ {' '.join(build_cmd)}")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user