4 Commits

15 changed files with 408 additions and 117 deletions

121
AGENTS.md
View File

@@ -1,61 +1,94 @@
# AGENTS.md # AGENTS.md
## Scope
These instructions apply to the entire repository unless a deeper `AGENTS.md` overrides them.
## Repository Overview ## Repository Overview
`PlantCtrl` is a mixed-discipline repository with embedded firmware, shared Rust crates, hardware design files, and a Hugo-based website. `PlantCtrl` is a mixed-discipline repo: two embedded Rust firmware targets, a shared crate, KiCad hardware, and a Hugo site.
Top-level layout: Top-level layout:
- `Software/MainBoard/rust`: main embedded Rust firmware for the controller board (`plant-ctrl2`). - `Software/MainBoard/rust`: main firmware (`plant-ctrl2`) for ESP32-C6 controller board.
- `Software/CAN_Sensor`: embedded Rust firmware for the CAN sensor / BMS board. - `Software/CAN_Sensor`: sensor firmware (`bms`) for CH32V203C8T6 (CAN sensor / BMS board).
- `Software/Shared/canapi`: shared Rust crate used by firmware projects. - `Software/Shared/canapi`: shared crate for CAN protocol/serialization — consumed by both firmware targets.
- `Hardware`: PCB, case, and related hardware design assets. - `Hardware`: KiCad PCB designs and 3D case files.
- `DataSheets`: reference material; treat as source data, not generated output. - `DataSheets`: reference material; treat as source data, not generated output.
- `website`: Hugo site based on the Blowfish theme. - `website`: Hugo site (Blowfish theme via git submodule at `website/themes/blowfish`).
- `bin`: helper scripts and local tooling, if present. - `bin`: helper scripts and dev tooling.
## Critical Constraints
- **No Cargo workspace** — each project has its own `Cargo.toml` and `Cargo.lock`. Run checks with `--manifest-path`.
- **Both firmware targets require `nightly` toolchain** (`rust-toolchain.toml`). Main board uses `build-std = ["alloc", "core"]` in `.cargo/config.toml`.
- **`no_std`/`no_main`** — both firmware crates are bare-metal. Do not introduce `std`-only dependencies.
- **Main board clippy rules** (`src/main.rs`): `#![deny(clippy::mem_forget, clippy::unwrap_used, clippy::expect_used, clippy::panic)]`. Do not add `#[allow(...)]` without explicit user request.
- **mcutie** (`src/mcutie_3_0_0/`): vendored sub-crate with `#![deny(unreachable_pub)]` and `#![warn(missing_docs)]`.
- **Flash layout** (main board `partitions.csv`): 16 MB total — `ota_0` (3968K), `ota_1` (3968K), `storage` (8M LittleFS), `nvs` (16K), `otadata` (8K), `phy_init` (4K).
- **CH32V203C8T6 memory** (`CAN_Sensor/memory.x`): 64 KB flash, 20 KB RAM. Tight budget — avoid allocations.
- **ESP IDF config** (`sdkconfig.defaults`): `CONFIG_ESP_MAIN_TASK_STACK_SIZE=50000`, `CONFIG_FREERTOS_HZ=1000`, `CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y`.
## Firmware Build & Flash
### Main Board (ESP32-C6)
```
# Quick check (no build):
cargo check --manifest-path Software/MainBoard/rust/Cargo.toml
# Full build + flash + monitor:
bash Software/MainBoard/rust/flash.sh
# Build only (no flash):
cargo build --release --manifest-path Software/MainBoard/rust/Cargo.toml
# Build OTA image without flashing:
bash Software/MainBoard/rust/image_build.sh
# Erase OTA data partition (recover from bad flash):
bash Software/MainBoard/rust/erase_ota.sh
```
`flash.sh` rebuilds the webpack frontend (`src_webpack/`) then flashes via `espflash` at 921600 baud. The frontend is bundled into the firmware binary — do not run `npm` separately unless modifying `src_webpack/`.
### Sensor Board (CH32V203C8T6)
```
cargo check --manifest-path Software/CAN_Sensor/Cargo.toml
cargo build --release --manifest-path Software/CAN_Sensor/Cargo.toml
# Flash:
wchisp flash Software/CAN_Sensor/target/riscv32imc-unknown-none-elf/release/bms
```
Runner is preconfigured in `.cargo/config.toml` as `wchisp flash`. Debug via `openocd` + `gdb` (scripts in `CAN_Sensor/bin/`). See `CAN_Sensor/README.md` for MCU unlock steps.
### Shared Crate
```
cargo check --manifest-path Software/Shared/canapi/Cargo.toml
```
### Cross-impact
Any change to `canapi` (IDs, frame layout, serialization) must be checked against both firmware targets. The crate uses `bincode` for serialization.
## Architecture Notes
- **Main board entry**: `Software/MainBoard/rust/src/main.rs``#[esp_rtos::main]` async entry, Embassy executor, ESP-HAL.
- **HAL layer**: `src/hal/mod.rs` defines `PlantHal` trait; `esp.rs` and `v4_hal.rs` are concrete implementations.
- **mcutie**: `src/mcutie_3_0_0/` is a vendored MQTT/HA client library.
- **Webserver**: `src/webserver/` serves OTA, logs, static files over HTTP. Frontend in `src_webpack/`.
- **Sensor entry**: `Software/CAN_Sensor/src/main.rs``#[embassy_executor::main]` async entry, CH32-HAL, CAN + USB CDC.
- **Both targets**: Embassy executor, `heapless` for heapless data structures, `option-lock` for optional state.
## Website
```
cd website
npm run dev # starts Hugo dev server with Blowfish theme
```
Theme is a git submodule. Do not edit `website/themes/` directly — make customizations in `assets/` or `config/` instead.
## Working Rules ## Working Rules
- Keep changes tightly scoped to the user request; this repo spans hardware, firmware, and website code. - Keep changes tightly scoped to the user request; this repo spans hardware, firmware, and website code.
- Prefer fixing the underlying cause instead of applying cosmetic workarounds.
- Preserve existing file structure and naming unless the user explicitly asks for restructuring. - Preserve existing file structure and naming unless the user explicitly asks for restructuring.
- Avoid mass formatting or opportunistic cleanup in KiCad files, lockfiles, generated assets, or vendored dependencies. - Avoid mass formatting or opportunistic cleanup in KiCad files, lockfiles, generated assets, or vendored dependencies.
- Do not edit dependency directories such as `website/themes` or `Software/MainBoard/rust/src_webpack/node_modules` unless the user explicitly asks for vendor changes. - Do not edit dependency directories (`website/themes`, `src_webpack/node_modules`) unless explicitly asked.
- When touching firmware code, keep resource usage and target constraints in mind; avoid unnecessary allocations or feature creep. - When touching firmware code, keep resource usage and target constraints in mind; avoid unnecessary allocations or feature creep.
- Shared protocol or serialization changes must be checked for impact across both firmware targets.
## Firmware Guidance
- Shared protocol or serialization changes must be checked for impact across both `Software/MainBoard/rust` and `Software/CAN_Sensor`.
- Prefer small, explicit changes in embedded code paths; do not introduce heavyweight abstractions without a clear payoff.
- Keep `no_std`/embedded assumptions intact unless the code clearly opts into something else.
- Be careful with feature flags, target-specific dependencies, and boot/runtime configuration in Cargo manifests.
## Hardware Guidance
- Treat hardware directories as design artifacts, not generic text files.
- Do not reorder, normalize, or bulk-edit PCB / CAD files unless the user specifically requests those changes.
- If a software change depends on hardware assumptions, call that out clearly in the final handoff.
## Website Guidance
- The site in `website` uses Hugo with the Blowfish theme.
- Prefer editing site content, config, or custom assets over modifying vendored theme internals.
- Keep frontend changes consistent with the existing site structure unless the user asks for a redesign.
## Validation ## Validation
Use the narrowest relevant check first. - Use the narrowest relevant check first.
- Embedded firmware may require target-specific toolchains or hardware-adjacent tooling. If you cannot run a meaningful validation step, say so explicitly and describe the likely prerequisite.
Useful commands:
- `cargo check --manifest-path Software/Shared/canapi/Cargo.toml`
- `cargo check --manifest-path Software/CAN_Sensor/Cargo.toml`
- `cargo check --manifest-path Software/MainBoard/rust/Cargo.toml`
- `npm run dev` from `website` for local Hugo development if the environment has the required tools installed.
Validation notes:
- Embedded firmware may require target-specific toolchains or hardware-adjacent tooling that is not always available.
- If you cannot run a meaningful validation step, say so explicitly and describe the likely prerequisite.
## File Hygiene
- Read large files in chunks.
- Prefer targeted searches (`rg`, or `find` if unavailable) over broad scans.
- Do not commit build outputs, generated binaries, or local IDE metadata unless the user explicitly requests it.
## Handoff Expectations ## Handoff Expectations
- Summarize what changed, where it changed, and any validation performed. - Summarize what changed, where it changed, and any validation performed.

