5 Commits

Author SHA1 Message Date
3cc5a0d2bd dependency lock upgrade 2026-05-05 00:50:18 +02:00
3be585ecbf Refactor flow meter handling with interrupt-based logic and global state
- Added `flow_interrupt_handler` for efficient interrupt processing.
- Replaced per-instance `flow_counter` with global atomic and mutex-based state (`FLOW_OVERFLOW_COUNTER`, `FLOW_UNIT`).
- Updated flow meter functions to leverage the new architecture for better modularity and thread safety.
- Switched debugging output from `println!` to `log` for improved logging consistency.
2026-05-05 00:50:18 +02:00
5b1a945ac3 Replace blocking http_server call with async task using spawner 2026-05-05 00:50:18 +02:00
f4e050d413 Add ChecksumError handling to FatError conversion 2026-05-05 00:50:18 +02:00
776db785c4 Update hardware and firmware documentation for new modules and features
- Removed outdated TODOs and legacy references in hardware documentation.
- Added details on the new CH32V203-based Sensor Module for CAN bus soil moisture sensors.
- Documented updates to the Battery Management System (CH32V203-based) replacing the older bq34z100 design.
- Refined sensor, pump, and power module descriptions with updated specifications.
- Expanded firmware documentation to include Rust-based ESP32-C6 platform details, new OTA procedure, and MQTT telemetry topics.
- Simplified toolchain setup and compilation process with updated scripts and instructions.
2026-05-05 00:50:18 +02:00
11 changed files with 372 additions and 297 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -316,9 +316,12 @@ impl From<sntpc::Error> for FatError {
impl From<BmsProtocolError> for FatError {
fn from(value: BmsProtocolError) -> Self {
match value {
BmsProtocolError::I2cCommunicationError => FatError::String {
BmsProtocolError::I2cCommunicationError =>FatError::String {
error: "I2C communication error".to_string(),
},
BmsProtocolError::ChecksumError => FatError::String {
error: "BMS checksum error".to_string(),
},
}
}
}

View File

@@ -290,7 +290,8 @@ impl PlantHal {
error: format!("Could not init wifi: {:?}", e),
})?;
let pcnt_module = Pcnt::new(peripherals.PCNT);
let mut pcnt_module = Pcnt::new(peripherals.PCNT);
pcnt_module.set_interrupt_handler(water::flow_interrupt_handler);
let free_pins = FreePeripherals {
gpio0: peripherals.GPIO0,

View File

@@ -1,6 +1,8 @@
use crate::bail;
use crate::fat_error::FatError;
use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use core::cell::RefCell;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcCalLine, AdcConfig, AdcPin, Attenuation};
use esp_hal::delay::Delay;
@@ -10,17 +12,21 @@ use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
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};
use portable_atomic::{AtomicUsize, Ordering};
unsafe impl Send for TankSensor<'_> {}
static FLOW_OVERFLOW_COUNTER: AtomicUsize = AtomicUsize::new(0);
static FLOW_UNIT: CriticalSectionMutex<RefCell<Option<Unit<'static, 1>>>> =
CriticalSectionMutex::new(RefCell::new(None));
pub struct TankSensor<'a> {
one_wire_bus: OneWire<Flex<'a>>,
tank_channel: Adc<'a, ADC1<'a>, Async>,
tank_power: Output<'a>,
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>, AdcCalLine<ADC1<'a>>>,
flow_counter: Unit<'a, 1>,
}
impl<'a> TankSensor<'a> {
@@ -30,7 +36,7 @@ impl<'a> TankSensor<'a> {
gpio5: GPIO5<'a>,
tank_power: Output<'a>,
flow_sensor: Input,
pcnt1: Unit<'a, 1>,
pcnt1: Unit<'static, 1>,
) -> Result<TankSensor<'a>, FatError> {
one_wire_pin.apply_output_config(
&OutputConfig::default()
@@ -55,33 +61,64 @@ impl<'a> TankSensor<'a> {
ch0.set_edge_signal(flow_sensor.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
pcnt1.listen();
FLOW_UNIT.lock(|refcell| {
refcell.borrow_mut().replace(pcnt1);
});
Ok(TankSensor {
one_wire_bus,
tank_channel,
tank_power,
tank_pin,
flow_counter: pcnt1,
})
}
pub fn reset_flow_meter(&mut self) {
self.flow_counter.pause();
self.flow_counter.clear();
FLOW_OVERFLOW_COUNTER.store(0, Ordering::SeqCst);
FLOW_UNIT.lock(|refcell| {
if let Some(unit) = refcell.borrow_mut().as_mut() {
unit.pause();
unit.clear();
}
});
}
pub fn start_flow_meter(&mut self) {
self.flow_counter.resume();
FLOW_UNIT.lock(|refcell| {
if let Some(unit) = refcell.borrow_mut().as_mut() {
unit.resume();
}
});
}
pub fn get_flow_meter_value(&mut self) -> i16 {
self.flow_counter.value()
FLOW_UNIT.lock(|refcell| {
refcell.borrow_mut().as_mut().map_or(0, |unit| unit.value())
})
}
pub fn stop_flow_meter(&mut self) -> i16 {
self.flow_counter.pause();
self.get_flow_meter_value()
FLOW_UNIT.lock(|refcell| {
let mut borrowed = refcell.borrow_mut();
if let Some(unit) = borrowed.as_mut() {
let val = unit.value();
unit.pause();
val
} else {
0
}
})
}
pub fn get_full_flow_count(&self) -> u32 {
let current = FLOW_UNIT.lock(|refcell| {
refcell.borrow().as_ref().map_or(0, |unit| unit.value() as u32)
});
let overflowed = FLOW_OVERFLOW_COUNTER.load(Ordering::SeqCst) as u32;
overflowed * (i16::MAX.wrapping_add(1) as u32) + current
}
pub async fn water_temperature_c(&mut self) -> Result<f32, FatError> {
@@ -90,9 +127,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 +137,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 +145,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 {
@@ -126,11 +163,11 @@ impl<'a> TankSensor<'a> {
.await;
match &temp {
Ok(res) => {
println!("Water temp is {}", res);
info!("Water temp is {}", res);
break temp;
}
Err(err) => {
println!("Could not get water temp {} attempt {}", err, attempt)
info!("Could not get water temp {} attempt {}", err, attempt)
}
}
if attempt == 5 {
@@ -178,3 +215,18 @@ impl<'a> TankSensor<'a> {
Ok(median_mv / 1000.0)
}
}
#[esp_hal::handler]
pub fn flow_interrupt_handler() {
FLOW_UNIT.lock(|refcell| {
if let Some(unit) = refcell.borrow_mut().as_mut() {
if unit.interrupt_is_set() {
let events = unit.events();
if events.high_limit {
FLOW_OVERFLOW_COUNTER.fetch_add(1, Ordering::SeqCst);
}
unit.reset_interrupt();
}
}
});
}

View File

@@ -240,7 +240,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
let reboot_now = Arc::new(AtomicBool::new(false));
println!("starting webserver");
let _ = http_server(reboot_now.clone(), stack);
spawner.spawn(http_server(reboot_now.clone(), stack)?);
wait_infinity(board, WaitType::MissingConfig, reboot_now.clone(), UTC).await;
}

View File

@@ -3,39 +3,55 @@ title: "BatteryManagement"
date: 2025-01-27
draft: false
description: "a description"
tags: ["battery", "bq34z100"]
tags: ["battery", "bms"]
---
# Battery Management Module
The project contains an additional companion board (Fuel Gauge), with a bq34z100 battery management IC.
It allows to track the health and charge for an external battery and is supposed to be soldered directly to the battery.
The MainBoard contains a connector for power, and additionally a two-pin I2C bus to communicate with the Battery Management module.
The PlantCtrl system uses an external **Battery Management System (BMS)** board that connects to the MainBoard. This module monitors battery voltage, current, and health metrics and communicates with the ESP32-C6 via I2C.
<!-- TODO: Add photo of the new modular Battery Management board -->
# Setup
{{< alert >}}
A protected Battery is required. There is only a very simplistic output voltage adjustment for the MPPT system and no charge termination. It is expected that the battery itself protects against overcharging and deep discharges!
The open-bms is a custom battery management board designed for this project. It uses a CH32V203 microcontroller to handle battery monitoring and protection. The older bq34z100-based battery management board is deprecated and located in the `__Legay_Unused` folder.
{{< /alert >}}
* BatteryManagement is purely optional, but recommended for solar power.
* If available it will be used for an extended low power deep sleep in case of critical charge.
* If available it will also be used, to reduce the nightlight, if the charge drops to a predefined level, so the nightlight cannot drain to much battery
* If available, all relevant battery metrics will be published via mqtt
Currently the setup requires a custom Ev2400 flasher and the properitary windows software from texas instruments.
{{< alert >}}
Before soldering to the battery
{{< /alert >}}
1. The voltage devider high side must be bridged, while being connected to the computer and being supplied with around 4.2 V from the battery solder leads.
2. Then the data/register for low voltage flash write protection should be set to 0V, as else with the voltage divider and no further configuration, the IC will refuse all write requests.
3. After this the supplied golden image can be used, it will setup the battery for 6Ah and a 4S lifepo. Different values can be adjusted after this to the users liking.
## Hardware
The Battery Management Board features:
* CH32V203 RISC-V microcontroller for battery monitoring
* I2C interface for communication with the MainBoard
* Battery voltage and current sensing
{{< alert >}}
The main board, does not care or process any of the charge discharge limits that can be set. Ensure that the battery can supply enough current as well as accept a 2.4A charging current from the MPPT system.
The open-bms board does not use the bq34z100 fuel gauge IC. That component was used in an older legacy design now located in the `__Legay_Unused` folder.
{{< /alert >}}
The golden image sets the statups led up, to be in blinky mode. one very long interval means, that the battery is pretty much full. A few very short flashes mean that the battery is nearly empty. No light means, that the battery is in discharge protection and shut down.
## Integration with MainBoard
If the red error led lights, something is wrong with the battery. This can be abnormal voltages or a very low health state.
The battery management board:
* Connects to the MainBoard via a two-pin I2C bus
* Provides power connection to the battery
* Reports battery metrics via MQTT (if configured)
# Todo?
If the battery reports that no discharging should occure, report this and then shutdown without using pumps
## Usage
* If available, the system will use battery metrics for deep sleep management when charge is critical
* The nightlight can be automatically disabled if battery level drops below a predefined threshold
* All battery metrics are published via MQTT when configured
* The system includes safety mechanisms to prevent overcharging and deep discharges through the battery's built-in protection circuitry
## Safety Notes
{{< alert >}}
The system requires a battery with built-in protection circuitry. The MPPT system does not include charge termination or overcharge protection - the battery itself must provide these safety features.
{{< /alert >}}
The CH32V203-based BMS monitors battery health and provides status information but does not control the charge/discharge limits. Ensure your battery can handle the maximum charging current from the MPPT system (up to 2.4A).
## Setup
1. **Connect Battery:** Connect your protected battery to the BMS board
2. **_connect MainBoard:** Connect the Battery Management Board to the MainBoard via the I2C bus connector
3. **Power On:** Power on the system and verify communication via MQTT
## Status Indicators
The BMS board includes status LEDs, they behave like every normal powerbank (1-5 lights, animted if charging)

View File

@@ -65,13 +65,9 @@ Software and Hardware may fail: It is your responsibility to ensure that a stuck
{{< /alert >}}
# Todo
## Flow Sensor
There is a input for a flow sensor, currently it is not used as the software is missing.
* Allow monitoring if pumps are actually moving water
* Allow to set limits for how much ml are allowed additinally to the current time limit per watering run
Currently it cannot be set how two sensor should be interpreted and they are only averaged. More complex functions would be nice here, eg. allowing a user settable interpolation (0.8*a+0.2*b)/2 and Min(a,b) as well as max(a,b)

View File

@@ -11,8 +11,6 @@ tags: ["esp32", "hardware"]
<img src="pcb_back.png" class="grid-w50" />
{{< /gallery >}}
<!-- TODO: Add new screenshots of the modular PCB setup -->
{{< gitea server="https://git.mannheim.ccc.de/" repo="C3MA/PlantCtrl" >}}
## Modular Design
@@ -27,17 +25,25 @@ The system now consists of a **MainBoard** which acts as the controller and seve
* **Fully Open Source:** Designed in KiCad
## Available Modules
* **MPPT Charger:** Efficient solar charging for batteries.
* **Pump Driver:** High-current outputs for pumps and valves.
* **Sensor Interface:** Support for multiple moisture sensors.
* **Light Controller:** For LED nightlights or growth lights.
* **MPPT Charger:** Efficient solar charging for batteries using CN3795.
* **Pump Driver:** High-current outputs (up to 3A) for pumps and valves.
* **Sensor Module:** CAN bus-based moisture sensors using CH32V203 microcontroller.
* **Battery Management:** External BMS board with CH32V203 for battery monitoring.
* **Light Controller:** For LED nightlights or growth lights using AP63200.
## Sensor Module (CAN bus)
The standard sensor module features its own **CH32V203 RISC-V microcontroller**, which handles the measurement of soil moisture and communicates the results back to the MainBoard via the CAN bus.
* **Capacity:** Supports up to 16 sensors (typically 8 plants with an A and B sensor each).
* **Reliability:** Digital communication via CAN bus ensures data integrity even over longer cable runs and in electrically noisy environments.
* **Addressing:** The A sensor is always used; the B sensor is optional and suggested for larger planters to provide a better average of the soil moisture.
## Capabilities
* **Moisture Sensors:** Supports multiple capacitive or resistive sensors via expansion modules.
* **Moisture Sensors:** Supports multiple capacitive or resistive sensors via CAN bus-based Sensor Modules.
* **Pumps/Valves:** Support for multiple independent watering zones.
* **Power:**
* Solar powered with MPPT
* Battery powered with optional Battery Management (Fuel Gauge)
* Battery powered with optional Battery Management System (BMS)
* Can also be used with a standard power supply (7-24V)
* **Efficient Power:** Use of high-efficiency DC-DC converters for 3.3V and peripherals.

View File

@@ -6,9 +6,12 @@ description: "a description"
tags: ["firmeware", "upload"]
---
# From Source
The PlantCtrl firmware is written in Rust for the ESP32-C6 RISC-V microcontroller.
## Preconditions
* **Rust:** Current version of `rustup`.
* **ESP32 Toolchain:** `espup` installed and configured.
* **ESP32 Toolchain:** `espup` installed and configured for ESP32-C6.
* **espflash:** Installed via `cargo install espflash`.
* **Node.js:** `npm` installed (for the web interface).
@@ -37,10 +40,8 @@ You can use the provided bash scripts to automate the build and flash process:
You can also update the firmware wirelessly if the system is already running and connected to your network.
1. Generate the OTA binary:
```bash
cargo build --release
```
2. The binary will be at `target/riscv32imac-unknown-none-elf/release/plant-ctrl2`.
**`./image.sh`**
2. The binary will be `image.bin`.
3. Open the PlantCtrl web interface in your browser.
4. Navigate to the **OTA** section.
5. Upload the `plant-ctrl2` file.

View File

@@ -6,23 +6,26 @@ description: "a description"
tags: ["mqtt", "esp"]
---
# MQTT
A configured MQTT server will receive statistical and status data from the controller.
The PlantCtrl firmware publishes comprehensive status and telemetry data via MQTT when configured. The system uses the **mcutie** crate for Home Assistant integration and standard MQTT topics.
### Topics
| Topic | Example | Description |
|-------|---------|-------------|
| `firmware/address` | `192.168.1.2` | IP address in station mode |
| `firmware/state` | `VersionInfo { ... }` | Debug information about the current firmware and OTA slots |
| `firmware/state` | `{...}` | Debug information about the current firmware and OTA slots |
| `firmware/last_online` | `2025-01-22T08:56:46.664+01:00` | Last time the board was online |
| `state` | `online` | Current state of the controller |
| `mppt` | `{"current_ma":1200,"voltage_ma":18500}` | MPPT charging metrics |
| `battery` | `{"Info":{"voltage_milli_volt":12860,"average_current_milli_ampere":-16,...}}` | Battery health and charge data |
| `water` | `{"enough_water":true,"warn_level":false,"left_ml":1337,...}` | Water tank status |
| `plant{1-8}` | `{"sensor_a":...,"sensor_b":...,"mode":"TargetMoisture",...}` | Detailed status for each plant slot |
| `pump{1-8}` | `{"enabled":true,"pump_ineffective":false,...}` | Metrics for the last pump activity |
| `mppt` | `{"current_ma":1200,"voltage_ma":18500}` | MPPT charging metrics (current and voltage from solar panel) |
| `battery` | `{"Info":{"voltage_milli_volt":12860,"state_of_charge":95,...}}` | Battery health and charge data from the BMS |
| `water` | `{"enough_water":true,"warn_level":false,"left_ml":1337,...}` | Water tank status (level, temperature, frozen detection) |
| `plant{1-8}` | `{"sensor_a":...,"sensor_b":...,"mode":"TargetMoisture",...}` | Detailed status for each plant slot including moisture sensors |
| `pump{1-8}` | `{"enabled":true,"median_current_ma":500,...}` | Metrics for each pump output |
| `light` | `{"enabled":true,"active":true,...}` | Night light status |
| `deepsleep` | `night 1h` | Why and how long the ESP will sleep |
| `deepsleep` | `night 1h` | Reason and duration of deep sleep |
Note: The batteries `average_current_milli_ampere` field uses a placeholder value (1337) and should be updated with actual current sensor readings when available.
### Data Structures
@@ -39,14 +42,15 @@ Contains a debug dump of the `VersionInfo` struct:
- `voltage_ma`: Solar panel voltage in mV
#### Battery (`battery`)
Can be `"Unknown"` or an `Info` object:
- `voltage_milli_volt`: Battery voltage
- `average_current_milli_ampere`: Current draw/charge
- `design_milli_ampere_hour`: Battery capacity
- `remaining_milli_ampere_hour`: Remaining capacity
Can be `"Unknown"` or an `Info` object. The battery data comes from a custom BMS (Battery Management System) board that uses the CH32V203 microcontroller with I2C communication.
- `voltage_milli_volt`: Battery voltage in millivolts
- `average_current_milli_ampere`: Current draw/charge in milliamperes (placeholder: 1337)
- `design_milli_ampere_hour`: Battery design capacity in milliampere-hours
- `remaining_milli_ampere_hour`: Remaining capacity in milliampere-hours
- `state_of_charge`: Charge percentage (0-100)
- `state_of_health`: Health percentage (0-100)
- `temperature`: Temperature in degrees Celsius
- `state_of_health`: Health percentage (0-100) based onLifetime capacity vs design capacity
- `temperature`: Battery temperature in degrees Celsius
#### Water (`water`)
- `enough_water`: Boolean, true if level is above empty threshold

View File

@@ -6,9 +6,9 @@ description: "How to compile the project"
tags: ["clone", "compile"]
---
# Preconditions:
* **Rust:** `rustup` installed.
* **ESP32 Toolchain:** `espup` installed.
* **Build Utilities:** `ldproxy` and `espflash` installed.
* **Rust:** `rustup` installed with the Rust toolchain.
* **ESP32 Toolchain:** `espup` installed for ESP32 support.
* **Build Utilities:** `ldproxy` and `espflash` installed via cargo.
* **Node.js:** `npm` installed (for the web interface).
# Cloning the Repository
@@ -19,24 +19,16 @@ cd PlantCtrl/Software/MainBoard/rust
```
# Toolchain Setup
1. **Install Rust:** If not already done, visit [rustup.rs](https://rustup.rs/).
2. **Install ldproxy:**
The project uses Rust with ESP32-C6 support. The toolchain setup involves installing the necessary components:
1. **Rust Toolchain:**
```bash
cargo install ldproxy
```
3. **Install espup:**
```bash
cargo install espup
```
4. **Install ESP toolchain:**
```bash
espup install
```
5. **Install espflash:**
```bash
cargo install espflash
rustup toolchain install stable
rustup default stable
```
# Building the Web Interface
The configuration website is built using TypeScript and Webpack, then embedded into the Rust binary.
```bash
@@ -46,14 +38,7 @@ npx webpack
cd ..
```
# Compiling the Firmware
Build the project using Cargo:
```bash
cargo build --release
```
The resulting binary will be located in `target/riscv32imac-unknown-none-elf/release/plant-ctrl2`.
# Using Build Scripts
# Compiling the Firmware using Build Scripts
To simplify the process, several bash scripts are provided in the `Software/MainBoard/rust` directory:
* **`image_build.sh`**: Automatically builds the web interface, compiles the Rust firmware in release mode, and creates a flashable `image.bin`.