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.
This commit is contained in:
zhangyisong 2026-08-03 22:12:31 +08:00
parent a9e8885ec8
commit 0e5d15c814
10 changed files with 266 additions and 180 deletions

View File

@ -15,5 +15,11 @@ elseif(CONFIG_SAMPLE_NTC)
target_sources(app PRIVATE src/sample_ntc.cpp)
else()
message("build main app")
target_sources(app PRIVATE src/main.cpp)
target_sources(app PRIVATE
src/main.cpp
src/led.cpp
src/temp.cpp
src/watdog.cpp
)
endif()

View File

@ -2,6 +2,8 @@
#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>
@ -12,96 +14,52 @@ 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 Send(sensor_value val) -> void {
static auto SendTemp(sensor_value val) -> void {
const uint8_t temp_data[] = {(uint8_t)val.val1, (uint8_t)val.val2};
s_proto->Send(TEMP, temp_data);
s_proto->Send(R_TEMP, temp_data);
}
static auto AddRunningState(RunningStateSignal::slot_type slot) -> bool {
return s_running_state_sig.connect(slot);
}
private:
enum HostStatus : uint8_t { STANDBY, RUNNING, PAUSE, ERROR };
enum Addr : uint8_t {
TEMP = 0,
GET_ID,
RUNNING_STATE,
W_HEATING_STATE,
R_HEATING_STATE
R_TEMP = 0,
R_GET_ID,
W_RUNNING_STATE,
};
enum DevAddr : uint8_t {
R_TEMP_POLE_NTC = 100,
R_TEMP_HETING_PAD_NTC = 101,
};
using CbTableValueType =
std::pair<const uint8_t, void (*)(uart_com::DataType)>;
struct Cb {
static auto GetId(uart_com::DataType data) -> void {
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));
}
inline static RunningStateSignal s_running_state_sig{};
static auto RunningState(uart_com::DataType data) -> void {
if (data.size() != 1) {
return;
}
if (data[0] == RUNNING) {
} else {
s_running_state_sig(HostStatus(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;
}
}
/// Read heating pad NTC (onboard ADC).
static auto ReadHeatingPadNtcTemperature(uart_com::DataType data) -> void {}
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[] = {
{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},
{R_GET_ID, GetId},
{W_RUNNING_STATE, 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)));
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

View File

@ -1,8 +1,10 @@
#ifndef __THER_INFRARED_HPP__
#define __THER_INFRARED_HPP__
#include <etl/algorithm.h>
#include <etl/bitset.h>
#include <etl/array.h>
#include <etl/delegate.h>
#include <etl/tuple.h>
#include <utility>
#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zpp/result.hpp>
@ -13,42 +15,34 @@ namespace ther {
class Infrared {
public:
static auto Init() -> zpp::error {
static sensor_trigger tri{.type = SENSOR_TRIG_DATA_READY,
.chan = SENSOR_CHAN_AMBIENT_TEMP};
for (auto dev : s_dev) {
if (auto r = sensor_trigger_set(
dev, &tri,
[](const device *dev, const sensor_trigger *trig) {
const size_t id = etl::distance(
s_dev.begin(), etl::find(s_dev.begin(), s_dev.end(), dev));
s_dev_ready[id] = 1;
});
r != 0) {
return -ENODEV;
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();
}
/* One-shot fetch of ALL sensors, return the hottest one. */
/* 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) {
/* -ENODATA just means no new sample for this channel; keep going. */
if (0 != sensor_sample_fetch(s_dev[id])) {
if (!s_latest_valid[id]) {
continue;
}
sensor_value val;
if (0 != sensor_channel_get(s_dev[id], SENSOR_CHAN_AMBIENT_TEMP, &val)) {
s_dev_ready[id] = 0; /* sample consumed, drop the ready flag */
continue;
}
s_dev_ready[id] = 0;
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;
@ -63,11 +57,81 @@ public:
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;
}
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();
}
#define INFRARED_DEV(node) DEVICE_DT_GET(node),
inline static etl::array s_dev{
DT_FOREACH_STATUS_OKAY(godtek_temp_uart, INFRARED_DEV)};
inline static etl::bitset<s_dev.size()> s_dev_ready;
/* 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

View File

@ -7,89 +7,26 @@
#include <zpp/work_queue.hpp>
namespace ther {
/* Compile-time check: every LED name must be distinct so that
* Led::On/Off/Flash<kName> name lookup stays unambiguous. */
template <typename T, std::size_t N>
consteval auto AreUniqueNames(const etl::array<T, N> &names) -> bool {
for (std::size_t i = 0; i < N; ++i) {
for (std::size_t j = i + 1; j < N; ++j) {
if (names[i] == names[j]) {
return false;
}
}
}
return true;
}
#define LED_SPEC(node) led_dt_spec LED_DT_SPEC_GET(node)
#define LED_DEV_CHILD_SPEC(node) DT_FOREACH_CHILD_SEP(node, LED_SPEC, (, )),
#define LED_NODE_NAME(node) DT_FOREACH_CHILD_SEP(node, DEVICE_DT_NAME, (, ))
class Led {
private:
template <led_dt_spec> struct Periodic {
static auto Flash(led_dt_spec *spec) -> void {
bool flag = false;
if (flag) {
led_on_dt(spec);
} else {
led_off_dt(spec);
}
flag = !flag;
}
static inline zpp::periodic_work<led_dt_spec *> work{Flash};
};
template <led_dt_spec kSpec> class Led {
public:
template <zpp::ct_string kName> static auto On() -> zpp::error {
static_assert(IsDeviceName<kName>());
const auto id = etl::find(s_led_node_name.begin(), s_led_node_name.end(),
std::string_view(kName)) -
s_led_node_name.begin();
return led_on_dt(&s_led_dev[id]);
}
template <zpp::ct_string kName> static auto Off() -> zpp::error {
static_assert(IsDeviceName<kName>());
const auto id = etl::find(s_led_node_name.begin(), s_led_node_name.end(),
std::string_view(kName)) -
s_led_node_name.begin();
return led_off_dt(&s_led_dev[id]);
}
template <zpp::ct_string kName, typename TRep, typename TPeriod>
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) {
static_assert(IsDeviceName<kName>());
const auto id = etl::find(s_led_node_name.begin(), s_led_node_name.end(),
std::string_view(kName)) -
s_led_node_name.begin();
Periodic<s_led_dev[id]>::work.submit(period);
s_work.submit(period);
}
private:
template <zpp::ct_string kName> static consteval auto IsDeviceName() {
for (size_t i = 0; i < s_led_node_name.size(); ++i) {
if (s_led_node_name[i] == std::string_view(kName)) {
return true;
}
}
return false;
}
static auto Flash(led_dt_spec *spec) -> void {
static auto Flash() -> void {
static bool flag = false;
if (flag) {
led_off_dt(spec);
led_off_dt(&kSpec);
} else {
led_on_dt(spec);
led_on_dt(&kSpec);
}
flag = !flag;
}
inline static constexpr etl::array s_led_dev{
DT_FOREACH_STATUS_OKAY(gpio_leds, LED_DEV_CHILD_SPEC)};
inline static constexpr auto s_led_node_name =
etl::make_array<std::string_view>(
DT_FOREACH_STATUS_OKAY(gpio_leds, LED_NODE_NAME));
static_assert(AreUniqueNames(s_led_node_name),
"LED node names must be unique");
static inline zpp::periodic_work<> s_work{Flash};
};
} // namespace ther

View File

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

View File

@ -8,9 +8,15 @@ CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=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

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,10 +1,11 @@
#include <com.hpp>
#include <zephyr/kernel.h>
auto main(void) -> int {
ther::Com::Init();
printk("[main] init on %s\n", CONFIG_BOARD);
while (1) {
k_sleep(K_MSEC(20));
k_sleep(K_SECONDS(1));
}
return 0;
}

69
src/temp.cpp Normal file
View File

@ -0,0 +1,69 @@
#include <com.hpp>
#include <etl/utility.h>
#include <infrared.hpp>
#include <zephyr/init.h>
#include <zephyr/kernel.h>
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);
}
k_work_schedule(&s_send_work, K_MSEC(kPeriodSend.count()));
}
} // namespace
static auto Init() -> int {
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);