View File

@@ -11,6 +11,7 @@ use esp_hal::pcnt::unit::Unit;
use esp_hal::peripherals::GPIO5; use esp_hal::peripherals::GPIO5;
use esp_hal::Async; use esp_hal::Async;
use esp_println::println; use esp_println::println;
use log::info;
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20}; use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
unsafe impl Send for TankSensor<'_> {} unsafe impl Send for TankSensor<'_> {}
@@ -90,9 +91,9 @@ impl<'a> TankSensor<'a> {
let mut delay = Delay::new(); let mut delay = Delay::new();
let presence = self.one_wire_bus.reset(&mut delay)?; let presence = self.one_wire_bus.reset(&mut delay)?;
println!("OneWire: reset presence pulse = {}", presence); info!("OneWire: reset presence pulse = {}", presence);
if !presence { if !presence {
println!("OneWire: no device responded to reset — check pull-up resistor and wiring"); info!("OneWire: no device responded to reset — check pull-up resistor and wiring");
} }
let mut search = DeviceSearch::new(); let mut search = DeviceSearch::new();
@@ -100,7 +101,7 @@ impl<'a> TankSensor<'a> {
let mut devices_found = 0u8; let mut devices_found = 0u8;
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? { while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
devices_found += 1; devices_found += 1;
println!( info!(
"OneWire: found device #{} family=0x{:02X} addr={:02X?}", "OneWire: found device #{} family=0x{:02X} addr={:02X?}",
devices_found, device.address[0], device.address devices_found, device.address[0], device.address
); );
@@ -108,16 +109,16 @@ impl<'a> TankSensor<'a> {
water_temp_sensor = Some(device); water_temp_sensor = Some(device);
break; break;
} else { } else {
println!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE); info!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE);
} }
} }
if devices_found == 0 { if devices_found == 0 {
println!("OneWire: search found zero devices on the bus"); info!("OneWire: search found zero devices on the bus");
} }
match water_temp_sensor { match water_temp_sensor {
Some(device) => { Some(device) => {
println!("Found one wire device: {:?}", device); info!("Found one wire device: {:?}", device);
let mut water_temp_sensor = DS18B20::new(device)?; let mut water_temp_sensor = DS18B20::new(device)?;
let water_temp: Result<f32, FatError> = loop { let water_temp: Result<f32, FatError> = loop {

View File

@@ -641,18 +641,44 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.get_esp() .get_esp()
.mqtt_publish("/deepsleep", "low Volt 12h").await; .mqtt_publish("/deepsleep", "low Volt 12h").await;
12 * 60 12 * 60
} else if is_day {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "normal 20m").await;
20
} else { } else {
let _ = board let base_duration: u32 = if is_day { 20 } else { 60 };
.board_hal
.get_esp() // Shorten sleep if any plant has a pending action sooner than the base duration
.mqtt_publish("/deepsleep", "night 1h").await; let min_plant_wakeup: Option<u32> = plantstate
60 .iter()
.zip(&board.board_hal.get_config().plants)
.filter_map(|(state, conf)| {
state.minutes_until_actionable(conf, &timezone_time)
})
.min();
let duration = min_plant_wakeup
.map(|m| base_duration.min(m.max(1)))
.unwrap_or(base_duration);
if duration < base_duration {
let msg = format!("reduced {}m", duration);
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", &msg)
.await;
} else if is_day {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "normal 20m")
.await;
} else {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "night 1h")
.await;
}
duration
}; };
let _ = board let _ = board
@@ -1225,14 +1251,15 @@ async fn wait_infinity(
info!("Rebooting now"); info!("Rebooting now");
//ensure clean http answer //ensure clean http answer
Timer::after_millis(500).await; Timer::after_millis(500).await;
BOARD_ACCESS let mut board = BOARD_ACCESS
.get() .get()
.await .await
.lock() .lock()
.await
.board_hal
.deep_sleep(0)
.await; .await;
board.board_hal.get_esp().mqtt_publish("/stay_alive", "0").await;
//give time to mqtt to send the last message
Timer::after_millis(500).await;
board.board_hal.deep_sleep(0).await;
} }
} }
} }
@@ -1336,7 +1363,6 @@ async fn get_version(
slot1_state: format!("{:?}", board.slot1_state), slot1_state: format!("{:?}", board.slot1_state),
heap_total: heap.size, heap_total: heap.size,
heap_used: heap.current_usage, heap_used: heap.current_usage,
heap_free: heap.size.saturating_sub(heap.current_usage),
heap_max_used: heap.max_usage, heap_max_used: heap.max_usage,
} }
} }
@@ -1350,6 +1376,5 @@ struct VersionInfo {
slot1_state: String, slot1_state: String,
heap_total: usize, heap_total: usize,
heap_used: usize, heap_used: usize,
heap_free: usize,
heap_max_used: usize, heap_max_used: usize,
} }

