forked from EmbeddedTeam/app_photomagnetic
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.
163 lines
5.6 KiB
C++
163 lines
5.6 KiB
C++
#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
|