Merge branch 'develop' of ssh://git.mannheim.ccc.de:1337/C3MA/PlantCtrl into develop
This commit is contained in:
1
Software/MainBoard/rust/erase_ota.sh
Executable file
1
Software/MainBoard/rust/erase_ota.sh
Executable file
@@ -0,0 +1 @@
|
||||
cargo espflash erase-parts otadata --partition-table partitions.csv
|
||||
BIN
Software/MainBoard/rust/panic_image.bin
Normal file
BIN
Software/MainBoard/rust/panic_image.bin
Normal file
Binary file not shown.
@@ -18,7 +18,7 @@ pub trait BatteryInteraction {
|
||||
async fn reset(&mut self) -> FatResult<()>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, Copy, Clone)]
|
||||
pub struct BatteryInfo {
|
||||
pub voltage_milli_volt: u32,
|
||||
pub average_current_milli_ampere: i32,
|
||||
|
||||
@@ -320,16 +320,16 @@ impl Esp<'_> {
|
||||
|
||||
let ntp_addrs = stack
|
||||
.dns_query(NTP_SERVER, DnsQueryType::A)
|
||||
.await
|
||||
.expect("Failed to resolve DNS");
|
||||
if ntp_addrs.is_empty() {
|
||||
.await;
|
||||
if ntp_addrs.is_err() {
|
||||
bail!("Failed to resolve DNS");
|
||||
}
|
||||
info!("NTP server: {ntp_addrs:?}");
|
||||
let ntp = ntp_addrs.unwrap()[0];
|
||||
info!("NTP server: {ntp:?}");
|
||||
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
let addr: IpAddr = ntp_addrs[0].into();
|
||||
let addr: IpAddr = ntp.into();
|
||||
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
|
||||
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
|
||||
.await;
|
||||
|
||||
@@ -357,6 +357,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
}
|
||||
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
|
||||
self.can_power.set_high();
|
||||
Timer::after_millis(500).await;
|
||||
let config = self.twai_config.take().expect("twai config not set");
|
||||
let mut twai = config.into_async().start();
|
||||
|
||||
@@ -364,7 +365,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(5000))
|
||||
.with_timeout(Duration::from_millis(1000))
|
||||
.await;
|
||||
|
||||
let config = twai.stop().into_blocking();
|
||||
@@ -374,6 +375,74 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
Ok(moistures)
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
|
||||
self.can_power.set_high();
|
||||
Timer::after_millis(500).await;
|
||||
let config = self.twai_config.take().expect("twai config not set");
|
||||
let mut twai = config.into_async().start();
|
||||
|
||||
Timer::after_millis(1000).await;
|
||||
info!("Sending info messages now");
|
||||
// Send a few test messages per potential sensor node
|
||||
for plant in 0..PLANT_COUNT {
|
||||
for sensor in [Sensor::A, Sensor::B] {
|
||||
let detect = if sensor == Sensor::A {
|
||||
request.plant[plant].sensor_a
|
||||
} else {
|
||||
request.plant[plant].sensor_b
|
||||
};
|
||||
if !detect {
|
||||
continue;
|
||||
}
|
||||
let target = StandardId::new(plant_id(
|
||||
IDENTIFY_CMD_OFFSET,
|
||||
sensor.into(),
|
||||
(plant + 1) as u16,
|
||||
))
|
||||
.context(">> Could not create address for sensor! (plant: {}) <<")?;
|
||||
let can_buffer = [0_u8; 0];
|
||||
info!(
|
||||
"Sending test message to plant {} sensor {sensor:?} with id {}",
|
||||
plant + 1,
|
||||
target.as_raw()
|
||||
);
|
||||
if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
|
||||
// Try a few times; we intentionally ignore rx here and rely on stub logic
|
||||
let resu = twai
|
||||
.transmit_async(&frame)
|
||||
.with_timeout(Duration::from_millis(500))
|
||||
.await;
|
||||
match resu {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Error sending test message to plant {} sensor {sensor:?}: {err:?}",
|
||||
plant + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Error building CAN frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(3000))
|
||||
.await;
|
||||
|
||||
let config = twai.stop().into_blocking();
|
||||
self.twai_config.replace(config);
|
||||
|
||||
self.can_power.set_low();
|
||||
|
||||
let result = moistures.into();
|
||||
|
||||
info!("Autodetection result: {result:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn general_fault(&mut self, enable: bool) {
|
||||
hold_disable(23);
|
||||
self.general_fault.set_level(enable.into());
|
||||
@@ -443,73 +512,6 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn detect_sensors(&mut self, request: Detection) -> FatResult<Detection> {
|
||||
self.can_power.set_high();
|
||||
let config = self.twai_config.take().expect("twai config not set");
|
||||
let mut twai = config.into_async().start();
|
||||
|
||||
Timer::after_millis(1000).await;
|
||||
info!("Sending info messages now");
|
||||
// Send a few test messages per potential sensor node
|
||||
for plant in 0..PLANT_COUNT {
|
||||
for sensor in [Sensor::A, Sensor::B] {
|
||||
let detect = if sensor == Sensor::A {
|
||||
request.plant[plant].sensor_a
|
||||
} else {
|
||||
request.plant[plant].sensor_b
|
||||
};
|
||||
if !detect {
|
||||
continue;
|
||||
}
|
||||
let target = StandardId::new(plant_id(
|
||||
IDENTIFY_CMD_OFFSET,
|
||||
sensor.into(),
|
||||
(plant + 1) as u16,
|
||||
))
|
||||
.context(">> Could not create address for sensor! (plant: {}) <<")?;
|
||||
let can_buffer = [0_u8; 0];
|
||||
info!(
|
||||
"Sending test message to plant {} sensor {sensor:?} with id {}",
|
||||
plant + 1,
|
||||
target.as_raw()
|
||||
);
|
||||
if let Some(frame) = EspTwaiFrame::new(target, &can_buffer) {
|
||||
// Try a few times; we intentionally ignore rx here and rely on stub logic
|
||||
let resu = twai
|
||||
.transmit_async(&frame)
|
||||
.with_timeout(Duration::from_millis(3000))
|
||||
.await;
|
||||
match resu {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
info!(
|
||||
"Error sending test message to plant {} sensor {sensor:?}: {err:?}",
|
||||
plant + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!("Error building CAN frame");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut moistures = Moistures::default();
|
||||
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
||||
.with_timeout(Duration::from_millis(3000))
|
||||
.await;
|
||||
|
||||
let config = twai.stop().into_blocking();
|
||||
self.twai_config.replace(config);
|
||||
|
||||
self.can_power.set_low();
|
||||
|
||||
let result = moistures.into();
|
||||
|
||||
info!("Autodetection result: {result:?}");
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_can_measurements(
|
||||
|
||||
@@ -509,11 +509,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
.unwrap_or(BatteryState::Unknown);
|
||||
info!("Battery state is {battery_state:?}");
|
||||
|
||||
let state_of_charge = match &battery_state {
|
||||
BatteryState::Unknown => 0,
|
||||
BatteryState::Info(data) => data.state_of_charge,
|
||||
};
|
||||
|
||||
let mut light_state = LightState {
|
||||
enabled: board.board_hal.get_config().night_lamp.enabled,
|
||||
..Default::default()
|
||||
@@ -530,13 +525,22 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
board.board_hal.get_config().night_lamp.night_lamp_hour_end,
|
||||
);
|
||||
|
||||
if state_of_charge < board.board_hal.get_config().night_lamp.low_soc_cutoff {
|
||||
board.board_hal.get_esp().set_low_voltage_in_cycle();
|
||||
info!("Set low voltage in cycle");
|
||||
} else if state_of_charge > board.board_hal.get_config().night_lamp.low_soc_restore {
|
||||
board.board_hal.get_esp().clear_low_voltage_in_cycle();
|
||||
info!("Clear low voltage in cycle");
|
||||
match battery_state {
|
||||
BatteryState::Unknown => {
|
||||
light_state.battery_low = false;
|
||||
}
|
||||
BatteryState::Info(data) => {
|
||||
if data.state_of_charge < board.board_hal.get_config().night_lamp.low_soc_cutoff {
|
||||
board.board_hal.get_esp().set_low_voltage_in_cycle();
|
||||
info!("Set low voltage in cycle");
|
||||
}
|
||||
if data.state_of_charge > board.board_hal.get_config().night_lamp.low_soc_restore {
|
||||
board.board_hal.get_esp().clear_low_voltage_in_cycle();
|
||||
info!("Clear low voltage in cycle");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
light_state.battery_low = board.board_hal.get_esp().low_voltage_in_cycle();
|
||||
|
||||
if !light_state.out_of_work_hour {
|
||||
@@ -583,7 +587,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
||||
|
||||
let deep_sleep_duration_minutes: u32 =
|
||||
// if battery soc is unknown assume battery has enough change
|
||||
if state_of_charge < 10 && !matches!(battery_state, BatteryState::Unknown) {
|
||||
if matches!(battery_state, BatteryState::Info(data) if data.state_of_charge < 10) {
|
||||
let _ = board
|
||||
.board_hal
|
||||
.get_esp()
|
||||
@@ -1002,7 +1006,64 @@ async fn wait_infinity(
|
||||
let mut pattern_step = 0;
|
||||
let serial_config_receive = AtomicBool::new(false);
|
||||
let mut suppress_further_mppt_error = false;
|
||||
|
||||
// Long-press exit (for webserver config modes): hold boot button for 5 seconds.
|
||||
let mut exit_hold_started: Option<Instant> = None;
|
||||
let exit_hold_duration = Duration::from_secs(5);
|
||||
let mut exit_hold_blink = false;
|
||||
loop {
|
||||
// While in config webserver mode, allow exiting via long-press.
|
||||
if matches!(wait_type, WaitType::MissingConfig | WaitType::ConfigButton) {
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
let pressed = board.board_hal.get_esp().mode_override_pressed();
|
||||
|
||||
match (pressed, exit_hold_started) {
|
||||
(true, None) => {
|
||||
exit_hold_started = Some(Instant::now());
|
||||
PROGRESS_ACTIVE.store(true, Ordering::Relaxed);
|
||||
}
|
||||
(false, Some(_)) => {
|
||||
exit_hold_started = None;
|
||||
// Clear any interim hold display.
|
||||
board.board_hal.clear_progress().await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if let Some(started) = exit_hold_started {
|
||||
let elapsed = Instant::now() - started;
|
||||
// Visible countdown: fill LEDs progressively during the hold.
|
||||
// Also toggle general fault LED to match the "enter config" visibility.
|
||||
exit_hold_blink = !exit_hold_blink;
|
||||
|
||||
let progress = core::cmp::min(elapsed, exit_hold_duration);
|
||||
let lit = ((progress.as_millis() as u64 * 8)
|
||||
/ exit_hold_duration.as_millis() as u64)
|
||||
.saturating_add(1)
|
||||
.min(8) as usize;
|
||||
|
||||
for i in 0..8 {
|
||||
let _ = board.board_hal.fault(i, i < lit).await;
|
||||
}
|
||||
board.board_hal.general_fault(exit_hold_blink).await;
|
||||
|
||||
if elapsed >= exit_hold_duration {
|
||||
info!("Exiting config mode due to 5s button hold");
|
||||
board.board_hal.get_esp().set_restart_to_conf(false);
|
||||
// ensure clean http answer / visible confirmation
|
||||
Timer::after_millis(500).await;
|
||||
board.board_hal.deep_sleep(0).await;
|
||||
}
|
||||
|
||||
// Short tick while holding so the pattern updates smoothly.
|
||||
drop(board);
|
||||
Timer::after_millis(100).await;
|
||||
continue;
|
||||
}
|
||||
// Release lock and continue with normal wait blinking.
|
||||
drop(board);
|
||||
}
|
||||
|
||||
{
|
||||
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||
match update_charge_indicator(&mut board).await {
|
||||
@@ -1145,7 +1206,6 @@ async fn main(spawner: Spawner) -> ! {
|
||||
}
|
||||
}
|
||||
println!("Hal init done, starting logic");
|
||||
panic!("test");
|
||||
match safe_main(spawner).await {
|
||||
// this should not get triggered, safe_main should not return but go into deep sleep or reboot
|
||||
Ok(_) => {
|
||||
|
||||
@@ -191,36 +191,72 @@ export class Controller {
|
||||
}
|
||||
|
||||
uploadNewFirmware(file: File) {
|
||||
let current = 0;
|
||||
let max = 100;
|
||||
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", event => {
|
||||
current = event.loaded / 1000;
|
||||
max = event.total / 1000;
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const arrayBuffer = reader.result as ArrayBuffer;
|
||||
const data = new Uint8Array(arrayBuffer);
|
||||
const crc = this.crc32(data);
|
||||
const size = data.length;
|
||||
|
||||
console.log("Uploading new firmware with size " + size + " and crc " + crc + "")
|
||||
|
||||
const payload = new Uint8Array(size + 8);
|
||||
const view = new DataView(payload.buffer);
|
||||
view.setUint32(0, size, true);
|
||||
view.setUint32(4, crc, true);
|
||||
payload.set(data, 8);
|
||||
|
||||
let current = 0;
|
||||
let max = 100;
|
||||
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
const status = ajax.status;
|
||||
if (status >= 200 && status < 300) {
|
||||
controller.reboot();
|
||||
} else {
|
||||
const statusText = ajax.statusText || "";
|
||||
const body = ajax.responseText || "";
|
||||
toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`);
|
||||
const ajax = new XMLHttpRequest();
|
||||
ajax.upload.addEventListener("progress", event => {
|
||||
current = event.loaded / 1000;
|
||||
max = event.total / 1000;
|
||||
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||
}, false);
|
||||
ajax.addEventListener("load", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
const status = ajax.status;
|
||||
if (status >= 200 && status < 300) {
|
||||
controller.reboot();
|
||||
} else {
|
||||
const statusText = ajax.statusText || "";
|
||||
const body = ajax.responseText || "";
|
||||
toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`);
|
||||
}
|
||||
}, false);
|
||||
ajax.addEventListener("error", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
toast.error("OTA upload failed due to a network error.");
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
toast.error("OTA upload was aborted.");
|
||||
}, false);
|
||||
ajax.open("POST", PUBLIC_URL + "/ota");
|
||||
ajax.send(payload);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
toast.error("Error reading firmware file.");
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
}
|
||||
|
||||
private crc32(data: Uint8Array): number {
|
||||
let crc = 0xFFFFFFFF;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let byte = data[i];
|
||||
crc ^= byte;
|
||||
for (let j = 0; j < 8; j++) {
|
||||
if (crc & 1) {
|
||||
crc = (crc >>> 1) ^ 0xEDB88320;
|
||||
} else {
|
||||
crc = crc >>> 1;
|
||||
}
|
||||
}
|
||||
}, false);
|
||||
ajax.addEventListener("error", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
toast.error("OTA upload failed due to a network error.");
|
||||
}, false);
|
||||
ajax.addEventListener("abort", () => {
|
||||
controller.progressview.removeProgress("ota_upload")
|
||||
toast.error("OTA upload was aborted.");
|
||||
}, false);
|
||||
ajax.open("POST", PUBLIC_URL + "/ota");
|
||||
ajax.send(file);
|
||||
}
|
||||
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||
}
|
||||
|
||||
async version(): Promise<void> {
|
||||
@@ -264,12 +300,14 @@ export class Controller {
|
||||
method: "POST",
|
||||
body: json,
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(text => statusCallback(text))
|
||||
.then( _ => {
|
||||
controller.progressview.removeProgress("set_config");
|
||||
setTimeout(() => { controller.downloadConfig() }, 250)
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(text => statusCallback(text))
|
||||
.then(_ => {
|
||||
controller.progressview.removeProgress("set_config");
|
||||
setTimeout(() => {
|
||||
controller.downloadConfig()
|
||||
}, 250)
|
||||
})
|
||||
}
|
||||
|
||||
async backupConfig(json: string): Promise<string> {
|
||||
@@ -290,7 +328,6 @@ export class Controller {
|
||||
method: "POST",
|
||||
body: pretty
|
||||
}).then(
|
||||
|
||||
_ => controller.progressview.removeProgress("write_rtc")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,12 +35,8 @@
|
||||
<span class="otavalue" id="firmware_partition"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">State0:</span>
|
||||
<span class="otavalue" id="firmware_state0"></span>
|
||||
</div>
|
||||
<div class="flexcontainer">
|
||||
<span class="otakey">State1:</span>
|
||||
<span class="otavalue" id="firmware_state1"></span>
|
||||
<span class="otakey">State:</span>
|
||||
<span class="otavalue" id="firmware_state"></span>
|
||||
</div>
|
||||
|
||||
<div class="flexcontainer">
|
||||
|
||||
Reference in New Issue
Block a user