View File

@@ -1,6 +1,6 @@
use crate::hal::Moistures; use crate::hal::Moistures;
use crate::{config::PlantConfig, hal::HAL, in_time_range}; use crate::{config::PlantConfig, hal::HAL, in_time_range};
use chrono::{DateTime, TimeDelta, Utc}; use chrono::{DateTime, TimeDelta, Timelike, Utc};
use chrono_tz::Tz; use chrono_tz::Tz;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -192,6 +192,67 @@ impl PlantState {
}) })
} }
/// Returns how many minutes until this plant becomes actionable (could be watered),
/// or `None` if no action is pending within any reasonable horizon.
///
/// Used to dynamically shorten deep-sleep so cooldowns are not prolonged.
pub fn minutes_until_actionable(
&self,
plant_conf: &PlantConfig,
current_time: &DateTime<Tz>,
) -> Option<u32> {
if matches!(plant_conf.mode, PlantWateringMode::Off) {
return None;
}
let mut earliest: Option<u32> = None;
// When does the current cooldown expire?
if self.pump_in_timeout(plant_conf, current_time) {
if let Some(last_pump) = self.pump.previous_pump {
if let Some(expiry) = last_pump
.checked_add_signed(TimeDelta::minutes(plant_conf.pump_cooldown_min.into()))
{
let diff = expiry.signed_duration_since(current_time.with_timezone(&Utc));
let mins = diff.num_minutes().max(0) as u32;
earliest = Some(earliest.map_or(mins, |e| e.min(mins)));
}
}
}
// For moisture-based modes: when does the watering window open, if the plant is dry
// but currently outside its time window (and not in cooldown)?
if matches!(
plant_conf.mode,
PlantWateringMode::TargetMoisture | PlantWateringMode::MinMoisture
) {
let (moisture, _) = self.plant_moisture();
if let Some(moisture) = moisture {
if moisture < plant_conf.target_moisture
&& !self.pump_in_timeout(plant_conf, current_time)
&& !in_time_range(
current_time,
plant_conf.pump_hour_start,
plant_conf.pump_hour_end,
)
{
// in_time_range uses `hour > start`, so the window opens at start+1:00
let cur_hour = current_time.hour() as u32;
let cur_minute = current_time.minute() as u32;
let open_at_hour = (plant_conf.pump_hour_start as u32 + 1) % 24;
let mins = if open_at_hour > cur_hour {
(open_at_hour - cur_hour) * 60 - cur_minute
} else {
(24 - cur_hour + open_at_hour) * 60 - cur_minute
};
earliest = Some(earliest.map_or(mins, |e| e.min(mins)));
}
}
}
earliest
}
pub fn is_err(&self) -> bool { pub fn is_err(&self) -> bool {
self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some() self.sensor_a.is_err().is_some() || self.sensor_b.is_err().is_some()
} }

