#ifndef __THER_INFRARED_HPP__ #define __THER_INFRARED_HPP__ #include #include #include #include #include #include #include #include #include #include #include namespace ther { class Infrared { public: static auto Init() -> zpp::error { return InitImpl(std::make_index_sequence{}); } /* 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 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> { 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(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 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 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 static auto InitImpl(std::index_sequence) -> 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)), ...); 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 s_cb; }; } // namespace ther #endif