4 Commits

15 changed files with 408 additions and 117 deletions

121
AGENTS.md
View File

@@ -1,61 +1,94 @@
# AGENTS.md
## Scope
These instructions apply to the entire repository unless a deeper `AGENTS.md` overrides them.
## 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:
- `Software/MainBoard/rust`: main embedded Rust firmware for the controller board (`plant-ctrl2`).
- `Software/CAN_Sensor`: embedded Rust firmware for the CAN sensor / BMS board.
- `Software/Shared/canapi`: shared Rust crate used by firmware projects.
- `Hardware`: PCB, case, and related hardware design assets.
- `Software/MainBoard/rust`: main firmware (`plant-ctrl2`) for ESP32-C6 controller board.
- `Software/CAN_Sensor`: sensor firmware (`bms`) for CH32V203C8T6 (CAN sensor / BMS board).
- `Software/Shared/canapi`: shared crate for CAN protocol/serialization — consumed by both firmware targets.
- `Hardware`: KiCad PCB designs and 3D case files.
- `DataSheets`: reference material; treat as source data, not generated output.
- `website`: Hugo site based on the Blowfish theme.
- `bin`: helper scripts and local tooling, if present.
- `website`: Hugo site (Blowfish theme via git submodule at `website/themes/blowfish`).
- `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
- 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.
- 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.
## 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.
- Shared protocol or serialization changes must be checked for impact across both firmware targets.
## Validation
Use the narrowest relevant check first.
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.
- 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.
## Handoff Expectations
- 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::Async;
use esp_println::println;
use log::info;
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
unsafe impl Send for TankSensor<'_> {}
@@ -90,9 +91,9 @@ impl<'a> TankSensor<'a> {
let mut delay = Delay::new();
let presence = self.one_wire_bus.reset(&mut delay)?;
println!("OneWire: reset presence pulse = {}", presence);
info!("OneWire: reset presence pulse = {}", 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();
@@ -100,7 +101,7 @@ impl<'a> TankSensor<'a> {
let mut devices_found = 0u8;
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
devices_found += 1;
println!(
info!(
"OneWire: found device #{} family=0x{:02X} addr={:02X?}",
devices_found, device.address[0], device.address
);
@@ -108,16 +109,16 @@ impl<'a> TankSensor<'a> {
water_temp_sensor = Some(device);
break;
} 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 {
println!("OneWire: search found zero devices on the bus");
info!("OneWire: search found zero devices on the bus");
}
match water_temp_sensor {
Some(device) => {
println!("Found one wire device: {:?}", device);
info!("Found one wire device: {:?}", device);
let mut water_temp_sensor = DS18B20::new(device)?;
let water_temp: Result<f32, FatError> = loop {

View File

@@ -641,18 +641,44 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.get_esp()
.mqtt_publish("/deepsleep", "low Volt 12h").await;
12 * 60
} else if is_day {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "normal 20m").await;
20
} else {
let _ = board
.board_hal
.get_esp()
.mqtt_publish("/deepsleep", "night 1h").await;
60
let base_duration: u32 = if is_day { 20 } else { 60 };
// Shorten sleep if any plant has a pending action sooner than the base duration
let min_plant_wakeup: Option<u32> = plantstate
.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
@@ -1225,14 +1251,15 @@ async fn wait_infinity(
info!("Rebooting now");
//ensure clean http answer
Timer::after_millis(500).await;
BOARD_ACCESS
let mut board = BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.deep_sleep(0)
.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),
heap_total: heap.size,
heap_used: heap.current_usage,
heap_free: heap.size.saturating_sub(heap.current_usage),
heap_max_used: heap.max_usage,
}
}
@@ -1350,6 +1376,5 @@ struct VersionInfo {
slot1_state: String,
heap_total: usize,
heap_used: usize,
heap_free: usize,
heap_max_used: usize,
}

View File

@@ -1,6 +1,6 @@
use crate::hal::Moistures;
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 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 {
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,
heap_total: number,
heap_used: number,
heap_free: number,
heap_max_used: number,
}

View File

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

View File

@@ -137,11 +137,157 @@
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>
<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 id="hardwareview" class="subcontainer"></div>
</div>

View File

@@ -560,6 +560,24 @@ export class Controller {
readonly can_power: HTMLInputElement;
readonly auto_refresh_moisture_sensors: HTMLInputElement;
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() {
this.timeView = new TimeView(this)
@@ -606,6 +624,14 @@ export class Controller {
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() {

View File

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

View File

@@ -28,21 +28,21 @@
<div class="lightkey">Light only when dark</div>
<input class="lightcheckbox" type="checkbox" id="night_lamp_only_when_dark">
</div>
<div class="flexcontainer">
<div class="flexcontainer hidden-mode-advanced">
<div class="lightkey">Start</div>
<select class="lightnumberbox" type="time" id="night_lamp_time_start">
</select>
</div>
<div class="flexcontainer">
<div class="flexcontainer hidden-mode-advanced">
<div class="lightkey">Stop</div>
<select class="lightnumberbox" type="time" id="night_lamp_time_end">
</select>
</div>
<div class="flexcontainer">
<div class="flexcontainer hidden-mode-expert">
<div class="lightkey">Disable if Battery below %</div>
<input class="lightcheckbox" type="number" id="night_lamp_soc_low" min="0" max="100">
</div>
<div class="flexcontainer">
<div class="flexcontainer hidden-mode-expert">
<div class="lightkey">Reenable if Battery higher %</div>
<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="otavalue" id="firmware_state1"></span>
</div>
<div class="flexcontainer">
<div class="flexcontainer hidden-mode-advanced">
<form class="otaform" id="upload_form" method="post">
<input class="otachooser" type="file" name="file1" id="firmware_file"><br>
</form>
@@ -55,8 +55,8 @@
<div></div>
</div>
<div class="flexcontainer">
<span class="otakey">Free:</span>
<span class="otavalue" id="heap_free"></span>
<span class="otakey">Minimum Free:</span>
<span class="otavalue" id="heap_min_free"></span>
</div>
<div class="flexcontainer">
<span class="otakey">Used:</span>
@@ -73,5 +73,5 @@
<div class="display:flex">
<button style="margin-left: 16px; margin-top: 8px;" class="col-6" type="button" id="test">Self-Test</button>
</div>
<button style="margin-left: 16px; margin-top: 8px;" class="col-6 hidden-mode-advanced" type="button" id="test">Self-Test</button>
</div>

View File

@@ -12,7 +12,7 @@ export class OTAView {
readonly firmware_partition: HTMLDivElement;
readonly firmware_state0: HTMLDivElement;
readonly firmware_state1: HTMLDivElement;
readonly heap_free: HTMLDivElement;
readonly heap_min_free: HTMLDivElement;
readonly heap_used: HTMLDivElement;
readonly heap_total: HTMLDivElement;
readonly heap_max_used: HTMLDivElement;
@@ -28,7 +28,7 @@ export class OTAView {
this.firmware_partition = document.getElementById("firmware_partition") as HTMLDivElement;
this.firmware_state0 = document.getElementById("firmware_state0") 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_total = document.getElementById("heap_total") 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_state0.innerText = versionInfo.slot0_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_total.innerText = fmtBytes(versionInfo.heap_total);
this.heap_max_used.innerText = fmtBytes(versionInfo.heap_max_used);

View File

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

View File

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

View File

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