Compare commits
11 Commits
fix/stay-a
...
45e948636b
| Author | SHA1 | Date | |
|---|---|---|---|
| 45e948636b | |||
| 083573de4a | |||
| e5c5f31112 | |||
| 7f3910bcd0 | |||
| 712e8c8b8f | |||
| 4ba68182e5 | |||
| a3cdd92af8 | |||
| 894be7c373 | |||
| 0ddf6a6886 | |||
| 27b18df78e | |||
| 9c6dcc465e |
Binary file not shown.
2
board/.gitignore
vendored
2
board/.gitignore
vendored
@@ -34,3 +34,5 @@ _autosave-*
|
|||||||
|
|
||||||
# Autorouter files (exported from Pcbnew)
|
# Autorouter files (exported from Pcbnew)
|
||||||
fp-info-cache
|
fp-info-cache
|
||||||
|
*.zip
|
||||||
|
netlist.ipc
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
1
board/modules/3v3/fabrication-toolkit-options.json
Normal file
1
board/modules/3v3/fabrication-toolkit-options.json
Normal file
@@ -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}
|
||||||
1
board/modules/LightOut/fabrication-toolkit-options.json
Normal file
1
board/modules/LightOut/fabrication-toolkit-options.json
Normal file
@@ -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}
|
||||||
1
board/modules/MPPT/fabrication-toolkit-options.json
Normal file
1
board/modules/MPPT/fabrication-toolkit-options.json
Normal file
@@ -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}
|
||||||
4
board/modules/Sensors_can/Sensors/fp-lib-table
Normal file
4
board/modules/Sensors_can/Sensors/fp-lib-table
Normal file
@@ -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 ""))
|
||||||
|
)
|
||||||
71
board/modules/Sensors_can/Sensors/production/netlist.ipc
Normal file
71
board/modules/Sensors_can/Sensors/production/netlist.ipc
Normal file
@@ -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
|
||||||
32
board/modules/Sensors_can/ch32-sensor/.doomrc
Normal file
32
board/modules/Sensors_can/ch32-sensor/.doomrc
Normal file
@@ -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
board/modules/Sensors_can/ch32-sensor/.idea/.gitignore
generated
vendored
Normal file
8
board/modules/Sensors_can/ch32-sensor/.idea/.gitignore
generated
vendored
Normal 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
|
||||||
11
board/modules/Sensors_can/ch32-sensor/.idea/ch32-sensor.iml
generated
Normal file
11
board/modules/Sensors_can/ch32-sensor/.idea/ch32-sensor.iml
generated
Normal file
@@ -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
bootloader/.gitignore
vendored
Normal file
10
bootloader/.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# ESP-IDF build artifacts
|
||||||
|
build/
|
||||||
|
.sdkconfig*
|
||||||
|
CMakeFiles/
|
||||||
|
CMakeCache.txt
|
||||||
|
cmake-build-*/
|
||||||
|
*.log
|
||||||
|
*.bin
|
||||||
|
*.elf
|
||||||
|
*.map
|
||||||
8
bootloader/.idea/.gitignore
generated
vendored
Normal file
8
bootloader/.idea/.gitignore
generated
vendored
Normal 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
bootloader/.idea/bootloader.iml
generated
Normal file
8
bootloader/.idea/bootloader.iml
generated
Normal 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
bootloader/.idea/modules.xml
generated
Normal file
8
bootloader/.idea/modules.xml
generated
Normal 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
bootloader/.idea/vcs.xml
generated
Normal file
7
bootloader/.idea/vcs.xml
generated
Normal 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
bootloader/CMakeLists.txt
Normal file
11
bootloader/CMakeLists.txt
Normal 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
bootloader/README.md
Normal file
43
bootloader/README.md
Normal 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 doesn’t 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
bootloader/build_bootloader.sh
Executable file
41
bootloader/build_bootloader.sh
Executable 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
bootloader/main/CMakeLists.txt
Normal file
1
bootloader/main/CMakeLists.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
idf_component_register(SRCS "dummy.c" INCLUDE_DIRS ".")
|
||||||
4
bootloader/main/dummy.c
Normal file
4
bootloader/main/dummy.c
Normal 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
bootloader/partitions.csv
Normal file
6
bootloader/partitions.csv
Normal 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,
|
||||||
|
2385
bootloader/sdkconfig
Normal file
2385
bootloader/sdkconfig
Normal file
File diff suppressed because it is too large
Load Diff
17
bootloader/sdkconfig.defaults
Normal file
17
bootloader/sdkconfig.defaults
Normal 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"
|
||||||
2214
bootloader/sdkconfig.old
Normal file
2214
bootloader/sdkconfig.old
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ rustflags = [
|
|||||||
target = "riscv32imac-unknown-none-elf"
|
target = "riscv32imac-unknown-none-elf"
|
||||||
|
|
||||||
[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 --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl
|
||||||
#runner = "espflash flash --monitor"
|
#runner = "espflash flash --monitor"
|
||||||
#runner = "cargo runner"
|
#runner = "cargo runner"
|
||||||
|
|||||||
@@ -13,19 +13,6 @@ test = false
|
|||||||
bench = false
|
bench = false
|
||||||
doc = 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
|
#this strips the bootloader, we need that tho
|
||||||
#strip = true
|
#strip = true
|
||||||
|
|
||||||
@@ -50,6 +37,8 @@ partition_table = "partitions.csv"
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
# Shared CAN API
|
||||||
|
canapi = { path = "canapi" }
|
||||||
#ESP stuff
|
#ESP stuff
|
||||||
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] }
|
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] }
|
||||||
esp-hal = { version = "=1.0.0-rc.0", features = [
|
esp-hal = { version = "=1.0.0-rc.0", features = [
|
||||||
@@ -160,6 +149,8 @@ option-lock = { version = "0.3.1", default-features = false }
|
|||||||
#stay in sync with mcutie version here!
|
#stay in sync with mcutie version here!
|
||||||
heapless = { version = "0.7.17", features = ["serde"] }
|
heapless = { version = "0.7.17", features = ["serde"] }
|
||||||
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
|
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
|
||||||
|
nb = "1.1.0"
|
||||||
|
embedded-can = "0.4.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
12
rust/all.sh
Executable file
12
rust/all.sh
Executable 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
|
||||||
BIN
rust/bootloader.bin
Normal file
BIN
rust/bootloader.bin
Normal file
Binary file not shown.
@@ -1,5 +1,3 @@
|
|||||||
use std::process::Command;
|
|
||||||
|
|
||||||
use vergen::EmitBuilder;
|
use vergen::EmitBuilder;
|
||||||
|
|
||||||
fn linker_be_nice() {
|
fn linker_be_nice() {
|
||||||
@@ -50,72 +48,6 @@ fn linker_be_nice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
webpack();
|
|
||||||
linker_be_nice();
|
linker_be_nice();
|
||||||
let _ = EmitBuilder::builder().all_git().all_build().emit();
|
let _ = EmitBuilder::builder().all_git().all_build().emit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn webpack() {
|
|
||||||
//println!("cargo:rerun-if-changed=./src/src_webpack");
|
|
||||||
Command::new("rm")
|
|
||||||
.arg("./src/webserver/bundle.js.gz")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
match Command::new("cmd").spawn() {
|
|
||||||
Ok(_) => {
|
|
||||||
println!("Assuming build on windows");
|
|
||||||
let output = Command::new("cmd")
|
|
||||||
.arg("/K")
|
|
||||||
.arg("npx")
|
|
||||||
.arg("webpack")
|
|
||||||
.current_dir("./src_webpack")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
println!("status: {}", output.status);
|
|
||||||
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
assert!(output.status.success());
|
|
||||||
|
|
||||||
// move webpack results to rust webserver src
|
|
||||||
let _ = Command::new("cmd")
|
|
||||||
.arg("/K")
|
|
||||||
.arg("move")
|
|
||||||
.arg("./src_webpack/bundle.js.gz")
|
|
||||||
.arg("./src/webserver")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let _ = Command::new("cmd")
|
|
||||||
.arg("/K")
|
|
||||||
.arg("move")
|
|
||||||
.arg("./src_webpack/index.html.gz")
|
|
||||||
.arg("./src/webserver")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
println!("Assuming build on linux");
|
|
||||||
let output = Command::new("npx")
|
|
||||||
.arg("webpack")
|
|
||||||
.current_dir("./src_webpack")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
println!("status: {}", output.status);
|
|
||||||
println!("stdout: {}", String::from_utf8_lossy(&output.stdout));
|
|
||||||
println!("stderr: {}", String::from_utf8_lossy(&output.stderr));
|
|
||||||
assert!(output.status.success());
|
|
||||||
|
|
||||||
// move webpack results to rust webserver src
|
|
||||||
let _ = Command::new("mv")
|
|
||||||
.arg("./src_webpack/bundle.js.gz")
|
|
||||||
.arg("./src/webserver")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
let _ = Command::new("mv")
|
|
||||||
.arg("./src_webpack/index.html.gz")
|
|
||||||
.arg("./src/webserver")
|
|
||||||
.output()
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
14
rust/canapi/Cargo.toml
Normal file
14
rust/canapi/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "canapi"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "canapi"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
|
||||||
138
rust/canapi/src/lib.rs
Normal file
138
rust/canapi/src/lib.rs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#![no_std]
|
||||||
|
//! CAN bus API shared crate for PlantCtrl sensors and controller.
|
||||||
|
//! Addressing and messages are defined here to be reused by all bus participants.
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
|
||||||
|
/// Total plants supported by addressing (0..=15)
|
||||||
|
pub const MAX_PLANTS: u8 = 16;
|
||||||
|
|
||||||
|
/// Sensors per plant: 0..=1 => A/B
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum SensorSlot {
|
||||||
|
A = 0,
|
||||||
|
B = 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SensorSlot {
|
||||||
|
pub const fn from_index(idx: u8) -> Option<Self> {
|
||||||
|
match idx {
|
||||||
|
0 => Some(SensorSlot::A),
|
||||||
|
1 => Some(SensorSlot::B),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy sensor base address kept for compatibility with existing code.
|
||||||
|
/// Each plant uses SENSOR_BASE_ADDRESS + plant_index (0..PLANT_COUNT-1).
|
||||||
|
/// 11-bit standard ID space, safe range.
|
||||||
|
pub const SENSOR_BASE_ADDRESS: u16 = 1000;
|
||||||
|
|
||||||
|
/// Typed topics within the SENSOR_BASE space.
|
||||||
|
/// Additional offsets allow distinct message semantics while keeping plant-indexed layout.
|
||||||
|
pub mod id {
|
||||||
|
use crate::{SensorSlot, MAX_PLANTS, SENSOR_BASE_ADDRESS};
|
||||||
|
|
||||||
|
/// Number of plants addressable per sensor slot group
|
||||||
|
pub const PLANTS_PER_GROUP: u16 = MAX_PLANTS as u16; // 16
|
||||||
|
/// Offset applied for SensorSlot::B within a message group
|
||||||
|
pub const B_OFFSET: u16 = PLANTS_PER_GROUP; // 16
|
||||||
|
|
||||||
|
// Message group base offsets relative to SENSOR_BASE_ADDRESS
|
||||||
|
pub const MOISTURE_DATA_OFFSET: u16 = 0; // periodic data from sensor (sensor -> controller)
|
||||||
|
pub const IDENTIFY_CMD_OFFSET: u16 = 32; // identify LED command (controller -> sensor)
|
||||||
|
|
||||||
|
// Convenience constants for per-slot base offsets
|
||||||
|
pub const IDENTIFY_CMD_OFFSET_A: u16 = IDENTIFY_CMD_OFFSET + 0;
|
||||||
|
pub const IDENTIFY_CMD_OFFSET_B: u16 = IDENTIFY_CMD_OFFSET + B_OFFSET;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub const fn plant_id(message_type_offset: u16, sensor: SensorSlot, plant: u16) -> u16 {
|
||||||
|
match sensor {
|
||||||
|
SensorSlot::A => SENSOR_BASE_ADDRESS + message_type_offset + plant,
|
||||||
|
SensorSlot::B => SENSOR_BASE_ADDRESS + message_type_offset + B_OFFSET + plant,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kinds of message spaces recognized by the addressing scheme.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum MessageKind {
|
||||||
|
MoistureData, // sensor -> controller
|
||||||
|
IdentifyCmd, // controller -> sensor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to classify a received 11-bit standard ID into a known message kind and extract plant and sensor slot.
|
||||||
|
/// Returns (kind, plant, slot) on success.
|
||||||
|
#[inline]
|
||||||
|
pub const fn classify(id: u16) -> Option<(MessageKind, u8, SensorSlot)> {
|
||||||
|
// Ensure the ID is within our base space
|
||||||
|
if id < SENSOR_BASE_ADDRESS {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rel = id - SENSOR_BASE_ADDRESS;
|
||||||
|
|
||||||
|
// Helper: decode within a given group offset
|
||||||
|
const fn decode_in_group(rel: u16, group_base: u16) -> Option<(u8, SensorSlot)> {
|
||||||
|
if rel < group_base { return None; }
|
||||||
|
let inner = rel - group_base;
|
||||||
|
if inner < PLANTS_PER_GROUP { // A slot
|
||||||
|
Some((inner as u8, SensorSlot::A))
|
||||||
|
} else if inner >= B_OFFSET && inner < B_OFFSET + PLANTS_PER_GROUP { // B slot
|
||||||
|
Some(((inner - B_OFFSET) as u8, SensorSlot::B))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check known groups in order
|
||||||
|
if let Some((plant, slot)) = decode_in_group(rel, MOISTURE_DATA_OFFSET) {
|
||||||
|
return Some((MessageKind::MoistureData, plant, slot));
|
||||||
|
}
|
||||||
|
if let Some((plant, slot)) = decode_in_group(rel, IDENTIFY_CMD_OFFSET) {
|
||||||
|
return Some((MessageKind::IdentifyCmd, plant, slot));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns Some((plant, slot)) regardless of message kind, if the id falls into any known group; otherwise None.
|
||||||
|
#[inline]
|
||||||
|
pub const fn extract_plant_slot(id: u16) -> Option<(u8, SensorSlot)> {
|
||||||
|
match classify(id) {
|
||||||
|
Some((_kind, plant, slot)) => Some((plant, slot)),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if an id corresponds exactly to the given message kind, plant and slot.
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_identify_for(id: u16, plant: u8, slot: SensorSlot) -> bool {
|
||||||
|
id == plant_id(IDENTIFY_CMD_OFFSET, slot, plant as u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub const fn is_moisture_data_for(id: u16, plant: u8, slot: SensorSlot) -> bool {
|
||||||
|
id == plant_id(MOISTURE_DATA_OFFSET, slot, plant as u16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Periodic moisture data sent by sensors.
|
||||||
|
/// Fits into 5 bytes with bincode-v2 (no varint): u8 + u8 + u16 = 4, alignment may keep 4.
|
||||||
|
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
||||||
|
pub struct MoistureData {
|
||||||
|
pub plant: u8, // 0..MAX_PLANTS-1
|
||||||
|
pub sensor: SensorSlot, // A/B
|
||||||
|
pub hz: u16, // measured frequency of moisture sensor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a sensor to report immediately (controller -> sensor).
|
||||||
|
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
||||||
|
pub struct MoistureRequest {
|
||||||
|
pub plant: u8,
|
||||||
|
pub sensor: SensorSlot, // target sensor (sensor filters by this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Control a sensor's identify LED, if received by sensor, blink for a few seconds
|
||||||
|
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
||||||
|
pub struct IdentifyLed {}
|
||||||
11
rust/flash.sh
Executable file
11
rust/flash.sh
Executable 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
rust/image_build.sh
Executable file
13
rust/image_build.sh
Executable 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
|
||||||
@@ -6,8 +6,10 @@ use core::str::Utf8Error;
|
|||||||
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
||||||
use embassy_executor::SpawnError;
|
use embassy_executor::SpawnError;
|
||||||
use embassy_sync::mutex::TryLockError;
|
use embassy_sync::mutex::TryLockError;
|
||||||
|
use embedded_storage::nor_flash::NorFlashErrorKind;
|
||||||
use esp_hal::i2c::master::ConfigError;
|
use esp_hal::i2c::master::ConfigError;
|
||||||
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
|
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
|
||||||
|
use esp_hal::twai::EspTwaiError;
|
||||||
use esp_wifi::wifi::WifiError;
|
use esp_wifi::wifi::WifiError;
|
||||||
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
|
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
|
||||||
use littlefs2_core::PathError;
|
use littlefs2_core::PathError;
|
||||||
@@ -60,6 +62,12 @@ pub enum FatError {
|
|||||||
ExpanderError {
|
ExpanderError {
|
||||||
error: String,
|
error: String,
|
||||||
},
|
},
|
||||||
|
CanBusError {
|
||||||
|
error: EspTwaiError,
|
||||||
|
},
|
||||||
|
SNTPError {
|
||||||
|
error: sntpc::Error,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type FatResult<T> = Result<T, FatError>;
|
pub type FatResult<T> = Result<T, FatError>;
|
||||||
@@ -88,6 +96,10 @@ impl fmt::Display for FatError {
|
|||||||
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
|
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
|
||||||
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
|
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
|
||||||
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use crate::fat_error::{FatError, FatResult};
|
use crate::fat_error::{FatError, FatResult};
|
||||||
use crate::hal::Box;
|
use crate::hal::Box;
|
||||||
use alloc::string::String;
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
|
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
|
||||||
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
|
||||||
@@ -11,7 +10,7 @@ use esp_hal::Blocking;
|
|||||||
use measurements::Temperature;
|
use measurements::Temperature;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
pub trait BatteryInteraction {
|
pub trait BatteryInteraction {
|
||||||
async fn state_charge_percent(&mut self) -> FatResult<f32>;
|
async fn state_charge_percent(&mut self) -> FatResult<f32>;
|
||||||
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
|
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
|
||||||
@@ -37,12 +36,6 @@ pub struct BatteryInfo {
|
|||||||
pub temperature: u16,
|
pub temperature: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
pub enum BatteryError {
|
|
||||||
NoBatteryMonitor,
|
|
||||||
CommunicationError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
pub enum BatteryState {
|
pub enum BatteryState {
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -51,7 +44,7 @@ pub enum BatteryState {
|
|||||||
|
|
||||||
/// If no battery monitor is installed this implementation will be used
|
/// If no battery monitor is installed this implementation will be used
|
||||||
pub struct NoBatteryMonitor {}
|
pub struct NoBatteryMonitor {}
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
impl BatteryInteraction for NoBatteryMonitor {
|
impl BatteryInteraction for NoBatteryMonitor {
|
||||||
async fn state_charge_percent(&mut self) -> FatResult<f32> {
|
async fn state_charge_percent(&mut self) -> FatResult<f32> {
|
||||||
// No monitor configured: assume full battery for lightstate logic
|
// No monitor configured: assume full battery for lightstate logic
|
||||||
@@ -105,7 +98,7 @@ pub struct BQ34Z100G1 {
|
|||||||
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
|
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
impl BatteryInteraction for BQ34Z100G1 {
|
impl BatteryInteraction for BQ34Z100G1 {
|
||||||
async fn state_charge_percent(&mut self) -> FatResult<f32> {
|
async fn state_charge_percent(&mut self) -> FatResult<f32> {
|
||||||
self.battery_driver
|
self.battery_driver
|
||||||
|
|||||||
13
rust/src/hal/can_api.rs
Normal file
13
rust/src/hal/can_api.rs
Normal 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,
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use crate::fat_error::{ContextExt, FatError, FatResult};
|
|||||||
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
||||||
use alloc::string::ToString;
|
use alloc::string::ToString;
|
||||||
use alloc::sync::Arc;
|
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::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
use core::sync::atomic::Ordering;
|
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::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::mutex::{Mutex, MutexGuard};
|
use embassy_sync::mutex::{Mutex, MutexGuard};
|
||||||
use embassy_sync::once_lock::OnceLock;
|
use embassy_sync::once_lock::OnceLock;
|
||||||
use embassy_time::{Duration, Timer};
|
use embassy_time::{Duration, Timer, WithTimeout};
|
||||||
use embedded_storage::nor_flash::ReadNorFlash;
|
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
||||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
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_bootloader_esp_idf::partitions::FlashRegion;
|
||||||
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
||||||
use esp_hal::rng::Rng;
|
use esp_hal::rng::Rng;
|
||||||
@@ -127,7 +128,10 @@ pub struct Esp<'a> {
|
|||||||
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
||||||
|
|
||||||
pub ota: Ota<'static, FlashStorage>,
|
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
|
// 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))
|
Ok((buf, read))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_ota_slot(&mut self) -> String {
|
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
||||||
match self.ota.current_slot() {
|
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
||||||
Ok(slot) => {
|
self.ota_target.erase(offset, offset + 4096)?;
|
||||||
format!("{:?}", slot)
|
|
||||||
}
|
let mut temp = vec![0; buf.len()];
|
||||||
Err(err) => {
|
let read_back = temp.as_mut_slice();
|
||||||
format!("{:?}", err)
|
//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 {
|
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
||||||
match self.ota.current_ota_state() {
|
let current = self.ota.current_slot()?;
|
||||||
Ok(state) => {
|
if self.ota.current_ota_state()? != OtaImageState::Valid {
|
||||||
format!("{:?}", state)
|
info!(
|
||||||
}
|
"Validating current slot {:?} as it was able to ota",
|
||||||
Err(err) => {
|
current
|
||||||
format!("{:?}", err)
|
);
|
||||||
}
|
self.ota.set_current_ota_state(Valid)?;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// let current = ota.current_slot()?;
|
self.ota.set_current_slot(current.next())?;
|
||||||
// println!(
|
info!("switched slot");
|
||||||
// "current image state {:?} (only relevant if the bootloader was built with auto-rollback support)",
|
self.ota.set_current_ota_state(OtaImageState::New)?;
|
||||||
// ota.current_ota_state()
|
info!("switched state for new partition");
|
||||||
// );
|
let state_new = self.ota.current_ota_state()?;
|
||||||
// println!("current {:?} - next {:?}", current, current.next());
|
info!("state on new partition now {:?}", state_new);
|
||||||
// let ota_state = ota.current_ota_state()?;
|
//determine nextslot crc
|
||||||
|
|
||||||
|
self.set_restart_to_conf(true);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn mode_override_pressed(&mut self) -> bool {
|
pub(crate) fn mode_override_pressed(&mut self) -> bool {
|
||||||
self.boot_button.is_low()
|
self.boot_button.is_low()
|
||||||
@@ -275,20 +290,24 @@ impl Esp<'_> {
|
|||||||
if ntp_addrs.is_empty() {
|
if ntp_addrs.is_empty() {
|
||||||
bail!("Failed to resolve DNS");
|
bail!("Failed to resolve DNS");
|
||||||
}
|
}
|
||||||
|
info!("NTP server: {:?}", ntp_addrs);
|
||||||
|
|
||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
loop {
|
loop {
|
||||||
let addr: IpAddr = ntp_addrs[0].into();
|
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 {
|
match timeout {
|
||||||
Ok(time) => {
|
Ok(result) => {
|
||||||
|
let time = result?;
|
||||||
info!("Time: {:?}", time);
|
info!("Time: {:?}", time);
|
||||||
return DateTime::from_timestamp(time.seconds as i64, 0)
|
return DateTime::from_timestamp(time.seconds as i64, 0)
|
||||||
.context("Could not convert Sntp result");
|
.context("Could not convert Sntp result");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(err) => {
|
||||||
warn!("Error: {:?}", e);
|
warn!("sntp timeout, retry: {:?}", err);
|
||||||
counter += 1;
|
counter += 1;
|
||||||
if counter > 10 {
|
if counter > 10 {
|
||||||
bail!("Failed to get time from NTP server");
|
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>> {
|
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
|
||||||
info!("start wifi scan");
|
info!("start wifi scan");
|
||||||
let mut lock = self.controller.try_lock()?;
|
let mut lock = self.controller.try_lock()?;
|
||||||
@@ -582,8 +595,7 @@ impl Esp<'_> {
|
|||||||
if duration_in_ms == 0 {
|
if duration_in_ms == 0 {
|
||||||
software_reset();
|
software_reset();
|
||||||
} else {
|
} else {
|
||||||
///let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
|
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
|
||||||
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(5000));
|
|
||||||
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
|
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
|
||||||
[(&mut self.wake_gpio1, WakeupLevel::Low)];
|
[(&mut self.wake_gpio1, WakeupLevel::Low)];
|
||||||
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
|
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
|
||||||
@@ -592,7 +604,11 @@ impl Esp<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
|
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 data = self.fs.lock().await.read::<4096>(&cfg)?;
|
||||||
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
@@ -892,8 +908,8 @@ async fn mqtt_incoming_task(
|
|||||||
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
|
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
MqttMessage::Publish(topic, payload) => match topic {
|
MqttMessage::Publish(topic, payload) => match topic {
|
||||||
Topic::DeviceType(type_topic) => {}
|
Topic::DeviceType(_type_topic) => {}
|
||||||
Topic::Device(device_topic) => {}
|
Topic::Device(_device_topic) => {}
|
||||||
Topic::General(topic) => {
|
Topic::General(topic) => {
|
||||||
let subtopic = topic.as_str();
|
let subtopic = topic.as_str();
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::fat_error::{FatError, FatResult};
|
|||||||
use crate::hal::esp::Esp;
|
use crate::hal::esp::Esp;
|
||||||
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
|
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
|
||||||
use crate::hal::water::TankSensor;
|
use crate::hal::water::TankSensor;
|
||||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, TIME_ACCESS};
|
use crate::hal::{BoardInteraction, FreePeripherals, Moistures, TIME_ACCESS};
|
||||||
use crate::{
|
use crate::{
|
||||||
bail,
|
bail,
|
||||||
config::PlantControllerConfig,
|
config::PlantControllerConfig,
|
||||||
@@ -24,7 +24,7 @@ pub struct Initial<'a> {
|
|||||||
|
|
||||||
pub(crate) struct NoRTC {}
|
pub(crate) struct NoRTC {}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
impl RTCModuleInteraction for NoRTC {
|
impl RTCModuleInteraction for NoRTC {
|
||||||
async fn get_backup_info(&mut self) -> Result<BackupHeader, FatError> {
|
async fn get_backup_info(&mut self) -> Result<BackupHeader, FatError> {
|
||||||
bail!("Please configure board revision")
|
bail!("Please configure board revision")
|
||||||
@@ -68,7 +68,7 @@ pub(crate) fn create_initial_board(
|
|||||||
Ok(Box::new(v))
|
Ok(Box::new(v))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
impl<'a> BoardInteraction<'a> for Initial<'a> {
|
impl<'a> BoardInteraction<'a> for Initial<'a> {
|
||||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
||||||
bail!("Please configure board revision")
|
bail!("Please configure board revision")
|
||||||
@@ -117,14 +117,11 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
|
|||||||
bail!("Please configure board revision")
|
bail!("Please configure board revision")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn measure_moisture_hz(
|
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
|
||||||
&mut self,
|
|
||||||
_plant: usize,
|
|
||||||
_sensor: Sensor,
|
|
||||||
) -> Result<f32, FatError> {
|
|
||||||
bail!("Please configure board revision")
|
bail!("Please configure board revision")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn general_fault(&mut self, enable: bool) {
|
async fn general_fault(&mut self, enable: bool) {
|
||||||
self.general_fault.set_level(enable.into());
|
self.general_fault.set_level(enable.into());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_bootloader_esp_idf::partitions::FlashRegion;
|
||||||
use esp_storage::FlashStorage;
|
use esp_storage::FlashStorage;
|
||||||
use littlefs2::consts::U512 as lfsCache;
|
use littlefs2::consts::U4096 as lfsCache;
|
||||||
use littlefs2::consts::U512 as lfsLookahead;
|
use littlefs2::consts::U512 as lfsLookahead;
|
||||||
use littlefs2::driver::Storage as lfs2Storage;
|
use littlefs2::driver::Storage as lfs2Storage;
|
||||||
use littlefs2::io::Error as lfs2Error;
|
use littlefs2::io::Error as lfs2Error;
|
||||||
@@ -13,18 +13,24 @@ pub struct LittleFs2Filesystem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl lfs2Storage for LittleFs2Filesystem {
|
impl lfs2Storage for LittleFs2Filesystem {
|
||||||
const READ_SIZE: usize = 256;
|
const READ_SIZE: usize = 4096;
|
||||||
const WRITE_SIZE: usize = 512;
|
const WRITE_SIZE: usize = 4096;
|
||||||
const BLOCK_SIZE: usize = 512; //usually optimal for flash access
|
const BLOCK_SIZE: usize = 4096; //usually optimal for flash access
|
||||||
const BLOCK_COUNT: usize = 8 * 1024 * 1024 / 512; //8mb in 32kb blocks
|
const BLOCK_COUNT: usize = 8 * 1000 * 1000 / 4096; //8Mb in 4k blocks + a little space for stupid calculation errors
|
||||||
const BLOCK_CYCLES: isize = 100;
|
const BLOCK_CYCLES: isize = 100;
|
||||||
type CACHE_SIZE = lfsCache;
|
type CACHE_SIZE = lfsCache;
|
||||||
type LOOKAHEAD_SIZE = lfsLookahead;
|
type LOOKAHEAD_SIZE = lfsLookahead;
|
||||||
|
|
||||||
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
|
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
|
||||||
let read_size: usize = Self::READ_SIZE;
|
let read_size: usize = Self::READ_SIZE;
|
||||||
assert_eq!(off % read_size, 0);
|
if off % read_size != 0 {
|
||||||
assert_eq!(buf.len() % 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) {
|
match self.storage.read(off as u32, buf) {
|
||||||
Ok(..) => Ok(buf.len()),
|
Ok(..) => Ok(buf.len()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -36,8 +42,14 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
|
|
||||||
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
|
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
|
||||||
let write_size: usize = Self::WRITE_SIZE;
|
let write_size: usize = Self::WRITE_SIZE;
|
||||||
assert_eq!(off % write_size, 0);
|
if off % write_size != 0 {
|
||||||
assert_eq!(data.len() % 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) {
|
match self.storage.write(off as u32, data) {
|
||||||
Ok(..) => Ok(data.len()),
|
Ok(..) => Ok(data.len()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -49,15 +61,28 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
|
|
||||||
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
|
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
|
||||||
let block_size: usize = Self::BLOCK_SIZE;
|
let block_size: usize = Self::BLOCK_SIZE;
|
||||||
debug_assert!(off % block_size == 0);
|
if off % block_size != 0 {
|
||||||
debug_assert!(len % block_size == 0);
|
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {} block_size: {}", off, block_size);
|
||||||
//match self.storage.erase(off as u32, len as u32) {
|
return lfs2Result::Err(lfs2Error::IO);
|
||||||
//anyhow::Result::Ok(..) => lfs2Result::Ok(len),
|
}
|
||||||
//Err(err) => {
|
if len % block_size != 0 {
|
||||||
//error!("Littlefs2Filesystem erase error: {:?}", err);
|
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {} block_size: {}", len, block_size);
|
||||||
//Err(lfs2Error::IO)
|
return lfs2Result::Err(lfs2Error::IO);
|
||||||
// }
|
}
|
||||||
//}
|
|
||||||
lfs2Result::Ok(len)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub(crate) mod battery;
|
pub(crate) mod battery;
|
||||||
|
// mod can_api; // replaced by external canapi crate
|
||||||
pub mod esp;
|
pub mod esp;
|
||||||
mod initial_hal;
|
mod initial_hal;
|
||||||
mod little_fs2storage_adapter;
|
mod little_fs2storage_adapter;
|
||||||
@@ -6,14 +7,12 @@ pub(crate) mod rtc;
|
|||||||
mod v3_hal;
|
mod v3_hal;
|
||||||
mod v3_shift_register;
|
mod v3_shift_register;
|
||||||
mod v4_hal;
|
mod v4_hal;
|
||||||
mod v4_sensor;
|
pub(crate) mod v4_sensor;
|
||||||
mod water;
|
mod water;
|
||||||
|
|
||||||
use crate::alloc::string::ToString;
|
use crate::alloc::string::ToString;
|
||||||
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
|
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
|
||||||
use esp_hal::peripherals::Peripherals;
|
use esp_hal::peripherals::Peripherals;
|
||||||
use esp_hal::peripherals::ADC1;
|
use esp_hal::peripherals::ADC1;
|
||||||
use esp_hal::peripherals::APB_SARADC;
|
|
||||||
use esp_hal::peripherals::GPIO0;
|
use esp_hal::peripherals::GPIO0;
|
||||||
use esp_hal::peripherals::GPIO10;
|
use esp_hal::peripherals::GPIO10;
|
||||||
use esp_hal::peripherals::GPIO11;
|
use esp_hal::peripherals::GPIO11;
|
||||||
@@ -41,7 +40,6 @@ use esp_hal::peripherals::GPIO5;
|
|||||||
use esp_hal::peripherals::GPIO6;
|
use esp_hal::peripherals::GPIO6;
|
||||||
use esp_hal::peripherals::GPIO7;
|
use esp_hal::peripherals::GPIO7;
|
||||||
use esp_hal::peripherals::GPIO8;
|
use esp_hal::peripherals::GPIO8;
|
||||||
use esp_hal::peripherals::PCNT;
|
|
||||||
use esp_hal::peripherals::TWAI0;
|
use esp_hal::peripherals::TWAI0;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -58,9 +56,11 @@ use alloc::boxed::Box;
|
|||||||
use alloc::format;
|
use alloc::format;
|
||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
use bq34z100::Bq34z100g1Driver;
|
use bq34z100::Bq34z100g1Driver;
|
||||||
use chrono::{DateTime, FixedOffset, Utc};
|
use chrono::{DateTime, FixedOffset, Utc};
|
||||||
use core::cell::RefCell;
|
use core::cell::RefCell;
|
||||||
|
use canapi::SensorSlot;
|
||||||
use ds323x::ic::DS3231;
|
use ds323x::ic::DS3231;
|
||||||
use ds323x::interface::I2cInterface;
|
use ds323x::interface::I2cInterface;
|
||||||
use ds323x::{DateTimeAccess, Ds323x};
|
use ds323x::{DateTimeAccess, Ds323x};
|
||||||
@@ -78,16 +78,18 @@ use esp_hal::clock::CpuClock;
|
|||||||
use esp_hal::gpio::{Input, InputConfig, Pull};
|
use esp_hal::gpio::{Input, InputConfig, Pull};
|
||||||
use measurements::{Current, Voltage};
|
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::battery::{print_battery_bq34z100, BQ34Z100G1};
|
||||||
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
||||||
use crate::hal::water::TankSensor;
|
use crate::hal::water::TankSensor;
|
||||||
use crate::log::LOG_ACCESS;
|
use crate::log::LOG_ACCESS;
|
||||||
use embassy_sync::mutex::Mutex;
|
use embassy_sync::mutex::Mutex;
|
||||||
use embassy_sync::once_lock::OnceLock;
|
use embassy_sync::once_lock::OnceLock;
|
||||||
|
use embedded_storage::nor_flash::ReadNorFlash;
|
||||||
use esp_alloc as _;
|
use esp_alloc as _;
|
||||||
use esp_backtrace 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::delay::Delay;
|
||||||
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
|
||||||
use esp_hal::pcnt::unit::Unit;
|
use esp_hal::pcnt::unit::Unit;
|
||||||
@@ -102,31 +104,45 @@ use esp_storage::FlashStorage;
|
|||||||
use esp_wifi::{init, EspWifiController};
|
use esp_wifi::{init, EspWifiController};
|
||||||
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
|
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
|
||||||
use littlefs2::object_safe::DynStorage;
|
use littlefs2::object_safe::DynStorage;
|
||||||
use log::{info, warn};
|
use log::{error, info, warn};
|
||||||
|
use portable_atomic::AtomicBool;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
|
||||||
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
|
pub static TIME_ACCESS: OnceLock<Mutex<CriticalSectionRawMutex, Rtc>> = OnceLock::new();
|
||||||
|
|
||||||
//Only support for 8 right now!
|
//Only support for 8 right now!
|
||||||
pub const PLANT_COUNT: usize = 8;
|
pub const PLANT_COUNT: usize = 8;
|
||||||
|
|
||||||
|
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
const TANK_MULTI_SAMPLE: usize = 11;
|
const TANK_MULTI_SAMPLE: usize = 11;
|
||||||
pub static I2C_DRIVER: OnceLock<
|
pub static I2C_DRIVER: OnceLock<
|
||||||
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
|
||||||
> = OnceLock::new();
|
> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Clone, Copy, Encode, Decode)]
|
||||||
pub enum Sensor {
|
pub enum Sensor {
|
||||||
A,
|
A,
|
||||||
B,
|
B,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Into<SensorSlot> for Sensor {
|
||||||
|
fn into(self) -> SensorSlot {
|
||||||
|
match self {
|
||||||
|
Sensor::A => SensorSlot::A,
|
||||||
|
Sensor::B => SensorSlot::B,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PlantHal {}
|
pub struct PlantHal {}
|
||||||
|
|
||||||
pub struct HAL<'a> {
|
pub struct HAL<'a> {
|
||||||
pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
|
pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
pub trait BoardInteraction<'a> {
|
pub trait BoardInteraction<'a> {
|
||||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
|
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
|
||||||
fn get_esp(&mut self) -> &mut Esp<'a>;
|
fn get_esp(&mut self) -> &mut Esp<'a>;
|
||||||
@@ -138,18 +154,26 @@ pub trait BoardInteraction<'a> {
|
|||||||
|
|
||||||
fn is_day(&self) -> bool;
|
fn is_day(&self) -> bool;
|
||||||
//should be multsampled
|
//should be multsampled
|
||||||
async fn light(&mut self, enable: bool) -> Result<(), FatError>;
|
async fn light(&mut self, enable: bool) -> FatResult<()>;
|
||||||
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
|
async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()>;
|
||||||
async fn pump_current(&mut self, plant: usize) -> Result<Current, FatError>;
|
async fn pump_current(&mut self, plant: usize) -> FatResult<Current>;
|
||||||
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
|
async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()>;
|
||||||
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError>;
|
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError>;
|
||||||
async fn general_fault(&mut self, enable: bool);
|
async fn general_fault(&mut self, enable: bool);
|
||||||
async fn test(&mut self) -> Result<(), FatError>;
|
async fn test(&mut self) -> FatResult<()>;
|
||||||
fn set_config(&mut self, config: PlantControllerConfig);
|
fn set_config(&mut self, config: PlantControllerConfig);
|
||||||
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
|
async fn get_mptt_voltage(&mut self) -> FatResult<Voltage>;
|
||||||
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
|
async fn get_mptt_current(&mut self) -> FatResult<Current>;
|
||||||
|
|
||||||
|
// Return JSON string with autodetected sensors per plant. Default: not supported.
|
||||||
|
async fn detect_sensors(&mut self) -> FatResult<DetectionResult> {
|
||||||
|
bail!("Autodetection is only available on v4 HAL with CAN bus");
|
||||||
|
}
|
||||||
|
|
||||||
async fn progress(&mut self, counter: u32) {
|
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;
|
let current = counter % PLANT_COUNT as u32;
|
||||||
for led in 0..PLANT_COUNT {
|
for led in 0..PLANT_COUNT {
|
||||||
if let Err(err) = self.fault(led, current == led as u32).await {
|
if let Err(err) = self.fault(led, current == led as u32).await {
|
||||||
@@ -167,6 +191,9 @@ pub trait BoardInteraction<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let _ = self.general_fault(false).await;
|
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 +332,6 @@ impl PlantHal {
|
|||||||
let pt =
|
let pt =
|
||||||
esp_bootloader_esp_idf::partitions::read_partition_table(storage_ota, tablebuffer)?;
|
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!(
|
let ota_data = mk_static!(
|
||||||
PartitionEntry,
|
PartitionEntry,
|
||||||
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
|
||||||
@@ -322,9 +345,18 @@ impl PlantHal {
|
|||||||
ota_data.as_embedded_storage(storage_ota)
|
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 => {
|
Slot::None => {
|
||||||
panic!("No OTA slot active?");
|
panic!("No OTA slot active?");
|
||||||
}
|
}
|
||||||
@@ -332,19 +364,19 @@ impl PlantHal {
|
|||||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
||||||
AppPartitionSubType::Ota0,
|
AppPartitionSubType::Ota0,
|
||||||
))?
|
))?
|
||||||
.expect("No OTA slot0 found"),
|
.context("Partition table invalid no ota0")?,
|
||||||
Slot::Slot1 => pt
|
Slot::Slot1 => pt
|
||||||
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::App(
|
||||||
AppPartitionSubType::Ota1,
|
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 storage_ota = mk_static!(FlashStorage, FlashStorage::new());
|
||||||
let ota_next = mk_static!(
|
let ota_target = mk_static!(
|
||||||
FlashRegion<FlashStorage>,
|
FlashRegion<FlashStorage>,
|
||||||
ota_next.as_embedded_storage(storage_ota)
|
ota_target.as_embedded_storage(storage_ota)
|
||||||
);
|
);
|
||||||
|
|
||||||
let data_partition = pt
|
let data_partition = pt
|
||||||
@@ -369,7 +401,7 @@ impl PlantHal {
|
|||||||
log::info!("Littlefs2 filesystem is formatted");
|
log::info!("Littlefs2 filesystem is formatted");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Littlefs2 filesystem could not be formatted: {:?}", err);
|
error!("Littlefs2 filesystem could not be formatted: {:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -389,7 +421,10 @@ impl PlantHal {
|
|||||||
boot_button,
|
boot_button,
|
||||||
wake_gpio1,
|
wake_gpio1,
|
||||||
ota,
|
ota,
|
||||||
ota_next,
|
ota_target,
|
||||||
|
current: running,
|
||||||
|
slot0_state: state_0,
|
||||||
|
slot1_state: state_1,
|
||||||
};
|
};
|
||||||
|
|
||||||
//init,reset rtc memory depending on cause
|
//init,reset rtc memory depending on cause
|
||||||
@@ -571,6 +606,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> {
|
pub async fn esp_time() -> DateTime<Utc> {
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
let guard = TIME_ACCESS.get().await.lock().await;
|
||||||
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
|
DateTime::from_timestamp_micros(guard.current_time_us() as i64).unwrap()
|
||||||
@@ -591,3 +697,19 @@ pub async fn esp_set_time(time: DateTime<FixedOffset>) -> FatResult<()> {
|
|||||||
.set_rtc_time(&time.to_utc())
|
.set_rtc_time(&time.to_utc())
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize)]
|
||||||
|
pub struct Moistures{
|
||||||
|
pub sensor_a_hz: [f32; PLANT_COUNT],
|
||||||
|
pub sensor_b_hz: [f32; PLANT_COUNT],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
|
||||||
|
pub struct DetectionResult {
|
||||||
|
plant: [DetectionSensorResult; crate::hal::PLANT_COUNT]
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize)]
|
||||||
|
pub struct DetectionSensorResult{
|
||||||
|
sensor_a: bool,
|
||||||
|
sensor_b: bool,
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
|
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
|
||||||
const CONFIG: Configuration = config::standard();
|
const CONFIG: Configuration = config::standard();
|
||||||
//
|
//
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
pub trait RTCModuleInteraction {
|
pub trait RTCModuleInteraction {
|
||||||
async fn get_backup_info(&mut self) -> FatResult<BackupHeader>;
|
async fn get_backup_info(&mut self) -> FatResult<BackupHeader>;
|
||||||
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)>;
|
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)>;
|
||||||
@@ -55,7 +55,7 @@ pub struct DS3231Module {
|
|||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait(?Send)]
|
||||||
impl RTCModuleInteraction for DS3231Module {
|
impl RTCModuleInteraction for DS3231Module {
|
||||||
async fn get_backup_info(&mut self) -> FatResult<BackupHeader> {
|
async fn get_backup_info(&mut self) -> FatResult<BackupHeader> {
|
||||||
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::hal::esp::{hold_disable, hold_enable};
|
|||||||
use crate::hal::rtc::RTCModuleInteraction;
|
use crate::hal::rtc::RTCModuleInteraction;
|
||||||
use crate::hal::v3_shift_register::ShiftRegister40;
|
use crate::hal::v3_shift_register::ShiftRegister40;
|
||||||
use crate::hal::water::TankSensor;
|
use crate::hal::water::TankSensor;
|
||||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT, TIME_ACCESS};
|
use crate::hal::{BoardInteraction, FreePeripherals, Moistures, Sensor, PLANT_COUNT, TIME_ACCESS};
|
||||||
use crate::log::{LogMessage, LOG_ACCESS};
|
use crate::log::{LogMessage, LOG_ACCESS};
|
||||||
use crate::{
|
use crate::{
|
||||||
config::PlantControllerConfig,
|
config::PlantControllerConfig,
|
||||||
@@ -20,7 +20,6 @@ use embassy_time::Timer;
|
|||||||
use embedded_hal::digital::OutputPin as _;
|
use embedded_hal::digital::OutputPin as _;
|
||||||
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
use esp_hal::gpio::{Flex, Input, InputConfig, Level, Output, OutputConfig, Pull};
|
||||||
use esp_hal::pcnt::channel::CtrlMode::Keep;
|
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::channel::EdgeMode::{Hold, Increment};
|
||||||
use esp_hal::pcnt::unit::Unit;
|
use esp_hal::pcnt::unit::Unit;
|
||||||
use measurements::{Current, Voltage};
|
use measurements::{Current, Voltage};
|
||||||
@@ -146,7 +145,6 @@ pub(crate) fn create_v3(
|
|||||||
|
|
||||||
let signal_counter = peripherals.pcnt0;
|
let signal_counter = peripherals.pcnt0;
|
||||||
|
|
||||||
signal_counter.set_low_limit(Some(0))?;
|
|
||||||
signal_counter.set_high_limit(Some(i16::MAX))?;
|
signal_counter.set_high_limit(Some(i16::MAX))?;
|
||||||
|
|
||||||
let ch0 = &signal_counter.channel0;
|
let ch0 = &signal_counter.channel0;
|
||||||
@@ -172,7 +170,113 @@ pub(crate) fn create_v3(
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl V3<'_> {
|
||||||
|
|
||||||
|
async fn inner_measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> {
|
||||||
|
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
||||||
|
for repeat in 0..REPEAT_MOIST_MEASURE {
|
||||||
|
self.signal_counter.pause();
|
||||||
|
self.signal_counter.clear();
|
||||||
|
//Disable all
|
||||||
|
{
|
||||||
|
let shift_register = self.shift_register.lock().await;
|
||||||
|
shift_register.decompose()[MS_4].set_high()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sensor_channel = match sensor {
|
||||||
|
Sensor::A => match plant {
|
||||||
|
0 => SENSOR_A_1,
|
||||||
|
1 => SENSOR_A_2,
|
||||||
|
2 => SENSOR_A_3,
|
||||||
|
3 => SENSOR_A_4,
|
||||||
|
4 => SENSOR_A_5,
|
||||||
|
5 => SENSOR_A_6,
|
||||||
|
6 => SENSOR_A_7,
|
||||||
|
7 => SENSOR_A_8,
|
||||||
|
_ => bail!("Invalid plant id {}", plant),
|
||||||
|
},
|
||||||
|
Sensor::B => match plant {
|
||||||
|
0 => SENSOR_B_1,
|
||||||
|
1 => SENSOR_B_2,
|
||||||
|
2 => SENSOR_B_3,
|
||||||
|
3 => SENSOR_B_4,
|
||||||
|
4 => SENSOR_B_5,
|
||||||
|
5 => SENSOR_B_6,
|
||||||
|
6 => SENSOR_B_7,
|
||||||
|
7 => SENSOR_B_8,
|
||||||
|
_ => bail!("Invalid plant id {}", plant),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
|
||||||
|
{
|
||||||
|
let shift_register = self.shift_register.lock().await;
|
||||||
|
let pin_0 = &mut shift_register.decompose()[MS_0];
|
||||||
|
let pin_1 = &mut shift_register.decompose()[MS_1];
|
||||||
|
let pin_2 = &mut shift_register.decompose()[MS_2];
|
||||||
|
let pin_3 = &mut shift_register.decompose()[MS_3];
|
||||||
|
if is_bit_set(0) {
|
||||||
|
pin_0.set_high()?;
|
||||||
|
} else {
|
||||||
|
pin_0.set_low()?;
|
||||||
|
}
|
||||||
|
if is_bit_set(1) {
|
||||||
|
pin_1.set_high()?;
|
||||||
|
} else {
|
||||||
|
pin_1.set_low()?;
|
||||||
|
}
|
||||||
|
if is_bit_set(2) {
|
||||||
|
pin_2.set_high()?;
|
||||||
|
} else {
|
||||||
|
pin_2.set_low()?;
|
||||||
|
}
|
||||||
|
if is_bit_set(3) {
|
||||||
|
pin_3.set_high()?;
|
||||||
|
} else {
|
||||||
|
pin_3.set_low()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
shift_register.decompose()[MS_4].set_low()?;
|
||||||
|
shift_register.decompose()[SENSOR_ON].set_high()?;
|
||||||
|
}
|
||||||
|
let measurement = 100; //how long to measure and then extrapolate to hz
|
||||||
|
let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz
|
||||||
|
|
||||||
|
//give some time to stabilize
|
||||||
|
Timer::after_millis(10).await;
|
||||||
|
self.signal_counter.resume();
|
||||||
|
Timer::after_millis(measurement).await;
|
||||||
|
self.signal_counter.pause();
|
||||||
|
{
|
||||||
|
let shift_register = self.shift_register.lock().await;
|
||||||
|
shift_register.decompose()[MS_4].set_high()?;
|
||||||
|
shift_register.decompose()[SENSOR_ON].set_low()?;
|
||||||
|
}
|
||||||
|
Timer::after_millis(10).await;
|
||||||
|
let unscaled = self.signal_counter.value();
|
||||||
|
let hz = unscaled as f32 * factor;
|
||||||
|
LOG_ACCESS
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.log(
|
||||||
|
LogMessage::RawMeasure,
|
||||||
|
unscaled as u32,
|
||||||
|
hz as u32,
|
||||||
|
&plant.to_string(),
|
||||||
|
&format!("{sensor:?}"),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
results[repeat] = hz;
|
||||||
|
}
|
||||||
|
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
|
||||||
|
|
||||||
|
let mid = results.len() / 2;
|
||||||
|
let median = results[mid];
|
||||||
|
Ok(median)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait(?Send)]
|
||||||
impl<'a> BoardInteraction<'a> for V3<'a> {
|
impl<'a> BoardInteraction<'a> for V3<'a> {
|
||||||
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
|
||||||
Ok(&mut self.tank_sensor)
|
Ok(&mut self.tank_sensor)
|
||||||
@@ -277,108 +381,24 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> {
|
async fn measure_moisture_hz(&mut self) -> Result<Moistures, FatError> {
|
||||||
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
|
let mut result = Moistures::default();
|
||||||
for repeat in 0..REPEAT_MOIST_MEASURE {
|
for plant in 0..PLANT_COUNT {
|
||||||
self.signal_counter.pause();
|
let a = self.inner_measure_moisture_hz(plant, Sensor::A).await;
|
||||||
self.signal_counter.clear();
|
let b = self.inner_measure_moisture_hz(plant, Sensor::B).await;
|
||||||
//Disable all
|
let aa = a.unwrap_or_else(|_| u32::MAX as f32);
|
||||||
{
|
let bb = b.unwrap_or_else(|_| u32::MAX as f32);
|
||||||
let shift_register = self.shift_register.lock().await;
|
|
||||||
shift_register.decompose()[MS_4].set_high()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sensor_channel = match sensor {
|
|
||||||
Sensor::A => match plant {
|
|
||||||
0 => SENSOR_A_1,
|
|
||||||
1 => SENSOR_A_2,
|
|
||||||
2 => SENSOR_A_3,
|
|
||||||
3 => SENSOR_A_4,
|
|
||||||
4 => SENSOR_A_5,
|
|
||||||
5 => SENSOR_A_6,
|
|
||||||
6 => SENSOR_A_7,
|
|
||||||
7 => SENSOR_A_8,
|
|
||||||
_ => bail!("Invalid plant id {}", plant),
|
|
||||||
},
|
|
||||||
Sensor::B => match plant {
|
|
||||||
0 => SENSOR_B_1,
|
|
||||||
1 => SENSOR_B_2,
|
|
||||||
2 => SENSOR_B_3,
|
|
||||||
3 => SENSOR_B_4,
|
|
||||||
4 => SENSOR_B_5,
|
|
||||||
5 => SENSOR_B_6,
|
|
||||||
6 => SENSOR_B_7,
|
|
||||||
7 => SENSOR_B_8,
|
|
||||||
_ => bail!("Invalid plant id {}", plant),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
|
|
||||||
{
|
|
||||||
let shift_register = self.shift_register.lock().await;
|
|
||||||
let pin_0 = &mut shift_register.decompose()[MS_0];
|
|
||||||
let pin_1 = &mut shift_register.decompose()[MS_1];
|
|
||||||
let pin_2 = &mut shift_register.decompose()[MS_2];
|
|
||||||
let pin_3 = &mut shift_register.decompose()[MS_3];
|
|
||||||
if is_bit_set(0) {
|
|
||||||
pin_0.set_high()?;
|
|
||||||
} else {
|
|
||||||
pin_0.set_low()?;
|
|
||||||
}
|
|
||||||
if is_bit_set(1) {
|
|
||||||
pin_1.set_high()?;
|
|
||||||
} else {
|
|
||||||
pin_1.set_low()?;
|
|
||||||
}
|
|
||||||
if is_bit_set(2) {
|
|
||||||
pin_2.set_high()?;
|
|
||||||
} else {
|
|
||||||
pin_2.set_low()?;
|
|
||||||
}
|
|
||||||
if is_bit_set(3) {
|
|
||||||
pin_3.set_high()?;
|
|
||||||
} else {
|
|
||||||
pin_3.set_low()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
shift_register.decompose()[MS_4].set_low()?;
|
|
||||||
shift_register.decompose()[SENSOR_ON].set_high()?;
|
|
||||||
}
|
|
||||||
let measurement = 100; //how long to measure and then extrapolate to hz
|
|
||||||
let factor = 1000f32 / measurement as f32; //scale raw cound by this number to get hz
|
|
||||||
|
|
||||||
//give some time to stabilize
|
|
||||||
Timer::after_millis(10).await;
|
|
||||||
self.signal_counter.resume();
|
|
||||||
Timer::after_millis(measurement).await;
|
|
||||||
self.signal_counter.pause();
|
|
||||||
{
|
|
||||||
let shift_register = self.shift_register.lock().await;
|
|
||||||
shift_register.decompose()[MS_4].set_high()?;
|
|
||||||
shift_register.decompose()[SENSOR_ON].set_low()?;
|
|
||||||
}
|
|
||||||
Timer::after_millis(10).await;
|
|
||||||
let unscaled = self.signal_counter.value();
|
|
||||||
let hz = unscaled as f32 * factor;
|
|
||||||
LOG_ACCESS
|
LOG_ACCESS
|
||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.log(
|
.log(LogMessage::TestSensor, aa as u32, bb as u32, &plant.to_string(), "")
|
||||||
LogMessage::RawMeasure,
|
|
||||||
unscaled as u32,
|
|
||||||
hz as u32,
|
|
||||||
&plant.to_string(),
|
|
||||||
&format!("{sensor:?}"),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
results[repeat] = hz;
|
result.sensor_a_hz[plant] = aa;
|
||||||
|
result.sensor_b_hz[plant] = bb;
|
||||||
|
}
|
||||||
|
Ok(result)
|
||||||
}
|
}
|
||||||
results.sort_by(|a, b| a.partial_cmp(b).unwrap()); // floats don't seem to implement total_ord
|
|
||||||
|
|
||||||
let mid = results.len() / 2;
|
|
||||||
let median = results[mid];
|
|
||||||
Ok(median)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn general_fault(&mut self, enable: bool) {
|
async fn general_fault(&mut self, enable: bool) {
|
||||||
hold_disable(6);
|
hold_disable(6);
|
||||||
@@ -412,23 +432,7 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
|
|||||||
self.pump(i, false).await?;
|
self.pump(i, false).await?;
|
||||||
Timer::after_millis(100).await;
|
Timer::after_millis(100).await;
|
||||||
}
|
}
|
||||||
for plant in 0..PLANT_COUNT {
|
self.measure_moisture_hz().await?;
|
||||||
let a = self.measure_moisture_hz(plant, Sensor::A).await;
|
|
||||||
let b = self.measure_moisture_hz(plant, Sensor::B).await;
|
|
||||||
let aa = match a {
|
|
||||||
Ok(a) => a as u32,
|
|
||||||
Err(_) => u32::MAX,
|
|
||||||
};
|
|
||||||
let bb = match b {
|
|
||||||
Ok(b) => b as u32,
|
|
||||||
Err(_) => u32::MAX,
|
|
||||||
};
|
|
||||||
LOG_ACCESS
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "")
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
Timer::after_millis(10).await;
|
Timer::after_millis(10).await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user