8 Commits

Author SHA1 Message Date
EmpirePhoenix 7f3910bcd0 sensor sweep tester 2025-10-07 21:50:33 +02:00
EmpirePhoenix 712e8c8b8f cleanups 2025-10-06 16:44:46 +02:00
EmpirePhoenix 4ba68182e5 start of plantsensor 2025-10-06 13:18:37 +02:00
EmpirePhoenix a3cdd92af8 fix ota abort/invalid switching 2025-10-06 02:43:37 +02:00
EmpirePhoenix 894be7c373 fix some ota stuff 2025-10-04 03:05:11 +02:00
EmpirePhoenix 0ddf6a6886 stuff 2025-10-04 01:24:00 +02:00
EmpirePhoenix 27b18df78e ota is back 2025-10-01 21:56:16 +02:00
EmpirePhoenix 9c6dcc465e canbus sensor stub and pcnt impl 2025-10-01 00:36:15 +02:00
133 changed files with 6714 additions and 374 deletions
+2
View File
@@ -34,3 +34,5 @@ _autosave-*
# Autorouter files (exported from Pcbnew)
fp-info-cache
*.zip
netlist.ipc

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

@@ -0,0 +1 @@
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
@@ -0,0 +1 @@
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
@@ -0,0 +1 @@
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
@@ -0,0 +1 @@
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
@@ -0,0 +1 @@
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
@@ -0,0 +1,4 @@
(fp_lib_table
(version 7)
(lib (name "Sensor")(type "KiCad")(uri "/home/empire/workspace/PlantCtrl/board/modules/Sensors/Sensors/Sensor.pretty")(options "")(descr ""))
)
@@ -0,0 +1,71 @@
P CODE 00
P UNITS CUST 0
P arrayDim N
317GND VIA MD0118PA00X+040551Y-024902X0236Y0000R000S3
317GND VIA MD0118PA00X+054232Y-022933X0236Y0000R000S3
317GND VIA MD0118PA00X+053346Y-021949X0236Y0000R000S3
317GND VIA MD0118PA00X+040846Y-019980X0236Y0000R000S3
317GND VIA MD0118PA00X+041043Y-025787X0236Y0000R000S3
317GND VIA MD0118PA00X+040059Y-019980X0236Y0000R000S3
317GND VIA MD0118PA00X+044291Y-025787X0236Y0000R000S3
317GND VIA MD0118PA00X+046654Y-025000X0236Y0000R000S3
317GND VIA MD0118PA00X+041240Y-021949X0236Y0000R000S3
317GND VIA MD0118PA00X+043307Y-020768X0236Y0000R000S3
317GND VIA MD0118PA00X+039961Y-022539X0236Y0000R000S3
317GND VIA MD0118PA00X+049606Y-021260X0236Y0000R000S3
317GND VIA MD0118PA00X+043898Y-024803X0236Y0000R000S3
317GND VIA MD0118PA00X+044587Y-023917X0236Y0000R000S3
317GND VIA MD0118PA00X+041142Y-024016X0236Y0000R000S3
317GND VIA MD0118PA00X+052461Y-022835X0236Y0000R000S3
317GND VIA MD0118PA00X+053642Y-024016X0236Y0000R000S3
317GND VIA MD0118PA00X+047835Y-026378X0236Y0000R000S3
317GND VIA MD0118PA00X+042618Y-020669X0236Y0000R000S3
317GND VIA MD0118PA00X+049409Y-022047X0236Y0000R000S3
317GND VIA MD0118PA00X+042520Y-022441X0236Y0000R000S3
317GND VIA MD0118PA00X+048622Y-022835X0236Y0000R000S3
327GND R_slop-1 A01X+053967Y-020177X0315Y0374R180S2
327NET-(U1-RS) R_slop-2 A01X+053317Y-020177X0315Y0374R180S2
3173_3V U6 -1 D0394PA00X+039016Y-019370X0669Y0669R000S0
317(U6-VBAT-PAD2) U6 -2 D0394PA00X+039016Y-020370X0669Y0000R000S0
317-(U6-SDA-PAD3) U6 -3 D0394PA00X+039016Y-021370X0669Y0000R000S0
317-(U6-SCL-PAD4) U6 -4 D0394PA00X+039016Y-022370X0669Y0000R000S0
317ENABLE_CAN U6 -5 D0394PA00X+039016Y-023370X0669Y0000R000S0
317CAN+ U6 -6 D0394PA00X+039016Y-024370X0669Y0000R000S0
317CAN- U6 -7 D0394PA00X+039016Y-025370X0669Y0000R000S0
317GND U6 -8 D0394PA00X+039016Y-026370X0669Y0000R000S0
317GND U6 -9 D0394PA00X+071260Y-034252X0669Y0669R000S0
317GND U6 -10 D0394PA00X+055512Y-031299X0669Y0669R000S0
317GND U6 -11 D0394PA00X+038780Y-034252X0669Y0669R000S0
317GND U6 -12 D0394PA00X+055512Y-020669X0669Y0669R000S0
317GND U6 -13 D0394PA00X+071260Y-019094X0669Y0669R000S0
327NET-(QP_1-G) QP_1 -1 A01X+047835Y-021280X0354Y0315R000S2
3273_3V QP_1 -2 A01X+047835Y-022028X0354Y0315R000S2
327CAN_POWER QP_1 -3 A01X+048622Y-021654X0354Y0315R000S2
327CAN_POWER R5 -1 A01X+047835Y-022904X0315Y0374R090S2
327NET-(I1-A) R5 -2 A01X+047835Y-023553X0315Y0374R090S2
327NET-(QP_1-G) R6 -1 A01X+046654Y-021329X0315Y0374R090S2
3273_3V R6 -2 A01X+046654Y-021978X0315Y0374R090S2
327NET-(QP_1-G) R7 -1 A01X+045669Y-021329X0315Y0374R090S2
327NET-(Q1-D) R7 -2 A01X+045669Y-021978X0315Y0374R090S2
317CAN_POWER J1 -1 D0374PA00X+055197Y-025394X0669Y0768R270S0
317NET-(J1-PIN_2) J1 -2 D0374PA00X+055197Y-024409X0669Y0768R270S0
317NET-(J1-PIN_3) J1 -3 D0374PA00X+055197Y-023425X0669Y0768R270S0
317GND J1 -4 D0374PA00X+055197Y-022441X0669Y0768R270S0
327NET-(Q1-G) Q1 -1 A01X+043819Y-021280X0354Y0315R000S2
327GND Q1 -2 A01X+043819Y-022028X0354Y0315R000S2
327NET-(Q1-D) Q1 -3 A01X+044606Y-021654X0354Y0315R000S2
327CAN+ U1 -1 A01X+050295Y-020707X0768Y0236R000S2
327GND U1 -2 A01X+050295Y-021207X0768Y0236R000S2
327CAN_POWER U1 -3 A01X+050295Y-021707X0768Y0236R000S2
327CAN- U1 -4 A01X+050295Y-022207X0768Y0236R000S2
327(U1-VREF-PAD5) U1 -5 A01X+052244Y-022207X0768Y0236R000S2
327NET-(J1-PIN_2) U1 -6 A01X+052244Y-021707X0768Y0236R000S2
327NET-(J1-PIN_3) U1 -7 A01X+052244Y-021207X0768Y0236R000S2
327NET-(U1-RS) U1 -8 A01X+052244Y-020707X0768Y0236R000S2
327NET-(Q1-G) R8 -1 A01X+042520Y-021329X0315Y0374R090S2
327GND R8 -2 A01X+042520Y-021978X0315Y0374R090S2
327GND I1 -1 A01X+047835Y-025541X0384Y0551R270S2
327NET-(I1-A) I1 -2 A01X+047835Y-024803X0384Y0551R270S2
327NET-(Q1-G) R4 -1 A01X+040846Y-023346X0315Y0374R180S2
327ENABLE_CAN R4 -2 A01X+040197Y-023346X0315Y0374R180S2
999
@@ -0,0 +1,32 @@
;;; .doomrc --- doom runtime config -*- mode: emacs-lisp; lexical-binding: t; -*-
;;; Commentary:
;;; Code:
(require 'doom) ; be silent, byte-compiler
(after! dape
(add-to-list
'dape-configs
`(gdb-dap-openocd
ensure (lambda (config)
(dape-ensure-command config)
(let* ((default-directory
(or (dape-config-get config 'command-cwd)
default-directory))
(command (dape-config-get config 'command))
(output (shell-command-to-string (format "%s --version" command)))
(version (save-match-data
(when (string-match "GNU gdb \\(?:(.*) \\)?\\([0-9.]+\\)" output)
(string-to-number (match-string 1 output))))))
(unless (>= version 14.1)
(user-error "Requires gdb version >= 14.1"))))
modes ()
command-cwd dape-command-cwd
command "gdb"
command-args ("--interpreter=dap")
:request nil
:program nil
:args []
:stopAtBeginningOfMainSubprogram nil))
)
;;; .doomrc ends here
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+10
View File
@@ -0,0 +1,10 @@
# ESP-IDF build artifacts
build/
.sdkconfig*
CMakeFiles/
CMakeCache.txt
cmake-build-*/
*.log
*.bin
*.elf
*.map
+8
View File
@@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/bootloader.iml" filepath="$PROJECT_DIR$/.idea/bootloader.iml" />
</modules>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/../website/themes/blowfish" vcs="Git" />
</component>
</project>
+11
View File
@@ -0,0 +1,11 @@
cmake_minimum_required(VERSION 3.16)
# Minimal ESP-IDF project to build only the bootloader
# You must have ESP-IDF installed and IDF_PATH exported.
# Pin the target to ESP32-C6 to ensure correct bootloader build
# (must be set before including project.cmake)
set(IDF_TARGET "esp32c6")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(custom_bootloader)
+43
View File
@@ -0,0 +1,43 @@
Custom ESP-IDF Bootloader (Rollback Enabled)
This minimal project builds a custom ESP-IDF bootloader with rollback support enabled.
You can flash it later alongside a Rust firmware using `espflash`.
What this provides
- A minimal ESP-IDF project (CMake) that can build just the bootloader.
- Rollback support enabled via sdkconfig.defaults (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y).
- A sample OTA partition table (partitions.csv) suitable for OTA and rollback (otadata + two OTA slots).
- A convenience script to build the bootloader for the desired target.
Requirements
- ESP-IDF installed and set up (IDF_PATH exported, Python env activated).
- A selected target (esp32, esp32s3, esp32c3, etc.).
Build
1) Ensure ESP-IDF is set up:
source "$IDF_PATH/export.sh"
2) Pick a target (examples):
idf.py set-target esp32
# or use the script:
./build_bootloader.sh esp32
3) Build only the bootloader:
idf.py bootloader
# or using the script (which also supports setting target):
./build_bootloader.sh esp32
Artifacts
- build/bootloader/bootloader.bin
Using with espflash (Rust)
- For a no_std Rust firmware, you can pass this custom bootloader to espflash:
espflash flash --bootloader build/bootloader/bootloader.bin \
--partition-table partitions.csv \
<your-app-binary-or-elf>
Notes
- Rollback logic requires an OTA layout (otadata + at least two OTA app partitions). The provided partitions.csv is a starting point; adjust sizes/offsets to match your needs.
- This project doesnt build an application; it exists solely to produce a bootloader with the right configuration.
- If you need different log verbosity or features, run `idf.py menuconfig` and then diff/port the changes back into sdkconfig.defaults.
- Targets supported depend on your ESP-IDF version. Use `idf.py set-target <chip>` or `./build_bootloader.sh <chip>`.
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
# Build script for custom ESP-IDF bootloader with rollback enabled.
# Requirements:
# - ESP-IDF installed
# - IDF_PATH exported
# - Python env prepared (the usual ESP-IDF setup)
# Usage:
# ./build_bootloader.sh [esp32|esp32s3|esp32c3|esp32s2|esp32c2|esp32c6|esp32h2]
# If target is omitted, the last configured target will be used.
TARGET=${1:-}
if [[ -z "${IDF_PATH:-}" ]]; then
echo "ERROR: IDF_PATH is not set. Please install ESP-IDF and export the environment (source export.sh)." >&2
exit 1
fi
# shellcheck source=/dev/null
source "$IDF_PATH/export.sh"
if [[ -n "$TARGET" ]]; then
idf.py set-target "$TARGET"
fi
# Ensure sdkconfig.defaults is considered (ESP-IDF does this automatically).
# Build only the bootloader.
idf.py bootloader
echo
BOOTLOADER_BIN="build/bootloader/bootloader.bin"
if [[ -f "$BOOTLOADER_BIN" ]]; then
echo "Bootloader built: $BOOTLOADER_BIN"
echo "You can use this with espflash via:"
echo " espflash flash --bootloader $BOOTLOADER_BIN [--partition-table partitions.csv] <your-app-binary>"
else
echo "ERROR: Bootloader binary not found. Check build logs above." >&2
exit 2
fi
cp build/bootloader/bootloader.bin ../rust/bootloader.bin
+1
View File
@@ -0,0 +1 @@
idf_component_register(SRCS "dummy.c" INCLUDE_DIRS ".")
+4
View File
@@ -0,0 +1,4 @@
// This file intentionally left almost empty.
// ESP-IDF expects at least one component; the bootloader build does not use this.
void __unused_dummy_symbol(void) {}
+6
View File
@@ -0,0 +1,6 @@
nvs, data, nvs, , 16k,
otadata, data, ota, , 8k,
phy_init, data, phy, , 4k,
ota_0, app, ota_0, , 3968k,
ota_1, app, ota_1, , 3968k,
storage, data, littlefs,, 8M,
1 nvs data nvs 16k
2 otadata data ota 8k
3 phy_init data phy 4k
4 ota_0 app ota_0 3968k
5 ota_1 app ota_1 3968k
6 storage data littlefs 8M
+2385
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
# Target can be set with: idf.py set-target esp32|esp32s3|esp32c3|...
# If not set via idf.py, ESP-IDF may default to a target; it's recommended to set it explicitly.
# Explicitly pin target to ESP32-C6
CONFIG_IDF_TARGET="esp32c6"
CONFIG_IDF_TARGET_ESP32C6=y
CONFIG_IDF_TARGET_ARCH_RISCV=y
# Bootloader configuration
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y
# Slightly faster boot by skipping GPIO checks unless you need that feature
CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y
# Partition table config is not required to build bootloader, but shown for clarity when you build full app later
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
# CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -10,7 +10,7 @@ rustflags = [
target = "riscv32imac-unknown-none-elf"
[target.riscv32imac-unknown-none-elf]
runner = "espflash flash --monitor --chip esp32c6 --baud 921600 --partition-table partitions.csv"
#runner = "espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv"
#runner = "espflash flash --monitor --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl
#runner = "espflash flash --monitor"
#runner = "cargo runner"
+2 -13
View File
@@ -13,19 +13,6 @@ test = false
bench = false
doc = false
[package.metadata.cargo_runner]
# The string `$TARGET_FILE` will be replaced with the path from cargo.
command = [
"cargo",
"espflash",
"save-image",
"--chip",
"esp32c6",
"image.bin",
"--partition-table",
"partitions.csv"
]
#this strips the bootloader, we need that tho
#strip = true
@@ -160,6 +147,8 @@ option-lock = { version = "0.3.1", default-features = false }
#stay in sync with mcutie version here!
heapless = { version = "0.7.17", features = ["serde"] }
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
nb = "1.1.0"
embedded-can = "0.4.1"
Executable
+12
View File
@@ -0,0 +1,12 @@
rm ./src/webserver/index.html.gz
rm ./src/webserver/bundle.js.gz
set -e
cd ./src_webpack/
npx webpack build
cp index.html.gz ../src/webserver/index.html.gz
cp bundle.js.gz ../src/webserver/bundle.js.gz
cd ../
cargo build --release
espflash save-image --bootloader bootloader.bin --partition-table partitions.csv --chip esp32c6 target/riscv32imac-unknown-none-elf/release/plant-ctrl2 image.bin
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
Binary file not shown.
+1 -1
View File
@@ -50,7 +50,7 @@ fn linker_be_nice() {
}
fn main() {
webpack();
//webpack();
linker_be_nice();
let _ = EmitBuilder::builder().all_git().all_build().emit();
}
Executable
+11
View File
@@ -0,0 +1,11 @@
rm ./src/webserver/index.html.gz
rm ./src/webserver/bundle.js.gz
set -e
cd ./src_webpack/
npx webpack build
cp index.html.gz ../src/webserver/index.html.gz
cp bundle.js.gz ../src/webserver/bundle.js.gz
cd ../
cargo build --release
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
+13
View File
@@ -0,0 +1,13 @@
rm image.bin
rm ./src/webserver/index.html.gz
rm ./src/webserver/bundle.js.gz
set -e
cd ./src_webpack/
npx webpack build
cp index.html.gz ../src/webserver/index.html.gz
cp bundle.js.gz ../src/webserver/bundle.js.gz
cd ../
set -e
cargo build --release
espflash save-image --bootloader bootloader.bin --partition-table partitions.csv --chip esp32c6 target/riscv32imac-unknown-none-elf/release/plant-ctrl2 image.bin
+37
View File
@@ -6,8 +6,10 @@ use core::str::Utf8Error;
use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_executor::SpawnError;
use embassy_sync::mutex::TryLockError;
use embedded_storage::nor_flash::NorFlashErrorKind;
use esp_hal::i2c::master::ConfigError;
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
use esp_hal::twai::EspTwaiError;
use esp_wifi::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use littlefs2_core::PathError;
@@ -60,6 +62,12 @@ pub enum FatError {
ExpanderError {
error: String,
},
CanBusError {
error: EspTwaiError,
},
SNTPError {
error: sntpc::Error,
},
}
pub type FatResult<T> = Result<T, FatError>;
@@ -88,6 +96,10 @@ impl fmt::Display for FatError {
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error),
FatError::CanBusError { error } => {
write!(f, "CanBusError {:?}", error)
}
FatError::SNTPError { error } => write!(f, "SNTPError {:?}", error),
}
}
}
@@ -279,3 +291,28 @@ impl From<InvalidHighLimit> for FatError {
}
}
}
impl From<nb::Error<EspTwaiError>> for FatError {
fn from(value: nb::Error<EspTwaiError>) -> Self {
match value {
nb::Error::Other(can_error) => FatError::CanBusError { error: can_error },
nb::Error::WouldBlock => FatError::String {
error: "Would block".to_string(),
},
}
}
}
impl From<NorFlashErrorKind> for FatError {
fn from(value: NorFlashErrorKind) -> Self {
FatError::String {
error: value.to_string(),
}
}
}
impl From<sntpc::Error> for FatError {
fn from(value: sntpc::Error) -> Self {
FatError::SNTPError { error: value }
}
}
+13
View File
@@ -0,0 +1,13 @@
use crate::hal::Sensor;
use bincode::{Decode, Encode};
pub(crate) const SENSOR_BASE_ADDRESS: u16 = 1000;
#[derive(Debug, Clone, Copy, Encode, Decode)]
pub(crate) struct AutoDetectRequest {}
#[derive(Debug, Clone, Copy, Encode, Decode)]
pub(crate) struct ResponseMoisture {
pub plant: u8,
pub sensor: Sensor,
pub hz: u32,
}
+61 -44
View File
@@ -1,6 +1,6 @@
use crate::bail;
use crate::config::{NetworkConfig, PlantControllerConfig};
use crate::hal::{PLANT_COUNT, TIME_ACCESS};
use crate::hal::{get_current_slot_and_fix_ota_data, PLANT_COUNT, TIME_ACCESS};
use crate::log::{LogMessage, LOG_ACCESS};
use chrono::{DateTime, Utc};
use serde::Serialize;
@@ -9,7 +9,7 @@ use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use alloc::string::ToString;
use alloc::sync::Arc;
use alloc::{format, string::String, vec::Vec};
use alloc::{format, string::String, vec, vec::Vec};
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
use core::str::FromStr;
use core::sync::atomic::Ordering;
@@ -19,9 +19,10 @@ use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticCon
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer};
use embedded_storage::nor_flash::ReadNorFlash;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use embassy_time::{Duration, Timer, WithTimeout};
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState, Slot};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_hal::gpio::{Input, RtcPinWithResistors};
use esp_hal::rng::Rng;
@@ -127,7 +128,10 @@ pub struct Esp<'a> {
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub ota: Ota<'static, FlashStorage>,
pub ota_next: &'static mut FlashRegion<'static, FlashStorage>,
pub ota_target: &'static mut FlashRegion<'static, FlashStorage>,
pub current: Slot,
pub slot0_state: OtaImageState,
pub slot1_state: OtaImageState,
}
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
@@ -212,35 +216,46 @@ impl Esp<'_> {
Ok((buf, read))
}
pub(crate) fn get_ota_slot(&mut self) -> String {
match self.ota.current_slot() {
Ok(slot) => {
format!("{:?}", slot)
}
Err(err) => {
format!("{:?}", err)
}
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
let _ = check_erase(self.ota_target, offset, offset + 4096);
self.ota_target.erase(offset, offset + 4096)?;
let mut temp = vec![0; buf.len()];
let read_back = temp.as_mut_slice();
//change to nor flash, align writes!
self.ota_target.write(offset, buf)?;
self.ota_target.read(offset, read_back)?;
if buf != read_back {
info!("Expected {:?} but got {:?}", buf, read_back);
bail!(
"Flash error, read back does not match write buffer at offset {:x}",
offset
)
}
Ok(())
}
pub(crate) fn get_ota_state(&mut self) -> String {
match self.ota.current_ota_state() {
Ok(state) => {
format!("{:?}", state)
}
Err(err) => {
format!("{:?}", err)
}
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
let current = self.ota.current_slot()?;
if self.ota.current_ota_state()? != OtaImageState::Valid {
info!(
"Validating current slot {:?} as it was able to ota",
current
);
self.ota.set_current_ota_state(Valid)?;
}
}
// let current = ota.current_slot()?;
// println!(
// "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)",
// ota.current_ota_state()
// );
// println!("current {:?} - next {:?}", current, current.next());
// let ota_state = ota.current_ota_state()?;
self.ota.set_current_slot(current.next())?;
info!("switched slot");
self.ota.set_current_ota_state(OtaImageState::New)?;
info!("switched state for new partition");
let state_new = self.ota.current_ota_state()?;
info!("state on new partition now {:?}", state_new);
//determine nextslot crc
self.set_restart_to_conf(true);
Ok(())
}
pub(crate) fn mode_override_pressed(&mut self) -> bool {
self.boot_button.is_low()
@@ -275,20 +290,24 @@ impl Esp<'_> {
if ntp_addrs.is_empty() {
bail!("Failed to resolve DNS");
}
info!("NTP server: {:?}", ntp_addrs);
let mut counter = 0;
loop {
let addr: IpAddr = ntp_addrs[0].into();
let result = get_time(SocketAddr::from((addr, 123)), &socket, context).await;
let timeout = get_time(SocketAddr::from((addr, 123)), &socket, context)
.with_timeout(Duration::from_millis((_max_wait_ms / 10) as u64))
.await;
match result {
Ok(time) => {
match timeout {
Ok(result) => {
let time = result?;
info!("Time: {:?}", time);
return DateTime::from_timestamp(time.seconds as i64, 0)
.context("Could not convert Sntp result");
}
Err(e) => {
warn!("Error: {:?}", e);
Err(err) => {
warn!("sntp timeout, retry: {:?}", err);
counter += 1;
if counter > 10 {
bail!("Failed to get time from NTP server");
@@ -299,12 +318,6 @@ impl Esp<'_> {
}
}
pub async fn flash_ota(&mut self) -> FatResult<()> {
let capacity = self.ota_next.capacity();
bail!("not implemented")
}
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
info!("start wifi scan");
let mut lock = self.controller.try_lock()?;
@@ -592,7 +605,11 @@ impl Esp<'_> {
}
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
let cfg = PathBuf::try_from(CONFIG_FILE).unwrap();
let cfg = PathBuf::try_from(CONFIG_FILE)?;
let config_exist = self.fs.lock().await.exists(&cfg);
if !config_exist {
bail!("No config file stored")
}
let data = self.fs.lock().await.read::<4096>(&cfg)?;
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
return Ok(config);
@@ -892,8 +909,8 @@ async fn mqtt_incoming_task(
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
}
MqttMessage::Publish(topic, payload) => match topic {
Topic::DeviceType(type_topic) => {}
Topic::Device(device_topic) => {}
Topic::DeviceType(_type_topic) => {}
Topic::Device(_device_topic) => {}
Topic::General(topic) => {
let subtopic = topic.as_str();
+45 -20
View File
@@ -1,7 +1,7 @@
use embedded_storage::{ReadStorage, Storage};
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use esp_storage::FlashStorage;
use littlefs2::consts::U512 as lfsCache;
use littlefs2::consts::U4096 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage;
use littlefs2::io::Error as lfs2Error;
@@ -13,18 +13,24 @@ pub struct LittleFs2Filesystem {
}
impl lfs2Storage for LittleFs2Filesystem {
const READ_SIZE: usize = 256;
const WRITE_SIZE: usize = 512;
const BLOCK_SIZE: usize = 512; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 512; //8mb in 32kb blocks
const READ_SIZE: usize = 4096;
const WRITE_SIZE: usize = 4096;
const BLOCK_SIZE: usize = 4096; //usually optimal for flash access
const BLOCK_COUNT: usize = 8 * 1000 * 1000 / 4096; //8Mb in 4k blocks + a little space for stupid calculation errors
const BLOCK_CYCLES: isize = 100;
type CACHE_SIZE = lfsCache;
type LOOKAHEAD_SIZE = lfsLookahead;
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
let read_size: usize = Self::READ_SIZE;
assert_eq!(off % read_size, 0);
assert_eq!(buf.len() % read_size, 0);
if off % read_size != 0 {
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {} read_size: {}", off, read_size);
return Err(lfs2Error::IO);
}
if buf.len() % read_size != 0 {
error!("Littlefs2Filesystem read error: length not aligned to read size length: {} read_size: {}", buf.len(), read_size);
return Err(lfs2Error::IO);
}
match self.storage.read(off as u32, buf) {
Ok(..) => Ok(buf.len()),
Err(err) => {
@@ -36,8 +42,14 @@ impl lfs2Storage for LittleFs2Filesystem {
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
let write_size: usize = Self::WRITE_SIZE;
assert_eq!(off % write_size, 0);
assert_eq!(data.len() % write_size, 0);
if off % write_size != 0 {
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {} write_size: {}", off, write_size);
return Err(lfs2Error::IO);
}
if data.len() % write_size != 0 {
error!("Littlefs2Filesystem write error: length not aligned to write size length: {} write_size: {}", data.len(), write_size);
return Err(lfs2Error::IO);
}
match self.storage.write(off as u32, data) {
Ok(..) => Ok(data.len()),
Err(err) => {
@@ -49,15 +61,28 @@ impl lfs2Storage for LittleFs2Filesystem {
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
let block_size: usize = Self::BLOCK_SIZE;
debug_assert!(off % block_size == 0);
debug_assert!(len % block_size == 0);
//match self.storage.erase(off as u32, len as u32) {
//anyhow::Result::Ok(..) => lfs2Result::Ok(len),
//Err(err) => {
//error!("Littlefs2Filesystem erase error: {:?}", err);
//Err(lfs2Error::IO)
// }
//}
lfs2Result::Ok(len)
if off % block_size != 0 {
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {} block_size: {}", off, block_size);
return lfs2Result::Err(lfs2Error::IO);
}
if len % block_size != 0 {
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {} block_size: {}", len, block_size);
return lfs2Result::Err(lfs2Error::IO);
}
match check_erase(self.storage, off as u32, (off+len) as u32) {
Ok(_) => {}
Err(err) => {
error!("Littlefs2Filesystem check erase error: {:?}", err);
return lfs2Result::Err(lfs2Error::IO);
}
}
match self.storage.erase(off as u32, (off + len) as u32) {
Ok(..) => lfs2Result::Ok(len),
Err(err) => {
error!("Littlefs2Filesystem erase error: {:?}", err);
lfs2Result::Err(lfs2Error::IO)
}
}
}
}
+114 -20
View File
@@ -1,4 +1,5 @@
pub(crate) mod battery;
mod can_api;
pub mod esp;
mod initial_hal;
mod little_fs2storage_adapter;
@@ -8,12 +9,10 @@ mod v3_shift_register;
mod v4_hal;
mod v4_sensor;
mod water;
use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::peripherals::Peripherals;
use esp_hal::peripherals::ADC1;
use esp_hal::peripherals::APB_SARADC;
use esp_hal::peripherals::GPIO0;
use esp_hal::peripherals::GPIO10;
use esp_hal::peripherals::GPIO11;
@@ -41,7 +40,6 @@ use esp_hal::peripherals::GPIO5;
use esp_hal::peripherals::GPIO6;
use esp_hal::peripherals::GPIO7;
use esp_hal::peripherals::GPIO8;
use esp_hal::peripherals::PCNT;
use esp_hal::peripherals::TWAI0;
use crate::{
@@ -58,6 +56,7 @@ use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;
use async_trait::async_trait;
use bincode::{Decode, Encode};
use bq34z100::Bq34z100g1Driver;
use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell;
@@ -78,16 +77,18 @@ use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Pull};
use measurements::{Current, Voltage};
use crate::fat_error::{FatError, FatResult};
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{print_battery_bq34z100, BQ34Z100G1};
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
use crate::hal::water::TankSensor;
use crate::log::LOG_ACCESS;
use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock;
use embedded_storage::nor_flash::ReadNorFlash;
use esp_alloc as _;
use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::Slot;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::ota::{Slot as ota_slot, Slot};
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::pcnt::unit::Unit;
@@ -102,19 +103,22 @@ use esp_storage::FlashStorage;
use esp_wifi::{init, EspWifiController};
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage;
use log::{info, warn};
use log::{error, info, warn};
use portable_atomic::AtomicBool;
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
//Only support for 8 right now!
pub const PLANT_COUNT: usize = 8;
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
const TANK_MULTI_SAMPLE: usize = 11;
pub static I2C_DRIVER: OnceLock<
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
> = OnceLock::new();
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
pub enum Sensor {
A,
B,
@@ -149,7 +153,15 @@ pub trait BoardInteraction<'a> {
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
// Return JSON string with autodetected sensors per plant. Default: not supported.
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> {
bail!("Autodetection is only available on v4 HAL with CAN bus");
}
async fn progress(&mut self, counter: u32) {
// Indicate progress is active to suppress default wait_infinity blinking
crate::hal::PROGRESS_ACTIVE.store(true, core::sync::atomic::Ordering::Relaxed);
let current = counter % PLANT_COUNT as u32;
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, current == led as u32).await {
@@ -167,6 +179,9 @@ pub trait BoardInteraction<'a> {
}
}
let _ = self.general_fault(false).await;
// Reset progress active flag so wait_infinity can resume blinking
crate::hal::PROGRESS_ACTIVE.store(false, core::sync::atomic::Ordering::Relaxed);
}
}
@@ -305,10 +320,6 @@ impl PlantHal {
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
// List all partitions - this is just FYI
for i in 0..pt.len() {
info!("{:?}", pt.get_partition(i));
}
let ota_data = mk_static!(
PartitionEntry,
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
@@ -322,9 +333,18 @@ impl PlantHal {
ota_data.as_embedded_storage(storage_ota)
);
let mut ota = esp_bootloader_esp_idf::ota::Ota::new(ota_data)?;
let state_0 = ota_state(ota_slot::Slot0, ota_data);
let state_1 = ota_state(ota_slot::Slot1, ota_data);
let mut ota = Ota::new(ota_data)?;
let running = get_current_slot_and_fix_ota_data(&mut ota, state_0, state_1)?;
let target = running.next();
let ota_partition = match ota.current_slot()? {
info!("Currently running OTA slot: {:?}", running);
info!("Slot0 state: {:?}", state_0);
info!("Slot1 state: {:?}", state_1);
//obtain current_state and next_state here!
let ota_target = match target {
Slot::None => {
panic!("No OTA slot active?");
}
@@ -332,19 +352,19 @@ impl PlantHal {
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota0,
))?
.expect("No OTA slot0 found"),
.context("Partition table invalid no ota0")?,
Slot::Slot1 => pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
AppPartitionSubType::Ota1,
))?
.expect("No OTA slot1 found"),
.context("Partition table invalid no ota1")?,
};
let ota_next = mk_static!(PartitionEntry, ota_partition);
let ota_target = mk_static!(PartitionEntry, ota_target);
let storage_ota = mk_static!(FlashStorage, FlashStorage::new());
let ota_next = mk_static!(
let ota_target = mk_static!(
FlashRegion<FlashStorage>,
ota_next.as_embedded_storage(storage_ota)
ota_target.as_embedded_storage(storage_ota)
);
let data_partition = pt
@@ -369,7 +389,7 @@ impl PlantHal {
log::info!("Littlefs2 filesystem is formatted");
}
Err(err) => {
bail!("Littlefs2 filesystem could not be formatted: {:?}", err);
error!("Littlefs2 filesystem could not be formatted: {:?}", err);
}
}
}
@@ -389,7 +409,10 @@ impl PlantHal {
boot_button,
wake_gpio1,
ota,
ota_next,
ota_target,
current: running,
slot0_state: state_0,
slot1_state: state_1,
};
//init,reset rtc memory depending on cause
@@ -571,6 +594,77 @@ impl PlantHal {
}
}
fn ota_state(slot: ota_slot, ota_data: &mut FlashRegion<FlashStorage>) -> OtaImageState {
// Read and log OTA states for both slots before constructing Ota
// Each OTA select entry is 32 bytes: [seq:4][label:20][state:4][crc:4]
// Offsets within the OTA data partition: slot0 @ 0x0000, slot1 @ 0x1000
if slot == ota_slot::None {
return OtaImageState::Undefined;
}
let mut slot_buf = [0u8; 32];
if slot == ota_slot::Slot0 {
let _ = ota_data.read(0x0000, &mut slot_buf);
} else {
let _ = ota_data.read(0x1000, &mut slot_buf);
}
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
let state0 = OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined);
state0
}
fn get_current_slot_and_fix_ota_data(
ota: &mut Ota<FlashStorage>,
state0: OtaImageState,
state1: OtaImageState,
) -> Result<ota_slot, FatError> {
let state = ota.current_ota_state().unwrap_or_default();
let swap = match state {
OtaImageState::Invalid => true,
OtaImageState::Aborted => true,
OtaImageState::Undefined => {
info!("Undefined image in current slot, bootloader wrong?");
false
}
_ => false,
};
let current = ota.current_slot()?;
if swap {
let other = match current {
ota_slot::Slot0 => state1,
ota_slot::Slot1 => state0,
_ => OtaImageState::Invalid,
};
match other {
OtaImageState::Invalid => {
bail!(
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
current,
state0,
state1
);
}
OtaImageState::Aborted => {
bail!(
"cannot recover slot, as both slots in invalid state {:?} {:?} {:?}",
current,
state0,
state1
);
}
_ => {}
}
info!(
"Current slot has state {:?} other state has {:?} swapping",
state, other
);
ota.set_current_slot(current.next())?;
//we actually booted other slot, than partition table assumes
return Ok(ota.current_slot()?);
};
Ok(current)
}
pub async fn esp_time() -> DateTime<Utc> {
let guard = TIME_ACCESS.get().await.lock().await;
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
-2
View File
@@ -20,7 +20,6 @@ use embassy_time::Timer;
use embedded_hal::digital::OutputPin as _;
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::pcnt::unit::Unit;
use measurements::{Current, Voltage};
@@ -146,7 +145,6 @@ pub(crate) fn create_v3(
let signal_counter = peripherals.pcnt0;
signal_counter.set_low_limit(Some(0))?;
signal_counter.set_high_limit(Some(i16::MAX))?;
let ch0 = &signal_counter.channel0;
+42 -21
View File
@@ -1,28 +1,25 @@
use crate::bail;
use crate::config::PlantControllerConfig;
use crate::fat_error::{FatError, FatResult};
use crate::hal::battery::BatteryInteraction;
use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT, TIME_ACCESS};
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::boxed::Box;
use alloc::string::ToString;
use async_trait::async_trait;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer;
use esp_hal::{twai, Blocking};
//use embedded_hal_bus::i2c::MutexDevice;
use crate::bail;
use crate::fat_error::{FatError, FatResult};
use crate::hal::v4_sensor::{SensorImpl, SensorInteraction};
use crate::log::{LogMessage, LOG_ACCESS};
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::pcnt::Pcnt;
use esp_hal::twai::{EspTwaiFrame, StandardId, TwaiMode};
use esp_println::println;
use esp_hal::twai::TwaiMode;
use esp_hal::{twai, Blocking};
use ina219::address::{Address, Pin};
use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode, Resolution};
@@ -179,7 +176,6 @@ pub(crate) async fn create_v4(
let signal_counter = peripherals.pcnt0;
signal_counter.set_low_limit(Some(0))?;
signal_counter.set_high_limit(Some(i16::MAX))?;
let ch0 = &signal_counter.channel0;
@@ -203,23 +199,20 @@ pub(crate) async fn create_v4(
}
Err(_) => {
log::info!("Can bus mode ");
let twai_config = twai::TwaiConfiguration::new(
let twai_config = Some(twai::TwaiConfiguration::new(
peripherals.twai,
peripherals.gpio0,
peripherals.gpio2,
peripherals.gpio0,
TWAI_BAUDRATE,
TwaiMode::Normal,
);
));
let can_power = Output::new(peripherals.gpio22, Level::Low, OutputConfig::default());
let mut twai = twai_config.start();
let frame = EspTwaiFrame::new(StandardId::ZERO, &[1, 2, 3]).unwrap();
twai.transmit(&frame).unwrap();
// let frame = twai.receive().unwrap();
println!("Received a frame: {frame:?}");
//can bus version
SensorImpl::CanBus { twai }
SensorImpl::CanBus {
twai_config,
can_power,
}
}
};
@@ -409,6 +402,14 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
Timer::after_millis(100).await;
self.general_fault(false).await;
Timer::after_millis(500).await;
self.extra1.set_high();
Timer::after_millis(500).await;
self.extra1.set_low();
Timer::after_millis(500).await;
self.extra2.set_high();
Timer::after_millis(500).await;
self.extra2.set_low();
Timer::after_millis(500).await;
self.light(true).await?;
Timer::after_millis(500).await;
self.light(false).await?;
@@ -457,4 +458,24 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
self.charger.get_mppt_current()
}
async fn detect_sensors(&mut self) -> Result<alloc::string::String, FatError> {
// Delegate to sensor autodetect and build JSON
use alloc::string::ToString;
let detected = self.sensor.autodetect().await?;
// Build JSON manually to avoid exposing internal types
let mut s = alloc::string::String::from("{\"plants\":[");
for (i, (a, b)) in detected.iter().enumerate() {
if i != 0 {
s.push(',');
}
s.push_str("{\"a\":");
s.push_str(if *a { "true" } else { "false" });
s.push_str(",\"b\":");
s.push_str(if *b { "true" } else { "false" });
s.push('}');
}
s.push_str("]}");
Ok(s)
}
}
+192 -8
View File
@@ -1,17 +1,27 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::Box;
use crate::bail;
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::can_api::ResponseMoisture;
use crate::hal::Sensor;
use crate::hal::{can_api, Box};
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::format;
use alloc::string::ToString;
use async_trait::async_trait;
use bincode::config;
use bincode::error::DecodeError;
use core::mem;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_time::Timer;
use embassy_sync::mutex::Mutex;
use embassy_time::{Instant, Timer, WithTimeout};
use embedded_can::nb::Can;
use embedded_can::Frame;
use esp_hal::gpio::Output;
use esp_hal::i2c::master::I2c;
use esp_hal::pcnt::unit::Unit;
use esp_hal::twai::Twai;
use esp_hal::twai::{EspTwaiError, EspTwaiFrame, StandardId, Twai, TwaiConfiguration};
use esp_hal::Blocking;
use log::info;
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
const REPEAT_MOIST_MEASURE: usize = 10;
@@ -35,7 +45,8 @@ pub enum SensorImpl {
Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
},
CanBus {
twai: Twai<'static, Blocking>,
twai_config: Option<TwaiConfiguration<'static, Blocking>>,
can_power: Output<'static>,
},
}
@@ -122,9 +133,182 @@ impl SensorInteraction for SensorImpl {
let median = results[mid];
Ok(median)
}
SensorImpl::CanBus { twai } => Err(FatError::String {
error: "Not yet implemented".to_string(),
}),
SensorImpl::CanBus {
twai_config,
can_power,
} => {
can_power.set_high();
let config = twai_config.take().expect("twai config not set");
let mut twai = config.start();
loop {
let rec = twai.receive();
match rec {
Ok(_) => {}
Err(err) => {
info!("Error receiving CAN message: {:?}", err);
break;
}
}
}
Timer::after_millis(10).await;
let can = Self::inner_can(plant, sensor, &mut twai).await;
can_power.set_low();
let config = twai.stop();
twai_config.replace(config);
let value = can?;
Ok(value)
}
}
}
}
impl SensorImpl {
pub async fn autodetect(&mut self) -> FatResult<[(bool, bool); crate::hal::PLANT_COUNT]> {
match self {
SensorImpl::PulseCounter { .. } => {
bail!("Only CAN bus implementation supports autodetection")
}
SensorImpl::CanBus {
twai_config,
can_power,
} => {
// Power on CAN transceiver and start controller
can_power.set_high();
let config = twai_config.take().expect("twai config not set");
let mut twai = config.start();
// Give CAN some time to stabilize
Timer::after_millis(10).await;
// Send a few test messages per potential sensor node
for plant in 0..crate::hal::PLANT_COUNT {
for sensor in [Sensor::A, Sensor::B] {
// Reuse CAN addressing scheme from moisture request
let can_buffer = [0_u8; 8];
let cfg = config::standard();
if let Some(address) =
StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16)
{
if let Some(frame) = EspTwaiFrame::new(address, &can_buffer) {
// Try a few times; we intentionally ignore rx here and rely on stub logic
let resu = twai.transmit(&frame);
match resu {
Ok(_) => {
info!(
"Sent test message to plant {} sensor {:?}",
plant, sensor
);
}
Err(err) => {
info!("Error sending test message to plant {} sensor {:?}: {:?}", plant, sensor, err);
}
}
} else {
info!("Error building CAN frame");
}
} else {
info!("Error creating address for sensor");
}
}
}
loop {
let rec = twai
.receive()
.with_timeout(embassy_time::Duration::from_millis(100))
.await;
match rec {
Ok(msg) => match msg {
Ok(or) => {
info!("Received CAN message: {:?}", or);
}
Err(err) => {
info!("Error receiving CAN message: {:?}", err);
break;
}
},
Err(err) => {
info!("Error receiving CAN message: {:?}", err);
break;
}
}
}
// Wait for acknowledgements on the bus (stub: just wait 5 seconds)
Timer::after_millis(5_000).await;
// Stop CAN and power down
can_power.set_low();
twai_config.replace(config);
// Stub: return no detections yet
let mut result = [(false, false); crate::hal::PLANT_COUNT];
Ok(result)
}
}
}
async fn inner_can(
plant: usize,
sensor: Sensor,
twai: &mut Twai<'static, Blocking>,
) -> FatResult<f32> {
let can_sensor: Sensor = sensor.into();
//let request = RequestMoisture { sensor: can_sensor };
let can_buffer = [0_u8; 8];
let config = config::standard();
//let encoded = bincode::encode_into_slice(&request, &mut can_buffer, config)?;
let address = StandardId::new(can_api::SENSOR_BASE_ADDRESS + plant as u16)
.context(">> Could not create address for sensor! (plant: {}) <<")?;
let request =
EspTwaiFrame::new(address, &can_buffer[0..8]).context("Error building CAN frame")?;
twai.transmit(&request)?;
let timeout = Instant::now()
.checked_add(embassy_time::Duration::from_millis(100))
.context("Timeout")?;
loop {
let answer = twai.receive();
match answer {
Ok(answer) => {
let data = EspTwaiFrame::data(&answer);
let response: Result<(ResponseMoisture, usize), DecodeError> =
bincode::decode_from_slice(&data, config);
info!("Can answer {response:?}");
let value = response?.0;
if (value.plant as usize) != plant {
bail!(
"Received answer for wrong plant! Expected: {}, got: {}",
plant,
value.plant
);
}
if value.sensor != can_sensor {
bail!(
"Received answer for wrong sensor! Expected: {:?}, got: {:?}",
can_sensor,
value.sensor
);
}
return Ok(value.hz as f32);
}
Err(error) => match error {
nb::Error::Other(error) => {
return Err(FatError::CanBusError { error });
}
nb::Error::WouldBlock => {
if Instant::now() > timeout {
bail!("Timeout waiting for CAN answer");
}
Timer::after_millis(10).await;
}
},
}
}
}
}
+18 -33
View File
@@ -5,6 +5,8 @@ use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcConfig, AdcPin, Attenuation};
use esp_hal::delay::Delay;
use esp_hal::gpio::{Flex, Input, Output, OutputConfig, Pull};
use esp_hal::pcnt::channel::CtrlMode::Keep;
use esp_hal::pcnt::channel::EdgeMode::{Hold, Increment};
use esp_hal::pcnt::unit::Unit;
use esp_hal::peripherals::GPIO5;
use esp_hal::Blocking;
@@ -16,8 +18,7 @@ pub struct TankSensor<'a> {
tank_channel: Adc<'a, ADC1<'a>, Blocking>,
tank_power: Output<'a>,
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>>,
// flow_counter: PcntDriver<'a>,
// delay: Delay,
flow_counter: Unit<'a, 1>,
}
impl<'a> TankSensor<'a> {
@@ -37,54 +38,38 @@ impl<'a> TankSensor<'a> {
let one_wire_bus = OneWire::new(one_wire_pin, false);
//
// let mut flow_counter = PcntDriver::new(
// pcnt1,
// Some(flow_sensor_pin),
// Option::<AnyInputPin>::None,
// Option::<AnyInputPin>::None,
// Option::<AnyInputPin>::None,
// )?;
//
// flow_counter.channel_config(
// PcntChannel::Channel1,
// PinIndex::Pin0,
// PinIndex::Pin1,
// &PcntChannelConfig {
// lctrl_mode: PcntControlMode::Keep,
// hctrl_mode: PcntControlMode::Keep,
// pos_mode: PcntCountMode::Increment,
// neg_mode: PcntCountMode::Hold,
// counter_h_lim: i16::MAX,
// counter_l_lim: 0,
// },
// )?;
//
pcnt1.set_high_limit(Some(i16::MAX))?;
let ch0 = &pcnt1.channel0;
ch0.set_edge_signal(flow_sensor.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
pcnt1.listen();
Ok(TankSensor {
one_wire_bus,
tank_channel,
tank_power,
tank_pin, // flow_counter,
// delay: Default::default(),
tank_pin,
flow_counter: pcnt1,
})
}
pub fn reset_flow_meter(&mut self) {
// self.flow_counter.counter_pause().unwrap();
// self.flow_counter.counter_clear().unwrap();
self.flow_counter.pause();
self.flow_counter.clear();
}
pub fn start_flow_meter(&mut self) {
//self.flow_counter.counter_resume().unwrap();
self.flow_counter.resume();
}
pub fn get_flow_meter_value(&mut self) -> i16 {
//self.flow_counter.get_counter_value().unwrap()
5_i16
self.flow_counter.value()
}
pub fn stop_flow_meter(&mut self) -> i16 {
//self.flow_counter.counter_pause().unwrap();
self.flow_counter.pause();
self.get_flow_meter_value()
}
+14
View File
@@ -77,6 +77,20 @@ impl From<LogEntryInner> for LogEntry {
}
}
pub async fn log(
message_key: LogMessage,
number_a: u32,
number_b: u32,
txt_short: &str,
txt_long: &str,
) {
LOG_ACCESS
.lock()
.await
.log(message_key, number_a, number_b, txt_short, txt_long)
.await
}
impl LogArray {
pub fn get(&mut self) -> Vec<LogEntry> {
let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
+144 -145
View File
@@ -17,7 +17,8 @@ use crate::config::{NetworkConfig, PlantConfig};
use crate::fat_error::FatResult;
use crate::hal::esp::MQTT_STAY_ALIVE;
use crate::hal::{esp_time, TIME_ACCESS};
use crate::log::LOG_ACCESS;
use crate::hal::PROGRESS_ACTIVE;
use crate::log::{log, LOG_ACCESS};
use crate::tank::{determine_tank_state, TankError, TankState, WATER_FROZEN_THRESH};
use crate::webserver::http_server;
use crate::{
@@ -326,7 +327,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
.await;
}
let _dry_run = false;
let dry_run = false;
let tank_state = determine_tank_state(&mut board).await;
@@ -383,7 +384,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
}
}
let mut _water_frozen = false;
let mut water_frozen = false;
let water_temp: FatResult<f32> = match board.board_hal.get_tank_sensor() {
Ok(sensor) => sensor.water_temperature_c().await,
Err(e) => Err(e),
@@ -391,7 +392,7 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
if let Ok(res) = water_temp {
if res < WATER_FROZEN_THRESH {
_water_frozen = true;
water_frozen = true;
}
}
info!("Water temp is {}", water_temp.as_ref().unwrap_or(&0.));
@@ -411,74 +412,81 @@ async fn safe_main(spawner: Spawner) -> FatResult<()> {
publish_plant_states(&mut board, &timezone_time.clone(), &plantstate).await;
// let pump_required = plantstate
// .iter()
// .zip(&board.board_hal.get_config().plants)
// .any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time))
// && !water_frozen;
// if pump_required {
// log(LogMessage::EnableMain, dry_run as u32, 0, "", "");
// for (plant_id, (state, plant_config)) in plantstate
// .iter()
// .zip(&board.board_hal.get_config().plants.clone())
// .enumerate()
// {
// if state.needs_to_be_watered(plant_config, &timezone_time) {
// let pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id) + 1;
// board
// .board_hal
// .get_esp()
// .store_consecutive_pump_count(plant_id, pump_count);
//
// let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32;
// if pump_ineffective {
// log(
// LogMessage::ConsecutivePumpCountLimit,
// pump_count,
// plant_config.max_consecutive_pump_count as u32,
// &(plant_id + 1).to_string(),
// "",
// );
// board.board_hal.fault(plant_id, true).await?;
// }
// log(
// LogMessage::PumpPlant,
// (plant_id + 1) as u32,
// plant_config.pump_time_s as u32,
// &dry_run.to_string(),
// "",
// );
// board
// .board_hal
// .get_esp()
// .store_last_pump_time(plant_id, cur);
// board.board_hal.get_esp().last_pump_time(plant_id);
// //state.active = true;
//
// pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await;
//
// let result = do_secure_pump(plant_id, plant_config, dry_run).await?;
// board.board_hal.pump(plant_id, false).await?;
// pump_info(
// plant_id,
// false,
// pump_ineffective,
// result.median_current_ma,
// result.max_current_ma,
// result.min_current_ma,
// result.error,
// )
// .await;
// } else if !state.pump_in_timeout(plant_config, &timezone_time) {
// // plant does not need to be watered and is not in timeout
// // -> reset consecutive pump count
// board
// .board_hal
// .get_esp()
// .store_consecutive_pump_count(plant_id, 0);
// }
// }
// }
let pump_required = plantstate
.iter()
.zip(&board.board_hal.get_config().plants)
.any(|(it, conf)| it.needs_to_be_watered(conf, &timezone_time))
&& !water_frozen;
if pump_required {
LOG_ACCESS
.lock()
.await
.log(LogMessage::EnableMain, dry_run as u32, 0, "", "")
.await;
for (plant_id, (state, plant_config)) in plantstate
.iter()
.zip(&board.board_hal.get_config().plants.clone())
.enumerate()
{
if state.needs_to_be_watered(plant_config, &timezone_time) {
let pump_count = board.board_hal.get_esp().consecutive_pump_count(plant_id) + 1;
board
.board_hal
.get_esp()
.store_consecutive_pump_count(plant_id, pump_count);
let pump_ineffective = pump_count > plant_config.max_consecutive_pump_count as u32;
if pump_ineffective {
log(
LogMessage::ConsecutivePumpCountLimit,
pump_count,
plant_config.max_consecutive_pump_count as u32,
&(plant_id + 1).to_string(),
"",
)
.await;
board.board_hal.fault(plant_id, true).await?;
}
log(
LogMessage::PumpPlant,
(plant_id + 1) as u32,
plant_config.pump_time_s as u32,
&dry_run.to_string(),
"",
)
.await;
board
.board_hal
.get_esp()
.store_last_pump_time(plant_id, cur);
board.board_hal.get_esp().last_pump_time(plant_id);
//state.active = true;
pump_info(plant_id, true, pump_ineffective, 0, 0, 0, false).await;
let result = do_secure_pump(&mut board, plant_id, plant_config, dry_run).await?;
//stop pump regardless of prior result//todo refactor to inner?
board.board_hal.pump(plant_id, false).await?;
pump_info(
plant_id,
false,
pump_ineffective,
result.median_current_ma,
result.max_current_ma,
result.min_current_ma,
result.error,
)
.await;
} else if !state.pump_in_timeout(plant_config, &timezone_time) {
// plant does not need to be watered and is not in timeout
// -> reset consecutive pump count
board
.board_hal
.get_esp()
.store_consecutive_pump_count(plant_id, 0);
}
}
}
info!("state of charg");
let is_day = board.board_hal.is_day();
@@ -671,17 +679,14 @@ pub async fn do_secure_pump(
let high_current = current_ma > plant_config.max_pump_current_ma;
if high_current {
if first_error {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::PumpOverCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
log(
LogMessage::PumpOverCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.max_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error {
@@ -694,17 +699,14 @@ pub async fn do_secure_pump(
let low_current = current_ma < plant_config.min_pump_current_ma;
if low_current {
if first_error {
LOG_ACCESS
.lock()
.await
.log(
LogMessage::PumpOpenLoopCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
log(
LogMessage::PumpOpenLoopCurrent,
plant_id as u32 + 1,
current_ma as u32,
plant_config.min_pump_current_ma.to_string().as_str(),
step.to_string().as_str(),
)
.await;
board.board_hal.general_fault(true).await;
board.board_hal.fault(plant_id, true).await?;
if !plant_config.ignore_current_error {
@@ -718,17 +720,14 @@ pub async fn do_secure_pump(
Err(err) => {
if !plant_config.ignore_current_error {
info!("Error getting pump current: {}", err);
LOG_ACCESS
.lock()
.await
.log(
LogMessage::PumpMissingSensorCurrent,
plant_id as u32,
0,
"",
"",
)
.await;
log(
LogMessage::PumpMissingSensorCurrent,
plant_id as u32,
0,
"",
"",
)
.await;
error = true;
break;
} else {
@@ -824,18 +823,9 @@ async fn publish_firmware_info(
let esp = board.board_hal.get_esp();
let _ = esp.mqtt_publish("/firmware/address", ip_address).await;
let _ = esp
.mqtt_publish("/firmware/githash", &version.git_hash)
.await;
let _ = esp
.mqtt_publish("/firmware/buildtime", &version.build_time)
.mqtt_publish("/firmware/state", format!("{:?}", &version).as_str())
.await;
let _ = esp.mqtt_publish("/firmware/last_online", timezone_time);
let state = esp.get_ota_state();
let _ = esp.mqtt_publish("/firmware/ota_state", &state).await;
let slot = esp.get_ota_slot();
let _ = esp
.mqtt_publish("/firmware/ota_slot", &format!("slot{slot}"))
.await;
let _ = esp.mqtt_publish("/state", "online").await;
}
macro_rules! mk_static {
@@ -997,41 +987,48 @@ async fn wait_infinity(
let mut board = BOARD_ACCESS.get().await.lock().await;
update_charge_indicator(&mut board).await;
match wait_type {
WaitType::MissingConfig => {
// Keep existing behavior: circular filling pattern
led_count %= 8;
led_count += 1;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < led_count);
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0);
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
let _ = board.board_hal.fault(i, i == pattern_step);
// Skip default blink code when a progress display is active
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
match wait_type {
WaitType::MissingConfig => {
// Keep existing behavior: circular filling pattern
led_count %= 8;
led_count += 1;
for i in 0..8 {
let _ = board.board_hal.fault(i, i < led_count).await;
}
}
WaitType::ConfigButton => {
// Alternating pattern: 1010 1010 -> 0101 0101
pattern_step = (pattern_step + 1) % 2;
for i in 0..8 {
let _ = board.board_hal.fault(i, (i + pattern_step) % 2 == 0).await;
}
}
WaitType::MqttConfig => {
// Moving dot pattern
pattern_step = (pattern_step + 1) % 8;
for i in 0..8 {
let _ = board.board_hal.fault(i, i == pattern_step).await;
}
}
}
board.board_hal.general_fault(true).await;
}
board.board_hal.general_fault(true).await;
}
Timer::after_millis(delay).await;
{
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.general_fault(false).await;
// Clear all LEDs
for i in 0..8 {
let _ = board.board_hal.fault(i, false);
// Skip clearing LEDs when progress is active to avoid interrupting the progress display
if !PROGRESS_ACTIVE.load(Ordering::Relaxed) {
board.board_hal.general_fault(false).await;
// Clear all LEDs
for i in 0..8 {
let _ = board.board_hal.fault(i, false).await;
}
}
}
@@ -1060,7 +1057,6 @@ async fn main(spawner: Spawner) -> ! {
// intialize embassy
logger::init_logger_from_env();
//force init here!
println!("Hal init");
match BOARD_ACCESS.init(PlantHal::create().await.unwrap()) {
Ok(_) => {}
Err(_) => {
@@ -1099,11 +1095,12 @@ async fn get_version(
let hash = &env!("VERGEN_GIT_SHA")[0..8];
let board = board.board_hal.get_esp();
let ota_slot = board.get_ota_slot();
VersionInfo {
git_hash: branch + "@" + hash,
build_time: env!("VERGEN_BUILD_TIMESTAMP").to_owned(),
partition: ota_slot,
current: format!("{:?}", board.current),
slot0_state: format!("{:?}", board.slot0_state),
slot1_state: format!("{:?}", board.slot1_state),
}
}
@@ -1111,5 +1108,7 @@ async fn get_version(
struct VersionInfo {
git_hash: String,
build_time: String,
partition: String,
current: String,
slot0_state: String,
slot1_state: String,
}

Some files were not shown because too many files have changed in this diff Show More