View File

@@ -185,7 +185,6 @@ export interface VersionInfo {
slot1_state: string, slot1_state: string,
heap_total: number, heap_total: number,
heap_used: number, heap_used: number,
heap_free: number,
heap_max_used: number, heap_max_used: number,
} }

View File

@@ -10,10 +10,10 @@
<div class="subtitle">Hardware:</div> <div class="subtitle">Hardware:</div>
<div class="flexcontainer"> <div class="flexcontainer">
<div class="boardkey">BoardRevision</div> <div class="boardkey">BoardRevision</div>
<select class="boardvalue" id="hardware_board_value"> <select class="boardvalue hidden-mode-advanced" id="hardware_board_value">
</select> </select>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="boardkey">BatteryMonitor</div> <div class="boardkey">BatteryMonitor</div>
<select class="boardvalue" id="hardware_battery_value"> <select class="boardvalue" id="hardware_battery_value">
</select> </select>

View File

@@ -137,11 +137,157 @@
font-weight: bold; font-weight: bold;
} }
.mode-slider-container {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 8px 0;
flex-wrap: wrap;
}
.mode-label {
font-weight: bold;
font-size: 0.85em;
min-width: 55px;
text-align: center;
}
.mode-slider {
-webkit-appearance: none;
appearance: none;
width: 200px;
height: 8px;
border-radius: 4px;
outline: none;
cursor: pointer;
}
.mode-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
border: 2px solid white;
}
.mode-slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
border: 2px solid white;
}
.mode-simple .mode-slider {
background: linear-gradient(to right, #28a745, #28a745);
}
.mode-simple .mode-slider::-webkit-slider-thumb {
background: #28a745;
}
.mode-simple .mode-slider::-moz-range-thumb {
background: #28a745;
}
.mode-advanced .mode-slider {
background: linear-gradient(to right, #28a745, #ffc107);
}
.mode-advanced .mode-slider::-webkit-slider-thumb {
background: #ffc107;
}
.mode-advanced .mode-slider::-moz-range-thumb {
background: #ffc107;
}
.mode-expert .mode-slider {
background: linear-gradient(to right, #28a745, #ffc107, #dc3545);
}
.mode-expert .mode-slider::-webkit-slider-thumb {
background: #dc3545;
}
.mode-expert .mode-slider::-moz-range-thumb {
background: #dc3545;
}
.mode-simple .mode-label {
color: #28a745;
}
.mode-advanced .mode-label {
color: #ffc107;
}
.mode-expert .mode-label {
color: #dc3545;
}
.mode-label-mode {
font-size: 0.9em;
font-weight: bold;
min-width: 60px;
}
.mode-simple .mode-label-mode {
color: #28a745;
}
.mode-advanced .mode-label-mode {
color: #ffc107;
}
.mode-expert .mode-label-mode {
color: #dc3545;
}
.mode-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
margin-right: 4px;
vertical-align: middle;
}
.mode-simple .mode-dot {
background: #28a745;
}
.mode-advanced .mode-dot {
background: #ffc107;
}
.mode-expert .mode-dot {
background: #dc3545;
}
.hidden-mode {
display: none !important;
}
.mode-simple .hidden-mode-simple,
.mode-advanced .hidden-mode-simple,
.mode-expert .hidden-mode-simple {
display: none !important;
}
.mode-advanced .hidden-mode-advanced,
.mode-expert .hidden-mode-advanced {
display: none !important;
}
.mode-expert .hidden-mode-expert {
display: none !important;
}
</style> </style>
<div class="container-xl"> <div class="container-xl">
<div class="mode-slider-container mode-simple" id="modeSliderContainer">
<span class="mode-dot"></span>
<span class="mode-label">Simple</span>
<input type="range" class="mode-slider" id="configModeSlider" min="0" max="2" value="0" step="1">
<span class="mode-label">Expert</span>
<span class="mode-label-mode" id="modeLabel">Simple</span>
</div>
<div style="display:flex; flex-wrap: wrap;"> <div style="display:flex; flex-wrap: wrap;">
<div id="hardwareview" class="subcontainer"></div> <div id="hardwareview" class="subcontainer"></div>
</div> </div>

View File

@@ -560,6 +560,24 @@ export class Controller {
readonly can_power: HTMLInputElement; readonly can_power: HTMLInputElement;
readonly auto_refresh_moisture_sensors: HTMLInputElement; readonly auto_refresh_moisture_sensors: HTMLInputElement;
private auto_refresh_timer: NodeJS.Timeout | undefined; private auto_refresh_timer: NodeJS.Timeout | undefined;
readonly configModeSlider: HTMLInputElement;
readonly modeSliderContainer: HTMLDivElement;
readonly modeLabel: HTMLElement;
setMode(mode: number) {
const container = this.modeSliderContainer;
container.classList.remove('mode-simple', 'mode-advanced', 'mode-expert');
if (mode === 0) {
container.classList.add('mode-simple');
this.modeLabel.textContent = 'Simple';
} else if (mode === 1) {
container.classList.add('mode-advanced');
this.modeLabel.textContent = 'Advanced';
} else {
container.classList.add('mode-expert');
this.modeLabel.textContent = 'Expert';
}
}
constructor() { constructor() {
this.timeView = new TimeView(this) this.timeView = new TimeView(this)
@@ -606,6 +624,14 @@ export class Controller {
this.autoRefreshLoop() this.autoRefreshLoop()
} }
} }
this.configModeSlider = document.getElementById("configModeSlider") as HTMLInputElement;
this.modeSliderContainer = document.getElementById("modeSliderContainer") as HTMLDivElement;
this.modeLabel = document.getElementById("modeLabel") as HTMLElement;
this.configModeSlider.oninput = () => {
this.setMode(parseInt(this.configModeSlider.value));
};
this.setMode(0);
} }
private async autoRefreshLoop() { private async autoRefreshLoop() {

View File

@@ -53,7 +53,7 @@
<input class="basicnetworkvalue" type="text" id="password"> <input class="basicnetworkvalue" type="text" id="password">
</div> </div>
</div> </div>
<div class="subcontainer"> <div class="subcontainer hidden-mode-advanced">
<div class="flexcontainer"> <div class="flexcontainer">
<div class="subtitle"> <div class="subtitle">
Mqtt Reporting Mqtt Reporting

View File

@@ -28,21 +28,21 @@
<div class="lightkey">Light only when dark</div> <div class="lightkey">Light only when dark</div>
<input class="lightcheckbox" type="checkbox" id="night_lamp_only_when_dark"> <input class="lightcheckbox" type="checkbox" id="night_lamp_only_when_dark">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="lightkey">Start</div> <div class="lightkey">Start</div>
<select class="lightnumberbox" type="time" id="night_lamp_time_start"> <select class="lightnumberbox" type="time" id="night_lamp_time_start">
</select> </select>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="lightkey">Stop</div> <div class="lightkey">Stop</div>
<select class="lightnumberbox" type="time" id="night_lamp_time_end"> <select class="lightnumberbox" type="time" id="night_lamp_time_end">
</select> </select>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-expert">
<div class="lightkey">Disable if Battery below %</div> <div class="lightkey">Disable if Battery below %</div>
<input class="lightcheckbox" type="number" id="night_lamp_soc_low" min="0" max="100"> <input class="lightcheckbox" type="number" id="night_lamp_soc_low" min="0" max="100">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-expert">
<div class="lightkey">Reenable if Battery higher %</div> <div class="lightkey">Reenable if Battery higher %</div>
<input class="lightcheckbox" type="number" id="night_lamp_soc_restore" min="0" max="100"> <input class="lightcheckbox" type="number" id="night_lamp_soc_restore" min="0" max="100">
</div> </div>

View File

@@ -43,7 +43,7 @@
<span class="otakey">State1:</span> <span class="otakey">State1:</span>
<span class="otavalue" id="firmware_state1"></span> <span class="otavalue" id="firmware_state1"></span>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<form class="otaform" id="upload_form" method="post"> <form class="otaform" id="upload_form" method="post">
<input class="otachooser" type="file" name="file1" id="firmware_file"><br> <input class="otachooser" type="file" name="file1" id="firmware_file"><br>
</form> </form>
@@ -55,8 +55,8 @@
<div></div> <div></div>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<span class="otakey">Free:</span> <span class="otakey">Minimum Free:</span>
<span class="otavalue" id="heap_free"></span> <span class="otavalue" id="heap_min_free"></span>
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<span class="otakey">Used:</span> <span class="otakey">Used:</span>
@@ -73,5 +73,5 @@
<div class="display:flex"> <div class="display:flex">
<button style="margin-left: 16px; margin-top: 8px;" class="col-6" type="button" id="test">Self-Test</button> <button style="margin-left: 16px; margin-top: 8px;" class="col-6 hidden-mode-advanced" type="button" id="test">Self-Test</button>
</div> </div>

View File

@@ -12,7 +12,7 @@ export class OTAView {
readonly firmware_partition: HTMLDivElement; readonly firmware_partition: HTMLDivElement;
readonly firmware_state0: HTMLDivElement; readonly firmware_state0: HTMLDivElement;
readonly firmware_state1: HTMLDivElement; readonly firmware_state1: HTMLDivElement;
readonly heap_free: HTMLDivElement; readonly heap_min_free: HTMLDivElement;
readonly heap_used: HTMLDivElement; readonly heap_used: HTMLDivElement;
readonly heap_total: HTMLDivElement; readonly heap_total: HTMLDivElement;
readonly heap_max_used: HTMLDivElement; readonly heap_max_used: HTMLDivElement;
@@ -28,7 +28,7 @@ export class OTAView {
this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement; this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement;
this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement; this.firmware_state0 = document.getElementById("firmware_state0") as HTMLDivElement;
this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement; this.firmware_state1 = document.getElementById("firmware_state1") as HTMLDivElement;
this.heap_free = document.getElementById("heap_free") as HTMLDivElement; this.heap_min_free = document.getElementById("heap_min_free") as HTMLDivElement;
this.heap_used = document.getElementById("heap_used") as HTMLDivElement; this.heap_used = document.getElementById("heap_used") as HTMLDivElement;
this.heap_total = document.getElementById("heap_total") as HTMLDivElement; this.heap_total = document.getElementById("heap_total") as HTMLDivElement;
this.heap_max_used = document.getElementById("heap_max_used") as HTMLDivElement; this.heap_max_used = document.getElementById("heap_max_used") as HTMLDivElement;
@@ -59,7 +59,7 @@ export class OTAView {
this.firmware_partition.innerText = versionInfo.current; this.firmware_partition.innerText = versionInfo.current;
this.firmware_state0.innerText = versionInfo.slot0_state; this.firmware_state0.innerText = versionInfo.slot0_state;
this.firmware_state1.innerText = versionInfo.slot1_state; this.firmware_state1.innerText = versionInfo.slot1_state;
this.heap_free.innerText = fmtBytes(versionInfo.heap_free); this.heap_min_free.innerText = fmtBytes(versionInfo.heap_total - versionInfo.heap_max_used);
this.heap_used.innerText = fmtBytes(versionInfo.heap_used); this.heap_used.innerText = fmtBytes(versionInfo.heap_used);
this.heap_total.innerText = fmtBytes(versionInfo.heap_total); this.heap_total.innerText = fmtBytes(versionInfo.heap_total);
this.heap_max_used.innerText = fmtBytes(versionInfo.heap_max_used); this.heap_max_used.innerText = fmtBytes(versionInfo.heap_max_used);

View File

@@ -37,7 +37,7 @@
<div> <div>
<div class="subtitle" <div class="subtitle"
id="plant_${plantId}_header"> id="plant_${plantId}_header">
Plant ${plantId} Plant ${plantId}
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
@@ -91,11 +91,11 @@
<input class="plantvalue" id="plant_${plantId}_max_consecutive_pump_count" type="number" min="1" max="50" <input class="plantvalue" id="plant_${plantId}_max_consecutive_pump_count" type="number" min="1" max="50"
placeholder="10"> placeholder="10">
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantkey">Min Frequency Override</div> <div class="plantkey">Min Frequency Override</div>
<input class="plantvalue" id="plant_${plantId}_min_frequency" type="number" min="1000" max="25000"> <input class="plantvalue" id="plant_${plantId}_min_frequency" type="number" min="1000" max="25000">
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantkey">Max Frequency Override</div> <div class="plantkey">Max Frequency Override</div>
<input class="plantvalue" id="plant_${plantId}_max_frequency" type="number" min="1000" max="25000"> <input class="plantvalue" id="plant_${plantId}_max_frequency" type="number" min="1000" max="25000">
</div> </div>
@@ -104,71 +104,71 @@
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId}">
<h2 class="plantkey">Current config:</h2> <h2 class="plantkey">Current config:</h2>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantkey">Min current</div> <div class="plantkey">Min current</div>
<input class="plantvalue" id="plant_${plantId}_min_pump_current_ma" type="number" min="0" max="4500"> <input class="plantvalue" id="plant_${plantId}_min_pump_current_ma" type="number" min="0" max="4500">
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantkey">Max current</div> <div class="plantkey">Max current</div>
<input class="plantvalue" id="plant_${plantId}_max_pump_current_ma" type="number" min="0" max="4500"> <input class="plantvalue" id="plant_${plantId}_max_pump_current_ma" type="number" min="0" max="4500">
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantkey">Ignore current sensor error</div> <div class="plantkey">Ignore current sensor error</div>
<input class="plantcheckbox" id="plant_${plantId}_ignore_current_error" type="checkbox"> <input class="plantcheckbox" id="plant_${plantId}_ignore_current_error" type="checkbox">
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<button class="subtitle" id="plant_${plantId}_test">Test Pump</button> <button class="subtitle" id="plant_${plantId}_test">Test Pump</button>
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<div class="subtitle">Live:</div> <div class="subtitle">Live:</div>
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<button class="subtitle" id="plant_${plantId}_test_sensor_a">Identify Sensor A</button> <button class="subtitle" id="plant_${plantId}_test_sensor_a">Identify Sensor A</button>
<button class="subtitle" id="plant_${plantId}_test_sensor_b">Identify Sensor B</button> <button class="subtitle" id="plant_${plantId}_test_sensor_b">Identify Sensor B</button>
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<span class="plantsensorkey">Sensor A:</span> <span class="plantsensorkey">Sensor A:</span>
<span class="plantsensorvalue" id="plant_${plantId}_moisture_a">not measured</span> <span class="plantsensorvalue" id="plant_${plantId}_moisture_a">not measured</span>
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<span class="plantsensorkey">Sensor A FW:</span> <span class="plantsensorkey">Sensor A FW:</span>
<span class="plantsensorvalue" id="plant_${plantId}_sensor_a_fw_build">unknown</span> <span class="plantsensorvalue" id="plant_${plantId}_sensor_a_fw_build">unknown</span>
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Sensor B:</div> <div class="plantsensorkey">Sensor B:</div>
<span class="plantsensorvalue" id="plant_${plantId}_moisture_b">not measured</span> <span class="plantsensorvalue" id="plant_${plantId}_moisture_b">not measured</span>
</div> </div>
<div class="flexcontainer plantSensorEnabledOnly_${plantId}"> <div class="flexcontainer plantSensorEnabledOnly_${plantId} hidden-mode-advanced">
<span class="plantsensorkey">Sensor B FW:</span> <span class="plantsensorkey">Sensor B FW:</span>
<span class="plantsensorvalue" id="plant_${plantId}_sensor_b_fw_build">unknown</span> <span class="plantsensorvalue" id="plant_${plantId}_sensor_b_fw_build">unknown</span>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Max Current</div> <div class="plantsensorkey">Max Current</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_max">not_tested</span> <span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_max">not_tested</span>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Min Current</div> <div class="plantsensorkey">Min Current</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_min">not_tested</span> <span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_min">not_tested</span>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Average</div> <div class="plantsensorkey">Average</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_average">not_tested</span> <span class="plantsensorvalue" id="plant_${plantId}_pump_test_current_average">not_tested</span>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Pump Time</div> <div class="plantsensorkey">Pump Time</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_pump_time">not_tested</span> <span class="plantsensorvalue" id="plant_${plantId}_pump_test_pump_time">not_tested</span>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Flow ml</div> <div class="plantsensorkey">Flow ml</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_flow_ml">not_tested</span> <span class="plantsensorvalue" id="plant_${plantId}_pump_test_flow_ml">not_tested</span>
</div> </div>
<div class="flexcontainer plantPumpEnabledOnly_${plantId}"> <div class="flexcontainer plantPumpEnabledOnly_${plantId} hidden-mode-advanced">
<div class="plantsensorkey">Flow raw</div> <div class="plantsensorkey">Flow raw</div>
<span class="plantsensorvalue" id="plant_${plantId}_pump_test_flow_raw">not_tested</span> <span class="plantsensorvalue" id="plant_${plantId}_pump_test_flow_raw">not_tested</span>
</div> </div>
</div> </div>

View File

@@ -27,14 +27,14 @@
</style> </style>
<button class="submitbutton" id="submit">Submit</button> <button class="submitbutton" id="submit">Submit</button>
<br> <br>
<button id="showJson">Show Json</button> <button class="hidden-mode-advanced" id="showJson">Show Json</button>
<div id="rawdata" class="flexcontainer" style="display: none;"> <div id="rawdata" class="flexcontainer hidden-mode-advanced" style="display: none;">
<div class="submitarea" id="json" contenteditable="true"></div> <div class="submitarea" id="json" contenteditable="true"></div>
<div class="submitarea" id="backupjson">backup will be here</div> <div class="submitarea" id="backupjson">backup will be here</div>
</div> </div>
<div>BackupStatus:</div> <div>BackupStatus:</div>
<div id="backuptimestamp"></div> <div id="backuptimestamp"></div>
<div id="backupsize"></div> <div id="backupsize"></div>
<button id="backup">Backup</button> <button class="hidden-mode-advanced" id="backup">Backup</button>
<button id="restorebackup">Restore</button> <button class="hidden-mode-advanced" id="restorebackup">Restore</button>
<div id="submit_status"></div> <div id="submit_status"></div>

View File

@@ -20,7 +20,7 @@
<span style="flex-grow: 1; text-align: center; font-weight: bold;"> <span style="flex-grow: 1; text-align: center; font-weight: bold;">
Tank: Tank:
</span> </span>
<input id="tankview_auto_refresh" type="checkbox"> <input id="tankview_auto_refresh" type="checkbox">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer">
<span class="tankkey">Enable Tank Sensor</span> <span class="tankkey">Enable Tank Sensor</span>
@@ -32,23 +32,23 @@
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="tankkey">Useable ml full% to empty%</div> <div class="tankkey">Useable ml full% to empty%</div>
<input class="tankvalue" type="number" min="2" max="500000" id="tank_useable_ml"> <input class="tankvalue" type="number" min="2" max="500000" id="tank_useable_ml">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="tankkey">Warn below %</div> <div class="tankkey">Warn below %</div>
<input class="tankvalue" type="number" min="1" max="500000" id="tank_warn_percent"> <input class="tankvalue" type="number" min="1" max="500000" id="tank_warn_percent">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="tankkey">Empty at %</div> <div class="tankkey">Empty at %</div>
<input class="tankvalue" type="number" min="0" max="100" id="tank_empty_percent"> <input class="tankvalue" type="number" min="0" max="100" id="tank_empty_percent">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-advanced">
<div class="tankkey">Full at %</div> <div class="tankkey">Full at %</div>
<input class="tankvalue" type="number" min="0" max="100" id="tank_full_percent"> <input class="tankvalue" type="number" min="0" max="100" id="tank_full_percent">
</div> </div>
<div class="flexcontainer"> <div class="flexcontainer hidden-mode-expert">
<div class="tankkey">Flow Sensor ml per pulse</div> <div class="tankkey">Flow Sensor ml per pulse</div>
<input class="tankvalue" type="number" min="0" max="1000" step="0.01" id="ml_per_pulse"> <input class="tankvalue" type="number" min="0" max="1000" step="0.01" id="ml_per_pulse">
</div> </div>
@@ -86,4 +86,4 @@
<div class="flexcontainer"> <div class="flexcontainer">
<div class="tankkey">Warn Level</div> <div class="tankkey">Warn Level</div>
<label class="tankvalue" id="tank_measure_warnlevel"></label> <label class="tankvalue" id="tank_measure_warnlevel"></label>
</div> </div>