Merge branch 'develop' of ssh://git.mannheim.ccc.de:1337/C3MA/PlantCtrl into develop
This commit is contained in:
BIN
Hardware/Sensor_Case/Body_v2.3mf
Normal file
BIN
Hardware/Sensor_Case/Body_v2.3mf
Normal file
Binary file not shown.
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<()>;
|
async fn reset(&mut self) -> FatResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize, Copy, Clone)]
|
||||||
pub struct BatteryInfo {
|
pub struct BatteryInfo {
|
||||||
pub voltage_milli_volt: u32,
|
pub voltage_milli_volt: u32,
|
||||||
pub average_current_milli_ampere: i32,
|
pub average_current_milli_ampere: i32,
|
||||||
|
|||||||
@@ -320,16 +320,16 @@ impl Esp<'_> {
|
|||||||
|
|
||||||
let ntp_addrs = stack
|
let ntp_addrs = stack
|
||||||
.dns_query(NTP_SERVER, DnsQueryType::A)
|
.dns_query(NTP_SERVER, DnsQueryType::A)
|
||||||
.await
|
.await;
|
||||||
.expect("Failed to resolve DNS");
|
if ntp_addrs.is_err() {
|
||||||
if ntp_addrs.is_empty() {
|
|
||||||
bail!("Failed to resolve DNS");
|
bail!("Failed to resolve DNS");
|
||||||
}
|
}
|
||||||
info!("NTP server: {ntp_addrs:?}");
|
let ntp = ntp_addrs.unwrap()[0];
|
||||||
|
info!("NTP server: {ntp:?}");
|
||||||
|
|
||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
loop {
|
loop {
|
||||||
let addr: IpAddr = ntp_addrs[0].into();
|
let addr: IpAddr = ntp.into();
|
||||||
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
|
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
|
||||||
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
|
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
|||||||
}
|
}
|
||||||
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
|
async fn measure_moisture_hz(&mut self) -> FatResult<Moistures> {
|
||||||
self.can_power.set_high();
|
self.can_power.set_high();
|
||||||
|
Timer::after_millis(500).await;
|
||||||
let config = self.twai_config.take().expect("twai config not set");
|
let config = self.twai_config.take().expect("twai config not set");
|
||||||
let mut twai = config.into_async().start();
|
let mut twai = config.into_async().start();
|
||||||
|
|
||||||
@@ -364,7 +365,7 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
|||||||
|
|
||||||
let mut moistures = Moistures::default();
|
let mut moistures = Moistures::default();
|
||||||
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
let _ = wait_for_can_measurements(&mut twai, &mut moistures)
|
||||||
.with_timeout(Duration::from_millis(5000))
|
.with_timeout(Duration::from_millis(1000))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let config = twai.stop().into_blocking();
|
let config = twai.stop().into_blocking();
|
||||||
@@ -374,6 +375,74 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
|||||||
Ok(moistures)
|
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) {
|
async fn general_fault(&mut self, enable: bool) {
|
||||||
hold_disable(23);
|
hold_disable(23);
|
||||||
self.general_fault.set_level(enable.into());
|
self.general_fault.set_level(enable.into());
|
||||||
@@ -443,73 +512,6 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
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(
|
async fn wait_for_can_measurements(
|
||||||
|
|||||||
@@ -509,11 +509,6 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
|||||||
.unwrap_or(BatteryState::Unknown);
|
.unwrap_or(BatteryState::Unknown);
|
||||||
info!("Battery state is {battery_state:?}");
|
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 {
|
let mut light_state = LightState {
|
||||||
enabled: board.board_hal.get_config().night_lamp.enabled,
|
enabled: board.board_hal.get_config().night_lamp.enabled,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
@@ -530,13 +525,22 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
|||||||
board.board_hal.get_config().night_lamp.night_lamp_hour_end,
|
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 {
|
match battery_state {
|
||||||
board.board_hal.get_esp().set_low_voltage_in_cycle();
|
BatteryState::Unknown => {
|
||||||
info!("Set low voltage in cycle");
|
light_state.battery_low = false;
|
||||||
} 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();
|
BatteryState::Info(data) => {
|
||||||
info!("Clear low voltage in cycle");
|
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();
|
light_state.battery_low = board.board_hal.get_esp().low_voltage_in_cycle();
|
||||||
|
|
||||||
if !light_state.out_of_work_hour {
|
if !light_state.out_of_work_hour {
|
||||||
@@ -583,7 +587,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
|
|||||||
|
|
||||||
let deep_sleep_duration_minutes: u32 =
|
let deep_sleep_duration_minutes: u32 =
|
||||||
// if battery soc is unknown assume battery has enough change
|
// 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
|
let _ = board
|
||||||
.board_hal
|
.board_hal
|
||||||
.get_esp()
|
.get_esp()
|
||||||
@@ -1002,7 +1006,64 @@ async fn wait_infinity(
|
|||||||
let mut pattern_step = 0;
|
let mut pattern_step = 0;
|
||||||
let serial_config_receive = AtomicBool::new(false);
|
let serial_config_receive = AtomicBool::new(false);
|
||||||
let mut suppress_further_mppt_error = 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 {
|
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;
|
let mut board = BOARD_ACCESS.get().await.lock().await;
|
||||||
match update_charge_indicator(&mut board).await {
|
match update_charge_indicator(&mut board).await {
|
||||||
@@ -1145,7 +1206,6 @@ async fn main(spawner: Spawner) -> ! {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Hal init done, starting logic");
|
println!("Hal init done, starting logic");
|
||||||
panic!("test");
|
|
||||||
match safe_main(spawner).await {
|
match safe_main(spawner).await {
|
||||||
// this should not get triggered, safe_main should not return but go into deep sleep or reboot
|
// this should not get triggered, safe_main should not return but go into deep sleep or reboot
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
|||||||
@@ -191,36 +191,72 @@ export class Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uploadNewFirmware(file: File) {
|
uploadNewFirmware(file: File) {
|
||||||
let current = 0;
|
const reader = new FileReader();
|
||||||
let max = 100;
|
reader.onload = () => {
|
||||||
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
const arrayBuffer = reader.result as ArrayBuffer;
|
||||||
const ajax = new XMLHttpRequest();
|
const data = new Uint8Array(arrayBuffer);
|
||||||
ajax.upload.addEventListener("progress", event => {
|
const crc = this.crc32(data);
|
||||||
current = event.loaded / 1000;
|
const size = data.length;
|
||||||
max = event.total / 1000;
|
|
||||||
|
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 + ")")
|
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||||
}, false);
|
const ajax = new XMLHttpRequest();
|
||||||
ajax.addEventListener("load", () => {
|
ajax.upload.addEventListener("progress", event => {
|
||||||
controller.progressview.removeProgress("ota_upload")
|
current = event.loaded / 1000;
|
||||||
const status = ajax.status;
|
max = event.total / 1000;
|
||||||
if (status >= 200 && status < 300) {
|
controller.progressview.addProgress("ota_upload", (current / max) * 100, "Uploading firmeware (" + current + "/" + max + ")")
|
||||||
controller.reboot();
|
}, false);
|
||||||
} else {
|
ajax.addEventListener("load", () => {
|
||||||
const statusText = ajax.statusText || "";
|
controller.progressview.removeProgress("ota_upload")
|
||||||
const body = ajax.responseText || "";
|
const status = ajax.status;
|
||||||
toast.error(`OTA update error (${status}${statusText ? ' ' + statusText : ''}): ${body}`);
|
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", () => {
|
return (crc ^ 0xFFFFFFFF) >>> 0;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async version(): Promise<void> {
|
async version(): Promise<void> {
|
||||||
@@ -264,12 +300,14 @@ export class Controller {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: json,
|
body: json,
|
||||||
})
|
})
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(text => statusCallback(text))
|
.then(text => statusCallback(text))
|
||||||
.then( _ => {
|
.then(_ => {
|
||||||
controller.progressview.removeProgress("set_config");
|
controller.progressview.removeProgress("set_config");
|
||||||
setTimeout(() => { controller.downloadConfig() }, 250)
|
setTimeout(() => {
|
||||||
})
|
controller.downloadConfig()
|
||||||
|
}, 250)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async backupConfig(json: string): Promise<string> {
|
async backupConfig(json: string): Promise<string> {
|
||||||
@@ -290,7 +328,6 @@ export class Controller {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
body: pretty
|
body: pretty
|
||||||
}).then(
|
}).then(
|
||||||
|
|
||||||
_ => controller.progressview.removeProgress("write_rtc")
|
_ => controller.progressview.removeProgress("write_rtc")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,12 +35,8 @@
|
|||||||
<span class="otavalue" id="firmware_partition"></span>
|
<span class="otavalue" id="firmware_partition"></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
<span class="otakey">State0:</span>
|
<span class="otakey">State:</span>
|
||||||
<span class="otavalue" id="firmware_state0"></span>
|
<span class="otavalue" id="firmware_state"></span>
|
||||||
</div>
|
|
||||||
<div class="flexcontainer">
|
|
||||||
<span class="otakey">State1:</span>
|
|
||||||
<span class="otavalue" id="firmware_state1"></span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flexcontainer">
|
<div class="flexcontainer">
|
||||||
|
|||||||
Submodule website/themes/blowfish updated: f9eb1d4e81...26d1205439
Reference in New Issue
Block a user