Compare commits
4 Commits
test_new_s
...
test_new_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30bcdf6f3b | ||
|
|
9cd20cd56b | ||
|
|
3be8dc7f6a | ||
|
|
e802af2a7a |
121
AGENTS.md
121
AGENTS.md
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -185,7 +185,6 @@ export interface VersionInfo {
|
||||
slot1_state: string,
|
||||
heap_total: number,
|
||||
heap_used: number,
|
||||
heap_free: number,
|
||||
heap_max_used: number,
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user