Add firmware build timestamp support for sensors; update detection workflows and UI accordingly.
This commit is contained in:
@@ -170,10 +170,20 @@ pub trait BoardInteraction<'a> {
|
||||
async fn backup_info(&mut self) -> FatResult<BackupHeader>;
|
||||
|
||||
// Return JSON string with autodetected sensors per plant. Default: not supported.
|
||||
async fn detect_sensors(&mut self, _request: Detection) -> FatResult<Detection> {
|
||||
async fn detect_sensors(&mut self, _request: DetectionRequest) -> FatResult<Detection> {
|
||||
bail!("Autodetection is only available on v4 HAL with CAN bus");
|
||||
}
|
||||
|
||||
/// Return the last known firmware build timestamps per sensor, set during detect_sensors.
|
||||
fn get_sensor_build_minutes(
|
||||
&self,
|
||||
) -> (
|
||||
[Option<u32>; PLANT_COUNT],
|
||||
[Option<u32>; PLANT_COUNT],
|
||||
) {
|
||||
([None; PLANT_COUNT], [None; PLANT_COUNT])
|
||||
}
|
||||
|
||||
async fn progress(&mut self, counter: u32) {
|
||||
// Indicate progress is active to suppress default wait_infinity blinking
|
||||
PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
|
||||
@@ -657,14 +667,34 @@ pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSub
|
||||
pub struct Moistures {
|
||||
pub sensor_a_hz: [Option<f32>; PLANT_COUNT],
|
||||
pub sensor_b_hz: [Option<f32>; PLANT_COUNT],
|
||||
pub sensor_a_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
pub sensor_b_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
}
|
||||
|
||||
/// Request: which sensors to send IDENTIFY_CMD to.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct DetectionRequest {
|
||||
pub plant: [SensorRequest; PLANT_COUNT],
|
||||
}
|
||||
|
||||
/// Per-sensor portion of a detection request.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct SensorRequest {
|
||||
pub sensor_a: bool,
|
||||
pub sensor_b: bool,
|
||||
}
|
||||
|
||||
/// Response: detection result per plant.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct Detection {
|
||||
plant: [DetectionSensorResult; PLANT_COUNT],
|
||||
pub plant: [DetectionSensorResult; PLANT_COUNT],
|
||||
}
|
||||
|
||||
/// Per-sensor detection result.
|
||||
/// `Some(build_minutes)` = sensor responded; value is its firmware build timestamp
|
||||
/// (minutes since Unix epoch, or 0 if not reported). `None` = not detected.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub struct DetectionSensorResult {
|
||||
sensor_a: bool,
|
||||
sensor_b: bool,
|
||||
pub sensor_a: Option<u32>,
|
||||
pub sensor_b: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@ use crate::hal::esp::{hold_disable, hold_enable, Esp};
|
||||
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction, EEPROM_PAGE, X25};
|
||||
use crate::hal::water::TankSensor;
|
||||
use crate::hal::{
|
||||
BoardInteraction, Detection, FreePeripherals, Moistures, Sensor, I2C_DRIVER, PLANT_COUNT,
|
||||
BoardInteraction, Detection, DetectionRequest, FreePeripherals, Moistures, Sensor, I2C_DRIVER,
|
||||
PLANT_COUNT,
|
||||
};
|
||||
use crate::log::{log, LogMessage};
|
||||
use alloc::boxed::Box;
|
||||
@@ -143,6 +144,11 @@ pub struct V4<'a> {
|
||||
extra1: Output<'a>,
|
||||
extra2: Output<'a>,
|
||||
twai_config: Option<TwaiConfiguration<'static, Blocking>>,
|
||||
|
||||
/// Last known firmware build timestamps per sensor (minutes since Unix epoch).
|
||||
/// Updated during detect_sensors; preserved across normal measurement cycles.
|
||||
sensor_a_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
sensor_b_build_minutes: [Option<u32>; PLANT_COUNT],
|
||||
}
|
||||
|
||||
pub(crate) async fn create_v4(
|
||||
@@ -272,6 +278,8 @@ pub(crate) async fn create_v4(
|
||||
extra2,
|
||||
can_power,
|
||||
twai_config,
|
||||
sensor_a_build_minutes: [None; PLANT_COUNT],
|
||||
sensor_b_build_minutes: [None; PLANT_COUNT],
|
||||
};
|
||||
Ok(Box::new(v))
|
||||
}
|
||||
@@ -393,6 +401,20 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
self.twai_config.replace(config);
|
||||
self.can_power.set_low();
|
||||
|
||||
// Persist any firmware build timestamps received alongside moisture data.
|
||||
if let Ok(ref moistures) = res {
|
||||
for (i, v) in moistures.sensor_a_build_minutes.iter().enumerate() {
|
||||
if v.is_some() {
|
||||
self.sensor_a_build_minutes[i] = *v;
|
||||
}
|
||||
}
|
||||
for (i, v) in moistures.sensor_b_build_minutes.iter().enumerate() {
|
||||
if v.is_some() {
|
||||
self.sensor_b_build_minutes[i] = *v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
@@ -535,7 +557,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
|
||||
async fn detect_sensors(&mut self, request: DetectionRequest) -> FatResult<Detection> {
|
||||
self.can_power.set_high();
|
||||
Timer::after_millis(500).await;
|
||||
let config = self.twai_config.take().context("twai config not set")?;
|
||||
@@ -558,6 +580,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
} else {
|
||||
request.plant[plant].sensor_b
|
||||
};
|
||||
|
||||
if !detect {
|
||||
continue;
|
||||
}
|
||||
@@ -602,7 +625,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
let result: Detection = moistures.into();
|
||||
|
||||
info!("Autodetection result: {result:?}");
|
||||
Ok(result)
|
||||
Ok((result, moistures.sensor_a_build_minutes, moistures.sensor_b_build_minutes))
|
||||
})
|
||||
.await;
|
||||
|
||||
@@ -610,7 +633,20 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
self.twai_config.replace(config);
|
||||
self.can_power.set_low();
|
||||
|
||||
res
|
||||
match res {
|
||||
Ok((detection, a_builds, b_builds)) => {
|
||||
self.sensor_a_build_minutes = a_builds;
|
||||
self.sensor_b_build_minutes = b_builds;
|
||||
Ok(detection)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_sensor_build_minutes(
|
||||
&self,
|
||||
) -> ([Option<u32>; PLANT_COUNT], [Option<u32>; PLANT_COUNT]) {
|
||||
(self.sensor_a_build_minutes, self.sensor_b_build_minutes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -631,10 +667,10 @@ async fn wait_for_can_measurements(
|
||||
"received message of kind {:?} (plant: {}, sensor: {:?})",
|
||||
msg.0, msg.1, msg.2
|
||||
);
|
||||
let plant = msg.1 as usize;
|
||||
let sensor = msg.2;
|
||||
let data = can_frame.data();
|
||||
if msg.0 == MessageKind::MoistureData {
|
||||
let plant = msg.1 as usize;
|
||||
let sensor = msg.2;
|
||||
let data = can_frame.data();
|
||||
info!("Received moisture data: {:?}", data);
|
||||
if let Ok(bytes) = data.try_into() {
|
||||
let frequency = u32::from_be_bytes(bytes);
|
||||
@@ -651,6 +687,23 @@ async fn wait_for_can_measurements(
|
||||
} else {
|
||||
error!("Received moisture data with invalid length: {} (expected 4)", data.len());
|
||||
}
|
||||
} else if msg.0 == MessageKind::FirmwareBuild {
|
||||
info!("Received firmware build data: {:?}", data);
|
||||
if let Ok(bytes) = data.try_into() {
|
||||
let build_minutes = u32::from_be_bytes(bytes);
|
||||
match sensor {
|
||||
SensorSlot::A => {
|
||||
moistures.sensor_a_build_minutes[plant - 1] =
|
||||
Some(build_minutes);
|
||||
}
|
||||
SensorSlot::B => {
|
||||
moistures.sensor_b_build_minutes[plant - 1] =
|
||||
Some(build_minutes);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("Received firmware build data with invalid length: {} (expected 4)", data.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -677,10 +730,17 @@ impl From<Moistures> for Detection {
|
||||
fn from(value: Moistures) -> Self {
|
||||
let mut result = Detection::default();
|
||||
for (plant, sensor) in value.sensor_a_hz.iter().enumerate() {
|
||||
result.plant[plant].sensor_a = sensor.is_some();
|
||||
if sensor.is_some() {
|
||||
// Sensor responded; include build timestamp (0 = timestamp not reported)
|
||||
result.plant[plant].sensor_a =
|
||||
Some(value.sensor_a_build_minutes[plant].unwrap_or(0));
|
||||
}
|
||||
}
|
||||
for (plant, sensor) in value.sensor_b_hz.iter().enumerate() {
|
||||
result.plant[plant].sensor_b = sensor.is_some();
|
||||
if sensor.is_some() {
|
||||
result.plant[plant].sensor_b =
|
||||
Some(value.sensor_b_build_minutes[plant].unwrap_or(0));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
@@ -84,6 +84,11 @@ pub struct PlantState {
|
||||
pub sensor_a: MoistureSensorState,
|
||||
pub sensor_b: MoistureSensorState,
|
||||
pub pump: PumpState,
|
||||
/// Last known firmware build timestamp for sensor A (minutes since Unix epoch).
|
||||
/// Set during sensor detection; None if detection has not been run yet.
|
||||
pub sensor_a_firmware_build_minutes: Option<u32>,
|
||||
/// Last known firmware build timestamp for sensor B.
|
||||
pub sensor_b_firmware_build_minutes: Option<u32>,
|
||||
}
|
||||
|
||||
fn map_range_moisture(
|
||||
@@ -157,6 +162,7 @@ impl PlantState {
|
||||
|
||||
let previous_pump = board.board_hal.get_esp().last_pump_time(plant_id);
|
||||
let consecutive_pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id);
|
||||
let (a_builds, b_builds) = board.board_hal.get_sensor_build_minutes();
|
||||
let state = Self {
|
||||
sensor_a,
|
||||
sensor_b,
|
||||
@@ -164,6 +170,8 @@ impl PlantState {
|
||||
consecutive_pump_count,
|
||||
previous_pump,
|
||||
},
|
||||
sensor_a_firmware_build_minutes: a_builds[plant_id],
|
||||
sensor_b_firmware_build_minutes: b_builds[plant_id],
|
||||
};
|
||||
if state.is_err() {
|
||||
let _ = board.board_hal.fault(plant_id, true).await;
|
||||
@@ -286,6 +294,8 @@ impl PlantState {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
sensor_a_firmware_build_minutes: self.sensor_a_firmware_build_minutes,
|
||||
sensor_b_firmware_build_minutes: self.sensor_b_firmware_build_minutes,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,4 +324,8 @@ pub struct PlantInfo<'a> {
|
||||
last_pump: Option<DateTime<Tz>>,
|
||||
/// next time when pump should activate
|
||||
next_pump: Option<DateTime<Tz>>,
|
||||
/// firmware build timestamp of sensor A (minutes since Unix epoch); None if unknown
|
||||
sensor_a_firmware_build_minutes: Option<u32>,
|
||||
/// firmware build timestamp of sensor B (minutes since Unix epoch); None if unknown
|
||||
sensor_b_firmware_build_minutes: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::config::PlantControllerConfig;
|
||||
use crate::fat_error::FatResult;
|
||||
use crate::hal::Detection;
|
||||
use crate::hal::DetectionRequest;
|
||||
use crate::webserver::read_up_to_bytes_from_request;
|
||||
use crate::{do_secure_pump, BOARD_ACCESS};
|
||||
use alloc::borrow::ToOwned;
|
||||
@@ -64,7 +64,7 @@ where
|
||||
T: Read + Write,
|
||||
{
|
||||
let actual_data = read_up_to_bytes_from_request(request, None).await?;
|
||||
let detect: Detection = serde_json::from_slice(&actual_data)?;
|
||||
let detect: DetectionRequest = serde_json::from_slice(&actual_data)?;
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let result = board.board_hal.detect_sensors(detect).await?;
|
||||
let json = serde_json::to_string(&result)?;
|
||||
|
||||
@@ -199,9 +199,22 @@ export interface BatteryState {
|
||||
state_of_health: string
|
||||
}
|
||||
|
||||
export interface DetectionPlant {
|
||||
/// Request: which sensors to send IDENTIFY_CMD to.
|
||||
export interface SensorRequest {
|
||||
sensor_a: boolean,
|
||||
sensor_b: boolean
|
||||
sensor_b: boolean,
|
||||
}
|
||||
|
||||
export interface DetectionRequest {
|
||||
plant: SensorRequest[]
|
||||
}
|
||||
|
||||
/// Response: detection result per plant.
|
||||
/// sensor_a / sensor_b: firmware build timestamp in minutes since Unix epoch,
|
||||
/// or null if the sensor did not respond.
|
||||
export interface DetectionPlant {
|
||||
sensor_a: number | null,
|
||||
sensor_b: number | null,
|
||||
}
|
||||
|
||||
export interface Detection {
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
SetTime, SSIDList, TankInfo,
|
||||
TestPump,
|
||||
VersionInfo,
|
||||
SaveInfo, SolarState, PumpTestResult, Detection, CanPower
|
||||
SaveInfo, SolarState, PumpTestResult, Detection, DetectionRequest, CanPower
|
||||
} from "./api";
|
||||
import {SolarView} from "./solarview";
|
||||
import {toast} from "./toast";
|
||||
@@ -339,7 +339,7 @@ export class Controller {
|
||||
)
|
||||
}
|
||||
|
||||
async detectSensors(detection: Detection, silent: boolean = false) {
|
||||
async detectSensors(detection: DetectionRequest, silent: boolean = false) {
|
||||
let counter = 0
|
||||
let limit = 5
|
||||
if (!silent) {
|
||||
@@ -577,7 +577,7 @@ export class Controller {
|
||||
this.hardwareView = new HardwareConfigView(this)
|
||||
this.detectBtn = document.getElementById("detect_sensors") as HTMLButtonElement
|
||||
this.detectBtn.onclick = () => {
|
||||
const detection: Detection = {
|
||||
const detection: DetectionRequest = {
|
||||
plant: Array.from({length: PLANT_COUNT}, () => ({
|
||||
sensor_a: true,
|
||||
sensor_b: true,
|
||||
@@ -615,7 +615,7 @@ export class Controller {
|
||||
|
||||
try {
|
||||
await this.measure_moisture(true);
|
||||
const detection: Detection = {
|
||||
const detection: DetectionRequest = {
|
||||
plant: Array.from({length: PLANT_COUNT}, () => ({
|
||||
sensor_a: true,
|
||||
sensor_b: true,
|
||||
|
||||
@@ -133,10 +133,18 @@
|
||||
<span class="plantsensorkey">Sensor A:</span>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_moisture_a">not measured</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<span class="plantsensorkey">Sensor A FW:</span>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_sensor_a_fw_build">unknown</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Sensor B:</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_moisture_b">not measured</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantSensorEnabledOnly_${plantId}">
|
||||
<span class="plantsensorkey">Sensor B FW:</span>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_sensor_b_fw_build">unknown</span>
|
||||
</div>
|
||||
<div class="flexcontainer plantPumpEnabledOnly_${plantId}">
|
||||
<div class="plantsensorkey">Max Current</div>
|
||||
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_max">not_tested</span>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import {DetectionPlant, Detection, PlantConfig, PumpTestResult} from "./api";
|
||||
import {Detection, DetectionPlant, DetectionRequest, PlantConfig, PumpTestResult} from "./api";
|
||||
|
||||
export const PLANT_COUNT = 8;
|
||||
|
||||
/** Format a firmware build timestamp (minutes since Unix epoch) as a human-readable date/time. */
|
||||
function formatBuildMinutes(buildMinutes: number | null): string {
|
||||
if (buildMinutes === null) return "not detected";
|
||||
if (buildMinutes === 0) return "detected (no timestamp)";
|
||||
const ms = buildMinutes * 60 * 1000;
|
||||
return new Date(ms).toISOString().replace("T", " ").slice(0, 16) + " UTC";
|
||||
}
|
||||
|
||||
import {Controller} from "./main";
|
||||
|
||||
@@ -79,6 +86,8 @@ export class PlantView {
|
||||
private readonly mode: HTMLSelectElement;
|
||||
private readonly moistureA: HTMLElement;
|
||||
private readonly moistureB: HTMLElement;
|
||||
private readonly sensorAFwBuild: HTMLElement;
|
||||
private readonly sensorBFwBuild: HTMLElement;
|
||||
private readonly maxConsecutivePumpCount: HTMLInputElement;
|
||||
private readonly minPumpCurrentMa: HTMLInputElement;
|
||||
private readonly maxPumpCurrentMa: HTMLInputElement;
|
||||
@@ -109,6 +118,8 @@ export class PlantView {
|
||||
|
||||
this.moistureA = document.getElementById("plant_" + plantId + "_moisture_a")! as HTMLElement;
|
||||
this.moistureB = document.getElementById("plant_" + plantId + "_moisture_b")! as HTMLElement;
|
||||
this.sensorAFwBuild = document.getElementById("plant_" + plantId + "_sensor_a_fw_build")! as HTMLElement;
|
||||
this.sensorBFwBuild = document.getElementById("plant_" + plantId + "_sensor_b_fw_build")! as HTMLElement;
|
||||
|
||||
this.pump_test_current_max = document.getElementById("plant_" + plantId + "_pump_test_current_max")! as HTMLElement;
|
||||
this.pump_test_current_min = document.getElementById("plant_" + plantId + "_pump_test_current_min")! as HTMLElement;
|
||||
@@ -124,7 +135,7 @@ export class PlantView {
|
||||
|
||||
this.testSensorAButton = document.getElementById("plant_" + plantId + "_test_sensor_a")! as HTMLButtonElement;
|
||||
this.testSensorAButton.onclick = () => {
|
||||
const detection: Detection = {
|
||||
const detection: DetectionRequest = {
|
||||
plant: Array.from({length: PLANT_COUNT}, (_v, idx) => ({
|
||||
sensor_a: idx === plantId,
|
||||
sensor_b: false,
|
||||
@@ -135,7 +146,7 @@ export class PlantView {
|
||||
|
||||
this.testSensorBButton = document.getElementById("plant_" + plantId + "_test_sensor_b")! as HTMLButtonElement;
|
||||
this.testSensorBButton.onclick = () => {
|
||||
const detection: Detection = {
|
||||
const detection: DetectionRequest = {
|
||||
plant: Array.from({length: PLANT_COUNT}, (_v, idx) => ({
|
||||
sensor_a: false,
|
||||
sensor_b: idx === plantId,
|
||||
@@ -360,19 +371,23 @@ export class PlantView {
|
||||
}
|
||||
|
||||
setDetectionResult(plantResult: DetectionPlant) {
|
||||
console.log("setDetectionResult plantResult: " + plantResult.sensor_a + " " + plantResult.sensor_b)
|
||||
const sensorADetected = plantResult.sensor_a !== null;
|
||||
const sensorBDetected = plantResult.sensor_b !== null;
|
||||
console.log("setDetectionResult plantResult: a=" + plantResult.sensor_a + " b=" + plantResult.sensor_b);
|
||||
var changed = false;
|
||||
if (this.sensorAInstalled.checked != plantResult.sensor_a) {
|
||||
if (this.sensorAInstalled.checked != sensorADetected) {
|
||||
changed = true;
|
||||
this.sensorAInstalled.checked = plantResult.sensor_a;
|
||||
this.sensorAInstalled.checked = sensorADetected;
|
||||
}
|
||||
if (this.sensorBInstalled.checked != plantResult.sensor_b) {
|
||||
if (this.sensorBInstalled.checked != sensorBDetected) {
|
||||
changed = true;
|
||||
this.sensorBInstalled.checked = plantResult.sensor_b;
|
||||
this.sensorBInstalled.checked = sensorBDetected;
|
||||
}
|
||||
if (changed) {
|
||||
this.controller.configChanged();
|
||||
}
|
||||
|
||||
this.sensorAFwBuild.innerText = formatBuildMinutes(plantResult.sensor_a);
|
||||
this.sensorBFwBuild.innerText = formatBuildMinutes(plantResult.sensor_b);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user