109 Commits

Author SHA1 Message Date
a66843a455 move all mqtt publishing functions to mqtt module 2026-05-10 21:00:55 +02:00
379808e659 refctor: TankInfo structure (consistent layout)
- fix: use tagged enum serialization for TankError
- fix: rename TankInfo fields for consistent naming (volume_ml, pct, water_temp_c)
- renamed some fields for better clarity on contained value
2026-05-10 21:00:53 +02:00
e2b2734301 refactor: PlantInfo structure (consistent layout)
- fix: use tagged enum serialization for MoistureSensorError and PumpError
- fix: flatten PlantInfo sensors to SensorTelemetry with top-level moisture_pct
2026-05-10 21:00:52 +02:00
43a0c3c274 refactor: BatteryInfo structure (consistent layout)
- use tagged enum serialization for BatteryError
- flatten BatteryInfo telemetry with consistent field names and typed error
2026-05-10 21:00:50 +02:00
2b83d99820 fix: serialize firmware/state as JSON instead of Debug format 2026-05-10 21:00:47 +02:00
4cc70f96de Merge branch 'fix/stay-alive-webserver-spawn' into legacy/v3-support 2026-05-10 20:59:50 +02:00
c90174be27 fix actually spawn async task for webserver when entering stay_alive mode 2026-05-10 20:49:43 +02:00
b569fa4b04 Merge branch 'fix/reenable-plant-watering-wtf-empire-this-should-never-have-happend' into legacy/v3-support 2026-05-10 19:47:27 +02:00
f71ca7ec6d activate plant watering code again
for some idiotic the most critical part of the code was disabled and
commited that should not have happend!
2026-05-10 19:41:51 +02:00
28f7ae20ef Merge branch 'fix/ota-update' into legacy/v3-support 2026-05-10 17:36:32 +02:00
52049c456e update read_up_to_bytes_from_request with more robust implementation 2026-05-10 17:22:45 +02:00
e3b7648a3f website update version display and ota handler 2026-05-10 16:45:06 +02:00
08ee9018cf website add toast service for notifications 2026-05-10 16:44:30 +02:00
c2929a65ae enable ota_update webserver path again
take latest ota code from `develop` branch and apply to legacy branch
2026-05-10 16:34:26 +02:00
0b386b0ca3 Merge branch 'feature/build-scripts-from-develop' into legacy/v3-support 2026-05-10 14:39:37 +02:00
0ab1ea3635 build: add build artifacts to .gitignore 2026-05-10 14:39:24 +02:00
ae73f12d1c build: add image_build.sh and erase_ota.sh scripts 2026-05-10 14:39:22 +02:00
cfe1c2c6d8 build: add flash.sh and all.sh scripts 2026-05-10 14:39:20 +02:00
578379c0d9 build: remove webpack integration from build.rs and add always-rebuild sentinel 2026-05-10 14:39:19 +02:00
2ff219a1cb build: add reusable build_website.sh script 2026-05-10 14:39:16 +02:00
96023c8dc3 Merge branch 'refactor/mkstatic-util' into legacy/v3-support 2026-05-10 14:04:25 +02:00
7497a8c05d refactor: create util module with shared mk_static
- use crate::util::mk_static in network module
- use crate::util::mk_static in mqtt module
- use crate::util::mk_static in hal module
- remove dead mk_static macro from esp module
2026-05-10 14:01:57 +02:00
d3d8d829be Merge branch 'refactor/network-module' into legacy/v3-support 2026-05-10 13:38:24 +02:00
6889ba4561 refactor: move try_connect_wifi_sntp_mqtt to network module 2026-05-10 13:34:34 +02:00
18095349f3 refactor: move wifi to network module 2026-05-10 13:34:32 +02:00
3d8fd893f5 refactor: move wifi_ap to network module 2026-05-10 13:34:30 +02:00
1bea7ef2f4 refactor: move sntp to network module 2026-05-10 13:34:28 +02:00
f5b9674840 refactor: move run_dhcp to network module 2026-05-10 13:34:25 +02:00
6f22881007 refactor: move net_task to network module 2026-05-10 13:34:23 +02:00
1d8af1b6c4 refactor: create network module
- move NetworkMode and SntpMode enums
2026-05-10 13:33:52 +02:00
2c532359fc Merge branch 'refactor/mqtt-module' into legacy/v3-support 2026-05-10 01:59:07 +02:00
53819484fb refactor: move Solar struct to mqtt module 2026-05-10 01:48:22 +02:00
1151d099cf refactor: move PumpInfo struct to mqtt module 2026-05-10 01:48:20 +02:00
3feaacd460 refactor: create mqtt module with core MQTT statics and tasks 2026-05-10 01:48:10 +02:00
e15e78cc26 rename deepsleep to deep_sleep_ms for clarity on expected duration
in main deep sleep was larger than required by a factor of 1000, fixed
this an renamed function to make expected duration count size obvious
from the name
2026-05-05 22:02:46 +02:00
d9aa96a3cb add containerized dev tools legacy branch 2026-05-05 21:47:41 +02:00
ecf989b859 relax wifi connect parameters for more reliable wifi connection 2026-05-05 01:38:07 +02:00
db401aac55 get most stuff working again, by upgrading to newer esp-hal version
this involved adding a lot of code from the develop branch step by step
there are still some bugs, but at least i can get into the web interface
and configure stuff again \o/ … measuring and pumping is working as well
2026-05-04 23:46:27 +02:00
ecb7707357 fix: add gpio_pad_hold on GPIO21 to prevent boot ROM from reconfiguring shift register enable pin 2026-05-04 02:31:49 +02:00
4cf7a1c94f counter limit 0 is invalid set None instead 2026-05-04 01:50:06 +02:00
9155676e06 commit Cargo.lock to make legacy version compile reproducably 2026-05-04 01:50:02 +02:00
e05f3d768f Add mcutie MQTT client implementation and improve library structure
- Integrated `mcutie` library as a core MQTT client for device communication.
- Added support for Home Assistant entities (binary sensor, button) via MQTT.
- Implemented buffer management, async operations, and packet encoding/decoding.
- Introduced structured error handling and device registration features.
- Updated `Cargo.toml` with new dependencies and enabled feature flags for `serde` and `log`.
- Enhanced logging macros with configurable options (`defmt` or `log`).
- Organized codebase into modules (buffer, components, IO, publish, etc.) for better maintainability.

fix legacy dependecencies and compatiblity with mcutie vendored lib

fix shit i hate this
2026-05-04 01:48:22 +02:00
cf58486cf5 we should be feature compatible with main version now! 2025-09-30 22:37:45 +02:00
cfe23c8a09 mqtt via mcutie 2025-09-29 01:00:11 +02:00
3d18b0dbf6 split webserver into submodules 2025-09-28 19:08:43 +02:00
7ebc147f51 v3 wip 2025-09-28 13:42:30 +02:00
f0bda32d7a use rom linked functions for pin keeping 2025-09-26 23:32:41 +02:00
76f59b093d enable deepsleep and gpio1 + timer wake 2025-09-26 22:07:48 +02:00
7fc8d0c882 sta config mode 2025-09-26 01:13:08 +02:00
6d5bb5b966 sntp and wifi sta mode working 2025-09-26 00:44:40 +02:00
336961f0a0 not working wip of station mode 2025-09-25 22:32:10 +02:00
e20b474dfd bring selftest online, bring tz online, fix set_config/get_config race 2025-09-24 22:29:58 +02:00
5b009f50e5 tanksensor and rtc sync 2025-09-23 22:59:08 +02:00
d010c5d12a get more functions online 2025-09-23 20:51:59 +02:00
b594a02870 fix progress display 2025-09-23 00:43:19 +02:00
1f3349c348 add pump expander 2025-09-23 00:26:05 +02:00
5b0e2b6797 i2c working, rtc working, eeprom working 2025-09-22 23:44:33 +02:00
1791f463b7 remove anyhow 2025-09-22 01:49:25 +02:00
c94f5bdb45 get log to work, make time accessible 2025-09-20 11:31:51 +02:00
584d6df2d0 file up & download and delete 2025-09-18 01:39:32 +02:00
cd63e76469 set read config initial somewhat ready 2025-09-17 03:50:21 +02:00
4c54edbcea littlefs2 impl stuff 2025-09-17 01:36:53 +02:00
8b938e7687 small adjustments 2025-09-16 22:41:45 +02:00
1c84cd00da rework wifi controller share 2025-09-16 02:24:03 +02:00
1397f5d775 keep ota around for alter queries to it 2025-09-16 01:41:38 +02:00
65f6670ca4 adda lot of basic webserver back 2025-09-15 01:21:50 +02:00
049a9d027c use ssid 2025-09-14 13:50:46 +02:00
4aa25c687b minimal startup running 2025-09-14 13:19:30 +02:00
b3cc313139 initial webserver stub running 2025-09-14 05:29:23 +02:00
be3c4a5095 startup and ota state detection working, now wifi_ap next 2025-09-13 20:56:11 +02:00
4160202cdc more changes and linking 2025-09-13 02:47:53 +02:00
9de85b6e37 it's alive 2025-09-13 01:39:47 +02:00
79087c9353 more async migration 2025-09-12 16:30:35 +02:00
0d495d0f56 probalby found most of the import related stuff, now the real refactor begins 2025-09-12 00:05:52 +02:00
242e748ca0 started cleanup and moving from std to no_std 2025-09-11 22:47:11 +02:00
f853b6f2b2 removed all std dependencies … WIP move to embassy pure rust code 2025-09-11 21:42:17 +02:00
5bc20d312a Merge branch 'feature/enable-mqtt-login' into develop 2025-09-11 20:21:34 +02:00
4faae86a6b reduce amount of warnings 2025-09-11 20:17:18 +02:00
4fa1a05fc3 remove duplicate import 2025-09-11 20:04:27 +02:00
3a24dcec53 add release profile 2025-09-11 19:56:53 +02:00
e7895577ca update frontent to include inputs for mqtt auth 2025-09-11 19:56:50 +02:00
be9a076b33 add configuration options to enable mqtt user login 2025-09-11 19:56:48 +02:00
47ad4c70de Merge branch 'can' into develop 2025-09-03 19:08:55 +02:00
c4aa3a1450 1.3 baseplane 2025-08-29 21:11:04 +02:00
f0d89417b6 Merge branch 'premerge' into develop 2025-08-29 21:09:20 +02:00
a8e17cda3b wip min 2025-08-29 17:14:15 +02:00
a38d704498 new case and can sensor board 2025-08-25 10:15:36 +02:00
50b2e994ca can prepare stuff 2025-08-14 22:10:53 +02:00
7a7f000743 add missing sensor port labels 2025-08-08 23:24:05 +02:00
d650358bab Add flowsensor prototype, add canbus to backplane 2025-07-27 13:38:53 +02:00
9f113adf7e v4 case 2025-07-26 17:52:19 +02:00
735f836458 fix unit being wrong for solar display 2025-07-02 19:11:36 +02:00
f02d935f40 hide unused html elements based on config test1 2025-06-27 21:23:18 +02:00
cc4e9efffd v4.0 backplane 1.1 2025-06-26 22:38:03 +02:00
5d91daf23d add pump current configuration 2025-06-26 02:39:32 +02:00
577c38d026 allow mppt module to fail during startup without aborting 2025-06-25 01:38:54 +02:00
6b711e29fc extract rtc module, extract tank module, fix backupview refresh, switch to embedded storage for eeprom 2025-06-25 01:18:36 +02:00
5fb8705d9a split rtc module out to reduce repreated exactly same code 2025-06-24 20:59:12 +02:00
e57e87af3f fix backplane usb placement, use uncritial pins for high during flash/boot 2025-06-23 23:19:36 +02:00
450197c7cd merge stuff 2025-06-23 22:21:03 +02:00
736925f7de Merge pull request 'Housekeeping' (#18) from housekeeping into develop
Reviewed-on: #18
2025-06-23 22:15:50 +02:00
d059b7b1db Merge branch 'develop' into housekeeping and clippy and spellcheck 2025-06-20 23:59:14 +02:00
e7b6bda747 Merge branch 'develop' into housekeeping 2025-06-20 23:40:32 +02:00
5621f17e61 fix all warnings 2025-06-20 21:31:12 +02:00
d0e5627815 keep node artifacts in webpack directory
add commands to build.rs to move assets to required location within src
2025-06-20 20:53:32 +02:00
7115809f2b fix all compiler warnings 2025-06-20 20:53:29 +02:00
c1d0ce542c fix confusing lifetime warning 2025-06-20 20:53:28 +02:00
b7b08d8747 fix unused variable warnings 2025-06-20 20:53:26 +02:00
8d68f0ef14 remove embassy references in Cargo.toml
in newer esp-rust toolchains having these dependencies defiened leeds to
compile errors.
2025-06-20 20:53:23 +02:00
120 changed files with 77103 additions and 16682 deletions

9
.gitignore vendored
View File

@@ -8,6 +8,15 @@ target
Cargo.lock
node_modules/
rust/src/webserver/bundle.js
rust/src/webserver/bundle.js.gz
rust/src/webserver/index.html
rust/src/webserver/index.html.gz
rust/src_webpack/bundle.js
rust/src_webpack/bundle.js.gz
rust/src_webpack/index.html
rust/src_webpack/index.html.gz
rust/build/
rust/image.bin
rust/target/
rust/Cargo.lock
rust/src_webpack/node_modules/

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_NAME="localhost/esp-plant-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
pushd "$CONTAINER_TOOLS_BASEDIR"
podman build -t "$CONTAINER_NAME" -f "esp-plant-dev-tools.Containerfile" .
popd

View File

@@ -0,0 +1,16 @@
FROM debian:latest
RUN apt update -y && apt upgrade -y && apt install unzip curl xz-utils nodejs -y
RUN cd /root && \
curl -L -o xpack-riscv-toolchain.tar.gz "https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v14.2.0-3/xpack-riscv-none-elf-gcc-14.2.0-3-linux-x64.tar.gz" && \
mkdir xpack-toolchain && \
tar -xvf xpack-riscv-toolchain.tar.gz -C xpack-toolchain --strip-components=1 && \
mv xpack-toolchain/bin/* /usr/local/bin && \
mv xpack-toolchain/lib/ /usr/local && \
mv xpack-toolchain/lib64/ /usr/local && \
mv xpack-toolchain/libexec /usr/local && \
mv xpack-toolchain/riscv-none-elf /usr/local && \
rm -rf xpack-toolchain xpack-riscv-toolchain.tar.gz
RUN apt install npm -y

29
bin/npm Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_IMAGE="localhost/esp-plant-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
PLANTCTL_PROJECT_DIR="$(readlink -f "$CONTAINER_TOOLS_BASEDIR/..")"
function _fatal {
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
exit 1
}
declare -a PODMAN_ARGS=(
"--rm" "-i" "--log-driver=none"
"-v" "$PLANTCTL_PROJECT_DIR:$PLANTCTL_PROJECT_DIR:rw"
"-v" "$PWD:$PWD:rw"
"-w" "$PWD"
)
[[ -t 1 ]] && PODMAN_ARGS+=("-t")
if ! podman image exists "$CONTAINER_IMAGE"; then
#attempt to build container
"$CONTAINER_TOOLS_BASEDIR/build-esp-plant-dev-tools.sh" 1>&2 ||
_fatal "faild to build local image, cannot continue! … please ensure you have an internet connection"
fi
podman run "${PODMAN_ARGS[@]}" --entrypoint npm "$CONTAINER_IMAGE" "$@"

29
bin/npx Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_IMAGE="localhost/esp-plant-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
PLANTCTL_PROJECT_DIR="$(readlink -f "$CONTAINER_TOOLS_BASEDIR/..")"
function _fatal {
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
exit 1
}
declare -a PODMAN_ARGS=(
"--rm" "-i" "--log-driver=none"
"-v" "$PLANTCTL_PROJECT_DIR:$PLANTCTL_PROJECT_DIR:rw"
"-v" "$PWD:$PWD:rw"
"-w" "$PWD"
)
[[ -t 1 ]] && PODMAN_ARGS+=("-t")
if ! podman image exists "$CONTAINER_IMAGE"; then
#attempt to build container
"$CONTAINER_TOOLS_BASEDIR/build-esp-plant-dev-tools.sh" 1>&2 ||
_fatal "faild to build local image, cannot continue! … please ensure you have an internet connection"
fi
podman run "${PODMAN_ARGS[@]}" --entrypoint npx "$CONTAINER_IMAGE" "$@"

29
bin/riscv32-unknown-elf-gcc Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_IMAGE="localhost/esp-plant-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
PLANTCTL_PROJECT_DIR="$(readlink -f "$CONTAINER_TOOLS_BASEDIR/..")"
function _fatal {
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
exit 1
}
declare -a PODMAN_ARGS=(
"--rm" "-i" "--log-driver=none"
"-v" "$PLANTCTL_PROJECT_DIR:$PLANTCTL_PROJECT_DIR:rw"
"-v" "$PWD:$PWD:rw"
"-w" "$PWD"
)
[[ -t 1 ]] && PODMAN_ARGS+=("-t")
if ! podman image exists "$CONTAINER_IMAGE"; then
#attempt to build container
"$CONTAINER_TOOLS_BASEDIR/build-esp-plant-dev-tools.sh" 1>&2 ||
_fatal "faild to build local image, cannot continue! … please ensure you have an internet connection"
fi
podman run "${PODMAN_ARGS[@]}" --entrypoint riscv-none-elf-gcc "$CONTAINER_IMAGE" "$@"

BIN
board/Body1.3mf Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -462,8 +462,8 @@
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "error",
"power_pin_not_driven": "error",
"pin_to_pin": "ignore",
"power_pin_not_driven": "ignore",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
@@ -472,6 +472,7 @@
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-battery-charging" viewBox="0 0 16 16">
<path d="M9.585 2.568a.5.5 0 0 1 .226.58L8.677 6.832h1.99a.5.5 0 0 1 .364.843l-5.334 5.667a.5.5 0 0 1-.842-.49L5.99 9.167H4a.5.5 0 0 1-.364-.843l5.333-5.667a.5.5 0 0 1 .616-.09z"/>
<path d="M2 4h4.332l-.94 1H2a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h2.38l-.308 1H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2"/>
<path d="M2 6h2.45L2.908 7.639A1.5 1.5 0 0 0 3.313 10H2zm8.595-2-.308 1H12a1 1 0 0 1 1 1v4a1 1 0 0 1-1 1H9.276l-.942 1H12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/>
<path d="M12 10h-1.783l1.542-1.639q.146-.156.241-.34zm0-3.354V6h-.646a1.5 1.5 0 0 1 .646.646M16 8a1.5 1.5 0 0 1-1.5 1.5v-3A1.5 1.5 0 0 1 16 8"/>
</svg>

After

Width:  |  Height:  |  Size: 738 B

View File

@@ -428,7 +428,7 @@
(symbol "Sensor_1_1"
(rectangle
(start -5.08 -1.27)
(end 5.08 -29.21)
(end 5.08 -34.29)
(stroke
(width 0)
(type solid)
@@ -455,7 +455,7 @@
)
)
)
(pin no_connect line
(pin power_in line
(at 7.62 -5.08 180)
(length 2.54)
(name "VBAT"
@@ -527,10 +527,10 @@
)
)
)
(pin output line
(pin input line
(at 7.62 -15.24 180)
(length 2.54)
(name "GND"
(name "CAN_H"
(effects
(font
(size 1.27 1.27)
@@ -545,10 +545,10 @@
)
)
)
(pin output line
(pin input line
(at 7.62 -17.78 180)
(length 2.54)
(name "GND"
(name "CAN_L"
(effects
(font
(size 1.27 1.27)
@@ -635,6 +635,42 @@
)
)
)
(pin output line
(at 7.62 -30.48 180)
(length 2.54)
(name "GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "12"
(effects
(font
(size 1.27 1.27)
)
)
)
)
(pin output line
(at 7.62 -33.02 180)
(length 2.54)
(name "GND"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "13"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)

View File

@@ -164,45 +164,61 @@
(remove_unused_layers no)
(uuid "eee06414-e977-45a8-a5e0-618644284d45")
)
(pad "7" thru_hole rect
(at -0.5 -12)
(size 0.85 0.85)
(drill 0.5)
(pad "7" thru_hole circle
(at -42.4 -0.06)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "536f0038-06c5-45e5-b1c8-898a364f6ec4")
(uuid "a794ee5a-cb4c-4bc2-9f30-7f38ae9862fc")
)
(pad "8" thru_hole rect
(at 39.5 -16)
(size 0.85 0.85)
(drill 0.5)
(pad "8" thru_hole circle
(at -42.4 2.48)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "3e0cdbaa-8219-48c4-bb0a-fd4f0e78a390")
(uuid "f9ed5486-c56c-4c91-b707-d09fae18d351")
)
(pad "9" thru_hole rect
(at 39.5 22.5)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f01565c2-eadd-4451-9dea-2ebe28d872f0")
)
(pad "10" thru_hole rect
(at -0.5 15)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "56ec7c50-069f-4f46-9b83-ac18e4928930")
)
(pad "11" thru_hole rect
(at -43 22.5)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f423be21-13b8-46de-8e19-e48325411a29")
)
(pad "12" thru_hole rect
(at -0.5 -12)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "536f0038-06c5-45e5-b1c8-898a364f6ec4")
)
(pad "13" thru_hole rect
(at 39.5 -16)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "3e0cdbaa-8219-48c4-bb0a-fd4f0e78a390")
)
(embedded_fonts no)
)

View File

@@ -0,0 +1,208 @@
(footprint "Sensor"
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(layer "F.Cu")
(property "Reference" "REF**"
(at 0 -0.5 0)
(unlocked yes)
(layer "F.SilkS")
(uuid "f71e9c26-7923-4e5c-828e-153927862740")
(effects
(font
(size 1 1)
(thickness 0.1)
)
)
)
(property "Value" "Sensor"
(at 0 1 0)
(unlocked yes)
(layer "F.Fab")
(uuid "d40c7203-0c06-49e1-8672-dbd216694fc8")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Datasheet" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "6720cb18-0687-4d55-a6ad-3ccf0819eac2")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(property "Description" ""
(at 0 0 0)
(unlocked yes)
(layer "F.Fab")
(hide yes)
(uuid "43905e6e-773d-4d5e-8d72-57c092c3495a")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(attr smd)
(fp_line
(start -45 -18)
(end 41 -18)
(stroke
(width 0.1)
(type default)
)
(layer "F.SilkS")
(uuid "e63ec799-95c4-407e-acc9-9c7e6d3e330c")
)
(fp_line
(start -45 24)
(end -45 -18)
(stroke
(width 0.1)
(type default)
)
(layer "F.SilkS")
(uuid "a66c286b-432b-493d-9619-2dd4fbfdb21c")
)
(fp_line
(start -44 24)
(end -45 24)
(stroke
(width 0.1)
(type default)
)
(layer "F.SilkS")
(uuid "71526e68-71d4-4b13-ab74-482657a06849")
)
(fp_line
(start 41 -18)
(end 41 24)
(stroke
(width 0.1)
(type default)
)
(layer "F.SilkS")
(uuid "55554342-bbff-4d1b-b931-67fb7eaa5895")
)
(fp_line
(start 41 24)
(end -44 24)
(stroke
(width 0.1)
(type default)
)
(layer "F.SilkS")
(uuid "2dc0ee59-36f6-407e-821a-c6acfe3ea887")
)
(fp_text user "${REFERENCE}"
(at 0 2.5 0)
(unlocked yes)
(layer "F.Fab")
(uuid "befc0725-b201-4f81-b0dc-b327becad9ba")
(effects
(font
(size 1 1)
(thickness 0.15)
)
)
)
(pad "1" thru_hole rect
(at -42.4 -15.3)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "5538ac99-6e48-4111-a3df-8ff33be72ff3")
)
(pad "2" thru_hole circle
(at -42.4 -12.76)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "0c1d4a6c-7871-46ae-a262-a8223571975e")
)
(pad "3" thru_hole circle
(at -42.4 -10.22)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "70df4148-0415-45a2-8365-0977fcda45a1")
)
(pad "4" thru_hole circle
(at -42.4 -7.68)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "179dd332-2ab2-4917-91ec-35f232b45ce3")
)
(pad "5" thru_hole circle
(at -42.4 -5.14)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "17bc8f3a-4727-4e31-90f4-8252dff5ad1c")
)
(pad "6" thru_hole circle
(at -42.4 -2.6)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "eee06414-e977-45a8-a5e0-618644284d45")
)
(pad "7" thru_hole rect
(at -0.5 -12)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "536f0038-06c5-45e5-b1c8-898a364f6ec4")
)
(pad "8" thru_hole rect
(at 39.5 -16)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "3e0cdbaa-8219-48c4-bb0a-fd4f0e78a390")
)
(pad "9" thru_hole rect
(at 39.5 22.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f01565c2-eadd-4451-9dea-2ebe28d872f0")
)
(pad "10" thru_hole rect
(at -0.5 15)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "56ec7c50-069f-4f46-9b83-ac18e4928930")
)
(pad "11" thru_hole rect
(at -43 22.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f423be21-13b8-46de-8e19-e48325411a29")
)
(embedded_fonts no)
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
{
"board": {
"active_layer": 6,
"active_layer_preset": "All Layers",
"auto_track_width": false,
"hidden_netclasses": [],
"hidden_nets": [],
"high_contrast_mode": 0,
"net_color_mode": 1,
"opacity": {
"images": 0.6,
"pads": 1.0,
"shapes": 1.0,
"tracks": 1.0,
"vias": 1.0,
"zones": 0.6
},
"selection_filter": {
"dimensions": true,
"footprints": true,
"graphics": true,
"keepouts": true,
"lockedItems": false,
"otherItems": true,
"pads": true,
"text": true,
"tracks": true,
"vias": true,
"zones": true
},
"visible_items": [
"vias",
"footprint_text",
"footprint_anchors",
"ratsnest",
"grid",
"footprints_front",
"footprints_back",
"footprint_values",
"footprint_references",
"tracks",
"drc_errors",
"drawing_sheet",
"bitmaps",
"pads",
"zones",
"drc_warnings",
"locked_item_shadows",
"conflict_shadows",
"shapes"
],
"visible_layers": "ffffffff_ffffffff_ffffffff_ffffffff",
"zone_display_mode": 1
},
"git": {
"repo_type": "",
"repo_username": "",
"ssh_key": ""
},
"meta": {
"filename": "Sensors.kicad_prl",
"version": 5
},
"net_inspector_panel": {
"col_hidden": [
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false,
false
],
"col_order": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11
],
"col_widths": [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"custom_group_rules": [],
"expanded_rows": [],
"filter_by_net_name": true,
"filter_by_netclass": true,
"filter_text": "",
"group_by_constraint": false,
"group_by_netclass": false,
"show_unconnected_nets": false,
"show_zero_pad_nets": false,
"sort_ascending": true,
"sorting_column": 0
},
"open_jobsets": [],
"project": {
"files": []
},
"schematic": {
"selection_filter": {
"graphics": true,
"images": true,
"labels": true,
"lockedItems": false,
"otherItems": true,
"pins": true,
"symbols": true,
"text": true,
"wires": true
}
}
}

View File

@@ -0,0 +1,674 @@
{
"board": {
"3dviewports": [],
"design_settings": {
"defaults": {
"apply_defaults_to_fp_fields": false,
"apply_defaults_to_fp_shapes": false,
"apply_defaults_to_fp_text": false,
"board_outline_line_width": 0.05,
"copper_line_width": 0.2,
"copper_text_italic": false,
"copper_text_size_h": 1.5,
"copper_text_size_v": 1.5,
"copper_text_thickness": 0.3,
"copper_text_upright": false,
"courtyard_line_width": 0.05,
"dimension_precision": 4,
"dimension_units": 3,
"dimensions": {
"arrow_length": 1270000,
"extension_offset": 500000,
"keep_text_aligned": true,
"suppress_zeroes": true,
"text_position": 0,
"units_format": 0
},
"fab_line_width": 0.1,
"fab_text_italic": false,
"fab_text_size_h": 1.0,
"fab_text_size_v": 1.0,
"fab_text_thickness": 0.15,
"fab_text_upright": false,
"other_line_width": 0.1,
"other_text_italic": false,
"other_text_size_h": 1.0,
"other_text_size_v": 1.0,
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 0.8,
"height": 1.27,
"width": 2.54
},
"silk_line_width": 0.1,
"silk_text_italic": false,
"silk_text_size_h": 1.0,
"silk_text_size_v": 1.0,
"silk_text_thickness": 0.1,
"silk_text_upright": false,
"zones": {
"min_clearance": 0.5
}
},
"diff_pair_dimensions": [
{
"gap": 0.0,
"via_gap": 0.0,
"width": 0.0
}
],
"drc_exclusions": [],
"meta": {
"version": 2
},
"rule_severities": {
"annular_width": "error",
"clearance": "error",
"connection_width": "warning",
"copper_edge_clearance": "error",
"copper_sliver": "warning",
"courtyards_overlap": "error",
"creepage": "error",
"diff_pair_gap_out_of_range": "error",
"diff_pair_uncoupled_length_too_long": "error",
"drill_out_of_range": "error",
"duplicate_footprints": "warning",
"extra_footprint": "warning",
"footprint": "error",
"footprint_filters_mismatch": "ignore",
"footprint_symbol_mismatch": "warning",
"footprint_type_mismatch": "ignore",
"hole_clearance": "error",
"hole_to_hole": "warning",
"holes_co_located": "warning",
"invalid_outline": "error",
"isolated_copper": "warning",
"item_on_disabled_layer": "error",
"items_not_allowed": "error",
"length_out_of_range": "error",
"lib_footprint_issues": "warning",
"lib_footprint_mismatch": "warning",
"malformed_courtyard": "ignore",
"microvia_drill_out_of_range": "error",
"mirrored_text_on_front_layer": "warning",
"missing_courtyard": "ignore",
"missing_footprint": "warning",
"net_conflict": "warning",
"nonmirrored_text_on_back_layer": "warning",
"npth_inside_courtyard": "ignore",
"padstack": "warning",
"pth_inside_courtyard": "ignore",
"shorting_items": "error",
"silk_edge_clearance": "ignore",
"silk_over_copper": "warning",
"silk_overlap": "warning",
"skew_out_of_range": "error",
"solder_mask_bridge": "error",
"starved_thermal": "warning",
"text_height": "warning",
"text_on_edge_cuts": "error",
"text_thickness": "warning",
"through_hole_pad_without_hole": "error",
"too_many_vias": "error",
"track_angle": "error",
"track_dangling": "warning",
"track_segment_length": "error",
"track_width": "error",
"tracks_crossing": "error",
"unconnected_items": "error",
"unresolved_variable": "error",
"via_dangling": "warning",
"zones_intersect": "error"
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.0,
"min_connection": 0.0,
"min_copper_edge_clearance": 0.5,
"min_groove_width": 0.0,
"min_hole_clearance": 0.15,
"min_hole_to_hole": 0.25,
"min_microvia_diameter": 0.2,
"min_microvia_drill": 0.1,
"min_resolved_spokes": 2,
"min_silk_clearance": 0.0,
"min_text_height": 0.8,
"min_text_thickness": 0.08,
"min_through_hole_diameter": 0.25,
"min_track_width": 0.0,
"min_via_annular_width": 0.05,
"min_via_diameter": 0.25,
"solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true
},
"teardrop_options": [
{
"td_onpthpad": true,
"td_onroundshapesonly": false,
"td_onsmdpad": true,
"td_ontrackend": false,
"td_onvia": true
}
],
"teardrop_parameters": [
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_round_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_rect_shape",
"td_width_to_size_filter_ratio": 0.9
},
{
"td_allow_use_two_tracks": true,
"td_curve_segcount": 0,
"td_height_ratio": 1.0,
"td_length_ratio": 0.5,
"td_maxheight": 2.0,
"td_maxlen": 1.0,
"td_on_pad_in_zone": false,
"td_target_name": "td_track_end",
"td_width_to_size_filter_ratio": 0.9
}
],
"track_widths": [
0.0,
0.1,
0.2,
0.5
],
"tuning_pattern_settings": {
"diff_pair_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 1.0
},
"diff_pair_skew_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
},
"single_track_defaults": {
"corner_radius_percentage": 80,
"corner_style": 1,
"max_amplitude": 1.0,
"min_amplitude": 0.2,
"single_sided": false,
"spacing": 0.6
}
},
"via_dimensions": [
{
"diameter": 0.0,
"drill": 0.0
},
{
"diameter": 0.3,
"drill": 0.25
}
],
"zones_allow_external_fillets": false
},
"ipc2581": {
"dist": "",
"distpn": "",
"internal_id": "",
"mfg": "",
"mpn": ""
},
"layer_pairs": [],
"layer_presets": [],
"viewports": []
},
"boards": [],
"cvpcb": {
"equivalence_files": []
},
"erc": {
"erc_exclusions": [],
"meta": {
"version": 0
},
"pin_map": [
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
1,
0,
1,
2
],
[
0,
1,
0,
0,
0,
0,
1,
1,
2,
1,
1,
2
],
[
0,
0,
0,
0,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
2
],
[
1,
1,
1,
1,
1,
0,
1,
1,
1,
1,
1,
2
],
[
0,
0,
0,
1,
0,
0,
1,
0,
0,
0,
0,
2
],
[
0,
2,
1,
2,
0,
0,
1,
0,
2,
2,
2,
2
],
[
0,
2,
0,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
0,
2,
1,
1,
0,
0,
1,
0,
2,
0,
0,
2
],
[
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2,
2
]
],
"rule_severities": {
"bus_definition_conflict": "error",
"bus_entry_needed": "error",
"bus_to_bus_conflict": "error",
"bus_to_net_conflict": "error",
"different_unit_footprint": "error",
"different_unit_net": "error",
"duplicate_reference": "error",
"duplicate_sheet_names": "error",
"endpoint_off_grid": "warning",
"extra_units": "error",
"footprint_filter": "ignore",
"footprint_link_issues": "warning",
"four_way_junction": "ignore",
"global_label_dangling": "warning",
"hier_label_mismatch": "error",
"label_dangling": "error",
"label_multiple_wires": "warning",
"lib_symbol_issues": "warning",
"lib_symbol_mismatch": "warning",
"missing_bidi_pin": "warning",
"missing_input_pin": "warning",
"missing_power_pin": "error",
"missing_unit": "warning",
"multiple_net_names": "warning",
"net_not_bus_member": "warning",
"no_connect_connected": "warning",
"no_connect_dangling": "warning",
"pin_not_connected": "error",
"pin_not_driven": "error",
"pin_to_pin": "ignore",
"power_pin_not_driven": "ignore",
"same_local_global_label": "warning",
"similar_label_and_power": "warning",
"similar_labels": "warning",
"similar_power": "warning",
"simulation_model_issue": "ignore",
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"
}
},
"libraries": {
"pinned_footprint_libs": [],
"pinned_symbol_libs": []
},
"meta": {
"filename": "Sensors.kicad_pro",
"version": 3
},
"net_settings": {
"classes": [
{
"bus_width": 12,
"clearance": 0.15,
"diff_pair_gap": 0.25,
"diff_pair_via_gap": 0.25,
"diff_pair_width": 0.2,
"line_style": 0,
"microvia_diameter": 0.3,
"microvia_drill": 0.1,
"name": "Default",
"pcb_color": "rgba(0, 0, 0, 0.000)",
"priority": 2147483647,
"schematic_color": "rgba(0, 0, 0, 0.000)",
"track_width": 0.2,
"via_diameter": 0.6,
"via_drill": 0.3,
"wire_width": 6
}
],
"meta": {
"version": 4
},
"net_colors": null,
"netclass_assignments": null,
"netclass_patterns": []
},
"pcbnew": {
"last_paths": {
"gencad": "",
"idf": "",
"netlist": "",
"plot": "",
"pos_files": "",
"specctra_dsn": "",
"step": "Sensors.step",
"svg": "",
"vrml": ""
},
"page_layout_descr_file": ""
},
"schematic": {
"annotate_start_num": 0,
"bom_export_filename": "${PROJECTNAME}.csv",
"bom_fmt_presets": [],
"bom_fmt_settings": {
"field_delimiter": ",",
"keep_line_breaks": false,
"keep_tabs": false,
"name": "CSV",
"ref_delimiter": ",",
"ref_range_delimiter": "",
"string_delimiter": "\""
},
"bom_presets": [],
"bom_settings": {
"exclude_dnp": false,
"fields_ordered": [
{
"group_by": false,
"label": "Reference",
"name": "Reference",
"show": true
},
{
"group_by": true,
"label": "Value",
"name": "Value",
"show": true
},
{
"group_by": true,
"label": "Footprint",
"name": "Footprint",
"show": true
},
{
"group_by": false,
"label": "Datasheet",
"name": "Datasheet",
"show": true
},
{
"group_by": false,
"label": "Description",
"name": "Description",
"show": false
},
{
"group_by": false,
"label": "Qty",
"name": "${QUANTITY}",
"show": true
},
{
"group_by": false,
"label": "#",
"name": "${ITEM_NUMBER}",
"show": false
},
{
"group_by": false,
"label": "LCSC_PART_NUMBER",
"name": "LCSC_PART_NUMBER",
"show": true
},
{
"group_by": false,
"label": "Sim.Device",
"name": "Sim.Device",
"show": false
},
{
"group_by": false,
"label": "Sim.Pins",
"name": "Sim.Pins",
"show": false
},
{
"group_by": false,
"label": "Sim.Type",
"name": "Sim.Type",
"show": false
},
{
"group_by": true,
"label": "DNP",
"name": "${DNP}",
"show": true
},
{
"group_by": true,
"label": "Exclude from BOM",
"name": "${EXCLUDE_FROM_BOM}",
"show": true
},
{
"group_by": true,
"label": "Exclude from Board",
"name": "${EXCLUDE_FROM_BOARD}",
"show": true
}
],
"filter_string": "",
"group_symbols": true,
"include_excluded_from_bom": true,
"name": "",
"sort_asc": true,
"sort_field": "Reference"
},
"connection_grid_size": 50.0,
"drawing": {
"dashed_lines_dash_length_ratio": 12.0,
"dashed_lines_gap_length_ratio": 3.0,
"default_line_thickness": 6.0,
"default_text_size": 50.0,
"field_names": [],
"intersheets_ref_own_page": false,
"intersheets_ref_prefix": "",
"intersheets_ref_short": false,
"intersheets_ref_show": false,
"intersheets_ref_suffix": "",
"junction_size_choice": 3,
"label_size_ratio": 0.375,
"operating_point_overlay_i_precision": 3,
"operating_point_overlay_i_range": "~A",
"operating_point_overlay_v_precision": 3,
"operating_point_overlay_v_range": "~V",
"overbar_offset_ratio": 1.23,
"pin_symbol_size": 25.0,
"text_offset_ratio": 0.15
},
"legacy_lib_dir": "",
"legacy_lib_list": [],
"meta": {
"version": 1
},
"net_format_name": "",
"page_layout_descr_file": "",
"plot_directory": "",
"space_save_all_events": true,
"spice_current_sheet_as_root": false,
"spice_external_command": "spice \"%I\"",
"spice_model_current_sheet_as_root": true,
"spice_save_all_currents": false,
"spice_save_all_dissipations": false,
"spice_save_all_voltages": false,
"subpart_first_id": 65,
"subpart_id_separator": 0
},
"sheets": [
[
"46346c04-8bed-48b4-837b-9342dd403232",
"Root"
]
],
"text_variables": {}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
(sym_lib_table
(version 7)
(lib (name "Modules")(type "KiCad")(uri "/home/empire/workspace/PlantCtrl/board/modules/Modules.kicad_sym")(options "")(descr ""))
)

View File

@@ -0,0 +1,14 @@
[build]
target = "riscv32imc-unknown-none-elf"
[target."riscv32imc-unknown-none-elf"]
rustflags = [
# "-C", "link-arg=-Tlink.x",
]
# runner = "riscv64-unknown-elf-gdb -q -x openocd.gdb"
# runner = "riscv-none-embed-gdb -q -x openocd.gdb"
# runner = "gdb -q -x openocd.gdb"
# runner = "wlink -v flash"
runner = "wlink -v flash --enable-sdi-print --watch-serial --erase"
# runner = "wlink -v flash"

View File

@@ -0,0 +1,7 @@
target extended-remote :3333
set remotetimeout 2000
#symbol-file target/riscv32imc-unknown-none-elf/release/ch32v203-examples
file target/riscv32imc-unknown-none-elf/release/bms
monitor reset halt

View File

@@ -0,0 +1 @@
target

View File

@@ -0,0 +1,43 @@
[package]
name = "bms"
version = "0.1.0"
edition = "2021"
[dependencies]
ch32-hal = { git = "https://github.com/ch32-rs/ch32-hal", features = [
"ch32v203c8t6",
"memory-x",
"embassy",
"rt",
"time-driver-tim2",
], default-features = false }
embassy-executor = { version = "0.7.0", features = [
"arch-riscv32",
"executor-thread",
] }
embassy-time = { version = "0.4.0" }
embassy-usb = { version = "0.3.0" }
embassy-futures = { version = "0.1.0" }
# This is okay because we should automatically use whatever ch32-hal uses
qingke-rt = "*"
qingke = "*"
panic-halt = "1.0"
embedded-hal = "1.0.0"
heapless = "0.8.0"
micromath = { version = "2.1.0", features = ["num-traits"] }
embedded-can = "0.4.1"
[profile.dev]
#lto = true
opt-level = 1
[profile.release]
strip = false # symbols are not flashed to the microcontroller, so don't strip them.
#lto = true
debug = true
opt-level = "z" # Optimize for size.

View File

@@ -0,0 +1,72 @@
# ch32v203-bms
A simple battery management controller software.
## CAN bus and hardware address
This firmware exposes a CAN interface on the CH32V203 and uses 4 hardware address pins to allow up to 16 sensors on the same bus.
- CAN pins (default mapping):
- CAN RX: PA11
- CAN TX: PA12
- Address select pins (with internal pull-ups):
- A0: PA0
- A1: PA1
- A2: PA2
- A3: PA3
Wire each address pin to GND to set its corresponding bit to 1. The 4-bit address range is 0..15. The nodes CAN Standard ID is `0x100 | addr`, i.e. 0x100..0x10F. The CAN acceptance filter is configured to only accept frames with the nodes own ID.
Adjust the pins above if your PCB routes CAN or address lines to different pads.
## 555 timer (software) emulation mode
To save the BOM cost of a classic NE555 in simple oscillator applications, this firmware implements a minimal 555-like Schmitt trigger using the MCUs ADC and a GPIO, approximating the behavior when the capacitor is charged/discharged via Q through a resistor, and the combined Trigger/Threshold senses the capacitor node.
- Pins used:
- Q output: PB2
- Combined Trigger/Threshold (ADC input): PA0
- Wiring:
- PB2 (Q) -> series resistor R -> capacitor node
- Capacitor node -> capacitor to GND
- Capacitor node -> PA0 (ADC input)
- Behavior:
- When ADC(PA0) <= ~1/3 Vref, PB2 is driven High.
- When ADC(PA0) >= ~2/3 Vref, PB2 is driven Low.
- Hysteresis avoids chatter; the actual charge/discharge dynamics follow your chosen R and C.
- Notes:
- Use an appropriate resistor from PB2 to the capacitor to set oscillation frequency. Start with 10k..100k and adjust with C.
- Ensure PA0 is routed to the capacitor node and left high impedance (no strong pull-ups/downs) so the ADC can sense the analog voltage.
- PB2 drives the on-board LED (if present), so the LED might blink at the oscillation frequency.
This mode is implemented in `src/main.rs` using `hal::adc::Adc::convert(&mut pin, SampleTime::...)` to take periodic samples and a simple state machine to toggle the Q output based on ~1/3 and ~2/3 Vref thresholds.
## Building
``` sh
cargo build --release
```
## Flash
``` sh
wchisp config reset
wchip wchisp flash target/riscv32imc-unknown-none-elf/release/bms
```
## Debugging
For debugging purposes a container file is provided together with wrapper scripts to start the containerized `openocd` and `riscv-gdb` transparently. The wrapper scripts assume that `podman` is setup.
Starting Debug server
```
./bin/openocd
```
Connecting with gdb for interactive debugging
```
./bin/gdb -f target/riscv32imc-unknown-none-elf/release/bms
```

View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_NAME="localhost/wch-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
pushd "$CONTAINER_TOOLS_BASEDIR"
podman build -t "$CONTAINER_NAME" -f "../wch-tools.Containerfile" .
popd

View File

@@ -0,0 +1,29 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_IMAGE="localhost/wch-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
function _fatal {
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
exit 1
}
declare -a PODMAN_ARGS=(
"--rm" "-i" "--log-driver=none"
"--network=host"
"--pid=host"
"-v" "$PWD:$PWD:rw"
"-w" "$PWD"
)
[[ -t 1 ]] && PODMAN_ARGS+=("-t")
if ! podman image exists "$CONTAINER_IMAGE"; then
#attempt to build container
"$CONTAINER_TOOLS_BASEDIR/build-wch-tools-container.sh" 1>&2 ||
_fatal "faild to build local image, cannot continue! … please ensure you have an internet connection"
fi
podman run "${PODMAN_ARGS[@]}" --entrypoint riscv-none-elf-gdb-py3 "$CONTAINER_IMAGE" "$@"

View File

@@ -0,0 +1,44 @@
#!/usr/bin/env bash
set -euo pipefail
CONTAINER_IMAGE="localhost/wch-dev-tools:latest"
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
function _fatal {
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
exit 1
}
declare -a PODMAN_ARGS=(
"--rm" "-i" "--log-driver=none"
"--network=host"
"-v" "$PWD:$PWD:rw"
"-w" "$PWD"
)
for device in /dev/bus/usb/*/*; do
if udevadm info "$device" | grep -q "ID_VENDOR=wch.cn" && \
udevadm info "$device" | grep -q "ID_MODEL=WCH-Link"; then
DEBUGGER_DEV_PATH="$device"
break
fi
done
if [[ -z "${DEBUGGER_DEV_PATH:-}" ]]; then
echo "Could not find hardware debugger … Exiting!" 1>&2
exit 1
else
# add jlink to podman device
PODMAN_ARGS+=("--device=$DEBUGGER_DEV_PATH")
fi
[[ -t 1 ]] && PODMAN_ARGS+=("-t")
if ! podman image exists "$CONTAINER_IMAGE"; then
#attempt to build container
"$CONTAINER_TOOLS_BASEDIR/build-wch-tools-container.sh" 1>&2 ||
_fatal "faild to build local image, cannot continue! … please ensure you have an internet connection"
fi
podman run "${PODMAN_ARGS[@]}" --entrypoint openocd "$CONTAINER_IMAGE" "$@"

View File

@@ -0,0 +1,11 @@
fn main() {
// println!("cargo:rustc-link-arg-bins=--nmagic");
println!("cargo:rustc-link-arg-bins=-Tlink.x");
// println!("cargo:rustc-link-arg-bins=-Tdefmt.x");
let out_dir = std::env::var("OUT_DIR").unwrap();
let out_dir = std::path::PathBuf::from(out_dir);
std::fs::write(out_dir.join("memory.x"), include_bytes!("memory.x")).unwrap();
println!("cargo:rustc-link-search={}", out_dir.display());
println!("cargo:rerun-if-changed=memory.x");
}

View File

@@ -0,0 +1,125 @@
/* CH32V203c8t6 */
MEMORY
{
FLASH : ORIGIN = 0x00000000, LENGTH = 64K /* BANK_1 */
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
REGION_ALIAS("REGION_TEXT", FLASH);
REGION_ALIAS("REGION_RODATA", FLASH);
REGION_ALIAS("REGION_DATA", RAM);
REGION_ALIAS("REGION_BSS", RAM);
REGION_ALIAS("REGION_HEAP", RAM);
REGION_ALIAS("REGION_STACK", RAM);
/* fault handlers */
PROVIDE(InstructionMisaligned = ExceptionHandler);
PROVIDE(InstructionFault = ExceptionHandler);
PROVIDE(IllegalInstruction = ExceptionHandler);
PROVIDE(Breakpoint = ExceptionHandler);
PROVIDE(LoadMisaligned = ExceptionHandler);
PROVIDE(LoadFault = ExceptionHandler);
PROVIDE(StoreMisaligned = ExceptionHandler);
PROVIDE(StoreFault = ExceptionHandler);;
PROVIDE(UserEnvCall = ExceptionHandler);
PROVIDE(SupervisorEnvCall = ExceptionHandler);
PROVIDE(MachineEnvCall = ExceptionHandler);
PROVIDE(InstructionPageFault = ExceptionHandler);
PROVIDE(LoadPageFault = ExceptionHandler);
PROVIDE(StorePageFault = ExceptionHandler);
/* core interrupt handlers */
PROVIDE(NonMaskableInt = DefaultHandler);
PROVIDE(Software = DefaultHandler);
/* external interrupt handlers */
PROVIDE(WWDG = DefaultHandler);
PROVIDE(PVD = DefaultHandler);
PROVIDE(TAMPER = DefaultHandler);
PROVIDE(RTC = DefaultHandler);
PROVIDE(FLASH = DefaultHandler);
PROVIDE(RCC = DefaultHandler);
PROVIDE(EXTI0 = DefaultHandler);
PROVIDE(EXTI1 = DefaultHandler);
PROVIDE(EXTI2 = DefaultHandler);
PROVIDE(EXTI3 = DefaultHandler);
PROVIDE(EXTI4 = DefaultHandler);
PROVIDE(DMA1_CHANNEL1 = DefaultHandler);
PROVIDE(DMA1_CHANNEL2 = DefaultHandler);
PROVIDE(DMA1_CHANNEL3 = DefaultHandler);
PROVIDE(DMA1_CHANNEL4 = DefaultHandler);
PROVIDE(DMA1_CHANNEL5 = DefaultHandler);
PROVIDE(DMA1_CHANNEL6 = DefaultHandler);
PROVIDE(DMA1_CHANNEL7 = DefaultHandler);
PROVIDE(ADC = DefaultHandler);
PROVIDE(USB_HP_CAN1_TX = DefaultHandler);
/*PROVIDE(USB_LP_CAN1_RX0 = DefaultHandler);*/
PROVIDE(CAN1_RX1 = DefaultHandler);
PROVIDE(CAN1_SCE = DefaultHandler);
PROVIDE(EXTI9_5 = DefaultHandler);
PROVIDE(TIM1_BRK = DefaultHandler);
PROVIDE(TIM1_UP_ = DefaultHandler);
PROVIDE(TIM1_TRG_COM = DefaultHandler);
PROVIDE(TIM1_CC = DefaultHandler);
PROVIDE(TIM2 = DefaultHandler);
PROVIDE(TIM3 = DefaultHandler);
PROVIDE(TIM4 = DefaultHandler);
PROVIDE(I2C1_EV = DefaultHandler);
PROVIDE(I2C1_ER = DefaultHandler);
PROVIDE(I2C2_EV = DefaultHandler);
PROVIDE(I2C2_ER = DefaultHandler);
PROVIDE(SPI1 = DefaultHandler);
PROVIDE(SPI2 = DefaultHandler);
PROVIDE(USART1 = DefaultHandler);
PROVIDE(USART2 = DefaultHandler);
PROVIDE(USART3 = DefaultHandler);
PROVIDE(EXTI15_10 = DefaultHandler);
PROVIDE(RTCALARM = DefaultHandler);
PROVIDE(USBWAKE_UP = DefaultHandler);
PROVIDE(TIM8_BRK = DefaultHandler);
PROVIDE(TIM8_UP_ = DefaultHandler);
PROVIDE(TIM8_TRG_COM = DefaultHandler);
PROVIDE(TIM8_CC = DefaultHandler);
PROVIDE(RNG = DefaultHandler);
PROVIDE(FSMC = DefaultHandler);
PROVIDE(SDIO = DefaultHandler);
PROVIDE(TIM5 = DefaultHandler);
PROVIDE(SPI3 = DefaultHandler);
PROVIDE(UART4 = DefaultHandler);
PROVIDE(UART5 = DefaultHandler);
PROVIDE(TIM6 = DefaultHandler);
PROVIDE(TIM7 = DefaultHandler);
PROVIDE(DMA2_CHANNEL1 = DefaultHandler);
PROVIDE(DMA2_CHANNEL2 = DefaultHandler);
PROVIDE(DMA2_CHANNEL3 = DefaultHandler);
PROVIDE(DMA2_CHANNEL4 = DefaultHandler);
PROVIDE(DMA2_CHANNEL5 = DefaultHandler);
PROVIDE(ETH = DefaultHandler);
PROVIDE(ETH_WKUP = DefaultHandler);
PROVIDE(CAN2_TX = DefaultHandler);
PROVIDE(CAN2_RX0 = DefaultHandler);
PROVIDE(CAN2_RX1 = DefaultHandler);
PROVIDE(CAN2_SCE = DefaultHandler);
PROVIDE(OTG_FS = DefaultHandler);
PROVIDE(USBHSWAKEUP = DefaultHandler);
PROVIDE(USBHS = DefaultHandler);
PROVIDE(DVP = DefaultHandler);
PROVIDE(UART6 = DefaultHandler);
PROVIDE(UART7 = DefaultHandler);
PROVIDE(UART8 = DefaultHandler);
PROVIDE(TIM9_BRK = DefaultHandler);
PROVIDE(TIM9_UP_ = DefaultHandler);
PROVIDE(TIM9_TRG_COM = DefaultHandler);
PROVIDE(TIM9_CC = DefaultHandler);
PROVIDE(TIM10_BRK = DefaultHandler);
PROVIDE(TIM10_UP_ = DefaultHandler);
PROVIDE(TIM10_TRG_COM = DefaultHandler);
PROVIDE(TIM10_CC = DefaultHandler);
PROVIDE(DMA2_CHANNEL6 = DefaultHandler);
PROVIDE(DMA2_CHANNEL7 = DefaultHandler);
PROVIDE(DMA2_CHANNEL8 = DefaultHandler);
PROVIDE(DMA2_CHANNEL9 = DefaultHandler);
PROVIDE(DMA2_CHANNEL10 = DefaultHandler);
PROVIDE(DMA2_CHANNEL11 = DefaultHandler);

View File

@@ -0,0 +1,17 @@
set _CHIPNAME ch32v203
set _TARGETNAME $_CHIPNAME.cpu
#bindto 0.0.0.0
adapter driver wlinke
adapter speed 6000
transport select sdi
sdi newtap $_CHIPNAME cpu -irlen 5 --expected-id 0x00001
target create $_TARGETNAME.0 wch_riscv -chain-position $_TARGETNAME
$_TARGETNAME.0 configure -work-area-phys 0x20000000 -work-area-size 10000 -work-area-backup 1
set _FLASHNAME $_CHIPNAME.flash
flash bank $_FLASHNAME wch_rsicv 0x00000000 0 0 0 $_TARGETNAME.0
init

View File

@@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

View File

@@ -0,0 +1,61 @@
#![no_std]
#![no_main]
#![feature(type_alias_impl_trait)]
#![feature(impl_trait_in_assoc_type)]
// Simple 555-like oscillator implemented in firmware.
// - Q output: PB2 (also drives the on-board LED if present)
// - Combined Trigger/Threshold analog input: PA0 (capacitor node)
// Wiring suggestion:
// Q (PB2) --[R]--+-- C -- GND
// |
// PA0 (ADC input)
// The firmware toggles Q high when PA0 <= 1/3 Vref and low when PA0 >= 2/3 Vref.
use embassy_executor::Spawner;
use embassy_time::Timer;
use hal::gpio::{Level, Output};
use {ch32_hal as hal, panic_halt as _};
use hal::adc::{Adc, SampleTime};
#[embassy_executor::main(entry = "qingke_rt::entry")]
async fn main(_spawner: Spawner) -> ! {
let p = hal::init(Default::default());
// Q output on PB2
let mut q = Output::new(p.PB2, Level::Low, Default::default());
// ADC on PA0 for combined Trigger/Threshold input
let mut adc = Adc::new(p.ADC1, Default::default());
let mut trig_thres = p.PA0; // analog-capable pin used as ADC channel
// ADC characteristics: assume 12-bit if HAL doesn't expose it.
// If the HAL provides a method to query resolution, prefer that.
let full_scale: u16 = 4095; // 12-bit default
let thr_low: u16 = (full_scale as u32 / 3) as u16; // ~1/3 Vref
let thr_high: u16 = ((full_scale as u32 * 2) / 3) as u16; // ~2/3 Vref
// Start with Q low. State variable to avoid redundant toggles.
let mut q_high = false;
q.set_low();
loop {
// Read capacitor node voltage via ADC
let sample: u16 = adc.convert(&mut trig_thres, SampleTime::CYCLES239_5);
// Implement Schmitt trigger behavior like NE555 using thresholds
if !q_high && sample <= thr_low {
// Trigger: voltage fell below 1/3 Vref -> set output high
q.set_high();
q_high = true;
} else if q_high && sample >= thr_high {
// Threshold: voltage rose above 2/3 Vref -> set output low
q.set_low();
q_high = false;
}
// Small delay to reduce CPU usage; adjust for responsiveness/noise
Timer::after_micros(200).await;
}
}

View File

@@ -0,0 +1,27 @@
FROM debian:bookworm
RUN apt update -y && apt upgrade -y && apt install git libjaylink-dev libusb-1.0-0 unzip curl libhidapi-hidraw0 xz-utils -y
RUN cd /root && \
curl -L -o mrs-toolchain.tar.xz "https://github.com/ch32-riscv-ug/MounRiver_Studio_Community_miror/releases/download/1.92-toolchain/MRS_Toolchain_Linux_x64_V1.92.tar.xz" && \
mkdir mrs-toolchain && \
tar -xvf mrs-toolchain.tar.xz -C mrs-toolchain --strip-components=1 && \
mv mrs-toolchain/OpenOCD/bin/openocd /usr/local/bin && \
mv mrs-toolchain/OpenOCD/share/openocd /usr/local/share && \
# mv mrs-toolchain/RISC-V_Embedded_GCC12/bin/riscv-none-elf-gdb /usr/local/bin && \ # both toolchains in MRS are to old to work with emacs dape
# mv mrs-toolchain/RISC-V_Embedded_GCC12/libexec /usr/local && \ # both toolchains in MRS are to old to work with emacs dape
rm -rf mrs-toolchain mrs-toolchain.tar.xz && \
# Use up to date xpack toolchains for gdb
curl -L -o xpack-riscv-toolchain.tar.gz "https://github.com/xpack-dev-tools/riscv-none-elf-gcc-xpack/releases/download/v14.2.0-3/xpack-riscv-none-elf-gcc-14.2.0-3-linux-x64.tar.gz" && \
mkdir xpack-toolchain && \
tar -xvf xpack-riscv-toolchain.tar.gz -C xpack-toolchain --strip-components=1 && \
mv xpack-toolchain/bin/* /usr/local/bin && \
mv xpack-toolchain/lib/ /usr/local && \
mv xpack-toolchain/lib64/ /usr/local && \
mv xpack-toolchain/libexec /usr/local && \
mv xpack-toolchain/riscv-none-elf /usr/local && \
rm -rf xpack-toolchain xpack-riscv-toolchain.tar.gz
RUN mkdir -p /root/.config/gdb && echo "set auto-load safe-path /" >> /root/.config/gdb/gdbinit
ENTRYPOINT [ "/usr/bin/bash" ]

View File

@@ -166,40 +166,40 @@
)
(pad "7" thru_hole rect
(at -0.5 -12)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "536f0038-06c5-45e5-b1c8-898a364f6ec4")
)
(pad "8" thru_hole rect
(at 39.5 -16)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "3e0cdbaa-8219-48c4-bb0a-fd4f0e78a390")
)
(pad "9" thru_hole rect
(at 39.5 22.5)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f01565c2-eadd-4451-9dea-2ebe28d872f0")
)
(pad "10" thru_hole rect
(at -0.5 15)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "56ec7c50-069f-4f46-9b83-ac18e4928930")
)
(pad "11" thru_hole rect
(at -43 22.5)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(uuid "f423be21-13b8-46de-8e19-e48325411a29")

View File

@@ -738,7 +738,7 @@
(type default)
)
(layer "F.SilkS")
(uuid "87f61a83-c9a2-45b6-a235-fe6e83f3dbe3")
(uuid "23d58535-2840-4798-8fe6-a41e5dd82cfc")
)
(fp_line
(start -45 24)
@@ -748,7 +748,7 @@
(type default)
)
(layer "F.SilkS")
(uuid "f24175c8-21e2-4899-af70-89e48457e9ba")
(uuid "ed3c0755-343c-4d19-9481-c2c33348ef82")
)
(fp_line
(start -44 24)
@@ -758,7 +758,7 @@
(type default)
)
(layer "F.SilkS")
(uuid "a884c661-0353-48a0-9bc6-995e02652e36")
(uuid "cd2e639b-4e20-40a4-8976-6223f2753aeb")
)
(fp_line
(start 41 -18)
@@ -768,7 +768,7 @@
(type default)
)
(layer "F.SilkS")
(uuid "675d135e-52c1-4dd1-b434-983f40a2b544")
(uuid "f34b006e-828a-45d3-b0f5-daad16782ed8")
)
(fp_line
(start 41 24)
@@ -778,7 +778,7 @@
(type default)
)
(layer "F.SilkS")
(uuid "fbfb2621-73ae-4c15-8403-abdec5cc45cd")
(uuid "03fcb85f-a42f-4eab-bf16-6ff7087e0441")
)
(fp_text user "${REFERENCE}"
(at 0 2.5 0)
@@ -860,8 +860,8 @@
)
(pad "7" thru_hole rect
(at -0.5 -12)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(net 1 "GND")
@@ -871,8 +871,8 @@
)
(pad "8" thru_hole rect
(at 39.5 -16)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(net 1 "GND")
@@ -882,8 +882,8 @@
)
(pad "9" thru_hole rect
(at 39.5 22.5)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(net 1 "GND")
@@ -893,8 +893,8 @@
)
(pad "10" thru_hole rect
(at -0.5 15)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(net 1 "GND")
@@ -904,8 +904,8 @@
)
(pad "11" thru_hole rect
(at -43 22.5)
(size 0.85 0.85)
(drill 0.5)
(size 1.7 1.7)
(drill 1)
(layers "*.Cu" "*.Mask")
(remove_unused_layers no)
(net 1 "GND")

View File

@@ -460,6 +460,7 @@
"single_global_label": "ignore",
"unannotated": "error",
"unconnected_wire_endpoint": "warning",
"undefined_netclass": "error",
"unit_value_mismatch": "error",
"unresolved_variable": "error",
"wire_dangling": "error"

View File

@@ -0,0 +1,4 @@
(fp_lib_table
(version 7)
(lib (name "Sensor")(type "KiCad")(uri "${KIPRJMOD}/Sensor.pretty")(options "")(descr ""))
)

Binary file not shown.

View File

@@ -1,154 +1,177 @@
P CODE 00
P UNITS CUST 0
P arrayDim N
317CAN- VIA MD0157PA00X+070354Y-006220X0315Y0000R000S3
317CAN+ VIA MD0157PA00X+064606Y-009213X0315Y0000R000S3
317FLOW VIA MD0157PA00X+048543Y-021063X0315Y0000R000S3
317TEMP VIA MD0157PA00X+048898Y-023268X0315Y0000R000S3
317EN VIA MD0157PA00X+062697Y-009154X0315Y0000R000S3
317EN VIA MD0157PA00X+062500Y-010827X0315Y0000R000S3
317VBAT VIA MD0157PA00X+043130Y-036024X0315Y0000R000S3
317VBAT VIA MD0157PA00X+043130Y-035039X0315Y0000R000S3
317GND VIA MD1181PA00X+077500Y-007000X1575Y0000R000S3
317GND VIA MD0157PA00X+062402Y-010433X0315Y0000R000S3
317GND VIA MD0157PA00X+054134Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+062205Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+058169Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+051772Y-009646X0315Y0000R000S3
317GND VIA MD1181PA00X+041500Y-042900X1575Y0000R000S3
317GND VIA MD0157PA00X+059685Y-012402X0315Y0000R000S3
317GND VIA MD0157PA00X+054429Y-011909X0315Y0000R000S3
317GND VIA MD0157PA00X+053346Y-010925X0315Y0000R000S3
317GND VIA MD0157PA00X+062205Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+040945Y-035630X0315Y0000R000S3
317GND VIA MD1181PA00X+077500Y-042900X1575Y0000R000S3
317GND VIA MD0157PA00X+059055Y-012402X0315Y0000R000S3
317GND VIA MD0157PA00X+056594Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+061122Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+054626Y-010827X0315Y0000R000S3
317GND VIA MD0157PA00X+056693Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+045669Y-007087X0315Y0000R000S3
317GND VIA MD0157PA00X+054724Y-012205X0315Y0000R000S3
317GND VIA MD0157PA00X+063386Y-011220X0315Y0000R000S3
317GND VIA MD0157PA00X+059685Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+045699Y-017156X0315Y0000R000S3
317GND VIA MD0157PA00X+048091Y-016535X0315Y0000R000S3
317GND VIA MD0157PA00X+053740Y-011220X0315Y0000R000S3
317GND VIA MD0157PA00X+048917Y-031102X0315Y0000R000S3
317GND VIA MD0157PA00X+056299Y-005591X0315Y0000R000S3
317GND VIA MD0157PA00X+055709Y-011220X0315Y0000R000S3
317GND VIA MD0157PA00X+058169Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+061654Y-008071X0315Y0000R000S3
317GND VIA MD0157PA00X+054035Y-010138X0315Y0000R000S3
317GND VIA MD0157PA00X+054331Y-010531X0315Y0000R000S3
317GND VIA MD0157PA00X+055217Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+056102Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+055315Y-011516X0315Y0000R000S3
317GND VIA MD0157PA00X+055709Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+054921Y-011220X0315Y0000R000S3
317GND VIA MD0157PA00X+059843Y-005591X0315Y0000R000S3
317GND VIA MD0157PA00X+050787Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+049016Y-034154X0315Y0000R000S3
317GND VIA MD0157PA00X+051772Y-008858X0315Y0000R000S3
317GND VIA MD0157PA00X+046555Y-030512X0315Y0000R000S3
317GND VIA MD0157PA00X+061122Y-012303X0315Y0000R000S3
317GND VIA MD0157PA00X+055709Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+059055Y-011614X0315Y0000R000S3
317GND VIA MD1181PA00X+041575Y-007087X1575Y0000R000S3
317GND VIA MD0157PA00X+040945Y-034646X0315Y0000R000S3
317GND VIA MD0157PA00X+063386Y-012598X0315Y0000R000S3
317GND VIA MD0157PA00X+048031Y-014567X0315Y0000R000S3
317GND VIA MD0157PA00X+057185Y-011516X0315Y0000R000S3
3173_3V VIA MD0157PA00X+064646Y-014567X0315Y0000R000S3
3173_3V VIA MD0157PA00X+075906Y-014331X0315Y0000R000S3
3173_3V VIA MD0157PA00X+074094Y-014331X0315Y0000R000S3
3173_3V VIA MD0157PA00X+067874Y-014567X0315Y0000R000S3
317TEMP VIA MD0157PA00X+072343Y-012303X0315Y0000R000S3
317TEMP VIA MD0157PA00X+049099Y-026344X0315Y0000R000S3
317TANK_SENSOR VIA MD0157PA00X+063091Y-007776X0315Y0000R000S3
317TANK_SENSOR VIA MD0157PA00X+047244Y-017421X0315Y0000R000S3
317CHARGE VIA MD0157PA00X+071949Y-006693X0315Y0000R000S3
317ESP_RX VIA MD0157PA00X+071949Y-007776X0315Y0000R000S3
317ESP_TX VIA MD0157PA00X+071949Y-007283X0315Y0000R000S3
317SDA VIA MD0157PA00X+048031Y-014094X0315Y0000R000S3
317CHARGE VIA MD0157PA00X+071575Y-006732X0315Y0000R000S3
317SDA VIA MD0157PA00X+072441Y-010630X0315Y0000R000S3
317SCL VIA MD0157PA00X+048819Y-013780X0315Y0000R000S3
317SDA VIA MD0157PA00X+050134Y-014937X0315Y0000R000S3
317SCL VIA MD0157PA00X+072441Y-011417X0315Y0000R000S3
317SCL VIA MD0157PA00X+051535Y-013898X0315Y0000R000S3
317NET-(CD1-A) VIA MD0157PA00X+058661Y-009843X0315Y0000R000S3
317NET-(CD1-A) VIA MD0157PA00X+045866Y-005906X0315Y0000R000S3
317CD_PROBE VIA MD0157PA00X+072638Y-009744X0315Y0000R000S3
317ISDAY VIA MD0157PA00X+066929Y-011024X0315Y0000R000S3
317LED_ENABLE VIA MD0157PA00X+064764Y-010728X0315Y0000R000S3
317WORKING VIA MD0157PA00X+058661Y-011417X0315Y0000R000S3
317WORKING VIA MD0157PA00X+071949Y-008169X0315Y0000R000S3
317WORKING VIA MD0157PA00X+069843Y-008858X0315Y0000R000S3
317ENABLE_TANK VIA MD0157PA00X+062894Y-011516X0315Y0000R000S3
317ENABLE_TANK VIA MD0157PA00X+050525Y-015584X0315Y0000R000S3
317USB_D- VIA MD0157PA00X+045276Y-010305X0315Y0000R000S3
317USB_D- VIA MD0157PA00X+063377Y-011752X0315Y0000R000S3
317USB_D- VIA MD0157PA00X+052902Y-009500X0315Y0000R000S3
317FLOW VIA MD0157PA00X+048543Y-023228X0315Y0000R000S3
317FLOW VIA MD0157PA00X+063484Y-007382X0315Y0000R000S3
317USB_D+ VIA MD0157PA00X+045276Y-010719X0315Y0000R000S3
317USB_D+ VIA MD0157PA00X+063377Y-012165X0315Y0000R000S3
317USB_D+ VIA MD0157PA00X+052610Y-009792X0315Y0000R000S3
317BOOT_SEL VIA MD0157PA00X+064764Y-009744X0315Y0000R000S3
317BOOT_SEL VIA MD0157PA00X+070374Y-012205X0315Y0000R000S3
317WARN_LED VIA MD0157PA00X+069803Y-008465X0315Y0000R000S3
317WARN_LED VIA MD0157PA00X+044390Y-005807X0315Y0000R000S3
317WARN_LED VIA MD0157PA00X+063386Y-008268X0315Y0000R000S3
327GND U2 -1 A01X+064094Y-005709X0591Y0354R000S2
3273_3V U2 -2 A01X+064094Y-006209X0591Y0354R000S2
327EN U2 -3 A01X+064094Y-006709X0591Y0354R000S2
327FLOW U2 -4 A01X+064094Y-007209X0591Y0354R000S2
327TANK_SENSOR U2 -5 A01X+064094Y-007709X0591Y0354R000S2
327WARN_LED U2 -6 A01X+064094Y-008209X0591Y0354R000S2
327ISDAY U2 -7 A01X+064094Y-008709X0591Y0354R000S2
327-(U2-IO0-PAD8) U2 -8 A01X+064094Y-009209X0591Y0354R000S2
327BOOT_SEL U2 -9 A01X+064094Y-009709X0591Y0354R000S2
327(U2-IO8-PAD10) U2 -10 A01X+064094Y-010209X0591Y0354R000S2
327LED_ENABLE U2 -11 A01X+064094Y-010709X0591Y0354R000S2
327ENABLE_TANK U2 -12 A01X+064094Y-011209X0591Y0354R000S2
327USB_D- U2 -13 A01X+064094Y-011709X0591Y0354R000S2
327USB_D+ U2 -14 A01X+064094Y-012209X0591Y0354R000S2
327BOOT_SEL U2 -15 A01X+070984Y-012209X0591Y0354R000S2
327TEMP U2 -16 A01X+070984Y-011709X0591Y0354R000S2
327SCL U2 -17 A01X+070984Y-011209X0591Y0354R000S2
327SDA U2 -18 A01X+070984Y-010709X0591Y0354R000S2
327EXTRA_2 U2 -19 A01X+070984Y-010209X0591Y0354R000S2
327CD_PROBE U2 -20 A01X+070984Y-009709X0591Y0354R000S2
327EXTRA_1 U2 -21 A01X+070984Y-009209X0591Y0354R000S2
327-(U2-NC-PAD22) U2 -22 A01X+070984Y-008709X0591Y0354R000S2
327WORKING U2 -23 A01X+070984Y-008209X0591Y0354R000S2
327ESP_RX U2 -24 A01X+070984Y-007709X0591Y0354R000S2
327ESP_TX U2 -25 A01X+070984Y-007209X0591Y0354R000S2
327CHARGE U2 -26 A01X+070984Y-006709X0591Y0354R000S2
327PUMP_ENABLE U2 -27 A01X+070984Y-006209X0591Y0354R000S2
327GND U2 -28 A01X+070984Y-005709X0591Y0354R000S2
327GND U2 -29_5 A01X+066947Y-007961X0315Y0315R000S2
327GND U2 -29_1 A01X+066455Y-007469X0315Y0315R000S2
327GND U2 -29_2 A01X+066947Y-007469X0315Y0315R000S2
327GND U2 -29_3 A01X+067439Y-007469X0315Y0315R000S2
327GND U2 -29_4 A01X+066455Y-007961X0315Y0315R000S2
327GND U2 -29_6 A01X+067439Y-007961X0315Y0315R000S2
327GND U2 -29_7 A01X+066455Y-008453X0315Y0315R000S2
327GND U2 -29_8 A01X+066947Y-008453X0315Y0315R000S2
327GND U2 -29_9 A01X+067439Y-008453X0315Y0315R000S2
317GND U2 -30_1 D0098PA00X+066701Y-007469X0138Y0000R000S3
317GND U2 -30_2 D0098PA00X+067193Y-007469X0138Y0000R000S3
317GND U2 -30_3 D0098PA00X+066455Y-007715X0138Y0000R000S3
317GND U2 -30_4 D0098PA00X+066947Y-007715X0138Y0000R000S3
317GND U2 -30_5 D0098PA00X+067439Y-007715X0138Y0000R000S3
317GND U2 -30_6 D0098PA00X+066701Y-007961X0138Y0000R000S3
317GND U2 -30_7 D0098PA00X+067193Y-007961X0138Y0000R000S3
317GND U2 -30_8 D0098PA00X+066455Y-008207X0138Y0000R000S3
317GND U2 -30_9 D0098PA00X+066947Y-008207X0138Y0000R000S3
317GND U2 -30_1 D0098PA00X+067439Y-008207X0138Y0000R000S3
317GND U2 -30_1 D0098PA00X+066701Y-008453X0138Y0000R000S3
317GND U2 -30_1 D0098PA00X+067193Y-008453X0138Y0000R000S3
317CONFIG2 VIA MD0157PA00X+060394Y-009724X0315Y0000R000S3
317GND VIA MD1181PA00X+077500Y-007000X1575Y0000R000S3
317GND VIA MD0157PA00X+062402Y-010433X0315Y0000R000S3
317GND VIA MD0157PA00X+054449Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+044606Y-010551X0315Y0000R000S3
317GND VIA MD0157PA00X+054921Y-011417X0315Y0000R000S3
317GND VIA MD0157PA00X+050787Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+042126Y-008976X0315Y0000R000S3
317GND VIA MD0157PA00X+062205Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+059291Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+058150Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+054843Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+043228Y-012480X0315Y0000R000S3
317GND VIA MD0157PA00X+050827Y-007402X0315Y0000R000S3
317GND VIA MD0157PA00X+047244Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+060374Y-011594X0315Y0000R000S3
317GND VIA MD1181PA00X+041500Y-042900X1575Y0000R000S3
317GND VIA MD0157PA00X+047165Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+042717Y-013031X0315Y0000R000S3
317GND VIA MD0157PA00X+057008Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+062205Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+058917Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+040945Y-035630X0315Y0000R000S3
317GND VIA MD0157PA00X+059606Y-011575X0315Y0000R000S3
317GND VIA MD1181PA00X+077500Y-042900X1575Y0000R000S3
317GND VIA MD0157PA00X+046890Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+054016Y-010591X0315Y0000R000S3
317GND VIA MD0157PA00X+046457Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+045039Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+045748Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+051850Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+051260Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+054646Y-011142X0315Y0000R000S3
317GND VIA MD0157PA00X+043819Y-011929X0315Y0000R000S3
317GND VIA MD0157PA00X+046102Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+059646Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+050079Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+056594Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+045118Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+061122Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+043346Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+056693Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+044055Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+052559Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+050197Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+048228Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+045669Y-007087X0315Y0000R000S3
317GND VIA MD0157PA00X+063386Y-011220X0315Y0000R000S3
317GND VIA MD0157PA00X+060433Y-005591X0315Y0000R000S3
317GND VIA MD0157PA00X+060472Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+045699Y-017156X0315Y0000R000S3
317GND VIA MD0157PA00X+048661Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+049724Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+048091Y-016535X0315Y0000R000S3
317GND VIA MD0157PA00X+048917Y-031102X0315Y0000R000S3
317GND VIA MD0157PA00X+053858Y-011693X0315Y0000R000S3
317GND VIA MD0157PA00X+049843Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+059232Y-011575X0315Y0000R000S3
317GND VIA MD0157PA00X+055709Y-011220X0315Y0000R000S3
317GND VIA MD0157PA00X+058169Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+044094Y-011654X0315Y0000R000S3
317GND VIA MD0157PA00X+061654Y-008071X0315Y0000R000S3
317GND VIA MD0157PA00X+050551Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+049370Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+042402Y-009213X0315Y0000R000S3
317GND VIA MD0157PA00X+059980Y-011594X0315Y0000R000S3
317GND VIA MD0157PA00X+056181Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+044409Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+055276Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+056181Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+045394Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+060748Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+044685Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+055276Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+051142Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+055748Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+045827Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+052835Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+053189Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+047953Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+052205Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+053780Y-010315X0315Y0000R000S3
317GND VIA MD0157PA00X+058465Y-005591X0315Y0000R000S3
317GND VIA MD0157PA00X+046181Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+051496Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+053583Y-011378X0315Y0000R000S3
317GND VIA MD0157PA00X+049016Y-034154X0315Y0000R000S3
317GND VIA MD0157PA00X+045472Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+058543Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+054331Y-010866X0315Y0000R000S3
317GND VIA MD0157PA00X+046811Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+046555Y-030512X0315Y0000R000S3
317GND VIA MD0157PA00X+052913Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+043701Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+048307Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+061181Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+049488Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+047520Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+060059Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+042283Y-013386X0315Y0000R000S3
317GND VIA MD0157PA00X+044764Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+055709Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+060827Y-012323X0315Y0000R000S3
317GND VIA MD0157PA00X+054134Y-011969X0315Y0000R000S3
317GND VIA MD0157PA00X+046535Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+043504Y-012205X0315Y0000R000S3
317GND VIA MD1181PA00X+041575Y-007087X1575Y0000R000S3
317GND VIA MD0157PA00X+050906Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+042992Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+040945Y-034646X0315Y0000R000S3
317GND VIA MD0157PA00X+063386Y-012598X0315Y0000R000S3
317GND VIA MD0157PA00X+047598Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+047874Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+042756Y-009291X0315Y0000R000S3
317GND VIA MD0157PA00X+050433Y-009646X0315Y0000R000S3
317GND VIA MD0157PA00X+048583Y-011260X0315Y0000R000S3
317GND VIA MD0157PA00X+057185Y-011614X0315Y0000R000S3
317GND VIA MD0157PA00X+042953Y-012756X0315Y0000R000S3
3273_3V R19 -2 A01X+048898Y-022037X0315Y0374R270S2
327TEMP R19 -1 A01X+048898Y-022687X0315Y0374R270S2
3173_3V U5 -1 D0394PA00X+046339Y-019764X0669Y0669R000S0
317(U5-VBAT-PAD2) U5 -2 D0394PA00X+046339Y-020764X0669Y0000R000S0
317VBAT U5 -2 D0394PA00X+046339Y-020764X0669Y0000R000S0
317SDA U5 -3 D0394PA00X+046339Y-021764X0669Y0000R000S0
317SCL U5 -4 D0394PA00X+046339Y-022764X0669Y0000R000S0
317CD_PROBE U5 -5 D0394PA00X+046339Y-023764X0669Y0000R000S0
317GND U5 -6 D0394PA00X+046339Y-024764X0669Y0000R000S0
317GND U5 -7 D0197PA00X+062835Y-021063X0335Y0335R000S0
317GND U5 -8 D0197PA00X+078583Y-019488X0335Y0335R000S0
317CAN+ U5 -6 D0394PA00X+046339Y-024764X0669Y0000R000S0
317CAN- U5 -7 D0394PA00X+046339Y-025764X0669Y0000R000S0
317GND U5 -8 D0394PA00X+046339Y-026764X0669Y0000R000S0
317GND U5 -9 D0197PA00X+078583Y-034646X0335Y0335R000S0
317GND U5 -10 D0197PA00X+062835Y-031693X0335Y0335R000S0
317GND U5 -11 D0197PA00X+046102Y-034646X0335Y0335R000S0
317GND U5 -12 D0197PA00X+062835Y-021063X0335Y0335R000S0
317GND U5 -13 D0197PA00X+078583Y-019488X0335Y0335R000S0
327GND D11 -1 A01X+046506Y-030000X0581Y0236R180S2
327VBAT D11 -2 A01X+046506Y-029252X0581Y0236R180S2
327NET-(D10-K) D11 -3 A01X+045768Y-029626X0581Y0236R180S2
@@ -163,8 +186,8 @@ P arrayDim N
317GND U3 -9 D0394PA00X+050827Y-017869X0669Y0669R000S0
317GND U3 -10 D0394PA00X+059685Y-010782X0669Y0669R000S0
317GND U3 -11 D0394PA00X+059685Y-017869X0669Y0669R000S0
327GND Reset1-1 A01X+059843Y-006024X0591Y0591R090S2
327NET-(C4-PAD2) Reset1-2 A01X+059843Y-009094X0591Y0591R090S2
327GND Reset1-1 A01X+060394Y-006024X0591Y0591R090S2
327CONFIG2 Reset1-2 A01X+060394Y-009094X0591Y0591R090S2
317GND U6 -1 D0394PA00X+078465Y-036024X0669Y0669R090S0
317GND U6 -2 D0394PA00X+071378Y-044016X0669Y0669R000S0
317GND U6 -3 D0394PA00X+047756Y-044016X0669Y0669R000S0
@@ -183,10 +206,10 @@ P arrayDim N
327SDA R11 -2 A01X+072904Y-010630X0315Y0374R180S2
327GND C2 -1 A01X+062062Y-008268X0423Y0374R000S2
327EN C2 -2 A01X+062741Y-008268X0423Y0374R000S2
327GND Boot1 -1 A01X+056299Y-006024X0591Y0591R090S2
327T-(BOOT1-PAD2) Boot1 -2 A01X+056299Y-009094X0591Y0591R090S2
327GND Boot1 -1 A01X+058425Y-006024X0591Y0591R090S2
327T-(BOOT1-PAD2) Boot1 -2 A01X+058425Y-009094X0591Y0591R090S2
327EN R7 -1 A01X+061614Y-011152X0315Y0374R270S2
327NET-(C4-PAD2) R7 -2 A01X+061614Y-010502X0315Y0374R270S2
327CONFIG2 R7 -2 A01X+061614Y-010502X0315Y0374R270S2
317GND J5 -1 D0295PA00X+042323Y-021457X0472Y0689R270S0
3173_3V J5 -2 D0295PA00X+042323Y-020669X0472Y0689R270S0
317FLOW J5 -3 D0295PA00X+042323Y-019882X0472Y0689R270S0
@@ -197,9 +220,9 @@ P arrayDim N
327GND R16 -2 A01X+048091Y-016043X0315Y0374R180S2
327NET-(Q2-G) R22 -1 A01X+047805Y-034843X0315Y0374R000S2
327GND R22 -2 A01X+048455Y-034843X0315Y0374R000S2
327FLOW D5 -3 A01X+047288Y-023228X0581Y0236R180S2
3273_3V D5 -2 A01X+048027Y-022854X0581Y0236R180S2
327GND D5 -1 A01X+048027Y-023602X0581Y0236R180S2
327FLOW D5 -3 A01X+047288Y-020295X0581Y0236R180S2
3273_3V D5 -2 A01X+048027Y-019921X0581Y0236R180S2
327GND D5 -1 A01X+048027Y-020669X0581Y0236R180S2
327NET-(D2-K) D2 -1 A01X+043701Y-006570X0384Y0551R270S2
327WARN_LED D2 -2 A01X+043701Y-005832X0384Y0551R270S2
327GND D6 -1 A01X+046043Y-016811X0581Y0236R180S2
@@ -207,25 +230,25 @@ P arrayDim N
327TANK_SENSOR D6 -3 A01X+045305Y-016437X0581Y0236R180S2
327NET-(CD1-A) R9 -1 A01X+058661Y-010305X0315Y0374R090S2
327WORKING R9 -2 A01X+058661Y-010955X0315Y0374R090S2
317GND J6 -1 D0394PA00X+041319Y-014661X0669Y0669R180S0
3173_3V J6 -2 D0394PA00X+041319Y-013661X0669Y0000R180S0
317SDA J6 -3 D0394PA00X+041319Y-012661X0669Y0000R180S0
317SCL J6 -4 D0394PA00X+041319Y-011661X0669Y0000R180S0
317SQW J6 -5 D0394PA00X+041319Y-010661X0669Y0000R180S0
31732K J6 -6 D0394PA00X+041319Y-009661X0669Y0000R180S0
317GND J6 -1 D0394PA00X+046906Y-006220X0669Y0669R270S0
3173_3V J6 -2 D0394PA00X+047906Y-006220X0669Y0000R270S0
317SDA J6 -3 D0394PA00X+048906Y-006220X0669Y0000R270S0
317SCL J6 -4 D0394PA00X+049906Y-006220X0669Y0000R270S0
317SQW J6 -5 D0394PA00X+050906Y-006220X0669Y0000R270S0
31732K J6 -6 D0394PA00X+051906Y-006220X0669Y0000R270S0
327NET-(D8-K) D8 -1 A01X+040945Y-033243X0384Y0551R270S2
327NET-(D8-A) D8 -2 A01X+040945Y-032505X0384Y0551R270S2
327NET-(J2-PIN_2) R2 -1 A01X+046457Y-005581X0315Y0374R090S2
327GND R2 -2 A01X+046457Y-006230X0315Y0374R090S2
327USB_D+ U1 -1 A01X+050251Y-009286X0522Y0236R180S2
327GND U1 -2 A01X+050251Y-008912X0522Y0236R180S2
327USB_D- U1 -3 A01X+050251Y-008538X0522Y0236R180S2
327SLASH}O2-PAD4) U1 -4 A01X+049355Y-008538X0522Y0236R180S2
327USB_BUS U1 -5 A01X+049355Y-008912X0522Y0236R180S2
327SLASH}O1-PAD6) U1 -6 A01X+049355Y-009286X0522Y0236R180S2
327TEMP D7 -3 A01X+047288Y-025295X0581Y0236R180S2
3273_3V D7 -2 A01X+048027Y-024921X0581Y0236R180S2
327GND D7 -1 A01X+048027Y-025669X0581Y0236R180S2
327NET-(J2-PIN_2) R2 -1 A01X+042943Y-014764X0315Y0374R000S2
327GND R2 -2 A01X+043593Y-014764X0315Y0374R000S2
327USB_D+ U1 -1 A01X+044124Y-010945X0522Y0236R180S2
327GND U1 -2 A01X+044124Y-010571X0522Y0236R180S2
327USB_D- U1 -3 A01X+044124Y-010197X0522Y0236R180S2
327SLASH}O2-PAD4) U1 -4 A01X+043228Y-010197X0522Y0236R180S2
327USB_BUS U1 -5 A01X+043228Y-010571X0522Y0236R180S2
327SLASH}O1-PAD6) U1 -6 A01X+043228Y-010945X0522Y0236R180S2
327TEMP D7 -3 A01X+047288Y-022362X0581Y0236R180S2
3273_3V D7 -2 A01X+048027Y-021988X0581Y0236R180S2
327GND D7 -1 A01X+048027Y-022736X0581Y0236R180S2
327VBAT C8 -1 A01X+042534Y-036024X0463Y0571R180S2
327GND C8 -2 A01X+041718Y-036024X0463Y0571R180S2
327NET-(R3-PAD1) R3 -1 A01X+057677Y-012933X0315Y0374R090S2
@@ -234,13 +257,18 @@ P arrayDim N
327ENABLE_TANK R14 -2 A01X+050089Y-016043X0315Y0374R000S2
327GND C1 -1 A01X+056299Y-011112X0423Y0374R270S2
327T-(BOOT1-PAD2) C1 -2 A01X+056299Y-010433X0423Y0374R270S2
327GND D3 -1 A01X+046752Y-012106X0581Y0236R000S2
3273_3V D3 -2 A01X+046752Y-012854X0581Y0236R000S2
327SCL D3 -3 A01X+047490Y-012480X0581Y0236R000S2
327TEMP R19 -1 A01X+049099Y-025295X0315Y0374R000S2
3273_3V R19 -2 A01X+049749Y-025295X0315Y0374R000S2
327GND D3 -1 A01X+051102Y-007992X0581Y0236R000S2
3273_3V D3 -2 A01X+051102Y-008740X0581Y0236R000S2
327SCL D3 -3 A01X+051841Y-008366X0581Y0236R000S2
3173_3V C9 -1 D0315PA00X+046165Y-012441X0630Y0630R000S0
3173_3V C9 -1 D0315PA00X+046429Y-013071X0630Y0630R000S0
317GND C9 -2 D0315PA00X+047413Y-013071X0630Y0000R000S0
317GND C9 -2 D0315PA00X+047677Y-013701X0630Y0000R000S0
3273_3V C6 -1 A01X+062205Y-007392X0463Y0571R270S2
327GND C6 -2 A01X+062205Y-006575X0463Y0571R270S2
317WARN_LED J8 -1 D0394PA00X+055787Y-006386X0669Y0669R000S0
317GND J8 -2 D0394PA00X+055787Y-007386X0669Y0000R000S0
317CONFIG2 J8 -3 D0394PA00X+055787Y-008386X0669Y0000R000S0
327NET-(Q3-G) R21 -1 A01X+047805Y-031102X0315Y0374R000S2
327GND R21 -2 A01X+048455Y-031102X0315Y0374R000S2
317LED_ENABLE U4 -1 D0394PA00X+074063Y-009173X0669Y0000R090S0
@@ -261,31 +289,80 @@ P arrayDim N
327BOOT_SEL C5 -2 A01X+062992Y-009700X0423Y0374R270S2
327TANK_SENSOR R12 -1 A01X+047215Y-016831X0404Y0551R270S2
3273_3V R12 -2 A01X+047215Y-016112X0404Y0551R270S2
327NET-(D2-K) R1 -1 A01X+045935Y-007874X0315Y0374R000S2
327GND R1 -2 A01X+046585Y-007874X0315Y0374R000S2
327NET-(D2-K) R1 -1 A01X+044488Y-008317X0315Y0374R270S2
327GND R1 -2 A01X+044488Y-007667X0315Y0374R270S2
327EXTRA_1 R20 -1 A01X+048455Y-031890X0315Y0374R180S2
327NET-(Q3-G) R20 -2 A01X+047805Y-031890X0315Y0374R180S2
327GND U2 -1 A01X+064094Y-005709X0591Y0354R000S2
3273_3V U2 -2 A01X+064094Y-006209X0591Y0354R000S2
327EN U2 -3 A01X+064094Y-006709X0591Y0354R000S2
327FLOW U2 -4 A01X+064094Y-007209X0591Y0354R000S2
327TANK_SENSOR U2 -5 A01X+064094Y-007709X0591Y0354R000S2
327EXTRA_1 U2 -6 A01X+064094Y-008209X0591Y0354R000S2
327ISDAY U2 -7 A01X+064094Y-008709X0591Y0354R000S2
327CAN+ U2 -8 A01X+064094Y-009209X0591Y0354R000S2
327BOOT_SEL U2 -9 A01X+064094Y-009709X0591Y0354R000S2
327(U2-IO8-PAD10) U2 -10 A01X+064094Y-010209X0591Y0354R000S2
327LED_ENABLE U2 -11 A01X+064094Y-010709X0591Y0354R000S2
327ENABLE_TANK U2 -12 A01X+064094Y-011209X0591Y0354R000S2
327USB_D- U2 -13 A01X+064094Y-011709X0591Y0354R000S2
327USB_D+ U2 -14 A01X+064094Y-012209X0591Y0354R000S2
327BOOT_SEL U2 -15 A01X+070984Y-012209X0591Y0354R000S2
327TEMP U2 -16 A01X+070984Y-011709X0591Y0354R000S2
327SCL U2 -17 A01X+070984Y-011209X0591Y0354R000S2
327SDA U2 -18 A01X+070984Y-010709X0591Y0354R000S2
327WORKING U2 -19 A01X+070984Y-010209X0591Y0354R000S2
327CD_PROBE U2 -20 A01X+070984Y-009709X0591Y0354R000S2
327WARN_LED U2 -21 A01X+070984Y-009209X0591Y0354R000S2
327-(U2-NC-PAD22) U2 -22 A01X+070984Y-008709X0591Y0354R000S2
327EXTRA_2 U2 -23 A01X+070984Y-008209X0591Y0354R000S2
327ESP_RX U2 -24 A01X+070984Y-007709X0591Y0354R000S2
327ESP_TX U2 -25 A01X+070984Y-007209X0591Y0354R000S2
327CHARGE U2 -26 A01X+070984Y-006709X0591Y0354R000S2
327CAN- U2 -27 A01X+070984Y-006209X0591Y0354R000S2
327GND U2 -28 A01X+070984Y-005709X0591Y0354R000S2
327GND U2 -29_1 A01X+066455Y-007469X0315Y0315R000S2
327GND U2 -29_2 A01X+066947Y-007469X0315Y0315R000S2
327GND U2 -29_3 A01X+067439Y-007469X0315Y0315R000S2
327GND U2 -29_4 A01X+066455Y-007961X0315Y0315R000S2
327GND U2 -29_5 A01X+066947Y-007961X0315Y0315R000S2
327GND U2 -29_6 A01X+067439Y-007961X0315Y0315R000S2
327GND U2 -29_7 A01X+066455Y-008453X0315Y0315R000S2
327GND U2 -29_8 A01X+066947Y-008453X0315Y0315R000S2
327GND U2 -29_9 A01X+067439Y-008453X0315Y0315R000S2
317GND U2 -30_1 D0098PA00X+066701Y-007469X0138Y0000R000S3
317GND U2 -30_2 D0098PA00X+067193Y-007469X0138Y0000R000S3
317GND U2 -30_3 D0098PA00X+066455Y-007715X0138Y0000R000S3
317GND U2 -30_4 D0098PA00X+066947Y-007715X0138Y0000R000S3
317GND U2 -30_5 D0098PA00X+067439Y-007715X0138Y0000R000S3
317GND U2 -30_6 D0098PA00X+066701Y-007961X0138Y0000R000S3
317GND U2 -30_7 D0098PA00X+067193Y-007961X0138Y0000R000S3
317GND U2 -30_8 D0098PA00X+066455Y-008207X0138Y0000R000S3
317GND U2 -30_9 D0098PA00X+066947Y-008207X0138Y0000R000S3
317GND U2 -30_1 D0098PA00X+067439Y-008207X0138Y0000R000S3
317GND U2 -30_1 D0098PA00X+066701Y-008453X0138Y0000R000S3
317GND U2 -30_1 D0098PA00X+067193Y-008453X0138Y0000R000S3
317NET-(D10-K) Extra_-1 D0394PA00X+042283Y-029616X0669Y0787R270S0
317VBAT Extra_-2 D0394PA00X+042283Y-028632X0669Y0787R270S0
327GND D1 -1 A01X+047441Y-014469X0581Y0236R180S2
3273_3V D1 -2 A01X+047441Y-013720X0581Y0236R180S2
327SDA D1 -3 A01X+046703Y-014094X0581Y0236R180S2
327GND D1 -1 A01X+046033Y-007933X0581Y0236R000S2
3273_3V D1 -2 A01X+046033Y-008681X0581Y0236R000S2
327SDA D1 -3 A01X+046772Y-008307X0581Y0236R000S2
3273_3V R10 -1 A01X+073553Y-011417X0315Y0374R180S2
327SCL R10 -2 A01X+072904Y-011417X0315Y0374R180S2
327EXTRA_2 R23 -1 A01X+048455Y-035728X0315Y0374R180S2
327NET-(Q2-G) R23 -2 A01X+047805Y-035728X0315Y0374R180S2
327VBAT R18 -1 A01X+046033Y-028445X0315Y0374R000S2
327NET-(D10-A) R18 -2 A01X+046683Y-028445X0315Y0374R000S2
327NET-(J2-PIN_5) R4 -1 A01X+053150Y-005581X0315Y0374R090S2
327GND R4 -2 A01X+053150Y-006230X0315Y0374R090S2
327NET-(J2-PIN_3) R4 -1 A01X+042943Y-014134X0315Y0374R000S2
327GND R4 -2 A01X+043593Y-014134X0315Y0374R000S2
327NET-(D10-K) D10 -1 A01X+040945Y-029542X0384Y0551R270S2
327NET-(D10-A) D10 -2 A01X+040945Y-028804X0384Y0551R270S2
317NET-(J4-PIN_1) J4 -1 D0295PA00X+042323Y-017638X0472Y0689R270S0
317TANK_SENSOR J4 -2 D0295PA00X+042323Y-016850X0472Y0689R270S0
327NET-(CD2-K) CD2 -1 A01X+040945Y-017594X0384Y0551R270S2
3273_3V CD2 -2 A01X+040945Y-016855X0384Y0551R270S2
317ESP_TX J3 -1 D0394PA00X+077559Y-005709X0669Y0669R270S0
317ESP_RX J3 -2 D0394PA00X+078559Y-005709X0669Y0000R270S0
317ESP_TX J3 -1 D0394PA00X+073051Y-006457X0669Y0669R270S0
317ESP_RX J3 -2 D0394PA00X+074051Y-006457X0669Y0000R270S0
327BOOT_SEL R5 -1 A01X+057677Y-010305X0315Y0374R090S2
327T-(BOOT1-PAD2) R5 -2 A01X+057677Y-010955X0315Y0374R090S2
327NET-(J4-PIN_1) R13 -1 A01X+047047Y-018346X0315Y0374R090S2
@@ -296,15 +373,15 @@ P arrayDim N
327GND CD1 -1 A01X+045177Y-006570X0384Y0551R270S2
327NET-(CD1-A) CD1 -2 A01X+045177Y-005832X0384Y0551R270S2
327GND C4 -1 A01X+060630Y-011166X0423Y0374R270S2
327NET-(C4-PAD2) C4 -2 A01X+060630Y-010487X0423Y0374R270S2
317USB_BUS J2 -1 D0295PA00X+047835Y-006142X0472Y0689R000S0
317NET-(J2-PIN_2) J2 -2 D0295PA00X+048622Y-006142X0472Y0689R000S0
317USB_D+ J2 -3 D0295PA00X+049409Y-006142X0472Y0689R000S0
317USB_D- J2 -4 D0295PA00X+050197Y-006142X0472Y0689R000S0
317NET-(J2-PIN_5) J2 -5 D0295PA00X+050984Y-006142X0472Y0689R000S0
317GND J2 -6 D0295PA00X+051772Y-006142X0472Y0689R000S0
3273_3V R15 -1 A01X+049680Y-023228X0315Y0374R180S2
327FLOW R15 -2 A01X+049031Y-023228X0315Y0374R180S2
327CONFIG2 C4 -2 A01X+060630Y-010487X0423Y0374R270S2
317USB_BUS J2 -1 D0295PA00X+041260Y-009803X0472Y0689R090S0
317NET-(J2-PIN_2) J2 -2 D0295PA00X+041260Y-010591X0472Y0689R090S0
317NET-(J2-PIN_3) J2 -3 D0295PA00X+041260Y-011378X0472Y0689R090S0
317USB_D- J2 -4 D0295PA00X+041260Y-012165X0472Y0689R090S0
317USB_D+ J2 -5 D0295PA00X+041260Y-012953X0472Y0689R090S0
317GND J2 -6 D0295PA00X+041260Y-013740X0472Y0689R090S0
3273_3V R15 -1 A01X+048858Y-019990X0315Y0374R090S2
327FLOW R15 -2 A01X+048858Y-020640X0315Y0374R090S2
327NET-(Q1-G) Q1 -1 A01X+048770Y-016831X0354Y0315R090S2
327GND Q1 -2 A01X+048022Y-016831X0354Y0315R090S2
327NET-(J4-PIN_1) Q1 -3 A01X+048396Y-017618X0354Y0315R090S2
@@ -313,12 +390,12 @@ P arrayDim N
327NET-(Q2-G) Q2 -1 A01X+047776Y-034142X0354Y0315R270S2
327GND Q2 -2 A01X+048524Y-034142X0354Y0315R270S2
327NET-(D8-K) Q2 -3 A01X+048150Y-033354X0354Y0315R270S2
317VBAT U7 -1 D0433PA00X+042795Y-040315X0750Y0787R180S0
317GND U7 -2 D0433PA00X+041795Y-040315X0750Y0787R180S0
3173_3V U7 -3 D0433PA00X+040795Y-040315X0750Y0787R180S0
317GND J1 -1 D0394PA00X+044882Y-012047X0669Y0669R000S0
317SCL J1 -2 D0394PA00X+044882Y-013047X0669Y0000R000S0
317SDA J1 -3 D0394PA00X+044882Y-014047X0669Y0000R000S0
317VBAT U7 -1 D0433PA00X+042795Y-037835X0750Y0787R180S0
317GND U7 -2 D0433PA00X+041795Y-037835X0750Y0787R180S0
3173_3V U7 -3 D0433PA00X+040795Y-037835X0750Y0787R180S0
317GND J1 -1 D0394PA00X+047913Y-008346X0669Y0669R270S0
317SDA J1 -2 D0394PA00X+048913Y-008346X0669Y0000R270S0
317SCL J1 -3 D0394PA00X+049913Y-008346X0669Y0000R270S0
327BOOT_SEL R6 -1 A01X+057677Y-011683X0315Y0374R090S2
327NET-(R3-PAD1) R6 -2 A01X+057677Y-012333X0315Y0374R090S2
999

BIN
case/PlantCtrl Case v9.f3z Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,26 +1,31 @@
[build]
#target = "xtensa-esp32-espidf"
target = "riscv32imac-esp-espidf"
rustflags = [
# Required to obtain backtraces (e.g. when using the "esp-backtrace" crate.)
# NOTE: May negatively impact performance of produced code
"-C", "force-frame-pointers",
"-Z", "stack-protector=all",
"-C", "link-arg=-Tlinkall.x",
]
[target.riscv32imac-esp-espidf]
linker = "ldproxy"
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 --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl
#runner = "espflash flash --monitor"
runner = "cargo runner"
#runner = "cargo runner"
#runner = "espflash flash --monitor --partition-table partitions.csv -b no-reset" # create upgrade image file for webupload
# runner = espflash erase-parts otadata //ensure flash is clean
rustflags = [ "--cfg", "espidf_time64"] # Extending time_t for ESP IDF 5: https://github.com/esp-rs/rust/issues/110
[unstable]
build-std = ["std", "panic_abort"]
[env]
MCU="esp32c6"
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
ESP_IDF_VERSION = "v5.2.1"
CHRONO_TZ_TIMEZONE_FILTER = "UTC|America/New_York|America/Chicago|America/Los_Angeles|Europe/London|Europe/Berlin|Europe/Paris|Asia/Tokyo|Asia/Shanghai|Asia/Kolkata|Australia/Sydney|America/Sao_Paulo|Africa/Johannesburg|Asia/Dubai|Pacific/Auckland"
CARGO_WORKSPACE_DIR = { value = "", relative = true }
RUST_BACKTRACE = "full"
ESP_LOG = "info"
[unstable]
build-std = ["alloc", "core"]

View File

@@ -1,7 +1,18 @@
<component name="ProjectDictionaryState">
<dictionary name="project">
<words>
<w>boardtest</w>
<w>buildtime</w>
<w>deepsleep</w>
<w>githash</w>
<w>lamptest</w>
<w>lightstate</w>
<w>mppt</w>
<w>plantstate</w>
<w>pumptest</w>
<w>sntp</w>
<w>vergen</w>
<w>wifiscan</w>
</words>
</dictionary>
</component>

View File

@@ -1,6 +1,12 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<Languages>
<language minSize="102" name="Rust" />
</Languages>
</inspection_tool>
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="NewCrateVersionAvailable" enabled="true" level="INFORMATION" enabled_by_default="true" />
</profile>
</component>

2931
rust/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,24 +1,18 @@
[package]
name = "plant-ctrl2"
version = "0.1.0"
authors = ["Empire Phoenix"]
edition = "2021"
resolver = "2"
#rust-version = "1.71"
name = "plant-ctrl2"
rust-version = "1.86"
version = "0.1.0"
[profile.dev]
# Explicitly disable LTO which the Xtensa codegen backend has issues
lto = false
strip = false
debug = true
overflow-checks = true
panic = "abort"
incremental = true
opt-level = "s"
[profile.dev.build-override]
opt-level = 1
incremental = true
# Explicitly configure the binary target and disable building it as a test/bench.
[[bin]]
name = "plant-ctrl2"
path = "src/main.rs"
# Prevent IDEs/Cargo from trying to compile a test harness for this no_std binary.
test = false
bench = false
doc = false
[package.metadata.cargo_runner]
# The string `$TARGET_FILE` will be replaced with the path from cargo.
@@ -33,70 +27,100 @@ command = [
"partitions.csv"
]
#this strips the bootloader, we need that tho
#strip = true
[profile.dev]
lto = "fat"
debug = false
overflow-checks = true
panic = "abort"
incremental = true
opt-level = "z"
[profile.release]
lto = "fat"
#debug = false
overflow-checks = true
panic = "abort"
incremental = false
opt-level = "z"
[package.metadata.espflash]
partition_table = "partitions.csv"
[features]
default = ["std", "embassy", "esp-idf-svc/native"]
pio = ["esp-idf-svc/pio"]
std = ["alloc", "esp-idf-svc/binstart", "esp-idf-svc/std"]
alloc = ["esp-idf-svc/alloc"]
nightly = ["esp-idf-svc/nightly"]
experimental = ["esp-idf-svc/experimental"]
embassy = ["esp-idf-svc/embassy-sync", "esp-idf-svc/critical-section", "esp-idf-svc/embassy-time-driver"]
[dependencies]
#ESP stuff
embedded-svc = { version = "0.28.1", features = ["experimental"] }
esp-idf-hal = "0.45.2"
esp-idf-sys = { version = "0.36.1", features = ["binstart", "native"] }
esp-idf-svc = { version = "0.51.0", default-features = false }
log = "0.4.28"
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6", "log-04"] }
esp-hal = { version = "1.1.0", features = ["esp32c6", "log-04"] }
esp-rtos = { version = "0.3.0", features = ["esp32c6", "embassy", "esp-radio"] }
esp-backtrace = { version = "0.19.0", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
esp-println = { version = "0.17.0", features = ["esp32c6", "log-04", "auto"] }
esp-storage = { version = "0.9.0", features = ["esp32c6"] }
esp-radio = { version = "0.18.0", features = ["esp32c6", "log-04", "wifi", "unstable"] }
esp-alloc = { version = "0.10.0", features = ["esp32c6", "internal-heap-stats"] }
# Async runtime (Embassy core)
embassy-executor = { version = "0.10.0", features = ["log", "nightly"] }
embassy-time = { version = "0.5.1", features = ["log"], default-features = false }
embassy-sync = { version = "0.8.0", features = ["log"] }
# Networking and protocol stacks
embassy-net = { version = "0.8.0", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp", "proto-ipv4", "dns", "proto-ipv6"] }
sntpc = { version = "0.6.1", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
edge-dhcp = "0.7.0"
edge-nal = "0.6.0"
edge-nal-embassy = "0.8.1"
edge-http = { version = "0.7.0", features = ["log"] }
esp32c6 = { version = "0.23.2" }
# Hardware abstraction traits and HAL adapters
embedded-hal = "1.0.0"
heapless = { version = "0.8", features = ["serde"] }
embedded-hal-bus = { version = "0.2.0", features = ["std"] }
embedded-storage = "0.3.1"
embassy-embedded-hal = "0.6.0"
nb = "1.1.0"
#Hardware additional driver
ds18b20 = "0.1.1"
bq34z100 = { version = "0.3.0", features = ["flashstream"] }
one-wire-bus = "0.1.1"
#bq34z100 = { version = "0.3.0", default-features = false }
lib-bms-protocol = { git = "https://gitea.wlandt.de/judge/ch32-bms.git", default-features = false }
onewire = "0.4.0"
#strum = { version = "0.27.0", default-feature = false, features = ["derive"] }
ds323x = "0.6.0"
#pure code dependencies
once_cell = "1.19.0"
anyhow = { version = "1.0.75", features = ["std", "backtrace"] }
strum = { version = "0.27.0", features = ["derive"] }
measurements = "0.11.0"
#json
serde = { version = "1.0.192", features = ["derive"] }
serde_json = "1.0.108"
serde = { version = "1.0.219", features = ["derive", "alloc"], default-features = false }
serde_json = { version = "1.0.143", default-features = false, features = ["alloc"] }
#timezone
chrono = { version = "0.4.23", default-features = false , features = ["iana-time-zone" , "alloc", "serde"] }
chrono-tz = {version="0.8.0", default-features = false , features = [ "filter-by-regex" ]}
chrono = { version = "0.4.42", default-features = false, features = ["iana-time-zone", "alloc", "serde"] }
chrono-tz = { version = "0.10.4", default-features = false, features = ["filter-by-regex"] }
eeprom24x = "0.7.2"
url = "2.5.3"
crc = "3.2.1"
bincode = "1.3.3"
ringbuffer = "0.15.0"
text-template = "0.1.0"
strum_macros = "0.27.0"
esp-ota = { version = "0.2.2", features = ["log"] }
unit-enum = "1.4.1"
pca9535 = { version = "2.0.0", features = ["std"] }
ina219 = { version = "0.2.0", features = ["std"] }
pca9535 = { version = "2.0.0" }
ina219 = { version = "0.2.0" }
portable-atomic = "1.11.1"
async-trait = "0.1.89"
bq34z100 = { version = "0.4.0", default-features = false }
static_cell = "2.1.1"
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
littlefs2-core = "0.1.1"
bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
deranged = "0.5.3"
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
option-lock = { version = "0.3.1", default-features = false }
measurements = "0.11.1"
heapless = { version = "0.7.17", features = ["serde"] }
mcutie = { path = "./src/mcutie_3_0_0/", default-features = false, features = ["log"] }
[patch.crates-io]
#esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal.git" }
#esp-idf-hal = { git = "https://github.com/empirephoenix/esp-idf-hal.git" }
#esp-idf-sys = { git = "https://github.com/empirephoenix/esp-idf-sys.git" }
#esp-idf-sys = { git = "https://github.com/esp-rs/esp-idf-sys.git" }
#esp-idf-svc = { git = "https://github.com/esp-rs/esp-idf-svc.git" }
#bq34z100 = { path = "../../bq34z100_rust" }
[build-dependencies]
cc = "=1.1.30"
embuild = { version= "0.32.0", features = ["espidf"]}
vergen = { version = "8.2.6", features = ["build", "git", "gitcl"] }

22
rust/all.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/build_website.sh"
cargo build --release
espflash save-image \
--bootloader "${SCRIPT_DIR}/bootloader.bin" \
--partition-table "${SCRIPT_DIR}/partitions.csv" \
--chip esp32c6 \
target/riscv32imac-unknown-none-elf/release/plant-ctrl2 \
"${SCRIPT_DIR}/image.bin"
espflash flash --monitor \
--bootloader "${SCRIPT_DIR}/bootloader.bin" \
--chip esp32c6 \
--baud 921600 \
--partition-table "${SCRIPT_DIR}/partitions.csv" \
target/riscv32imac-unknown-none-elf/release/plant-ctrl2

BIN
rust/bootloader.bin Normal file

Binary file not shown.

View File

@@ -1,42 +1,56 @@
use std::process::Command;
use vergen::EmitBuilder;
fn main() {
println!("cargo:rerun-if-changed=./src/src_webpack");
Command::new("rm")
.arg("./src/webserver/bundle.js")
.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());
}
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());
fn linker_be_nice() {
let args: Vec<String> = std::env::args().collect();
if args.len() > 1 {
let kind = &args[1];
let what = &args[2];
match kind.as_str() {
"undefined-symbol" => match what.as_str() {
"_defmt_timestamp" => {
eprintln!();
eprintln!("💡 `defmt` not found - make sure `defmt.x` is added as a linker script and you have included `use defmt_rtt as _;`");
eprintln!();
}
"_stack_start" => {
eprintln!();
eprintln!("💡 Is the linker script `linkall.x` missing?");
eprintln!();
}
"esp_wifi_preempt_enable"
| "esp_wifi_preempt_yield_task"
| "esp_wifi_preempt_task_create" => {
eprintln!();
eprintln!("💡 `esp-wifi` has no scheduler enabled. Make sure you have the `builtin-scheduler` feature enabled, or that you provide an external scheduler.");
eprintln!();
}
"embedded_test_linker_file_not_added_to_rustflags" => {
eprintln!();
eprintln!("💡 `embedded-test` not found - make sure `embedded-test.x` is added as a linker script for tests");
eprintln!();
}
_ => (),
},
// we don't have anything helpful for "missing-lib" yet
_ => {
std::process::exit(1);
}
}
std::process::exit(0);
}
embuild::espidf::sysenv::output();
println!(
"cargo:rustc-link-arg=--error-handling-script={}",
std::env::current_exe().unwrap().display()
);
}
fn main() {
linker_be_nice();
// Non-existent path causes Cargo to always re-run this script,
// keeping VERGEN_BUILD_TIMESTAMP fresh on every build.
println!("cargo:rerun-if-changed=ALWAYS_REBUILD_SENTINEL");
let _ = EmitBuilder::builder().all_git().all_build().emit();
}

21
rust/build_website.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WEBPACK_DIR="${SCRIPT_DIR}/src_webpack"
WEBSERVER_DIR="${SCRIPT_DIR}/src/webserver"
rm -f "${WEBSERVER_DIR}/index.html.gz"
rm -f "${WEBSERVER_DIR}/bundle.js.gz"
rm -f "${WEBPACK_DIR}/index.html.gz"
rm -f "${WEBPACK_DIR}/bundle.js.gz"
rm -f "${WEBPACK_DIR}/index.html"
rm -f "${WEBPACK_DIR}/bundle.js"
pushd "${WEBPACK_DIR}"
npm install
npx webpack build
cp index.html.gz "${WEBSERVER_DIR}/index.html.gz"
cp bundle.js.gz "${WEBSERVER_DIR}/bundle.js.gz"
popd

7
rust/erase_ota.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cargo espflash erase-parts otadata --partition-table "${SCRIPT_DIR}/partitions.csv"

View File

@@ -1,6 +1,8 @@
partition_table="partitions.csv"
[connection]
serial = "/dev/ttyACM0"
baudrate = 921600
[[usb_device]]
vid = "303a"
pid = "1001"
[flash]
size = "16MB"
size = "16MB"

15
rust/flash.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
"${SCRIPT_DIR}/build_website.sh"
cargo build --release
espflash flash --monitor \
--bootloader "${SCRIPT_DIR}/bootloader.bin" \
--chip esp32c6 \
--baud 921600 \
--partition-table "${SCRIPT_DIR}/partitions.csv" \
target/riscv32imac-unknown-none-elf/release/plant-ctrl2

17
rust/image_build.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
rm -f "${SCRIPT_DIR}/image.bin"
"${SCRIPT_DIR}/build_website.sh"
cargo build --release
espflash save-image \
--bootloader "${SCRIPT_DIR}/bootloader.bin" \
--partition-table "${SCRIPT_DIR}/partitions.csv" \
--chip esp32c6 \
target/riscv32imac-unknown-none-elf/release/plant-ctrl2 \
"${SCRIPT_DIR}/image.bin"

View File

@@ -2,5 +2,5 @@ nvs, data, nvs, , 16k,
otadata, data, ota, , 8k,
phy_init, data, phy, , 4k,
ota_0, app, ota_0, , 3968k,
ota_1, app, ota_0, , 3968k,
storage, data, spiffs, , 8M,
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_0 ota_1 3968k
6 storage data spiffs littlefs 8M

View File

@@ -1,3 +1,2 @@
[toolchain]
channel = "nightly"
toolchain = "esp"

View File

@@ -1,7 +1,8 @@
use crate::hal::PLANT_COUNT;
use crate::plant_state::PlantWateringMode;
use alloc::string::String;
use core::str::FromStr;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
#[serde(default)]
@@ -9,8 +10,10 @@ pub struct NetworkConfig {
pub ap_ssid: heapless::String<32>,
pub ssid: Option<heapless::String<32>>,
pub password: Option<heapless::String<64>>,
pub mqtt_url: Option<heapless::String<128>>,
pub mqtt_url: Option<String>,
pub base_topic: Option<heapless::String<64>>,
pub mqtt_user: Option<String>,
pub mqtt_password: Option<String>,
pub max_wait: u32,
}
impl Default for NetworkConfig {
@@ -21,6 +24,8 @@ impl Default for NetworkConfig {
password: None,
mqtt_url: None,
base_topic: None,
mqtt_user: None,
mqtt_password: None,
max_wait: 10000,
}
}
@@ -58,6 +63,7 @@ pub struct TankConfig {
pub tank_warn_percent: u8,
pub tank_empty_percent: u8,
pub tank_full_percent: u8,
pub ml_per_pulse: f32,
}
impl Default for TankConfig {
fn default() -> Self {
@@ -68,6 +74,7 @@ impl Default for TankConfig {
tank_warn_percent: 40,
tank_empty_percent: 5,
tank_full_percent: 95,
ml_per_pulse: 0.0,
}
}
}
@@ -109,7 +116,9 @@ pub struct PlantControllerConfig {
pub struct PlantConfig {
pub mode: PlantWateringMode,
pub target_moisture: f32,
pub min_moisture: f32,
pub pump_time_s: u16,
pub pump_limit_ml: u16,
pub pump_cooldown_min: u16,
pub pump_hour_start: u8,
pub pump_hour_end: u8,
@@ -118,6 +127,9 @@ pub struct PlantConfig {
pub max_consecutive_pump_count: u8,
pub moisture_sensor_min_frequency: Option<f32>, // Optional min frequency
pub moisture_sensor_max_frequency: Option<f32>, // Optional max frequency
pub min_pump_current_ma: u16,
pub max_pump_current_ma: u16,
pub ignore_current_error: bool,
}
impl Default for PlantConfig {
@@ -125,7 +137,9 @@ impl Default for PlantConfig {
Self {
mode: PlantWateringMode::OFF,
target_moisture: 40.,
min_moisture: 30.,
pump_time_s: 30,
pump_limit_ml: 5000,
pump_cooldown_min: 60,
pump_hour_start: 9,
pump_hour_end: 20,
@@ -134,6 +148,9 @@ impl Default for PlantConfig {
max_consecutive_pump_count: 10,
moisture_sensor_min_frequency: None, // No override by default
moisture_sensor_max_frequency: None, // No override by default
min_pump_current_ma: 10,
max_pump_current_ma: 3000,
ignore_current_error: true,
}
}
}

321
rust/src/fat_error.rs Normal file
View File

@@ -0,0 +1,321 @@
use alloc::format;
use alloc::string::{String, ToString};
use core::convert::Infallible;
use core::fmt;
use core::str::Utf8Error;
use embassy_embedded_hal::shared_bus::I2cDeviceError;
use embassy_executor::SpawnError;
use embassy_sync::mutex::TryLockError;
use esp_hal::i2c::master::ConfigError;
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
use esp_radio::wifi::WifiError;
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
use littlefs2_core::PathError;
use onewire::Error;
use pca9535::ExpanderError;
//All error superconstruct
#[derive(Debug)]
pub enum FatError {
OneWireError {
error: Error<Infallible>,
},
String {
error: String,
},
LittleFSError {
error: littlefs2_core::Error,
},
PathError {
error: PathError,
},
TryLockError {
error: TryLockError,
},
WifiError {
error: WifiError,
},
SerdeError {
error: serde_json::Error,
},
PreconditionFailed {
error: String,
},
NoBatteryMonitor,
SpawnError {
error: SpawnError,
},
OTAError,
PartitionError {
error: esp_bootloader_esp_idf::partitions::Error,
},
I2CConfigError {
error: ConfigError,
},
DS323 {
error: String,
},
Eeprom24x {
error: String,
},
ExpanderError {
error: String,
},
SNTPError {
error: sntpc::Error,
},
}
pub type FatResult<T> = Result<T, FatError>;
impl fmt::Display for FatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FatError::SpawnError { error } => {
write!(f, "SpawnError {:?}", error.to_string())
}
FatError::OneWireError { error } => write!(f, "OneWireError {:?}", error),
FatError::String { error } => write!(f, "{}", error),
FatError::LittleFSError { error } => write!(f, "LittleFSError {:?}", error),
FatError::PathError { error } => write!(f, "PathError {:?}", error),
FatError::TryLockError { error } => write!(f, "TryLockError {:?}", error),
FatError::WifiError { error } => write!(f, "WifiError {:?}", error),
FatError::SerdeError { error } => write!(f, "SerdeError {:?}", error),
FatError::PreconditionFailed { error } => write!(f, "PreconditionFailed {:?}", error),
FatError::PartitionError { error } => {
write!(f, "PartitionError {:?}", error)
}
FatError::NoBatteryMonitor => {
write!(f, "No Battery Monitor")
}
FatError::I2CConfigError { error } => write!(f, "I2CConfigError {:?}", error),
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error),
FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
FatError::OTAError => {
write!(f, "OTA missing partition")
}
}
}
}
#[macro_export]
macro_rules! bail {
($msg:literal $(,)?) => {
return $crate::fat_error::fat_bail($msg)
};
($fmt:literal, $($arg:tt)*) => {
return $crate::fat_error::fat_bail(&alloc::format!($fmt, $($arg)*))
};
}
pub fn fat_bail<X>(message: &str) -> Result<X, FatError> {
Err(FatError::String {
error: message.to_string(),
})
}
pub trait ContextExt<T> {
fn context<C>(self, context: C) -> Result<T, FatError>
where
C: AsRef<str>;
}
impl<T> ContextExt<T> for Option<T> {
fn context<C>(self, context: C) -> Result<T, FatError>
where
C: AsRef<str>,
{
match self {
Some(value) => Ok(value),
None => Err(FatError::PreconditionFailed {
error: context.as_ref().to_string(),
}),
}
}
}
impl<T, E> ContextExt<T> for Result<T, E>
where
E: fmt::Debug,
{
fn context<C>(self, context: C) -> Result<T, FatError>
where
C: AsRef<str>,
{
match self {
Ok(value) => Ok(value),
Err(err) => Err(FatError::String {
error: format!("{}: {:?}", context.as_ref(), err),
}),
}
}
}
impl From<Error<Infallible>> for FatError {
fn from(error: Error<Infallible>) -> Self {
FatError::OneWireError { error }
}
}
impl From<littlefs2_core::Error> for FatError {
fn from(value: littlefs2_core::Error) -> Self {
FatError::LittleFSError { error: value }
}
}
impl From<PathError> for FatError {
fn from(value: PathError) -> Self {
FatError::PathError { error: value }
}
}
impl From<TryLockError> for FatError {
fn from(value: TryLockError) -> Self {
FatError::TryLockError { error: value }
}
}
impl From<WifiError> for FatError {
fn from(value: WifiError) -> Self {
FatError::WifiError { error: value }
}
}
impl From<serde_json::error::Error> for FatError {
fn from(value: serde_json::Error) -> Self {
FatError::SerdeError { error: value }
}
}
impl From<SpawnError> for FatError {
fn from(value: SpawnError) -> Self {
FatError::SpawnError { error: value }
}
}
impl From<sntpc::Error> for FatError {
fn from(value: sntpc::Error) -> Self {
FatError::SNTPError { error: value }
}
}
impl From<esp_bootloader_esp_idf::partitions::Error> for FatError {
fn from(value: esp_bootloader_esp_idf::partitions::Error) -> Self {
FatError::PartitionError { error: value }
}
}
impl From<Utf8Error> for FatError {
fn from(value: Utf8Error) -> Self {
FatError::String {
error: value.to_string(),
}
}
}
impl<E: core::fmt::Debug> From<edge_http::io::Error<E>> for FatError {
fn from(value: edge_http::io::Error<E>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<ds323x::Error<E>> for FatError {
fn from(value: ds323x::Error<E>) -> Self {
FatError::DS323 {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<eeprom24x::Error<E>> for FatError {
fn from(value: eeprom24x::Error<E>) -> Self {
FatError::Eeprom24x {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<ExpanderError<I2cDeviceError<E>>> for FatError {
fn from(value: ExpanderError<I2cDeviceError<E>>) -> Self {
FatError::ExpanderError {
error: format!("{:?}", value),
}
}
}
impl From<bincode::error::DecodeError> for FatError {
fn from(value: bincode::error::DecodeError) -> Self {
FatError::Eeprom24x {
error: format!("{:?}", value),
}
}
}
impl From<bincode::error::EncodeError> for FatError {
fn from(value: bincode::error::EncodeError) -> Self {
FatError::Eeprom24x {
error: format!("{:?}", value),
}
}
}
impl From<ConfigError> for FatError {
fn from(value: ConfigError) -> Self {
FatError::I2CConfigError { error: value }
}
}
impl<E: core::fmt::Debug> From<I2cDeviceError<E>> for FatError {
fn from(value: I2cDeviceError<E>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<BusVoltageReadError<I2cDeviceError<E>>> for FatError {
fn from(value: BusVoltageReadError<I2cDeviceError<E>>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl<E: core::fmt::Debug> From<ShuntVoltageReadError<I2cDeviceError<E>>> for FatError {
fn from(value: ShuntVoltageReadError<I2cDeviceError<E>>) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl From<Infallible> for FatError {
fn from(value: Infallible) -> Self {
panic!("Infallible error: {:?}", value)
}
}
impl From<InvalidLowLimit> for FatError {
fn from(value: InvalidLowLimit) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl From<InvalidHighLimit> for FatError {
fn from(value: InvalidHighLimit) -> Self {
FatError::String {
error: format!("{:?}", value),
}
}
}
impl From<chrono::format::ParseError> for FatError {
fn from(value: chrono::format::ParseError) -> Self {
FatError::String {
error: format!("Parsing error: {value:?}"),
}
}
}

View File

@@ -1,49 +1,48 @@
use crate::to_string;
use anyhow::anyhow;
use bq34z100::{Bq34Z100Error, Bq34z100g1, Bq34z100g1Driver};
use embedded_hal_bus::i2c::MutexDevice;
use esp_idf_hal::delay::Delay;
use esp_idf_hal::i2c::{I2cDriver, I2cError};
use crate::fat_error::{FatError, FatResult};
use crate::hal::Box;
use alloc::string::String;
use async_trait::async_trait;
use bq34z100::{Bq34z100g1, Bq34z100g1Driver, Flags};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use esp_hal::delay::Delay;
use esp_hal::i2c::master::I2c;
use esp_hal::Blocking;
use measurements::Temperature;
use serde::Serialize;
#[async_trait]
pub trait BatteryInteraction {
fn state_charge_percent(&mut self) -> Result<f32, BatteryError>;
fn remaining_milli_ampere_hour(&mut self) -> Result<u16, BatteryError>;
fn max_milli_ampere_hour(&mut self) -> Result<u16, BatteryError>;
fn design_milli_ampere_hour(&mut self) -> Result<u16, BatteryError>;
fn voltage_milli_volt(&mut self) -> Result<u16, BatteryError>;
fn average_current_milli_ampere(&mut self) -> Result<i16, BatteryError>;
fn cycle_count(&mut self) -> Result<u16, BatteryError>;
fn state_health_percent(&mut self) -> Result<u16, BatteryError>;
fn bat_temperature(&mut self) -> Result<u16, BatteryError>;
fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError>;
async fn state_charge_percent(&mut self) -> FatResult<f32>;
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16>;
async fn voltage_milli_volt(&mut self) -> FatResult<u16>;
async fn average_current_milli_ampere(&mut self) -> FatResult<i16>;
async fn cycle_count(&mut self) -> FatResult<u16>;
async fn state_health_percent(&mut self) -> FatResult<u16>;
async fn bat_temperature(&mut self) -> FatResult<u16>;
async fn get_battery_state(&mut self) -> FatResult<BatteryState>;
}
#[derive(Debug, Serialize)]
pub struct BatteryInfo {
pub voltage_milli_volt: u16,
pub average_current_milli_ampere: i16,
pub cycle_count: u16,
pub design_milli_ampere_hour: u16,
pub remaining_milli_ampere_hour: u16,
pub state_of_charge: f32,
pub state_of_health: u16,
pub temperature: u16,
pub voltage_mv: Option<u16>,
pub avg_current_ma: Option<i16>,
pub soc_pct: Option<f32>,
pub soh_pct: Option<u16>,
pub temperature_c: Option<u16>,
pub cycle_count: Option<u16>,
pub remaining_mah: Option<u16>,
pub design_mah: Option<u16>,
pub error: Option<BatteryError>,
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind")]
pub enum BatteryError {
NoBatteryMonitor,
CommunicationError(String),
}
impl From<Bq34Z100Error<esp_idf_hal::i2c::I2cError>> for BatteryError {
fn from(err: Bq34Z100Error<esp_idf_hal::i2c::I2cError>) -> Self {
BatteryError::CommunicationError(
anyhow!("failed to communicate with battery monitor: {:?}", err).to_string(),
)
}
CommunicationError { message: String },
}
#[derive(Debug, Serialize)]
@@ -54,161 +53,216 @@ pub enum BatteryState {
/// If no battery monitor is installed this implementation will be used
pub struct NoBatteryMonitor {}
#[async_trait]
impl BatteryInteraction for NoBatteryMonitor {
fn state_charge_percent(&mut self) -> Result<f32, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn state_charge_percent(&mut self) -> FatResult<f32> {
// No monitor configured: assume full battery for lightstate logic
Ok(100.0)
}
fn remaining_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn max_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn design_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn voltage_milli_volt(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn average_current_milli_ampere(&mut self) -> Result<i16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
Err(FatError::NoBatteryMonitor)
}
fn cycle_count(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn cycle_count(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn state_health_percent(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn state_health_percent(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn bat_temperature(&mut self) -> Result<u16, BatteryError> {
Err(BatteryError::NoBatteryMonitor)
async fn bat_temperature(&mut self) -> FatResult<u16> {
Err(FatError::NoBatteryMonitor)
}
fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError> {
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Unknown)
}
}
//TODO implement this battery monitor kind once controller is complete
#[allow(dead_code)]
pub struct WchI2cSlave {}
pub struct BQ34Z100G1<'a> {
pub battery_driver: Bq34z100g1Driver<MutexDevice<'a, I2cDriver<'a>>, Delay>,
pub type I2cDev = I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>;
pub struct BQ34Z100G1 {
pub battery_driver: Bq34z100g1Driver<I2cDev, Delay>,
}
impl BatteryInteraction for BQ34Z100G1<'_> {
fn state_charge_percent(&mut self) -> Result<f32, BatteryError> {
Ok(self.battery_driver.state_of_charge().map(f32::from)?)
#[async_trait]
impl BatteryInteraction for BQ34Z100G1 {
async fn state_charge_percent(&mut self) -> FatResult<f32> {
self.battery_driver
.state_of_charge()
.map(|v| v as f32)
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn remaining_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.remaining_capacity()?)
async fn remaining_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.remaining_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn max_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.full_charge_capacity()?)
async fn max_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.full_charge_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn design_milli_ampere_hour(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.design_capacity()?)
async fn design_milli_ampere_hour(&mut self) -> FatResult<u16> {
self.battery_driver
.design_capacity()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn voltage_milli_volt(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.voltage()?)
async fn voltage_milli_volt(&mut self) -> FatResult<u16> {
self.battery_driver.voltage().map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn average_current_milli_ampere(&mut self) -> Result<i16, BatteryError> {
Ok(self.battery_driver.average_current()?)
async fn average_current_milli_ampere(&mut self) -> FatResult<i16> {
self.battery_driver
.average_current()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn cycle_count(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.cycle_count()?)
async fn cycle_count(&mut self) -> FatResult<u16> {
self.battery_driver
.cycle_count()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn state_health_percent(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.state_of_health()?)
async fn state_health_percent(&mut self) -> FatResult<u16> {
self.battery_driver
.state_of_health()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn bat_temperature(&mut self) -> Result<u16, BatteryError> {
Ok(self.battery_driver.temperature()?)
async fn bat_temperature(&mut self) -> FatResult<u16> {
self.battery_driver
.temperature()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
}
fn get_battery_state(&mut self) -> Result<BatteryState, BatteryError> {
async fn get_battery_state(&mut self) -> FatResult<BatteryState> {
Ok(BatteryState::Info(BatteryInfo {
voltage_milli_volt: self.voltage_milli_volt()?,
average_current_milli_ampere: self.average_current_milli_ampere()?,
cycle_count: self.cycle_count()?,
design_milli_ampere_hour: self.design_milli_ampere_hour()?,
remaining_milli_ampere_hour: self.remaining_milli_ampere_hour()?,
state_of_charge: self.state_charge_percent()?,
state_of_health: self.state_health_percent()?,
temperature: self.bat_temperature()?,
voltage_mv: Some(self.voltage_milli_volt().await?),
avg_current_ma: Some(self.average_current_milli_ampere().await?),
soc_pct: Some(self.state_charge_percent().await?),
soh_pct: Some(self.state_health_percent().await?),
temperature_c: Some(self.bat_temperature().await?),
cycle_count: Some(self.cycle_count().await?),
remaining_mah: Some(self.remaining_milli_ampere_hour().await?),
design_mah: Some(self.design_milli_ampere_hour().await?),
error: None,
}))
}
}
pub fn print_battery_bq34z100(
battery_driver: &mut Bq34z100g1Driver<MutexDevice<I2cDriver<'_>>, Delay>,
) -> anyhow::Result<(), Bq34Z100Error<I2cError>> {
println!("Try communicating with battery");
battery_driver: &mut Bq34z100g1Driver<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>, Delay>,
) -> FatResult<()> {
log::info!("Try communicating with battery");
let fwversion = battery_driver.fw_version().unwrap_or_else(|e| {
println!("Firmware {:?}", e);
log::info!("Firmware {:?}", e);
0
});
println!("fw version is {}", fwversion);
log::info!("fw version is {}", fwversion);
let design_capacity = battery_driver.design_capacity().unwrap_or_else(|e| {
println!("Design capacity {:?}", e);
log::info!("Design capacity {:?}", e);
0
});
println!("Design Capacity {}", design_capacity);
log::info!("Design Capacity {}", design_capacity);
if design_capacity == 1000 {
println!("Still stock configuring battery, readouts are likely to be wrong!");
log::info!("Still stock configuring battery, readouts are likely to be wrong!");
}
let flags = battery_driver.get_flags_decoded()?;
println!("Flags {:?}", flags);
let flags = battery_driver.get_flags_decoded().unwrap_or(Flags {
fast_charge_allowed: false,
full_chage: false,
charging_not_allowed: false,
charge_inhibit: false,
bat_low: false,
bat_high: false,
over_temp_discharge: false,
over_temp_charge: false,
discharge: false,
state_of_charge_f: false,
state_of_charge_1: false,
cf: false,
ocv_taken: false,
});
log::info!("Flags {:?}", flags);
let chem_id = battery_driver.chem_id().unwrap_or_else(|e| {
println!("Chemid {:?}", e);
log::info!("Chemid {:?}", e);
0
});
let bat_temp = battery_driver.internal_temperature().unwrap_or_else(|e| {
println!("Bat Temp {:?}", e);
log::info!("Bat Temp {:?}", e);
0
});
let temp_c = Temperature::from_kelvin(bat_temp as f64 / 10_f64).as_celsius();
let voltage = battery_driver.voltage().unwrap_or_else(|e| {
println!("Bat volt {:?}", e);
log::info!("Bat volt {:?}", e);
0
});
let current = battery_driver.current().unwrap_or_else(|e| {
println!("Bat current {:?}", e);
log::info!("Bat current {:?}", e);
0
});
let state = battery_driver.state_of_charge().unwrap_or_else(|e| {
println!("Bat Soc {:?}", e);
log::info!("Bat Soc {:?}", e);
0
});
let charge_voltage = battery_driver.charge_voltage().unwrap_or_else(|e| {
println!("Bat Charge Volt {:?}", e);
log::info!("Bat Charge Volt {:?}", e);
0
});
let charge_current = battery_driver.charge_current().unwrap_or_else(|e| {
println!("Bat Charge Current {:?}", e);
log::info!("Bat Charge Current {:?}", e);
0
});
println!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current);
log::info!("ChemId: {} Current voltage {} and current {} with charge {}% and temp {} CVolt: {} CCur {}", chem_id, voltage, current, state, temp_c, charge_voltage, charge_current);
let _ = battery_driver.unsealed();
let _ = battery_driver.it_enable();
anyhow::Result::Ok(())
Ok(())
}

View File

@@ -1,42 +1,64 @@
use crate::bail;
use crate::config::{NetworkConfig, PlantControllerConfig};
use crate::hal::PLANT_COUNT;
use crate::log::{log, LogMessage};
use crate::STAY_ALIVE;
use anyhow::{anyhow, bail, Context};
use alloc::vec;
use chrono::{DateTime, Utc};
use embedded_svc::ipv4::IpInfo;
use embedded_svc::mqtt::client::QoS::{AtLeastOnce, ExactlyOnce};
use embedded_svc::wifi::{
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
};
use esp_idf_hal::delay::Delay;
use esp_idf_hal::gpio::{Level, PinDriver};
use esp_idf_svc::mqtt::client::{EspMqttClient, LwtConfiguration, MqttClientConfiguration};
use esp_idf_svc::sntp;
use esp_idf_svc::sntp::SyncStatus;
use esp_idf_svc::systime::EspSystemTime;
use esp_idf_svc::wifi::config::{ScanConfig, ScanType};
use esp_idf_svc::wifi::EspWifi;
use esp_idf_sys::{esp_spiffs_info, vTaskDelay};
use esp_hal::Blocking;
use esp_hal::uart::Uart;
use serde::Serialize;
use std::ffi::CString;
use std::fs;
use std::fs::File;
use std::path::Path;
use std::result::Result::Ok as OkStd;
use std::str::FromStr;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use std::time::Duration;
#[link_section = ".rtc.data"]
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 core::net::{IpAddr, Ipv4Addr, SocketAddr};
use core::str::FromStr;
use core::sync::atomic::Ordering;
use embassy_executor::Spawner;
use embassy_net::{DhcpConfig, IpAddress, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_time::{Duration, Timer, WithTimeout};
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
use esp_hal::gpio::{Input, RtcPinWithResistors};
use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{
sleep::{TimerWakeupSource, WakeupLevel},
Rtc,
};
use esp_hal::system::software_reset;
use esp_println::println;
use esp_radio::wifi::ap::{AccessPointConfig, AccessPointInfo};
use esp_radio::wifi::scan::{ScanConfig, ScanTypeConfig};
use esp_radio::wifi::sta::StationConfig;
use esp_radio::wifi::{AuthenticationMethod, Config, Interface, WifiController};
use esp_storage::FlashStorage;
use littlefs2::fs::Filesystem;
use littlefs2_core::{FileType, PathBuf, SeekFrom};
use log::{info, warn, error};
use portable_atomic::AtomicBool;
use super::shared_flash::MutexFlashStorage;
use crate::network::{net_task, run_dhcp};
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
#[link_section = ".rtc.data"]
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
#[link_section = ".rtc.data"]
static mut LOW_VOLTAGE_DETECTED: bool = false;
#[link_section = ".rtc.data"]
static mut RESTART_TO_CONF: bool = false;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOW_VOLTAGE_DETECTED: i8 = 0;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut RESTART_TO_CONF: i8 = 0;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LAST_CORROSION_PROTECTION_CHECK_DAY: i8 = -1;
const CONFIG_FILE: &str = "config.json";
#[derive(Serialize, Debug)]
pub struct FileInfo {
@@ -49,64 +71,227 @@ pub struct FileList {
total: usize,
used: usize,
files: Vec<FileInfo>,
file_system_corrupt: Option<String>,
iter_error: Option<String>,
}
pub struct FileSystemSizeInfo {
pub total_size: usize,
pub used_size: usize,
pub free_size: usize,
// Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions
extern "C" {
fn gpio_pad_hold(gpio_num: u32);
fn gpio_pad_unhold(gpio_num: u32);
}
pub struct MqttClient<'a> {
mqtt_client: EspMqttClient<'a>,
base_topic: heapless::String<64>,
}
pub struct ESP<'a> {
pub(crate) mqtt_client: Option<MqttClient<'a>>,
pub(crate) wifi_driver: EspWifi<'a>,
pub(crate) boot_button: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>,
pub(crate) delay: Delay,
#[inline(always)]
pub fn hold_enable(gpio_num: u8) {
unsafe { gpio_pad_hold(gpio_num as u32) }
}
impl ESP<'_> {
const SPIFFS_PARTITION_NAME: &'static str = "storage";
const CONFIG_FILE: &'static str = "/spiffs/config.cfg";
const BASE_PATH: &'static str = "/spiffs";
#[inline(always)]
pub fn hold_disable(gpio_num: u8) {
unsafe { gpio_pad_unhold(gpio_num as u32) }
}
pub(crate) fn mode_override_pressed(&mut self) -> bool {
self.boot_button.get_level() == Level::Low
pub struct Esp<'a> {
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
pub rng: Rng,
//first starter (ap or sta will take these)
pub interface_sta: Option<Interface<'static>>,
pub interface_ap: Option<Interface<'static>>,
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
pub boot_button: Input<'a>,
// RTC-capable GPIO used as external wake source (store the raw peripheral)
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
pub uart0: Uart<'a, Blocking>,
pub rtc: Rtc<'a>,
pub ota: Ota<'static, RmwNorFlashStorage<'static, &'static mut MutexFlashStorage>>,
pub ota_target: &'static mut FlashRegion<'static, MutexFlashStorage>,
pub current: AppPartitionSubType,
pub slot0_state: OtaImageState,
pub slot1_state: OtaImageState,
}
// SAFETY: On this target we never move Esp across OS threads; the firmware runs single-core
// cooperative tasks with Embassy. All interior mutability of non-Send peripherals is gated
// behind &mut self or embassy_sync Mutex with CriticalSectionRawMutex, which does not rely on
// thread scheduling. Therefore it is sound to mark Esp as Send to satisfy trait object bounds
// (e.g., Box<dyn BoardInteraction + Send>). If you add fields that are accessed from multiple
// CPU cores/threads, reconsider this.
unsafe impl Send for Esp<'_> {}
impl Esp<'_> {
pub fn get_time(&self) -> DateTime<Utc> {
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64)
.unwrap_or(DateTime::UNIX_EPOCH)
}
pub(crate) fn sntp(&mut self, max_wait_ms: u32) -> anyhow::Result<DateTime<Utc>> {
let sntp = sntp::EspSntp::new_default()?;
let mut counter = 0;
while sntp.get_sync_status() != SyncStatus::Completed {
self.delay.delay_ms(100);
counter += 100;
if counter > max_wait_ms {
bail!("Reached sntp timeout, aborting")
pub fn set_time(&mut self, time: DateTime<Utc>) {
self.rtc.set_current_time_us(time.timestamp_micros() as u64);
}
pub(crate) async fn read_serial_line(&mut self) -> FatResult<Option<alloc::string::String>> {
let mut buf = [0u8; 1];
let mut line = String::new();
loop {
match self.uart0.read_buffered(&mut buf) {
Ok(read) => {
if read == 0 {
return Ok(None);
}
let c = buf[0] as char;
if c == '\n' {
return Ok(Some(line));
}
line.push(c);
}
Err(error) => {
if line.is_empty() {
return Ok(None);
} else {
error!("Error reading serial line: {error:?}");
// If we already have some data, we should probably wait a bit or just return what we have?
// But the protocol expects a full line or message.
// For simplicity in config mode, we can block here or just return None if nothing is there yet.
// However, if we started receiving, we should probably finish or timeout.
continue;
}
}
}
}
self.time()
}
pub(crate) fn time(&mut self) -> anyhow::Result<DateTime<Utc>> {
let time = EspSystemTime {}.now().as_millis();
let smaller_time = time as i64;
let local_time = DateTime::from_timestamp_millis(smaller_time)
.ok_or(anyhow!("could not convert timestamp"))?;
anyhow::Ok(local_time)
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
access.remove(&*file)?;
Ok(())
}
pub(crate) fn wifi_scan(&mut self) -> anyhow::Result<Vec<AccessPointInfo>> {
self.wifi_driver.start_scan(
&ScanConfig {
scan_type: ScanType::Passive(Duration::from_secs(5)),
show_hidden: false,
..Default::default()
pub(crate) async fn write_file(
&mut self,
filename: String,
offset: u32,
buf: &[u8],
) -> Result<(), FatError> {
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
access.open_file_with_options_and_then(
|options| options.read(true).write(true).create(true),
&*file,
|file| {
file.seek(SeekFrom::Start(offset))?;
file.write(buf)?;
Ok(())
},
true,
)?;
anyhow::Ok(self.wifi_driver.get_scan_result()?)
Ok(())
}
pub async fn get_size(&mut self, filename: String) -> FatResult<usize> {
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
let data = access.metadata(&*file)?;
Ok(data.len())
}
pub(crate) async fn get_file(
&mut self,
filename: String,
chunk: u32,
) -> FatResult<([u8; 512], usize)> {
use littlefs2::io::Error as lfs2Error;
let file = PathBuf::try_from(filename.as_str())?;
let access = self.fs.lock().await;
let mut buf = [0_u8; 512];
let mut read = 0;
let offset = chunk * buf.len() as u32;
access.open_file_with_options_and_then(
|options| options.read(true),
&*file,
|file| {
let length = file.len()? as u32;
if length == 0 {
Err(lfs2Error::IO)
} else if length > offset {
file.seek(SeekFrom::Start(offset))?;
read = file.read(&mut buf)?;
Ok(())
} else {
//exactly at end, do nothing
Ok(())
}
},
)?;
Ok((buf, read))
}
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
let _ = check_erase(self.ota_target, offset, offset + 4096);
info!("erasing and writing block 0x{offset:x}");
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 {buf:?} but got {read_back:?}");
bail!(
"Flash error, read back does not match write buffer at offset {:x}",
offset
)
}
Ok(())
}
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
let current = self.ota.current_app_partition()?;
if self.ota.current_ota_state()? != Valid {
info!("Validating current slot {current:?} as it was able to ota");
self.ota.set_current_ota_state(Valid)?;
}
let next = match current {
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
_ => {
bail!("Invalid current slot {current:?} for ota");
}
};
self.ota.set_current_app_partition(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:?}");
self.set_restart_to_conf(true);
Ok(())
}
// 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()?;
pub(crate) fn mode_override_pressed(&mut self) -> bool {
self.boot_button.is_low()
}
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
info!("start wifi scan");
let mut lock = self.controller.try_lock()?;
info!("start wifi scan lock");
let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
min: esp_hal::time::Duration::from_millis(0),
max: esp_hal::time::Duration::from_millis(0),
});
let rv = lock.scan_async(&scan_config).await?;
info!("end wifi scan lock");
Ok(rv)
}
pub(crate) fn last_pump_time(&self, plant: usize) -> Option<DateTime<Utc>> {
@@ -120,17 +305,17 @@ impl ESP<'_> {
}
pub(crate) fn set_low_voltage_in_cycle(&mut self) {
unsafe {
LOW_VOLTAGE_DETECTED = true;
LOW_VOLTAGE_DETECTED = 1;
}
}
pub(crate) fn clear_low_voltage_in_cycle(&mut self) {
unsafe {
LOW_VOLTAGE_DETECTED = false;
LOW_VOLTAGE_DETECTED = 0;
}
}
pub(crate) fn low_voltage_in_cycle(&mut self) -> bool {
unsafe { LOW_VOLTAGE_DETECTED }
unsafe { LOW_VOLTAGE_DETECTED == 1 }
}
pub(crate) fn store_consecutive_pump_count(&mut self, plant: usize, count: u32) {
unsafe {
@@ -141,237 +326,103 @@ impl ESP<'_> {
unsafe { CONSECUTIVE_WATERING_PLANT[plant] }
}
pub(crate) fn get_restart_to_conf(&mut self) -> bool {
unsafe { RESTART_TO_CONF }
unsafe { RESTART_TO_CONF == 1 }
}
pub(crate) fn set_restart_to_conf(&mut self, to_conf: bool) {
unsafe {
RESTART_TO_CONF = to_conf;
}
}
pub(crate) fn wifi_ap(&mut self) -> anyhow::Result<()> {
let ssid = match self.load_config() {
Ok(config) => config.network.ap_ssid.clone(),
Err(_) => heapless::String::from_str("PlantCtrl Emergency Mode").unwrap(),
};
let apconfig = AccessPointConfiguration {
ssid,
auth_method: AuthMethod::None,
ssid_hidden: false,
..Default::default()
};
self.wifi_driver
.set_configuration(&Configuration::AccessPoint(apconfig))?;
self.wifi_driver.start()?;
anyhow::Ok(())
}
pub(crate) fn wifi(&mut self, network_config: &NetworkConfig) -> anyhow::Result<IpInfo> {
let ssid = network_config
.ssid
.clone()
.ok_or(anyhow!("No ssid configured"))?;
let password = network_config.password.clone();
let max_wait = network_config.max_wait;
match password {
Some(pw) => {
//TODO expect error due to invalid pw or similar! //call this during configuration and check if works, revert to config mode if not
self.wifi_driver.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid,
password: pw,
..Default::default()
},
))?;
}
None => {
self.wifi_driver.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid,
auth_method: AuthMethod::None,
..Default::default()
},
))?;
if to_conf {
RESTART_TO_CONF = 1;
} else {
RESTART_TO_CONF = 0;
}
}
self.wifi_driver.start()?;
self.wifi_driver.connect()?;
let delay = Delay::new_default();
let mut counter = 0_u32;
while !self.wifi_driver.is_connected()? {
delay.delay_ms(250);
counter += 250;
if counter > max_wait {
//ignore these errors, Wi-Fi will not be used this
self.wifi_driver.disconnect().unwrap_or(());
self.wifi_driver.stop().unwrap_or(());
bail!("Did not manage wifi connection within timeout");
}
}
println!("Should be connected now, waiting for link to be up");
while !self.wifi_driver.is_up()? {
delay.delay_ms(250);
counter += 250;
if counter > max_wait {
//ignore these errors, Wi-Fi will not be used this
self.wifi_driver.disconnect().unwrap_or(());
self.wifi_driver.stop().unwrap_or(());
bail!("Did not manage wifi connection within timeout");
}
}
//update freertos registers ;)
let address = self.wifi_driver.sta_netif().get_ip_info()?;
log(LogMessage::WifiInfo, 0, 0, "", &format!("{address:?}"));
anyhow::Ok(address)
}
pub(crate) fn load_config(&mut self) -> anyhow::Result<PlantControllerConfig> {
let cfg = File::open(Self::CONFIG_FILE)?;
let config: PlantControllerConfig = serde_json::from_reader(cfg)?;
anyhow::Ok(config)
}
pub(crate) fn save_config(&mut self, config: &PlantControllerConfig) -> anyhow::Result<()> {
let mut cfg = File::create(Self::CONFIG_FILE)?;
serde_json::to_writer(&mut cfg, &config)?;
println!("Wrote config config {:?}", config);
anyhow::Ok(())
}
pub(crate) fn delete_config(&self) -> anyhow::Result<()> {
let config = Path::new(Self::CONFIG_FILE);
if config.exists() {
println!("Removing config");
fs::remove_file(config)?
}
anyhow::Ok(())
}
pub(crate) fn mount_file_system(&mut self) -> anyhow::Result<()> {
log(LogMessage::MountingFilesystem, 0, 0, "", "");
let base_path = CString::new("/spiffs")?;
let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?;
let conf = esp_idf_sys::esp_vfs_spiffs_conf_t {
base_path: base_path.as_ptr(),
partition_label: storage.as_ptr(),
max_files: 5,
format_if_mount_failed: true,
};
unsafe {
esp_idf_sys::esp!(esp_idf_sys::esp_vfs_spiffs_register(&conf))?;
}
let free_space = self.file_system_size()?;
log(
LogMessage::FilesystemMount,
free_space.free_size as u32,
free_space.total_size as u32,
&free_space.used_size.to_string(),
"",
);
anyhow::Ok(())
}
fn file_system_size(&mut self) -> anyhow::Result<FileSystemSizeInfo> {
let storage = CString::new(Self::SPIFFS_PARTITION_NAME)?;
let mut total_size = 0;
let mut used_size = 0;
unsafe {
esp_idf_sys::esp!(esp_spiffs_info(
storage.as_ptr(),
&mut total_size,
&mut used_size
))?;
}
anyhow::Ok(FileSystemSizeInfo {
total_size,
used_size,
free_size: total_size - used_size,
})
}
pub(crate) fn list_files(&self) -> FileList {
let storage = CString::new(Self::SPIFFS_PARTITION_NAME).unwrap();
let mut file_system_corrupt = None;
let mut iter_error = None;
let mut result = Vec::new();
let filepath = Path::new(Self::BASE_PATH);
let read_dir = fs::read_dir(filepath);
match read_dir {
OkStd(read_dir) => {
for item in read_dir {
match item {
OkStd(file) => {
let f = FileInfo {
filename: file.file_name().into_string().unwrap(),
size: file
.metadata()
.and_then(|it| Ok(it.len()))
.unwrap_or_default()
as usize,
};
result.push(f);
}
Err(err) => {
iter_error = Some(format!("{err:?}"));
break;
}
}
pub fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
// Mark the current OTA image as valid if we reached here while in pending verify.
if let Ok(cur) = self.ota.current_ota_state() {
if cur == OtaImageState::PendingVerify {
info!("Marking OTA image as valid");
if let Err(err) = self.ota.set_current_ota_state(Valid) {
error!("Could not set image to valid: {:?}", err);
}
}
Err(err) => {
file_system_corrupt = Some(format!("{err:?}"));
}
}
let mut total: usize = 0;
let mut used: usize = 0;
unsafe {
esp_spiffs_info(storage.as_ptr(), &mut total, &mut used);
}
FileList {
total,
used,
file_system_corrupt,
files: result,
iter_error,
}
}
pub(crate) fn delete_file(&self, filename: &str) -> anyhow::Result<()> {
let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename));
match fs::remove_file(filepath) {
OkStd(_) => anyhow::Ok(()),
Err(err) => {
bail!(format!("{err:?}"))
}
}
}
pub(crate) fn get_file_handle(&self, filename: &str, write: bool) -> anyhow::Result<File> {
let filepath = Path::new(Self::BASE_PATH).join(Path::new(filename));
anyhow::Ok(if write {
File::create(filepath)?
} else {
File::open(filepath)?
})
info!("No OTA image to mark as valid");
}
if duration_in_ms == 0 {
software_reset();
} else {
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
[(&mut self.wake_gpio1, WakeupLevel::Low)];
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
self.rtc.sleep_deep(&[&timer, &ext1]);
}
}
pub(crate) fn init_rtc_deepsleep_memory(&self, init_rtc_store: bool, to_config_mode: bool) {
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
let cfg = PathBuf::try_from(CONFIG_FILE).unwrap();
let data = self.fs.lock().await.read::<4096>(&cfg)?;
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
return Ok(config);
}
pub(crate) async fn save_config(&mut self, config: Vec<u8>) -> FatResult<()> {
let filesystem = self.fs.lock().await;
let cfg = PathBuf::try_from(CONFIG_FILE)?;
filesystem.write(&cfg, &*config)?;
Ok(())
}
pub(crate) async fn list_files(&self) -> FatResult<FileList> {
let path = PathBuf::new();
let fs = self.fs.lock().await;
let free_size = fs.available_space()?;
let total_size = fs.total_space();
let mut result = FileList {
total: total_size,
used: total_size - free_size,
files: Vec::new(),
};
fs.read_dir_and_then(&path, |dir| {
for entry in dir {
let e = entry?;
if e.file_type() == FileType::File {
result.files.push(FileInfo {
filename: e.path().to_string(),
size: e.metadata().len(),
});
}
}
Ok(())
})?;
Ok(result)
}
pub(crate) async fn init_rtc_deepsleep_memory(
&self,
init_rtc_store: bool,
to_config_mode: bool,
) {
if init_rtc_store {
unsafe {
LAST_WATERING_TIMESTAMP = [0; PLANT_COUNT];
CONSECUTIVE_WATERING_PLANT = [0; PLANT_COUNT];
LOW_VOLTAGE_DETECTED = false;
crate::log::init();
RESTART_TO_CONF = to_config_mode;
LOW_VOLTAGE_DETECTED = 0;
if to_config_mode {
RESTART_TO_CONF = 1
} else {
RESTART_TO_CONF = 0;
}
LAST_CORROSION_PROTECTION_CHECK_DAY = -1;
};
} else {
unsafe {
if to_config_mode {
RESTART_TO_CONF = true;
RESTART_TO_CONF = 1;
}
log(
LogMessage::RestartToConfig,
@@ -387,225 +438,18 @@ impl ESP<'_> {
"",
"",
);
for i in 0..PLANT_COUNT {
println!(
"LAST_WATERING_TIMESTAMP[{}] = UTC {}",
i, LAST_WATERING_TIMESTAMP[i]
);
// is executed before main, no other code will alter these values during printing
#[allow(static_mut_refs)]
for (i, time) in LAST_WATERING_TIMESTAMP.iter().enumerate() {
info!("LAST_WATERING_TIMESTAMP[{i}] = UTC {time}");
}
for i in 0..PLANT_COUNT {
println!(
"CONSECUTIVE_WATERING_PLANT[{}] = {}",
i, CONSECUTIVE_WATERING_PLANT[i]
);
// is executed before main, no other code will alter these values during printing
#[allow(static_mut_refs)]
for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() {
info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}");
}
}
}
}
pub(crate) fn mqtt(&mut self, network_config: &NetworkConfig) -> anyhow::Result<()> {
let base_topic = network_config
.base_topic
.as_ref()
.context("missing base topic")?;
if base_topic.is_empty() {
bail!("Mqtt base_topic was empty")
}
let base_topic_copy = base_topic.clone();
let mqtt_url = network_config
.mqtt_url
.as_ref()
.context("missing mqtt url")?;
if mqtt_url.is_empty() {
bail!("Mqtt url was empty")
}
let last_will_topic = format!("{}/state", base_topic);
let mqtt_client_config = MqttClientConfiguration {
lwt: Some(LwtConfiguration {
topic: &last_will_topic,
payload: "lost".as_bytes(),
qos: AtLeastOnce,
retain: true,
}),
client_id: Some("plantctrl"),
keep_alive_interval: Some(Duration::from_secs(60 * 60 * 2)),
//room for improvement
..Default::default()
};
let mqtt_connected_event_received = Arc::new(AtomicBool::new(false));
let mqtt_connected_event_ok = Arc::new(AtomicBool::new(false));
let round_trip_ok = Arc::new(AtomicBool::new(false));
let round_trip_topic = format!("{}/internal/roundtrip", base_topic);
let stay_alive_topic = format!("{}/stay_alive", base_topic);
log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
let mqtt_connected_event_received_copy = mqtt_connected_event_received.clone();
let mqtt_connected_event_ok_copy = mqtt_connected_event_ok.clone();
let stay_alive_topic_copy = stay_alive_topic.clone();
let round_trip_topic_copy = round_trip_topic.clone();
let round_trip_ok_copy = round_trip_ok.clone();
let client_id = mqtt_client_config.client_id.unwrap_or("not set");
log(LogMessage::MqttInfo, 0, 0, client_id, mqtt_url);
let mut client = EspMqttClient::new_cb(mqtt_url, &mqtt_client_config, move |event| {
let payload = event.payload();
match payload {
embedded_svc::mqtt::client::EventPayload::Received {
id: _,
topic,
data,
details: _,
} => {
let data = String::from_utf8_lossy(data);
if let Some(topic) = topic {
//todo use enums
if topic.eq(round_trip_topic_copy.as_str()) {
round_trip_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed);
} else if topic.eq(stay_alive_topic_copy.as_str()) {
let value =
data.eq_ignore_ascii_case("true") || data.eq_ignore_ascii_case("1");
log(LogMessage::MqttStayAliveRec, 0, 0, &data, "");
STAY_ALIVE.store(value, std::sync::atomic::Ordering::Relaxed);
} else {
log(LogMessage::UnknownTopic, 0, 0, "", topic);
}
}
}
esp_idf_svc::mqtt::client::EventPayload::Connected(_) => {
mqtt_connected_event_received_copy
.store(true, std::sync::atomic::Ordering::Relaxed);
mqtt_connected_event_ok_copy.store(true, std::sync::atomic::Ordering::Relaxed);
println!("Mqtt connected");
}
esp_idf_svc::mqtt::client::EventPayload::Disconnected => {
mqtt_connected_event_received_copy
.store(true, std::sync::atomic::Ordering::Relaxed);
mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed);
println!("Mqtt disconnected");
}
esp_idf_svc::mqtt::client::EventPayload::Error(esp_error) => {
println!("EspMqttError reported {:?}", esp_error);
mqtt_connected_event_received_copy
.store(true, std::sync::atomic::Ordering::Relaxed);
mqtt_connected_event_ok_copy.store(false, std::sync::atomic::Ordering::Relaxed);
println!("Mqtt error");
}
esp_idf_svc::mqtt::client::EventPayload::BeforeConnect => {
println!("Mqtt before connect")
}
esp_idf_svc::mqtt::client::EventPayload::Subscribed(_) => {
println!("Mqtt subscribed")
}
esp_idf_svc::mqtt::client::EventPayload::Unsubscribed(_) => {
println!("Mqtt unsubscribed")
}
esp_idf_svc::mqtt::client::EventPayload::Published(_) => {
println!("Mqtt published")
}
esp_idf_svc::mqtt::client::EventPayload::Deleted(_) => {
println!("Mqtt deleted")
}
}
})?;
let mut wait_for_connections_event = 0;
while wait_for_connections_event < 100 {
wait_for_connections_event += 1;
match mqtt_connected_event_received.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Mqtt connection callback received, progressing");
match mqtt_connected_event_ok.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Mqtt did callback as connected, testing with roundtrip now");
//subscribe to roundtrip
client.subscribe(round_trip_topic.as_str(), ExactlyOnce)?;
client.subscribe(stay_alive_topic.as_str(), ExactlyOnce)?;
//publish to roundtrip
client.publish(
round_trip_topic.as_str(),
ExactlyOnce,
false,
"online_test".as_bytes(),
)?;
let mut wait_for_roundtrip = 0;
while wait_for_roundtrip < 100 {
wait_for_roundtrip += 1;
match round_trip_ok.load(std::sync::atomic::Ordering::Relaxed) {
true => {
println!("Round trip registered, proceeding");
self.mqtt_client = Some(MqttClient {
mqtt_client: client,
base_topic: base_topic_copy,
});
return anyhow::Ok(());
}
false => {
unsafe { vTaskDelay(10) };
}
}
}
bail!("Mqtt did not complete roundtrip in time");
}
false => {
bail!("Mqtt did respond but with failure")
}
}
}
false => {
unsafe { vTaskDelay(10) };
}
}
}
bail!("Mqtt did not fire connection callback in time");
}
pub(crate) fn mqtt_publish(&mut self, subtopic: &str, message: &[u8]) -> anyhow::Result<()> {
if self.mqtt_client.is_none() {
return anyhow::Ok(());
}
if !subtopic.starts_with("/") {
println!("Subtopic without / at start {}", subtopic);
bail!("Subtopic without / at start {}", subtopic);
}
if subtopic.len() > 192 {
println!("Subtopic exceeds 192 chars {}", subtopic);
bail!("Subtopic exceeds 192 chars {}", subtopic);
}
let client = self.mqtt_client.as_mut().unwrap();
let mut full_topic: heapless::String<256> = heapless::String::new();
if full_topic.push_str(client.base_topic.as_str()).is_err() {
println!("Some error assembling full_topic 1");
bail!("Some error assembling full_topic 1")
};
if full_topic.push_str(subtopic).is_err() {
println!("Some error assembling full_topic 2");
bail!("Some error assembling full_topic 2")
};
let publish = client
.mqtt_client
.publish(&full_topic, ExactlyOnce, true, message);
Delay::new(10).delay_ms(50);
match publish {
OkStd(message_id) => {
println!(
"Published mqtt topic {} with message {:#?} msgid is {:?}",
full_topic,
String::from_utf8_lossy(message),
message_id
);
anyhow::Ok(())
}
Err(err) => {
println!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
full_topic,
String::from_utf8_lossy(message),
err
);
Err(err)?
}
}
}
}

View File

@@ -1,45 +1,80 @@
use crate::config::{BoardHardware, PlantControllerConfig};
use crate::hal::battery::{BatteryInteraction, NoBatteryMonitor};
use crate::hal::esp::ESP;
use crate::hal::{deep_sleep, BackupHeader, BoardInteraction, FreePeripherals, Sensor};
use anyhow::{bail, Result};
use chrono::{DateTime, Utc};
use embedded_hal::digital::OutputPin;
use esp_idf_hal::gpio::{IOPin, Pull};
use esp_idf_hal::gpio::{InputOutput, PinDriver};
use crate::alloc::boxed::Box;
use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::Esp;
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor};
use crate::{
bail,
config::PlantControllerConfig,
hal::battery::{BatteryInteraction, NoBatteryMonitor},
};
use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
use esp_hal::gpio::{Level, Output, OutputConfig};
use measurements::{Current, Voltage};
pub struct Initial<'a> {
pub(crate) general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
pub(crate) esp: ESP<'a>,
pub(crate) general_fault: Output<'a>,
pub(crate) esp: Esp<'a>,
pub(crate) config: PlantControllerConfig,
pub(crate) battery: Box<dyn BatteryInteraction + Send>,
pub rtc: Box<dyn RTCModuleInteraction + Send>,
}
pub(crate) struct NoRTC {}
#[async_trait]
impl RTCModuleInteraction for NoRTC {
async fn get_backup_info(&mut self) -> Result<BackupHeader, FatError> {
bail!("Please configure board revision")
}
async fn get_backup_config(&mut self, _chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
bail!("Please configure board revision")
}
async fn backup_config(&mut self, _offset: usize, _bytes: &[u8]) -> FatResult<()> {
bail!("Please configure board revision")
}
async fn backup_config_finalize(&mut self, _crc: u16, _length: usize) -> FatResult<()> {
bail!("Please configure board revision")
}
async fn get_rtc_time(&mut self) -> Result<DateTime<Utc>, FatError> {
bail!("Please configure board revision")
}
async fn set_rtc_time(&mut self, _time: &DateTime<Utc>) -> Result<(), FatError> {
bail!("Please configure board revision")
}
}
pub(crate) fn create_initial_board(
free_pins: FreePeripherals,
fs_mount_error: bool,
free_pins: FreePeripherals<'static>,
config: PlantControllerConfig,
esp: ESP<'static>,
) -> Result<Box<dyn BoardInteraction<'static> + Send>> {
let mut general_fault = PinDriver::input_output(free_pins.gpio6.downgrade())?;
general_fault.set_pull(Pull::Floating)?;
general_fault.set_low()?;
if fs_mount_error {
general_fault.set_high()?
}
esp: Esp<'static>,
) -> Result<Box<dyn BoardInteraction<'static> + Send>, FatError> {
log::info!("Start initial");
let general_fault = Output::new(free_pins.gpio23, Level::Low, OutputConfig::default());
let v = Initial {
general_fault,
config,
esp,
battery: Box::new(NoBatteryMonitor {}),
rtc: Box::new(NoRTC {}),
};
Ok(Box::new(v))
}
#[async_trait]
impl<'a> BoardInteraction<'a> for Initial<'a> {
fn get_esp(&mut self) -> &mut ESP<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
bail!("Please configure board revision")
}
fn get_esp(&mut self) -> &mut Esp<'a> {
&mut self.esp
}
@@ -51,85 +86,71 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
&mut self.battery
}
fn set_charge_indicator(&mut self, charging: bool) -> Result<()> {
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc
}
async fn get_time(&mut self) -> DateTime<Utc> {
self.esp.get_time()
}
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
self.rtc.set_rtc_time(&time.to_utc()).await?;
self.esp.set_time(time.to_utc());
Ok(())
}
async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
deep_sleep(duration_in_ms)
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
self.esp.deep_sleep_ms(duration_in_ms);
}
fn get_backup_info(&mut self) -> Result<BackupHeader> {
bail!("Please configure board revision")
}
fn get_backup_config(&mut self) -> Result<Vec<u8>> {
bail!("Please configure board revision")
}
fn backup_config(&mut self, bytes: &[u8]) -> Result<()> {
bail!("Please configure board revision")
}
fn is_day(&self) -> bool {
false
}
fn water_temperature_c(&mut self) -> Result<f32> {
async fn light(&mut self, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
fn tank_sensor_voltage(&mut self) -> Result<f32> {
bail!("Please configure board revision")
}
fn light(&mut self, enable: bool) -> Result<()> {
async fn pump(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
fn pump(&mut self, plant: usize, enable: bool) -> Result<()> {
bail!("Please configure board revision")
}
fn fault(&mut self, plant: usize, _enable: bool) -> Result<()> {
bail!("Please configure board revision")
}
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32> {
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
bail!("Please configure board revision")
}
fn general_fault(&mut self, enable: bool) {
let _ = self.general_fault.set_state(enable.into());
}
fn factory_reset(&mut self) -> Result<()> {
async fn fault(&mut self, _plant: usize, _enable: bool) -> Result<(), FatError> {
bail!("Please configure board revision")
}
fn get_rtc_time(&mut self) -> Result<DateTime<Utc>> {
async fn measure_moisture_hz(
&mut self,
_plant: usize,
_sensor: Sensor,
) -> Result<f32, FatError> {
bail!("Please configure board revision")
}
fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> Result<()> {
bail!("Please configure board revision")
async fn general_fault(&mut self, enable: bool) {
self.general_fault.set_level(enable.into());
}
fn test_pump(&mut self, plant: usize) -> Result<()> {
async fn test(&mut self) -> Result<(), FatError> {
bail!("Please configure board revision")
}
fn test(&mut self) -> Result<()> {
bail!("Please configure board revision")
}
fn set_config(&mut self, config: PlantControllerConfig) -> anyhow::Result<()> {
fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config;
self.esp.save_config(&self.config)?;
anyhow::Ok(())
}
fn get_mptt_voltage(&mut self) -> Result<Voltage> {
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
bail!("Please configure board revision")
}
fn get_mptt_current(&mut self) -> Result<Current> {
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
bail!("Please configure board revision")
}
}

View File

@@ -0,0 +1,88 @@
use crate::hal::shared_flash::MutexFlashStorage;
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
use esp_bootloader_esp_idf::partitions::FlashRegion;
use littlefs2::consts::U4096 as lfsCache;
use littlefs2::consts::U512 as lfsLookahead;
use littlefs2::driver::Storage as lfs2Storage;
use littlefs2::io::Error as lfs2Error;
use littlefs2::io::Result as lfs2Result;
use log::error;
pub struct LittleFs2Filesystem {
pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
}
impl lfs2Storage for LittleFs2Filesystem {
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;
if off % read_size != 0 {
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {off} read_size: {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) => {
error!("Littlefs2Filesystem read error: {err:?}");
Err(lfs2Error::IO)
}
}
}
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
let write_size: usize = Self::WRITE_SIZE;
if off % write_size != 0 {
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {off} write_size: {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) => {
error!("Littlefs2Filesystem write error: {err:?}");
Err(lfs2Error::IO)
}
}
}
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
let block_size: usize = Self::BLOCK_SIZE;
if off % block_size != 0 {
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
return Err(lfs2Error::IO);
}
if len % block_size != 0 {
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
return 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 Err(lfs2Error::IO);
}
}
match self.storage.erase(off as u32, (off + len) as u32) {
Ok(..) => Ok(len),
Err(err) => {
error!("Littlefs2Filesystem erase error: {err:?}");
Err(lfs2Error::IO)
}
}
}
}

View File

@@ -1,90 +1,133 @@
pub(crate) mod battery;
mod esp;
pub mod esp;
mod initial_hal;
mod little_fs2storage_adapter;
pub(crate) mod rtc;
mod shared_flash;
mod v3_hal;
mod v3_shift_register;
mod v4_hal;
mod v4_sensor;
mod water;
use battery::BQ34Z100G1;
use crate::alloc::string::ToString;
use crate::hal::rtc::{DS3231Module, RTCModuleInteraction};
use esp_hal::interrupt::software::SoftwareInterruptControl;
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;
use esp_hal::peripherals::GPIO12;
use esp_hal::peripherals::GPIO13;
use esp_hal::peripherals::GPIO14;
use esp_hal::peripherals::GPIO15;
use esp_hal::peripherals::GPIO16;
use esp_hal::peripherals::GPIO17;
use esp_hal::peripherals::GPIO18;
use esp_hal::peripherals::GPIO2;
use esp_hal::peripherals::GPIO21;
use esp_hal::peripherals::GPIO22;
use esp_hal::peripherals::GPIO23;
use esp_hal::peripherals::GPIO24;
use esp_hal::peripherals::GPIO25;
use esp_hal::peripherals::GPIO26;
use esp_hal::peripherals::GPIO27;
use esp_hal::peripherals::GPIO28;
use esp_hal::peripherals::GPIO29;
use esp_hal::peripherals::GPIO3;
use esp_hal::peripherals::GPIO30;
use esp_hal::peripherals::GPIO4;
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 portable_atomic::AtomicBool;
use crate::{
bail,
config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig},
hal::{
battery::{BatteryInteraction, NoBatteryMonitor},
esp::Esp,
},
log::LogMessage,
BOARD_ACCESS,
};
use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;
use async_trait::async_trait;
use bq34z100::Bq34z100g1Driver;
use crate::log::LogMessage;
use ds323x::DateTimeAccess;
use esp_ota::mark_app_valid;
use eeprom24x::Eeprom24xTrait;
use embedded_hal_bus::i2c::MutexDevice;
use esp_idf_hal::adc::ADC1;
use esp_idf_hal::i2c::{APBTickType, I2cConfig, I2cDriver};
use esp_idf_hal::units::FromValueType;
use esp_idf_svc::eventloop::EspSystemEventLoop;
use esp_idf_svc::nvs::EspDefaultNvsPartition;
use esp_idf_svc::wifi::EspWifi;
use esp_idf_sys::esp_restart;
use esp_idf_sys::{
esp_deep_sleep, esp_sleep_enable_ext1_wakeup,
esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW,
use chrono::{DateTime, FixedOffset, Utc};
use core::cell::RefCell;
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No;
use eeprom24x::{Eeprom24x, SlaveAddr, Storage};
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embedded_storage::nor_flash::RmwNorFlashStorage;
use embedded_storage::ReadStorage;
use esp_bootloader_esp_idf::partitions::{
AppPartitionSubType, DataPartitionSubType, FlashRegion, PartitionEntry, PartitionTable,
PartitionType,
};
use once_cell::sync::Lazy;
use anyhow::{Ok, Result};
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use std::result::Result::Ok as OkStd;
use std::sync::Mutex;
use std::time::Duration;
use crate::config::{BatteryBoardVersion, BoardVersion, PlantControllerConfig};
use crate::hal::battery::{print_battery_bq34z100, BatteryInteraction, NoBatteryMonitor};
use crate::hal::esp::ESP;
use crate::hal::initial_hal::Initial;
use crate::log::log;
use embedded_hal::digital::OutputPin;
use esp_idf_hal::delay::Delay;
use esp_idf_hal::gpio::{
Gpio0, Gpio1, Gpio10, Gpio11, Gpio12, Gpio13, Gpio14, Gpio15, Gpio16, Gpio17, Gpio18, Gpio2,
Gpio21, Gpio22, Gpio23, Gpio24, Gpio25, Gpio26, Gpio27, Gpio28, Gpio29, Gpio3, Gpio30, Gpio4,
Gpio5, Gpio6, Gpio7, Gpio8, IOPin, PinDriver, Pull,
};
use esp_idf_hal::pcnt::PCNT0;
use esp_idf_hal::prelude::Peripherals;
use esp_idf_hal::reset::ResetReason;
use esp_hal::clock::CpuClock;
use esp_hal::gpio::{Input, InputConfig, Pull};
use esp_hal::uart::{Config as UartConfig, Uart};
use esp_storage::FlashStorage;
use lib_bms_protocol::{BmsReadable, ProtocolVersion};
use measurements::{Current, Voltage};
use pca9535::StandardExpanderInterface;
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;
use embassy_sync::mutex::Mutex;
use embassy_sync::once_lock::OnceLock;
use esp_alloc as _;
use esp_backtrace as _;
use esp_bootloader_esp_idf::ota::{OtaImageState, Ota};
use esp_hal::delay::Delay;
use esp_hal::i2c::master::{BusTimeout, Config, I2c};
use esp_hal::pcnt::unit::Unit;
use esp_hal::pcnt::Pcnt;
use esp_hal::rng::Rng;
use esp_hal::rtc_cntl::{Rtc, SocResetReason};
use esp_hal::system::reset_reason;
use esp_hal::time::Rate;
use esp_hal::timer::timg::{TimerGroup, Wdt};
use esp_hal::Blocking;
use littlefs2::fs::{Allocation, Filesystem as lfs2Filesystem};
use littlefs2::object_safe::DynStorage;
use log::{error, info, warn};
use shared_flash::MutexFlashStorage;
pub static PROGRESS_ACTIVE: AtomicBool = AtomicBool::new(false);
//Only support for 8 right now!
pub const PLANT_COUNT: usize = 8;
const REPEAT_MOIST_MEASURE: usize = 1;
pub static WATCHDOG: OnceLock<
embassy_sync::blocking_mutex::Mutex<
CriticalSectionRawMutex,
RefCell<Wdt<esp_hal::peripherals::TIMG0>>,
>,
> = OnceLock::new();
const TANK_MULTI_SAMPLE: usize = 11;
pub static I2C_DRIVER: Lazy<Mutex<I2cDriver<'static>>> = Lazy::new(PlantHal::create_i2c);
#[non_exhaustive]
struct V3Constants;
impl V3Constants {}
const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
fn deep_sleep(duration_in_ms: u64) -> ! {
unsafe {
//if we don't do this here, we might just revert newly flashed firmware
mark_app_valid();
//allow early wakeup by pressing the boot button
if duration_in_ms == 0 {
esp_restart();
} else {
//configure gpio 1 to wakeup on low, reused boot button for this
esp_sleep_enable_ext1_wakeup(
0b10u64,
esp_sleep_ext1_wakeup_mode_t_ESP_EXT1_WAKEUP_ANY_LOW,
);
esp_deep_sleep(duration_in_ms);
}
};
}
pub static I2C_DRIVER: OnceLock<
embassy_sync::blocking_mutex::Mutex<CriticalSectionRawMutex, RefCell<I2c<Blocking>>>,
> = OnceLock::new();
#[derive(Debug, PartialEq)]
pub enum Sensor {
@@ -98,177 +141,354 @@ pub struct HAL<'a> {
pub board_hal: Box<dyn BoardInteraction<'a> + Send>,
}
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct BackupHeader {
pub timestamp: i64,
crc16: u16,
pub size: usize,
}
impl Default for BackupHeader {
fn default() -> Self {
Self {
timestamp: Default::default(),
crc16: Default::default(),
size: Default::default(),
}
fn ota_state(
slot: AppPartitionSubType,
ota_data: &mut FlashRegion<RmwNorFlashStorage<&mut MutexFlashStorage>>,
) -> 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
let mut slot_buf = [0u8; 32];
if slot == AppPartitionSubType::Ota0 {
let _ = ReadStorage::read(ota_data, 0x0000, &mut slot_buf);
} else {
let _ = ReadStorage::read(ota_data, 0x1000, &mut slot_buf);
}
let raw_state = u32::from_le_bytes(slot_buf[24..28].try_into().unwrap_or([0xff; 4]));
OtaImageState::try_from(raw_state).unwrap_or(OtaImageState::Undefined)
}
fn get_current_slot(
pt: &PartitionTable,
ota: &mut Ota<RmwNorFlashStorage<&mut MutexFlashStorage>>,
) -> Result<AppPartitionSubType, FatError> {
let booted = pt.booted_partition()?.ok_or(FatError::OTAError)?;
let booted_type = booted.partition_type();
let booted_ota_type = match booted_type {
PartitionType::App(subtype) => subtype,
_ => {
bail!("Booted partition is not an app partition");
}
};
let expected_partition = ota.current_app_partition()?;
if expected_partition == booted_ota_type {
info!("Booted partition matches expected partition");
} else {
info!("Booted partition does not match expected partition, fixing ota entry");
ota.set_current_app_partition(booted_ota_type)?;
}
let fixed = ota.current_app_partition()?;
let state = ota.current_ota_state();
info!("Expected partition: {expected_partition:?}, current partition: {booted_ota_type:?}, state: {state:?}");
if fixed != booted_ota_type {
bail!(
"Could not fix ota entry, booted partition is still not correct: {:?} != {:?}",
booted_ota_type,
fixed
);
}
Ok(booted_ota_type)
}
pub trait BoardInteraction<'a> {
fn set_charge_indicator(&mut self, charging: bool) -> Result<()>;
fn is_day(&self) -> bool;
fn get_mptt_voltage(&mut self) -> Result<Voltage>;
fn get_mptt_current(&mut self) -> Result<Current>;
pub fn next_partition(current: AppPartitionSubType) -> FatResult<AppPartitionSubType> {
let next = match current {
AppPartitionSubType::Ota0 => AppPartitionSubType::Ota1,
AppPartitionSubType::Ota1 => AppPartitionSubType::Ota0,
_ => {
bail!("Current slot is not ota0 or ota1");
}
};
Ok(next)
}
fn get_esp(&mut self) -> &mut ESP<'a>;
#[async_trait]
pub trait BoardInteraction<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError>;
fn get_esp(&mut self) -> &mut Esp<'a>;
fn get_config(&mut self) -> &PlantControllerConfig;
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send>;
fn deep_sleep(&mut self, duration_in_ms: u64) -> !;
fn get_backup_info(&mut self) -> Result<BackupHeader>;
fn get_backup_config(&mut self) -> Result<Vec<u8>>;
fn backup_config(&mut self, bytes: &[u8]) -> Result<()>;
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send>;
async fn get_time(&mut self) -> DateTime<Utc>;
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()>;
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError>;
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> !;
fn is_day(&self) -> bool;
//should be multsampled
fn water_temperature_c(&mut self) -> Result<f32>;
/// return median tank sensor value in milli volt
fn tank_sensor_voltage(&mut self) -> Result<f32>;
fn light(&mut self, enable: bool) -> Result<()>;
fn pump(&mut self, plant: usize, enable: bool) -> Result<()>;
fn fault(&mut self, plant: usize, enable: bool) -> Result<()>;
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32>;
fn general_fault(&mut self, enable: bool);
fn factory_reset(&mut self) -> Result<()>;
fn get_rtc_time(&mut self) -> Result<DateTime<Utc>>;
fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> Result<()>;
fn test_pump(&mut self, plant: usize) -> Result<()>;
fn test(&mut self) -> Result<()>;
fn set_config(&mut self, config: PlantControllerConfig) -> Result<()>;
}
async fn light(&mut self, enable: bool) -> Result<(), FatError>;
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
async fn pump_current(&mut self, plant: usize) -> Result<Current, FatError>;
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError>;
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError>;
async fn general_fault(&mut self, enable: bool);
async fn test(&mut self) -> Result<(), FatError>;
fn set_config(&mut self, config: PlantControllerConfig);
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError>;
async fn get_mptt_current(&mut self) -> Result<Current, FatError>;
pub struct FreePeripherals {
pub gpio0: Gpio0,
pub gpio1: Gpio1,
pub gpio2: Gpio2,
pub gpio3: Gpio3,
pub gpio4: Gpio4,
pub gpio5: Gpio5,
pub gpio6: Gpio6,
pub gpio7: Gpio7,
pub gpio8: Gpio8,
//config button here
pub gpio10: Gpio10,
pub gpio11: Gpio11,
pub gpio12: Gpio12,
pub gpio13: Gpio13,
pub gpio14: Gpio14,
pub gpio15: Gpio15,
pub gpio16: Gpio16,
pub gpio17: Gpio17,
pub gpio18: Gpio18,
//i2c here
pub gpio21: Gpio21,
pub gpio22: Gpio22,
pub gpio23: Gpio23,
pub gpio24: Gpio24,
pub gpio25: Gpio25,
pub gpio26: Gpio26,
pub gpio27: Gpio27,
pub gpio28: Gpio28,
pub gpio29: Gpio29,
pub gpio30: Gpio30,
pub pcnt0: PCNT0,
pub adc1: ADC1,
}
impl PlantHal {
fn create_i2c() -> Mutex<I2cDriver<'static>> {
let peripherals = unsafe { Peripherals::new() };
let config = I2cConfig::new()
.scl_enable_pullup(true)
.sda_enable_pullup(true)
.baudrate(100_u32.kHz().into())
.timeout(APBTickType::from(Duration::from_millis(100)));
let i2c = peripherals.i2c0;
let scl = peripherals.pins.gpio19.downgrade();
let sda = peripherals.pins.gpio20.downgrade();
Mutex::new(I2cDriver::new(i2c, sda, scl, &config).unwrap())
async fn progress(&mut self, counter: u32) {
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 {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let even = counter % 2 == 0;
let _ = self.general_fault(even.into()).await;
}
pub fn create() -> Result<Mutex<HAL<'static>>> {
let peripherals = Peripherals::take()?;
let sys_loop = EspSystemEventLoop::take()?;
let nvs = EspDefaultNvsPartition::take()?;
let wifi_driver = EspWifi::new(peripherals.modem, sys_loop, Some(nvs))?;
async fn clear_progress(&mut self) {
for led in 0..PLANT_COUNT {
if let Err(err) = self.fault(led, false).await {
warn!("Fault on plant {}: {:?}", led, err);
}
}
let _ = self.general_fault(false).await;
}
}
let mut boot_button = PinDriver::input(peripherals.pins.gpio9.downgrade())?;
boot_button.set_pull(Pull::Floating)?;
#[allow(dead_code)]
pub struct FreePeripherals<'a> {
pub gpio0: GPIO0<'a>,
pub gpio2: GPIO2<'a>,
pub gpio3: GPIO3<'a>,
pub gpio4: GPIO4<'a>,
pub gpio5: GPIO5<'a>,
pub gpio6: GPIO6<'a>,
pub gpio7: GPIO7<'a>,
pub gpio8: GPIO8<'a>,
// //config button here
pub gpio10: GPIO10<'a>,
pub gpio11: GPIO11<'a>,
pub gpio12: GPIO12<'a>,
pub gpio13: GPIO13<'a>,
pub gpio14: GPIO14<'a>,
pub gpio15: GPIO15<'a>,
pub gpio16: GPIO16<'a>,
pub gpio17: GPIO17<'a>,
pub gpio18: GPIO18<'a>,
// //i2c here
pub gpio21: GPIO21<'a>,
pub gpio22: GPIO22<'a>,
pub gpio23: GPIO23<'a>,
pub gpio27: GPIO27<'a>,
pub twai: TWAI0<'a>,
pub pcnt0: Unit<'a, 0>,
pub pcnt1: Unit<'a, 1>,
pub adc1: ADC1<'a>,
}
use crate::util::mk_static;
impl PlantHal {
pub async fn create() -> Result<Mutex<CriticalSectionRawMutex, HAL<'static>>, FatError> {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals: Peripherals = esp_hal::init(config);
esp_alloc::heap_allocator!(size: 64 * 1024);
esp_alloc::heap_allocator!(#[link_section = ".dram2_uninit"] size: 64000);
let mut rtc_peripheral: Rtc = Rtc::new(peripherals.LPWR);
rtc_peripheral.rwdt.disable();
let timg0 = TimerGroup::new(peripherals.TIMG0);
let sw_int = SoftwareInterruptControl::new(peripherals.SW_INTERRUPT);
esp_rtos::start(timg0.timer0, sw_int.software_interrupt0);
let boot_button = Input::new(
peripherals.GPIO9,
InputConfig::default().with_pull(Pull::None),
);
// Reserve GPIO1 for deep sleep wake (configured just before entering sleep)
let wake_gpio1 = peripherals.GPIO1;
let rng = Rng::new();
let (controller, interfaces) = esp_radio::wifi::new(peripherals.WIFI, Default::default())
.expect("Could not init wifi");
let pcnt_module = Pcnt::new(peripherals.PCNT);
let free_pins = FreePeripherals {
adc1: peripherals.adc1,
pcnt0: peripherals.pcnt0,
gpio0: peripherals.pins.gpio0,
gpio1: peripherals.pins.gpio1,
gpio2: peripherals.pins.gpio2,
gpio3: peripherals.pins.gpio3,
gpio4: peripherals.pins.gpio4,
gpio5: peripherals.pins.gpio5,
gpio6: peripherals.pins.gpio6,
gpio7: peripherals.pins.gpio7,
gpio8: peripherals.pins.gpio8,
gpio10: peripherals.pins.gpio10,
gpio11: peripherals.pins.gpio11,
gpio12: peripherals.pins.gpio12,
gpio13: peripherals.pins.gpio13,
gpio14: peripherals.pins.gpio14,
gpio15: peripherals.pins.gpio15,
gpio16: peripherals.pins.gpio16,
gpio17: peripherals.pins.gpio17,
gpio18: peripherals.pins.gpio18,
gpio21: peripherals.pins.gpio21,
gpio22: peripherals.pins.gpio22,
gpio23: peripherals.pins.gpio23,
gpio24: peripherals.pins.gpio24,
gpio25: peripherals.pins.gpio25,
gpio26: peripherals.pins.gpio26,
gpio27: peripherals.pins.gpio27,
gpio28: peripherals.pins.gpio28,
gpio29: peripherals.pins.gpio29,
gpio30: peripherals.pins.gpio30,
gpio0: peripherals.GPIO0,
gpio2: peripherals.GPIO2,
gpio3: peripherals.GPIO3,
gpio4: peripherals.GPIO4,
gpio5: peripherals.GPIO5,
gpio6: peripherals.GPIO6,
gpio7: peripherals.GPIO7,
gpio8: peripherals.GPIO8,
gpio10: peripherals.GPIO10,
gpio11: peripherals.GPIO11,
gpio12: peripherals.GPIO12,
gpio13: peripherals.GPIO13,
gpio14: peripherals.GPIO14,
gpio15: peripherals.GPIO15,
gpio16: peripherals.GPIO16,
gpio17: peripherals.GPIO17,
gpio18: peripherals.GPIO18,
gpio21: peripherals.GPIO21,
gpio22: peripherals.GPIO22,
gpio23: peripherals.GPIO23,
gpio27: peripherals.GPIO27,
twai: peripherals.TWAI0,
pcnt0: pcnt_module.unit0,
pcnt1: pcnt_module.unit1,
adc1: peripherals.ADC1,
};
let mut esp = ESP {
mqtt_client: None,
wifi_driver,
let tablebuffer = mk_static!(
[u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN],
[0u8; esp_bootloader_esp_idf::partitions::PARTITION_TABLE_MAX_LEN]
);
let bullshit = MutexFlashStorage {
inner: Arc::new(CriticalSectionMutex::new(RefCell::new(FlashStorage::new(
peripherals.FLASH,
)))),
};
let flash_storage = mk_static!(MutexFlashStorage, bullshit.clone());
let flash_storage_2 = mk_static!(MutexFlashStorage, bullshit.clone());
let flash_storage_3 = mk_static!(MutexFlashStorage, bullshit.clone());
let pt =
esp_bootloader_esp_idf::partitions::read_partition_table(flash_storage, tablebuffer)?;
let ota_data = mk_static!(
PartitionEntry,
pt.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::Ota,
))?
.expect("No OTA data partition found")
);
let mut ota_data = ota_data.as_embedded_storage(mk_static!(
RmwNorFlashStorage<&mut MutexFlashStorage>,
RmwNorFlashStorage::new(flash_storage_2, mk_static!([u8; 4096], [0_u8; 4096]))
));
let state_0 = ota_state(AppPartitionSubType::Ota0, &mut ota_data);
let state_1 = ota_state(AppPartitionSubType::Ota1, &mut ota_data);
let mut ota = Ota::new(ota_data, 2)?;
let running = get_current_slot(&pt, &mut ota)?;
let target = next_partition(running)?;
info!("Currently running OTA slot: {running:?}");
info!("Updates will be stored in OTA slot: {target:?}");
info!("Slot0 state: {state_0:?}");
info!("Slot1 state: {state_1:?}");
//get current_state and next_state here!
let ota_target = match target {
AppPartitionSubType::Ota0 => pt
.find_partition(PartitionType::App(AppPartitionSubType::Ota0))?
.context("Partition table invalid no ota0")?,
AppPartitionSubType::Ota1 => pt
.find_partition(PartitionType::App(AppPartitionSubType::Ota1))?
.context("Partition table invalid no ota1")?,
_ => {
bail!("Invalid target partition");
}
};
let ota_target = mk_static!(PartitionEntry, ota_target);
let ota_target = mk_static!(
FlashRegion<MutexFlashStorage>,
ota_target.as_embedded_storage(flash_storage)
);
let data_partition = pt
.find_partition(esp_bootloader_esp_idf::partitions::PartitionType::Data(
DataPartitionSubType::LittleFs,
))?
.expect("Data partition with littlefs not found");
let data_partition = mk_static!(PartitionEntry, data_partition);
let data = mk_static!(
FlashRegion<MutexFlashStorage>,
data_partition.as_embedded_storage(flash_storage_3)
);
let lfs2filesystem = mk_static!(LittleFs2Filesystem, LittleFs2Filesystem { storage: data });
let alloc = mk_static!(Allocation<LittleFs2Filesystem>, lfs2Filesystem::allocate());
if lfs2filesystem.is_mountable() {
info!("Littlefs2 filesystem is mountable");
} else {
match lfs2filesystem.format() {
Ok(..) => {
info!("Littlefs2 filesystem is formatted");
}
Err(err) => {
error!("Littlefs2 filesystem could not be formatted: {err:?}");
}
}
}
#[allow(clippy::arc_with_non_send_sync)]
let fs = Arc::new(Mutex::new(
lfs2Filesystem::mount(alloc, lfs2filesystem).expect("Could not mount lfs2 filesystem"),
));
let uart0 =
Uart::new(peripherals.UART0, UartConfig::default()).map_err(|_| FatError::String {
error: "Uart creation failed".to_string(),
})?;
let ap = interfaces.access_point;
let sta = interfaces.station;
let mut esp = Esp {
fs,
rng,
controller: Arc::new(Mutex::new(controller)),
interface_sta: Some(sta),
interface_ap: Some(ap),
boot_button,
delay: Delay::new(1000),
wake_gpio1,
ota,
ota_target,
current: running,
slot0_state: state_0,
slot1_state: state_1,
uart0,
rtc: rtc_peripheral,
};
//init,reset rtc memory depending on cause
let mut init_rtc_store: bool = false;
let mut to_config_mode: bool = false;
let reasons = ResetReason::get();
match reasons {
ResetReason::Software => {}
ResetReason::ExternalPin => {}
ResetReason::Watchdog => {
init_rtc_store = true;
}
ResetReason::Sdio => init_rtc_store = true,
ResetReason::Panic => init_rtc_store = true,
ResetReason::InterruptWatchdog => init_rtc_store = true,
ResetReason::PowerOn => init_rtc_store = true,
ResetReason::Unknown => init_rtc_store = true,
ResetReason::Brownout => init_rtc_store = true,
ResetReason::TaskWatchdog => init_rtc_store = true,
ResetReason::DeepSleep => {}
ResetReason::USBPeripheral => {
init_rtc_store = true;
to_config_mode = true;
}
ResetReason::JTAG => init_rtc_store = true,
let reasons = match reset_reason() {
None => "unknown",
Some(reason) => match reason {
SocResetReason::ChipPowerOn => "power on",
SocResetReason::CoreSDIO => "sdio reset",
SocResetReason::CoreMwdt0 => "Watchdog Main",
SocResetReason::CoreMwdt1 => "Watchdog 1",
SocResetReason::CoreRtcWdt => "Watchdog RTC",
SocResetReason::Cpu0Mwdt0 => "Watchdog MCpu0",
SocResetReason::Cpu0Sw => "software reset cpu0",
SocResetReason::SysRtcWdt => "Watchdog Sys rtc",
SocResetReason::Cpu0Mwdt1 => "cpu0 mwdt1",
SocResetReason::SysSuperWdt => "Watchdog Super",
SocResetReason::Cpu0RtcWdt => {
init_rtc_store = true;
"Watchdog RTC cpu0"
}
SocResetReason::CoreSw => "software reset",
SocResetReason::CoreDeepSleep => "deep sleep",
SocResetReason::SysBrownOut => "sys brown out",
SocResetReason::CoreEfuseCrc => "core efuse crc",
SocResetReason::CoreUsbUart => {
//TODO still required? or via button ignore? to_config_mode = true;
to_config_mode = true;
"core usb uart"
}
SocResetReason::CoreUsbJtag => "core usb jtag",
SocResetReason::Cpu0JtagCpu => "cpu0 jtag cpu",
},
};
log(
LogMessage::ResetReason,
@@ -278,52 +498,100 @@ impl PlantHal {
&format!("{reasons:?}"),
);
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode);
let fs_mount_error = esp.mount_file_system().is_err();
let config = esp.load_config();
esp.init_rtc_deepsleep_memory(init_rtc_store, to_config_mode)
.await;
let config = esp.load_config().await;
info!("Init rtc driver");
let sda = peripherals.GPIO20;
let scl = peripherals.GPIO19;
let i2c = I2c::new(
peripherals.I2C0,
Config::default()
.with_frequency(Rate::from_hz(100))
.with_timeout(BusTimeout::Maximum),
)?
.with_scl(scl)
.with_sda(sda);
let i2c_bus: embassy_sync::blocking_mutex::Mutex<
CriticalSectionRawMutex,
RefCell<I2c<Blocking>>,
> = CriticalSectionMutex::new(RefCell::new(i2c));
I2C_DRIVER.init(i2c_bus).expect("Could not init i2c driver");
let i2c_bus = I2C_DRIVER.get().await;
let rtc_device = I2cDevice::new(i2c_bus);
let mut bms_device = I2cDevice::new(i2c_bus);
let eeprom_device = I2cDevice::new(i2c_bus);
let mut rtc: Ds323x<
I2cInterface<I2cDevice<CriticalSectionRawMutex, I2c<Blocking>>>,
DS3231,
> = Ds323x::new_ds3231(rtc_device);
info!("Init rtc eeprom driver");
let eeprom = Eeprom24x::new_24x32(eeprom_device, SlaveAddr::Alternative(true, true, true));
let rtc_time = rtc.datetime();
match rtc_time {
Ok(tt) => {
info!("Rtc Module reports time at UTC {tt}");
}
Err(err) => {
info!("Rtc Module could not be read {err:?}");
}
}
let storage: Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<Blocking>>,
B32,
TwoBytes,
No,
Delay,
> = Storage::new(eeprom, Delay::new());
let rtc_module: Box<dyn RTCModuleInteraction + Send> =
Box::new(DS3231Module { rtc, storage }) as Box<dyn RTCModuleInteraction + Send>;
let hal = match config {
Result::Ok(config) => {
Ok(config) => {
let battery_interaction: Box<dyn BatteryInteraction + Send> =
match config.hardware.battery {
BatteryBoardVersion::Disabled => Box::new(NoBatteryMonitor {}),
BatteryBoardVersion::BQ34Z100G1 => {
let mut battery_driver = Bq34z100g1Driver {
i2c: MutexDevice::new(&I2C_DRIVER),
delay: Delay::new(0),
flash_block_data: [0; 32],
};
let status = print_battery_bq34z100(&mut battery_driver);
match status {
OkStd(_) => {}
Err(err) => {
log(
LogMessage::BatteryCommunicationError,
0u32,
0,
"",
&format!("{err:?})"),
);
}
}
Box::new(BQ34Z100G1 { battery_driver })
}
BatteryBoardVersion::WchI2cSlave => {
// TODO use correct implementation once availible
Box::new(NoBatteryMonitor {})
let version = ProtocolVersion::read_from_i2c(&mut bms_device);
let version_val = match version {
Ok(v) => unsafe { core::mem::transmute::<ProtocolVersion, u32>(v) },
Err(_) => 0,
};
if version_val == 1 {
//Box::new(WCHI2CSlave { i2c: bms_device })
// todo fix the type above
Box::new(NoBatteryMonitor {})
} else {
//todo should be an error variant instead?
Box::new(NoBatteryMonitor {})
}
}
BatteryBoardVersion::BQ34Z100G1 => Box::new(NoBatteryMonitor {}),
};
let board_hal: Box<dyn BoardInteraction + Send> = match config.hardware.board {
BoardVersion::INITIAL => {
initial_hal::create_initial_board(free_pins, fs_mount_error, config, esp)?
initial_hal::create_initial_board(free_pins, config, esp)?
}
BoardVersion::V3 => {
v3_hal::create_v3(free_pins, esp, config, battery_interaction)?
v3_hal::create_v3(free_pins, esp, config, battery_interaction, rtc_module)?
}
BoardVersion::V4 => {
v4_hal::create_v4(free_pins, esp, config, battery_interaction)?
v4_hal::create_v4(free_pins, esp, config, battery_interaction, rtc_module)
.await?
}
};
@@ -340,7 +608,6 @@ impl PlantHal {
HAL {
board_hal: initial_hal::create_initial_board(
free_pins,
fs_mount_error,
PlantControllerConfig::default(),
esp,
)?,
@@ -350,4 +617,13 @@ impl PlantHal {
Ok(Mutex::new(hal))
}
/// Feed the watchdog timer to prevent system reset
pub fn feed_watchdog() {
if let Some(wdt_mutex) = WATCHDOG.try_get() {
wdt_mutex.lock(|cell| {
cell.borrow_mut().feed();
});
}
}
}

133
rust/src/hal/rtc.rs Normal file
View File

@@ -0,0 +1,133 @@
use crate::hal::Box;
use crate::fat_error::FatResult;
use async_trait::async_trait;
use bincode::config::Configuration;
use bincode::{config, Decode, Encode};
use chrono::{DateTime, Utc};
use ds323x::ic::DS3231;
use ds323x::interface::I2cInterface;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::addr_size::TwoBytes;
use eeprom24x::page_size::B32;
use eeprom24x::unique_serial::No;
use embassy_embedded_hal::shared_bus::blocking::i2c::I2cDevice;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embedded_storage::{ReadStorage, Storage};
use esp_hal::delay::Delay;
use esp_hal::i2c::master::I2c;
use esp_hal::Blocking;
use serde::{Deserialize, Serialize};
pub const X25: crc::Crc<u16> = crc::Crc::<u16>::new(&crc::CRC_16_IBM_SDLC);
const CONFIG: Configuration = config::standard();
//
#[async_trait]
pub trait RTCModuleInteraction {
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 backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()>;
async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()>;
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>>;
async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()>;
}
//
const BACKUP_HEADER_MAX_SIZE: usize = 64;
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Encode, Decode)]
pub struct BackupHeader {
pub timestamp: i64,
crc16: u16,
pub size: u16,
}
//
pub struct DS3231Module {
pub(crate) rtc: Ds323x<
I2cInterface<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
DS3231,
>,
pub(crate) storage: eeprom24x::Storage<
I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>,
B32,
TwoBytes,
No,
Delay,
>,
}
#[async_trait]
impl RTCModuleInteraction for DS3231Module {
async fn get_backup_info(&mut self) -> FatResult<BackupHeader> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.storage.read(0, &mut header_page_buffer)?;
let (header, len): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
log::info!("Raw header is {:?} with size {}", header_page_buffer, len);
Ok(header)
}
async fn get_backup_config(&mut self, chunk: usize) -> FatResult<([u8; 32], usize, u16)> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
self.storage.read(0, &mut header_page_buffer)?;
let (header, _header_size): (BackupHeader, usize) =
bincode::decode_from_slice(&header_page_buffer[..], CONFIG)?;
let mut buf = [0_u8; 32];
let offset = chunk * buf.len() + BACKUP_HEADER_MAX_SIZE;
let end: usize = header.size as usize + BACKUP_HEADER_MAX_SIZE;
let current_end = offset + buf.len();
let chunk_size = if current_end > end {
end - offset
} else {
buf.len()
};
if chunk_size == 0 {
Ok((buf, 0, header.crc16))
} else {
self.storage.read(offset as u32, &mut buf)?;
//&buf[..chunk_size];
Ok((buf, chunk_size, header.crc16))
}
}
async fn backup_config(&mut self, offset: usize, bytes: &[u8]) -> FatResult<()> {
//skip header and write after
self.storage
.write((BACKUP_HEADER_MAX_SIZE + offset) as u32, &bytes)?;
Ok(())
}
async fn backup_config_finalize(&mut self, crc: u16, length: usize) -> FatResult<()> {
let mut header_page_buffer = [0_u8; BACKUP_HEADER_MAX_SIZE];
let time = self.get_rtc_time().await?.timestamp_millis();
let header = BackupHeader {
crc16: crc,
timestamp: time,
size: length as u16,
};
let config = config::standard();
let encoded = bincode::encode_into_slice(&header, &mut header_page_buffer, config)?;
log::info!(
"Raw header is {:?} with size {}",
header_page_buffer,
encoded
);
self.storage.write(0, &header_page_buffer)?;
Ok(())
}
async fn get_rtc_time(&mut self) -> FatResult<DateTime<Utc>> {
Ok(self.rtc.datetime()?.and_utc())
}
async fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> FatResult<()> {
let naive_time = time.naive_utc();
Ok(self.rtc.set_datetime(&naive_time)?)
}
}

View File

@@ -0,0 +1,65 @@
use alloc::sync::Arc;
use core::cell::RefCell;
use core::ops::{Deref, DerefMut};
use embassy_sync::blocking_mutex::CriticalSectionMutex;
use embedded_storage::nor_flash::{ErrorType, NorFlash, ReadNorFlash};
use embedded_storage::ReadStorage;
use esp_storage::{FlashStorage, FlashStorageError};
use log::info;
#[derive(Clone)]
pub struct MutexFlashStorage {
pub(crate) inner: Arc<CriticalSectionMutex<RefCell<FlashStorage<'static>>>>,
}
impl ReadStorage for MutexFlashStorage {
type Error = FlashStorageError;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), FlashStorageError> {
self.inner
.lock(|f| ReadStorage::read(f.borrow_mut().deref_mut(), offset, bytes))
}
fn capacity(&self) -> usize {
self.inner
.lock(|f| ReadStorage::capacity(f.borrow().deref()))
}
}
impl embedded_storage::Storage for MutexFlashStorage {
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
NorFlash::write(self, offset, bytes)
}
}
impl ErrorType for MutexFlashStorage {
type Error = FlashStorageError;
}
impl ReadNorFlash for MutexFlashStorage {
const READ_SIZE: usize = 1;
fn read(&mut self, offset: u32, bytes: &mut [u8]) -> Result<(), Self::Error> {
ReadStorage::read(self, offset, bytes)
}
fn capacity(&self) -> usize {
ReadStorage::capacity(self)
}
}
impl NorFlash for MutexFlashStorage {
const WRITE_SIZE: usize = 1;
const ERASE_SIZE: usize = 4096;
fn erase(&mut self, from: u32, to: u32) -> Result<(), Self::Error> {
info!("Erasing flash from 0x{:x} to 0x{:x}", from, to);
self.inner
.lock(|f| NorFlash::erase(f.borrow_mut().deref_mut(), from, to))
}
fn write(&mut self, offset: u32, bytes: &[u8]) -> Result<(), Self::Error> {
self.inner
.lock(|f| NorFlash::write(f.borrow_mut().deref_mut(), offset, bytes))
}
}

View File

@@ -1,33 +1,30 @@
use crate::config::PlantControllerConfig;
use crate::hal::battery::BatteryInteraction;
use crate::hal::esp::ESP;
use crate::hal::{
deep_sleep, BackupHeader, BoardInteraction, FreePeripherals, Sensor, V3Constants, I2C_DRIVER,
PLANT_COUNT, REPEAT_MOIST_MEASURE, TANK_MULTI_SAMPLE, X25,
use crate::bail;
use crate::fat_error::{FatError, FatResult};
use crate::hal::esp::{hold_disable, hold_enable};
use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::v3_shift_register::ShiftRegister40;
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, PLANT_COUNT};
use crate::log::{log, LogMessage, LOG_ACCESS};
use crate::{
config::PlantControllerConfig,
hal::{battery::BatteryInteraction, esp::Esp},
};
use crate::log::{log, LogMessage};
use anyhow::{anyhow, bail, Ok, Result};
use chrono::{DateTime, Utc};
use ds18b20::Ds18b20;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::{Eeprom24x, Eeprom24xTrait, SlaveAddr};
use embedded_hal::digital::OutputPin;
use embedded_hal_bus::i2c::MutexDevice;
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
use esp_idf_hal::adc::{attenuation, Resolution};
use esp_idf_hal::delay::Delay;
use esp_idf_hal::gpio::{AnyInputPin, Gpio5, IOPin, InputOutput, PinDriver, Pull};
use esp_idf_hal::i2c::I2cDriver;
use esp_idf_hal::pcnt::{
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex,
};
use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError};
use alloc::boxed::Box;
use alloc::format;
use alloc::string::ToString;
use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::Mutex;
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};
use one_wire_bus::OneWire;
use plant_ctrl2::sipo::ShiftRegister40;
use std::result::Result::Ok as OkStd;
use std::sync::Arc;
const PUMP8_BIT: usize = 0;
const PUMP1_BIT: usize = 1;
@@ -74,201 +71,117 @@ const FAULT_4: usize = 21;
const FAULT_1: usize = 22;
const FAULT_2: usize = 23;
const REPEAT_MOIST_MEASURE: usize = 1;
pub struct V3<'a> {
config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>,
esp: ESP<'a>,
shift_register: ShiftRegister40<
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
>,
shift_register_enable_invert:
PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Output>,
tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, esp_idf_hal::adc::ADC1>>,
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>,
light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
main_pump: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
signal_counter: PcntDriver<'a>,
one_wire_bus: OneWire<PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>>,
rtc:
Ds323x<ds323x::interface::I2cInterface<MutexDevice<'a, I2cDriver<'a>>>, ds323x::ic::DS3231>,
eeprom: Eeprom24x<
MutexDevice<'a, I2cDriver<'a>>,
eeprom24x::page_size::B32,
eeprom24x::addr_size::TwoBytes,
eeprom24x::unique_serial::No,
>,
rtc_module: Box<dyn RTCModuleInteraction + Send>,
esp: Esp<'a>,
shift_register:
Mutex<CriticalSectionRawMutex, ShiftRegister40<Output<'a>, Output<'a>, Output<'a>>>,
_shift_register_enable_invert: Output<'a>,
tank_sensor: TankSensor<'a>,
solar_is_day: Input<'a>,
light: Output<'a>,
main_pump: Output<'a>,
general_fault: Output<'a>,
pub signal_counter: Unit<'static, 0>,
}
pub(crate) fn create_v3(
peripherals: FreePeripherals,
esp: ESP<'static>,
peripherals: FreePeripherals<'static>,
esp: Esp<'static>,
config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>,
) -> Result<Box<dyn BoardInteraction + Send + '_>> {
let mut clock = PinDriver::input_output(peripherals.gpio15.downgrade())?;
clock.set_pull(Pull::Floating)?;
let mut latch = PinDriver::input_output(peripherals.gpio3.downgrade())?;
latch.set_pull(Pull::Floating)?;
let mut data = PinDriver::input_output(peripherals.gpio23.downgrade())?;
data.set_pull(Pull::Floating)?;
rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
log::info!("Start v3");
let clock = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
let latch = Output::new(peripherals.gpio3, Level::Low, OutputConfig::default());
let data = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
let shift_register = ShiftRegister40::new(clock, latch, data);
//disable all
for mut pin in shift_register.decompose() {
pin.set_low()?;
let _ = pin.set_low();
}
let awake = &mut shift_register.decompose()[AWAKE];
awake.set_high()?;
// Set always-on status bits
let _ = shift_register.decompose()[AWAKE].set_high();
let _ = shift_register.decompose()[CHARGING].set_high();
let charging = &mut shift_register.decompose()[CHARGING];
charging.set_high()?;
// Multiplexer defaults: ms0..ms3 low, ms4 high (disabled)
let _ = shift_register.decompose()[MS_0].set_low();
let _ = shift_register.decompose()[MS_1].set_low();
let _ = shift_register.decompose()[MS_2].set_low();
let _ = shift_register.decompose()[MS_3].set_low();
let _ = shift_register.decompose()[MS_4].set_high();
let ms0 = &mut shift_register.decompose()[MS_0];
ms0.set_low()?;
let ms1 = &mut shift_register.decompose()[MS_1];
ms1.set_low()?;
let ms2 = &mut shift_register.decompose()[MS_2];
ms2.set_low()?;
let ms3 = &mut shift_register.decompose()[MS_3];
ms3.set_low()?;
let one_wire_pin = Flex::new(peripherals.gpio18);
let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
let ms4 = &mut shift_register.decompose()[MS_4];
ms4.set_high()?;
let flow_sensor_pin = Input::new(
peripherals.gpio4,
InputConfig::default().with_pull(Pull::Up),
);
println!("Init battery driver");
println!("Init rtc driver");
let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER));
println!("Init rtc eeprom driver");
let mut eeprom = {
Eeprom24x::new_24x32(
MutexDevice::new(&I2C_DRIVER),
SlaveAddr::Alternative(true, true, true),
)
};
let mut one_wire_pin = PinDriver::input_output_od(peripherals.gpio18.downgrade())?;
one_wire_pin.set_pull(Pull::Floating)?;
let rtc_time = rtc.datetime();
match rtc_time {
OkStd(tt) => {
println!("Rtc Module reports time at UTC {}", tt);
}
Err(err) => {
println!("Rtc Module could not be read {:?}", err);
}
}
match eeprom.read_byte(0) {
OkStd(byte) => {
println!("Read first byte with status {}", byte);
}
Err(err) => {
println!("Eeprom could not read first byte {:?}", err);
}
}
let mut signal_counter = PcntDriver::new(
peripherals.pcnt0,
Some(peripherals.gpio22),
Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None,
let tank_sensor = TankSensor::create(
one_wire_pin,
peripherals.adc1,
peripherals.gpio5,
tank_power_pin,
flow_sensor_pin,
peripherals.pcnt1,
)?;
signal_counter.channel_config(
PcntChannel::Channel0,
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,
},
)?;
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
let light = Output::new(peripherals.gpio10, Level::Low, OutputConfig::default());
let mut main_pump = Output::new(peripherals.gpio2, Level::Low, OutputConfig::default());
main_pump.set_low();
let mut general_fault = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
general_fault.set_low();
let adc_config = AdcChannelConfig {
attenuation: attenuation::DB_11,
resolution: Resolution::Resolution12Bit,
calibration: esp_idf_hal::adc::oneshot::config::Calibration::Curve,
};
let tank_driver = AdcDriver::new(peripherals.adc1)?;
let tank_channel: AdcChannelDriver<Gpio5, AdcDriver<esp_idf_hal::adc::ADC1>> =
AdcChannelDriver::new(tank_driver, peripherals.gpio5, &adc_config)?;
hold_disable(21);
let mut shift_register_enable_invert =
Output::new(peripherals.gpio21, Level::Low, OutputConfig::default());
shift_register_enable_invert.set_low();
hold_enable(21);
let mut solar_is_day = PinDriver::input(peripherals.gpio7.downgrade())?;
solar_is_day.set_pull(Pull::Floating)?;
let signal_counter = peripherals.pcnt0;
let mut light = PinDriver::input_output(peripherals.gpio10.downgrade())?;
light.set_pull(Pull::Floating)?;
signal_counter.set_low_limit(None)?;
signal_counter.set_high_limit(Some(i16::MAX))?;
let mut main_pump = PinDriver::input_output(peripherals.gpio2.downgrade())?;
main_pump.set_pull(Pull::Floating)?;
main_pump.set_low()?;
let mut tank_power = PinDriver::input_output(peripherals.gpio11.downgrade())?;
tank_power.set_pull(Pull::Floating)?;
let mut general_fault = PinDriver::input_output(peripherals.gpio6.downgrade())?;
general_fault.set_pull(Pull::Floating)?;
general_fault.set_low()?;
let one_wire_bus = OneWire::new(one_wire_pin)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let mut shift_register_enable_invert = PinDriver::output(peripherals.gpio21.downgrade())?;
unsafe { gpio_hold_dis(shift_register_enable_invert.pin()) };
shift_register_enable_invert.set_low()?;
unsafe { gpio_hold_en(shift_register_enable_invert.pin()) };
let ch0 = &signal_counter.channel0;
let edge_pin = Input::new(peripherals.gpio22, InputConfig::default());
ch0.set_edge_signal(edge_pin.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
signal_counter.listen();
Ok(Box::new(V3 {
config,
battery_monitor,
rtc_module,
esp,
shift_register,
shift_register_enable_invert,
tank_channel,
shift_register: Mutex::new(shift_register),
_shift_register_enable_invert: shift_register_enable_invert,
tank_sensor,
solar_is_day,
light,
main_pump,
tank_power,
general_fault,
signal_counter,
one_wire_bus,
rtc,
eeprom,
}))
}
#[async_trait]
impl<'a> BoardInteraction<'a> for V3<'a> {
fn set_charge_indicator(&mut self, charging: bool) -> Result<()> {
Ok(self.shift_register.decompose()[CHARGING].set_state(charging.into())?)
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
Ok(&mut self.tank_sensor)
}
fn is_day(&self) -> bool {
self.solar_is_day.get_level().into()
}
fn get_mptt_voltage(&mut self) -> Result<Voltage> {
//if working this is the hardware set mppt voltage
if self.is_day() {
Ok(Voltage::from_volts(15_f64))
} else {
Ok(Voltage::from_volts(0_f64))
}
}
fn get_mptt_current(&mut self) -> Result<Current> {
bail!("Board does not have current sensor")
}
fn get_esp(&mut self) -> &mut ESP<'a> {
fn get_esp(&mut self) -> &mut Esp<'a> {
&mut self.esp
}
@@ -276,165 +189,56 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
&self.config
}
fn get_battery_monitor(&mut self) -> &mut Box<(dyn BatteryInteraction + Send + 'static)> {
fn get_battery_monitor(&mut self) -> &mut Box<dyn BatteryInteraction + Send> {
&mut self.battery_monitor
}
fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
let _ = self.shift_register.decompose()[AWAKE].set_low();
deep_sleep(duration_in_ms)
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc_module
}
fn get_backup_info(&mut self) -> Result<BackupHeader> {
let store = bincode::serialize(&BackupHeader::default())?.len();
let mut header_page_buffer = vec![0_u8; store];
self.eeprom
.read_data(0, &mut header_page_buffer)
.map_err(|err| anyhow!("Error reading eeprom header {:?}", err))?;
println!("Raw header is {:?} with size {}", header_page_buffer, store);
let header: BackupHeader = bincode::deserialize(&header_page_buffer)?;
Ok(header)
async fn get_time(&mut self) -> DateTime<Utc> {
self.esp.get_time()
}
fn get_backup_config(&mut self) -> Result<Vec<u8>> {
let store = bincode::serialize(&BackupHeader::default())?.len();
let mut header_page_buffer = vec![0_u8; store];
self.eeprom
.read_data(0, &mut header_page_buffer)
.map_err(|err| anyhow!("Error reading eeprom header {:?}", err))?;
let header: BackupHeader = bincode::deserialize(&header_page_buffer)?;
//skip page 0, used by the header
let data_start_address = self.eeprom.page_size() as u32;
let mut data_buffer = vec![0_u8; header.size];
self.eeprom
.read_data(data_start_address, &mut data_buffer)
.map_err(|err| anyhow!("Error reading eeprom data {:?}", err))?;
let checksum = X25.checksum(&data_buffer);
if checksum != header.crc16 {
bail!(
"Invalid checksum, got {} but expected {}",
checksum,
header.crc16
);
}
Ok(data_buffer)
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
self.esp.set_time(time.to_utc());
Ok(())
}
fn backup_config(&mut self, bytes: &[u8]) -> Result<()> {
let time = self.get_rtc_time()?.timestamp_millis();
let delay = Delay::new_default();
let checksum = X25.checksum(bytes);
let page_size = self.eeprom.page_size();
let header = BackupHeader {
crc16: checksum,
timestamp: time,
size: bytes.len(),
};
let encoded = bincode::serialize(&header)?;
if encoded.len() > page_size {
bail!(
"Size limit reached header is {}, but firest page is only {}",
encoded.len(),
page_size
)
}
let as_u8: &[u8] = &encoded;
match self.eeprom.write_page(0, as_u8) {
OkStd(_) => {}
Err(err) => bail!("Error writing eeprom {:?}", err),
};
delay.delay_ms(5);
let to_write = bytes.chunks(page_size);
let mut lastiter = 0;
let mut current_page = 1;
for chunk in to_write {
let address = current_page * page_size as u32;
self.eeprom
.write_page(address, chunk)
.map_err(|err| anyhow!("Error writing eeprom {:?}", err))?;
current_page += 1;
let iter = (current_page % 8) as usize;
if iter != lastiter {
for i in 0..PLANT_COUNT {
let _ = self.fault(i, iter == i);
}
lastiter = iter;
}
delay.delay_ms(5);
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
let shift_register = self.shift_register.lock().await;
if charging {
let _ = shift_register.decompose()[CHARGING].set_high();
} else {
let _ = shift_register.decompose()[CHARGING].set_low();
}
Ok(())
}
fn water_temperature_c(&mut self) -> Result<f32> {
self.one_wire_bus
.reset(&mut self.esp.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let first = self.one_wire_bus.devices(false, &mut self.esp.delay).next();
if first.is_none() {
bail!("Not found any one wire Ds18b20");
}
let device_address = first
.unwrap()
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let water_temp_sensor = Ds18b20::new::<EspError>(device_address)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
water_temp_sensor
.start_temp_measurement(&mut self.one_wire_bus, &mut self.esp.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut self.esp.delay);
let sensor_data = water_temp_sensor
.read_data(&mut self.one_wire_bus, &mut self.esp.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
if sensor_data.temperature == 85_f32 {
bail!("Ds18b20 dummy temperature returned");
}
Ok(sensor_data.temperature / 10_f32)
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
let _ = self.shift_register.lock().await.decompose()[AWAKE].set_low();
self.esp.deep_sleep_ms(duration_in_ms)
}
fn tank_sensor_voltage(&mut self) -> Result<f32> {
self.tank_power.set_high()?;
//let stabilize
self.esp.delay.delay_ms(100);
let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE {
let value = self.tank_channel.read()?;
store[multisample] = value;
}
self.tank_power.set_low()?;
store.sort();
let median_mv = store[6] as f32 / 1000_f32;
Ok(median_mv)
}
fn light(&mut self, enable: bool) -> Result<()> {
unsafe { gpio_hold_dis(self.light.pin()) };
self.light.set_state(enable.into())?;
unsafe { gpio_hold_en(self.light.pin()) };
Ok(())
fn is_day(&self) -> bool {
self.solar_is_day.is_high()
}
fn pump(&mut self, plant: usize, enable: bool) -> Result<()> {
async fn light(&mut self, enable: bool) -> Result<(), FatError> {
hold_disable(10);
if enable {
self.main_pump.set_high()?;
self.light.set_high();
} else {
self.light.set_low();
}
hold_enable(10);
Ok(())
}
async fn pump(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
if enable {
self.main_pump.set_high();
}
let index = match plant {
@@ -446,18 +250,26 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
5 => PUMP6_BIT,
6 => PUMP7_BIT,
7 => PUMP8_BIT,
_ => bail!("Invalid pump {plant}",),
_ => bail!("Invalid pump {plant}"),
};
//currently infallible error, keep for future as result anyway
self.shift_register.decompose()[index].set_state(enable.into())?;
let shift_register = self.shift_register.lock().await;
if enable {
let _ = shift_register.decompose()[index].set_high();
} else {
let _ = shift_register.decompose()[index].set_low();
}
if !enable {
self.main_pump.set_low()?;
self.main_pump.set_low();
}
Ok(())
}
fn fault(&mut self, plant: usize, enable: bool) -> Result<()> {
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
bail!("Not implemented in v3")
}
async fn fault(&mut self, plant: usize, enable: bool) -> Result<(), FatError> {
let index = match plant {
0 => FAULT_1,
1 => FAULT_2,
@@ -469,17 +281,25 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
7 => FAULT_8,
_ => panic!("Invalid plant id {}", plant),
};
self.shift_register.decompose()[index].set_state(enable.into())?;
let shift_register = self.shift_register.lock().await;
if enable {
let _ = shift_register.decompose()[index].set_high();
} else {
let _ = shift_register.decompose()[index].set_low();
}
Ok(())
}
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32> {
async fn 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.counter_pause()?;
self.signal_counter.counter_clear()?;
self.signal_counter.pause();
self.signal_counter.clear();
//Disable all
self.shift_register.decompose()[MS_4].set_high()?;
{
let shift_register = self.shift_register.lock().await;
shift_register.decompose()[MS_4].set_high()?;
}
let sensor_channel = match sensor {
Sensor::A => match plant {
@@ -507,46 +327,51 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
};
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
let pin_0 = &mut self.shift_register.decompose()[MS_0];
let pin_1 = &mut self.shift_register.decompose()[MS_1];
let pin_2 = &mut self.shift_register.decompose()[MS_2];
let pin_3 = &mut self.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()?;
}
{
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()?;
}
self.shift_register.decompose()[MS_4].set_low()?;
self.shift_register.decompose()[SENSOR_ON].set_high()?;
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000f32 / measurement as f32;
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
self.esp.delay.delay_ms(10);
self.signal_counter.counter_resume()?;
self.esp.delay.delay_ms(measurement);
self.signal_counter.counter_pause()?;
self.shift_register.decompose()[MS_4].set_high()?;
self.shift_register.decompose()[SENSOR_ON].set_low()?;
self.esp.delay.delay_ms(10);
let unscaled = self.signal_counter.get_counter_value()? as i32;
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(
LogMessage::RawMeasure,
@@ -564,88 +389,63 @@ impl<'a> BoardInteraction<'a> for V3<'a> {
Ok(median)
}
fn general_fault(&mut self, enable: bool) {
unsafe { gpio_hold_dis(self.general_fault.pin()) };
self.general_fault.set_state(enable.into()).unwrap();
unsafe { gpio_hold_en(self.general_fault.pin()) };
}
fn factory_reset(&mut self) -> Result<()> {
println!("factory resetting");
self.esp.delete_config()?;
//destroy backup header
let dummy: [u8; 0] = [];
self.backup_config(&dummy)?;
Ok(())
}
fn get_rtc_time(&mut self) -> Result<DateTime<Utc>> {
match self.rtc.datetime() {
OkStd(rtc_time) => Ok(rtc_time.and_utc()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
async fn general_fault(&mut self, enable: bool) {
hold_disable(6);
if enable {
self.general_fault.set_high();
} else {
self.general_fault.set_low();
}
hold_enable(6);
}
fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> Result<()> {
let naive_time = time.naive_utc();
match self.rtc.set_datetime(&naive_time) {
OkStd(_) => Ok(()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
}
}
async fn test(&mut self) -> Result<(), FatError> {
self.general_fault(true).await;
Timer::after_millis(100).await;
self.general_fault(false).await;
Timer::after_millis(100).await;
self.light(true).await?;
Timer::after_millis(500).await;
fn test_pump(&mut self, plant: usize) -> Result<()> {
self.pump(plant, true)?;
unsafe { vTaskDelay(30000) };
self.pump(plant, false)?;
Ok(())
}
fn test(&mut self) -> Result<()> {
self.general_fault(true);
self.esp.delay.delay_ms(100);
self.general_fault(false);
self.esp.delay.delay_ms(100);
self.light(true)?;
self.esp.delay.delay_ms(500);
self.light(false)?;
self.esp.delay.delay_ms(500);
self.light(false).await?;
Timer::after_millis(500).await;
for i in 0..PLANT_COUNT {
self.fault(i, true)?;
self.esp.delay.delay_ms(500);
self.fault(i, false)?;
self.esp.delay.delay_ms(500);
self.fault(i, true).await?;
Timer::after_millis(500).await;
self.fault(i, false).await?;
Timer::after_millis(500).await;
}
for i in 0..PLANT_COUNT {
self.pump(i, true)?;
self.esp.delay.delay_ms(100);
self.pump(i, false)?;
self.esp.delay.delay_ms(100);
self.pump(i, true).await?;
Timer::after_millis(100).await;
self.pump(i, false).await?;
Timer::after_millis(100).await;
}
for plant in 0..PLANT_COUNT {
let a = self.measure_moisture_hz(plant, Sensor::A);
let b = self.measure_moisture_hz(plant, Sensor::B);
let a = self.measure_moisture_hz(plant, Sensor::A).await;
let b = self.measure_moisture_hz(plant, Sensor::B).await;
let aa = match a {
OkStd(a) => a as u32,
Ok(a) => a as u32,
Err(_) => u32::MAX,
};
let bb = match b {
OkStd(b) => b as u32,
Ok(b) => b as u32,
Err(_) => u32::MAX,
};
log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "");
}
self.esp.delay.delay_ms(10);
Timer::after_millis(10).await;
Ok(())
}
fn set_config(&mut self, config: PlantControllerConfig) -> anyhow::Result<()> {
fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config;
self.esp.save_config(&self.config)?;
anyhow::Ok(())
}
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
bail!("Not implemented in v3")
}
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
bail!("Not implemented in v3")
}
}

View File

@@ -1,12 +1,13 @@
//! Serial-in parallel-out shift register
#![allow(warnings)]
use core::cell::RefCell;
use core::convert::Infallible;
use core::iter::Iterator;
use core::mem::{self, MaybeUninit};
use std::convert::Infallible;
use core::result::{Result, Result::Ok};
use embedded_hal::digital::OutputPin;
use hal::digital::OutputPin;
trait ShiftRegisterInternal {
trait ShiftRegisterInternal: Send {
fn update(&self, index: usize, command: bool) -> Result<(), ()>;
}
@@ -46,9 +47,9 @@ macro_rules! ShiftRegisterBuilder {
/// Serial-in parallel-out shift register
pub struct $name<Pin1, Pin2, Pin3>
where
Pin1: OutputPin,
Pin2: OutputPin,
Pin3: OutputPin,
Pin1: OutputPin + Send,
Pin2: OutputPin + Send,
Pin3: OutputPin + Send,
{
clock: RefCell<Pin1>,
latch: RefCell<Pin2>,
@@ -58,9 +59,9 @@ macro_rules! ShiftRegisterBuilder {
impl<Pin1, Pin2, Pin3> ShiftRegisterInternal for $name<Pin1, Pin2, Pin3>
where
Pin1: OutputPin,
Pin2: OutputPin,
Pin3: OutputPin,
Pin1: OutputPin + Send,
Pin2: OutputPin + Send,
Pin3: OutputPin + Send,
{
/// Sets the value of the shift register output at `index` to value `command`
fn update(&self, index: usize, command: bool) -> Result<(), ()> {
@@ -85,9 +86,9 @@ macro_rules! ShiftRegisterBuilder {
impl<Pin1, Pin2, Pin3> $name<Pin1, Pin2, Pin3>
where
Pin1: OutputPin,
Pin2: OutputPin,
Pin3: OutputPin,
Pin1: OutputPin + Send,
Pin2: OutputPin + Send,
Pin3: OutputPin + Send,
{
/// Creates a new SIPO shift register from clock, latch, and data output pins
pub fn new(clock: Pin1, latch: Pin2, data: Pin3) -> Self {
@@ -100,7 +101,7 @@ macro_rules! ShiftRegisterBuilder {
}
/// Get embedded-hal output pins to control the shift register outputs
pub fn decompose(&self) -> [ShiftRegisterPin; $size] {
pub fn decompose(&self) -> [ShiftRegisterPin<'_>; $size] {
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
// safe because the type we are claiming to have initialized here is a
// bunch of `MaybeUninit`s, which do not require initialization.

View File

@@ -1,228 +1,235 @@
use crate::config::PlantControllerConfig;
use crate::hal::battery::BatteryInteraction;
use crate::hal::esp::ESP;
use crate::hal::{
deep_sleep, BackupHeader, BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT,
REPEAT_MOIST_MEASURE, TANK_MULTI_SAMPLE, X25,
};
use crate::log::{log, LogMessage};
use anyhow::{anyhow, bail};
use chrono::{DateTime, Utc};
use ds18b20::Ds18b20;
use ds323x::{DateTimeAccess, Ds323x};
use eeprom24x::{Eeprom24x, Eeprom24xTrait, SlaveAddr};
use embedded_hal::digital::OutputPin;
use embedded_hal_bus::i2c::MutexDevice;
use esp_idf_hal::adc::oneshot::config::AdcChannelConfig;
use esp_idf_hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
use esp_idf_hal::adc::{attenuation, Resolution};
use esp_idf_hal::delay::Delay;
use esp_idf_hal::gpio::{AnyInputPin, Gpio5, IOPin, InputOutput, Output, PinDriver, Pull};
use esp_idf_hal::i2c::I2cDriver;
use esp_idf_hal::pcnt::{
PcntChannel, PcntChannelConfig, PcntControlMode, PcntCountMode, PcntDriver, PinIndex,
};
use esp_idf_sys::{gpio_hold_dis, gpio_hold_en, vTaskDelay, EspError};
use crate::hal::esp::{hold_disable, hold_enable, Esp};
use crate::hal::rtc::RTCModuleInteraction;
use crate::hal::water::TankSensor;
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, I2C_DRIVER, PLANT_COUNT};
use alloc::boxed::Box;
use alloc::string::ToString;
use async_trait::async_trait;
use chrono::{DateTime, FixedOffset, Utc};
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 ina219::address::{Address, Pin};
use ina219::calibration::{Calibration, UnCalibrated};
use ina219::calibration::UnCalibrated;
use ina219::configuration::{Configuration, OperatingMode, Resolution};
use ina219::SyncIna219;
use measurements::{Current, Resistance, Voltage};
use one_wire_bus::OneWire;
use measurements::Resistance;
use measurements::{Current, Voltage};
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
use std::result::Result::Ok as OkStd;
use ina219::configuration::{Configuration, OperatingMode};
const MS0: u8 = 1_u8;
const MS1: u8 = 0_u8;
const MS2: u8 = 3_u8;
const MS3: u8 = 4_u8;
const MS4: u8 = 2_u8;
const SENSOR_ON: u8 = 5_u8;
const MPPT_CURRENT_SHUNT_OHMS: f64 = 0.05_f64;
const TWAI_BAUDRATE: twai::BaudRate = twai::BaudRate::B125K;
pub enum Charger<'a> {
SolarMpptV1 {
mppt_ina: SyncIna219<MutexDevice<'a, I2cDriver<'a>>, UnCalibrated>,
solar_is_day: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, esp_idf_hal::gpio::Input>,
charge_indicator: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
mppt_ina: SyncIna219<
I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>,
UnCalibrated,
>,
solar_is_day: Input<'a>,
charge_indicator: Output<'a>,
},
ErrorInit {},
}
impl<'a> Charger<'a> {
pub(crate) fn powersave(&mut self) {
match self { Charger::SolarMpptV1 { mppt_ina, .. } => {
let _ = mppt_ina.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: Default::default(),
operating_mode: OperatingMode::PowerDown,
}).map_err(|e| {
println!(
"Error setting ina mppt configuration during deepsleep preparation{:?}",
pub(crate) fn get_mppt_current(&mut self) -> FatResult<Current> {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let v = mppt_ina.shunt_voltage()?;
let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(MPPT_CURRENT_SHUNT_OHMS);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Ok(Current::from_amperes(current))
}
Charger::ErrorInit { .. } => {
bail!("hardware error during init");
}
}
}
pub(crate) fn get_mptt_voltage(&mut self) -> FatResult<Voltage> {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let v = mppt_ina.bus_voltage()?;
Ok(Voltage::from_millivolts(v.voltage_mv() as f64))
}
Charger::ErrorInit { .. } => {
bail!("hardware error during init");
}
}
}
}
impl Charger<'_> {
pub(crate) fn power_save(&mut self) {
match self {
Charger::SolarMpptV1 { mppt_ina, .. } => {
let _ = mppt_ina
.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: Default::default(),
operating_mode: OperatingMode::PowerDown,
})
.map_err(|e| {
log::info!(
"Error setting ina mppt configuration during deep sleep preparation{:?}",
e
);
});
} }
});
}
_ => {}
}
}
fn set_charge_indicator(&mut self, charging: bool) -> anyhow::Result<()> {
fn set_charge_indicator(&mut self, charging: bool) -> FatResult<()> {
match self {
Self::SolarMpptV1 {
charge_indicator, ..
} => {
charge_indicator.set_state(charging.into())?;
charge_indicator.set_level(charging.into());
}
_ => {}
}
Ok(())
}
fn is_day(&self) -> bool {
match self {
Charger::SolarMpptV1 { solar_is_day, .. } => solar_is_day.get_level().into(),
Charger::SolarMpptV1 { solar_is_day, .. } => solar_is_day.is_high(),
_ => true,
}
}
fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage> {
let voltage = match self {
Charger::SolarMpptV1 { mppt_ina, .. } => mppt_ina
.bus_voltage()
.map(|v| Voltage::from_millivolts(v.voltage_mv() as f64))?,
};
Ok(voltage)
}
fn get_mptt_current(&mut self) -> anyhow::Result<Current> {
let current = match self {
Charger::SolarMpptV1 { mppt_ina, .. } => mppt_ina.shunt_voltage().map(|v| {
let shunt_voltage = Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(0.05_f64);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Current::from_amperes(current)
})?,
};
Ok(current)
}
}
pub struct V4<'a> {
esp: ESP<'a>,
esp: Esp<'a>,
tank_sensor: TankSensor<'a>,
charger: Charger<'a>,
rtc_module: Box<dyn RTCModuleInteraction + Send>,
battery_monitor: Box<dyn BatteryInteraction + Send>,
config: PlantControllerConfig,
tank_channel: AdcChannelDriver<'a, Gpio5, AdcDriver<'a, esp_idf_hal::adc::ADC1>>,
signal_counter: PcntDriver<'a>,
awake: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, Output>,
light: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
tank_power: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
one_wire_bus: OneWire<PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>>,
rtc:
Ds323x<ds323x::interface::I2cInterface<MutexDevice<'a, I2cDriver<'a>>>, ds323x::ic::DS3231>,
eeprom: Eeprom24x<
MutexDevice<'a, I2cDriver<'a>>,
eeprom24x::page_size::B32,
eeprom24x::addr_size::TwoBytes,
eeprom24x::unique_serial::No,
awake: Output<'a>,
light: Output<'a>,
general_fault: Output<'a>,
pump_expander: Pca9535Immediate<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
pump_ina: Option<
SyncIna219<I2cDevice<'a, CriticalSectionRawMutex, I2c<'static, Blocking>>, UnCalibrated>,
>,
general_fault: PinDriver<'a, esp_idf_hal::gpio::AnyIOPin, InputOutput>,
pump_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>,
sensor_expander: Pca9535Immediate<MutexDevice<'a, I2cDriver<'a>>>,
sensor: SensorImpl,
extra1: Output<'a>,
extra2: Output<'a>,
}
pub(crate) fn create_v4(
peripherals: FreePeripherals,
esp: ESP<'static>,
pub(crate) async fn create_v4(
peripherals: FreePeripherals<'static>,
esp: Esp<'static>,
config: PlantControllerConfig,
battery_monitor: Box<dyn BatteryInteraction + Send>,
) -> anyhow::Result<Box<dyn BoardInteraction + Send + '_>> {
let mut awake = PinDriver::output(peripherals.gpio15.downgrade())?;
awake.set_high()?;
rtc_module: Box<dyn RTCModuleInteraction + Send>,
) -> Result<Box<dyn BoardInteraction<'static> + Send + 'static>, FatError> {
log::info!("Start v4");
let mut awake = Output::new(peripherals.gpio21, Level::High, OutputConfig::default());
awake.set_high();
let mut general_fault = PinDriver::input_output(peripherals.gpio6.downgrade())?;
general_fault.set_pull(Pull::Floating)?;
general_fault.set_low()?;
let mut general_fault = Output::new(peripherals.gpio23, Level::Low, OutputConfig::default());
general_fault.set_low();
println!("Init rtc driver");
let mut rtc = Ds323x::new_ds3231(MutexDevice::new(&I2C_DRIVER));
let extra1 = Output::new(peripherals.gpio6, Level::Low, OutputConfig::default());
let extra2 = Output::new(peripherals.gpio15, Level::Low, OutputConfig::default());
println!("Init rtc eeprom driver");
let mut eeprom = {
Eeprom24x::new_24x32(
MutexDevice::new(&I2C_DRIVER),
SlaveAddr::Alternative(true, true, true),
)
};
let one_wire_pin = Flex::new(peripherals.gpio18);
let tank_power_pin = Output::new(peripherals.gpio11, Level::Low, OutputConfig::default());
let flow_sensor_pin = Input::new(
peripherals.gpio4,
InputConfig::default().with_pull(Pull::Up),
);
let mut one_wire_pin = PinDriver::input_output_od(peripherals.gpio18.downgrade())?;
one_wire_pin.set_pull(Pull::Floating)?;
let one_wire_bus = OneWire::new(one_wire_pin)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let rtc_time = rtc.datetime();
match rtc_time {
OkStd(tt) => {
println!("Rtc Module reports time at UTC {}", tt);
}
Err(err) => {
println!("Rtc Module could not be read {:?}", err);
}
}
match eeprom.read_byte(0) {
OkStd(byte) => {
println!("Read first byte with status {}", byte);
}
Err(err) => {
println!("Eeprom could not read first byte {:?}", err);
}
}
let mut signal_counter = PcntDriver::new(
peripherals.pcnt0,
Some(peripherals.gpio22),
Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None,
Option::<AnyInputPin>::None,
let tank_sensor = TankSensor::create(
one_wire_pin,
peripherals.adc1,
peripherals.gpio5,
tank_power_pin,
flow_sensor_pin,
peripherals.pcnt1,
)?;
signal_counter.channel_config(
PcntChannel::Channel0,
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,
},
)?;
let sensor_expander_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut sensor_expander = Pca9535Immediate::new(sensor_expander_device, 34);
let sensor = match sensor_expander.pin_into_output(GPIOBank::Bank0, 0) {
Ok(_) => {
log::info!("SensorExpander answered");
let adc_config = AdcChannelConfig {
attenuation: attenuation::DB_11,
resolution: Resolution::Resolution12Bit,
calibration: esp_idf_hal::adc::oneshot::config::Calibration::Curve,
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;
let edge_pin = Input::new(peripherals.gpio22, InputConfig::default());
ch0.set_edge_signal(edge_pin.peripheral_input());
ch0.set_input_mode(Hold, Increment);
ch0.set_ctrl_mode(Keep, Keep);
signal_counter.listen();
for pin in 0..8 {
let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin);
let _ = sensor_expander.pin_into_output(GPIOBank::Bank1, pin);
let _ = sensor_expander.pin_set_low(GPIOBank::Bank0, pin);
let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin);
}
SensorImpl::PulseCounter {
signal_counter,
sensor_expander,
}
}
Err(_) => {
log::info!("Can bus mode ");
let twai_config = twai::TwaiConfiguration::new(
peripherals.twai,
peripherals.gpio0,
peripherals.gpio2,
TWAI_BAUDRATE,
TwaiMode::Normal,
);
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 }
}
};
let tank_driver = AdcDriver::new(peripherals.adc1)?;
let tank_channel: AdcChannelDriver<Gpio5, AdcDriver<esp_idf_hal::adc::ADC1>> =
AdcChannelDriver::new(tank_driver, peripherals.gpio5, &adc_config)?;
let mut solar_is_day = PinDriver::input(peripherals.gpio7.downgrade())?;
solar_is_day.set_pull(Pull::Floating)?;
let solar_is_day = Input::new(peripherals.gpio7, InputConfig::default());
let light = Output::new(peripherals.gpio10, Level::Low, Default::default());
let charge_indicator = Output::new(peripherals.gpio3, Level::Low, Default::default());
let mut light = PinDriver::input_output(peripherals.gpio10.downgrade())?;
light.set_pull(Pull::Floating)?;
let mut tank_power = PinDriver::input_output(peripherals.gpio11.downgrade())?;
tank_power.set_pull(Pull::Floating)?;
let mut charge_indicator = PinDriver::input_output(peripherals.gpio3.downgrade())?;
charge_indicator.set_pull(Pull::Floating)?;
charge_indicator.set_low()?;
let mut pump_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 32);
//todo error handing if init error
let pump_device = I2cDevice::new(I2C_DRIVER.get().await);
let mut pump_expander = Pca9535Immediate::new(pump_device, 32);
for pin in 0..8 {
let _ = pump_expander.pin_into_output(GPIOBank::Bank0, pin);
let _ = pump_expander.pin_into_output(GPIOBank::Bank1, pin);
@@ -230,65 +237,81 @@ pub(crate) fn create_v4(
let _ = pump_expander.pin_set_low(GPIOBank::Bank1, pin);
}
let mut sensor_expander = Pca9535Immediate::new(MutexDevice::new(&I2C_DRIVER), 34);
for pin in 0..8 {
let _ = sensor_expander.pin_into_output(GPIOBank::Bank0, pin);
let _ = sensor_expander.pin_into_output(GPIOBank::Bank1, pin);
let _ = sensor_expander.pin_set_low(GPIOBank::Bank0, pin);
let _ = sensor_expander.pin_set_low(GPIOBank::Bank1, pin);
}
let mppt_current = I2cDevice::new(I2C_DRIVER.get().await);
let mppt_ina = match SyncIna219::new(mppt_current, Address::from_pins(Pin::Vcc, Pin::Gnd)) {
Ok(mut ina) => {
// Prefer higher averaging for more stable readings
let _ = ina.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: Resolution::Avg128,
operating_mode: Default::default(),
});
Some(ina)
}
Err(err) => {
log::info!("Error creating mppt ina: {:?}", err);
None
}
};
//TODO error handling is not done nicely here, should not break if ina is not responsive
let mut mppt_ina = SyncIna219::new(
MutexDevice::new(&I2C_DRIVER),
Address::from_pins(Pin::Vcc, Pin::Gnd),
)?;
let pump_current_dev = I2cDevice::new(I2C_DRIVER.get().await);
let pump_ina = match SyncIna219::new(pump_current_dev, Address::from_pins(Pin::Gnd, Pin::Sda)) {
Ok(ina) => Some(ina),
Err(err) => {
log::info!("Error creating pump ina: {:?}", err);
None
}
};
mppt_ina.set_configuration(Configuration{
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: ina219::configuration::Resolution::Avg128,
operating_mode: Default::default(),
})?;
//TODO this is probably laready done untill we are ready first time?, maybee add startup time comparison on access?
esp.delay.delay_ms(
mppt_ina
.configuration()?
.conversion_time()
.unwrap()
.as_millis() as u32,
);
let charger = match mppt_ina {
Some(mut mppt_ina) => {
mppt_ina.set_configuration(Configuration {
reset: Default::default(),
bus_voltage_range: Default::default(),
shunt_voltage_range: Default::default(),
bus_resolution: Default::default(),
shunt_resolution: ina219::configuration::Resolution::Avg128,
operating_mode: Default::default(),
})?;
let charger = Charger::SolarMpptV1 {
mppt_ina,
solar_is_day,
charge_indicator,
Charger::SolarMpptV1 {
mppt_ina,
solar_is_day,
charge_indicator,
}
}
None => Charger::ErrorInit {},
};
let v = V4 {
rtc_module,
esp,
awake,
tank_channel,
signal_counter,
tank_sensor,
light,
tank_power,
one_wire_bus,
rtc,
eeprom,
general_fault,
pump_expander,
sensor_expander,
config,
battery_monitor,
pump_ina,
charger,
extra1,
extra2,
sensor,
};
Ok(Box::new(v))
}
#[async_trait]
impl<'a> BoardInteraction<'a> for V4<'a> {
fn get_esp(&mut self) -> &mut ESP<'a> {
fn get_tank_sensor(&mut self) -> Result<&mut TankSensor<'a>, FatError> {
Ok(&mut self.tank_sensor)
}
fn get_esp(&mut self) -> &mut Esp<'a> {
&mut self.esp
}
@@ -300,356 +323,148 @@ impl<'a> BoardInteraction<'a> for V4<'a> {
&mut self.battery_monitor
}
fn set_charge_indicator(&mut self, charging: bool) -> anyhow::Result<()> {
fn get_rtc_module(&mut self) -> &mut Box<dyn RTCModuleInteraction + Send> {
&mut self.rtc_module
}
async fn get_time(&mut self) -> DateTime<Utc> {
self.esp.get_time()
}
async fn set_time(&mut self, time: &DateTime<FixedOffset>) -> FatResult<()> {
self.rtc_module.set_rtc_time(&time.to_utc()).await?;
self.esp.set_time(time.to_utc());
Ok(())
}
async fn set_charge_indicator(&mut self, charging: bool) -> Result<(), FatError> {
self.charger.set_charge_indicator(charging)
}
fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
self.awake.set_low().unwrap();
self.charger.powersave();
deep_sleep(duration_in_ms);
}
fn get_backup_info(&mut self) -> anyhow::Result<BackupHeader> {
let store = bincode::serialize(&BackupHeader::default())?.len();
let mut header_page_buffer = vec![0_u8; store];
self.eeprom
.read_data(0, &mut header_page_buffer)
.map_err(|err| anyhow!("Error reading eeprom header {:?}", err))?;
println!("Raw header is {:?} with size {}", header_page_buffer, store);
let header: BackupHeader = bincode::deserialize(&header_page_buffer)?;
anyhow::Ok(header)
}
fn get_backup_config(&mut self) -> anyhow::Result<Vec<u8>> {
let store = bincode::serialize(&BackupHeader::default())?.len();
let mut header_page_buffer = vec![0_u8; store];
self.eeprom
.read_data(0, &mut header_page_buffer)
.map_err(|err| anyhow!("Error reading eeprom header {:?}", err))?;
let header: BackupHeader = bincode::deserialize(&header_page_buffer)?;
//skip page 0, used by the header
let data_start_address = self.eeprom.page_size() as u32;
let mut data_buffer = vec![0_u8; header.size];
self.eeprom
.read_data(data_start_address, &mut data_buffer)
.map_err(|err| anyhow!("Error reading eeprom data {:?}", err))?;
let checksum = X25.checksum(&data_buffer);
if checksum != header.crc16 {
bail!(
"Invalid checksum, got {} but expected {}",
checksum,
header.crc16
);
}
anyhow::Ok(data_buffer)
}
fn backup_config(&mut self, bytes: &[u8]) -> anyhow::Result<()> {
let time = self.get_rtc_time()?.timestamp_millis();
let delay = Delay::new_default();
let checksum = X25.checksum(bytes);
let page_size = self.eeprom.page_size();
let header = BackupHeader {
crc16: checksum,
timestamp: time,
size: bytes.len(),
};
let encoded = bincode::serialize(&header)?;
if encoded.len() > page_size {
bail!(
"Size limit reached header is {}, but firest page is only {}",
encoded.len(),
page_size
)
}
let as_u8: &[u8] = &encoded;
match self.eeprom.write_page(0, as_u8) {
OkStd(_) => {}
Err(err) => bail!("Error writing eeprom {:?}", err),
};
delay.delay_ms(5);
let to_write = bytes.chunks(page_size);
let mut lastiter = 0;
let mut current_page = 1;
for chunk in to_write {
let address = current_page * page_size as u32;
self.eeprom
.write_page(address, chunk)
.map_err(|err| anyhow!("Error writing eeprom {:?}", err))?;
current_page += 1;
let iter = (current_page % 8) as usize;
if iter != lastiter {
for i in 0..PLANT_COUNT {
let _ = self.fault(i, iter == i);
}
lastiter = iter;
}
delay.delay_ms(5);
}
anyhow::Ok(())
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
self.awake.set_low();
self.charger.power_save();
self.esp.deep_sleep_ms(duration_in_ms);
}
fn is_day(&self) -> bool {
self.charger.is_day()
}
fn water_temperature_c(&mut self) -> anyhow::Result<f32> {
self.one_wire_bus
.reset(&mut self.esp.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let first = self.one_wire_bus.devices(false, &mut self.esp.delay).next();
if first.is_none() {
bail!("Not found any one wire Ds18b20");
}
let device_address = first
.unwrap()
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
let water_temp_sensor = Ds18b20::new::<EspError>(device_address)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
water_temp_sensor
.start_temp_measurement(&mut self.one_wire_bus, &mut self.esp.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
ds18b20::Resolution::Bits12.delay_for_measurement_time(&mut self.esp.delay);
let sensor_data = water_temp_sensor
.read_data(&mut self.one_wire_bus, &mut self.esp.delay)
.map_err(|err| -> anyhow::Error { anyhow!("Missing attribute: {:?}", err) })?;
if sensor_data.temperature == 85_f32 {
bail!("Ds18b20 dummy temperature returned");
}
anyhow::Ok(sensor_data.temperature / 10_f32)
async fn light(&mut self, enable: bool) -> Result<(), FatError> {
hold_disable(10);
self.light.set_level(enable.into());
hold_enable(10);
Ok(())
}
fn tank_sensor_voltage(&mut self) -> anyhow::Result<f32> {
self.tank_power.set_high()?;
//let stabilize
self.esp.delay.delay_ms(100);
let mut store = [0_u16; TANK_MULTI_SAMPLE];
for multisample in 0..TANK_MULTI_SAMPLE {
let value = self.tank_channel.read()?;
store[multisample] = value;
}
self.tank_power.set_low()?;
store.sort();
let median_mv = store[6] as f32 / 1000_f32;
anyhow::Ok(median_mv)
}
fn light(&mut self, enable: bool) -> anyhow::Result<()> {
unsafe { gpio_hold_dis(self.light.pin()) };
self.light.set_state(enable.into())?;
unsafe { gpio_hold_en(self.light.pin()) };
anyhow::Ok(())
}
fn pump(&mut self, plant: usize, enable: bool) -> anyhow::Result<()> {
async fn pump(&mut self, plant: usize, enable: bool) -> FatResult<()> {
if enable {
self.pump_expander
.pin_set_high(GPIOBank::Bank0, plant.try_into()?)?;
.pin_set_high(GPIOBank::Bank0, plant as u8)?;
} else {
self.pump_expander
.pin_set_low(GPIOBank::Bank0, plant.try_into()?)?;
.pin_set_low(GPIOBank::Bank0, plant as u8)?;
}
anyhow::Ok(())
Ok(())
}
fn fault(&mut self, plant: usize, enable: bool) -> anyhow::Result<()> {
async fn pump_current(&mut self, _plant: usize) -> Result<Current, FatError> {
// sensor is shared for all pumps, ignore plant id
match self.pump_ina.as_mut() {
None => {
bail!("pump current sensor not available");
}
Some(pump_ina) => {
let v = pump_ina
.shunt_voltage()
.map_err(|e| FatError::String {
error: alloc::format!("{:?}", e),
})
.map(|v| {
let shunt_voltage =
Voltage::from_microvolts(v.shunt_voltage_uv().abs() as f64);
let shut_value = Resistance::from_ohms(0.05_f64);
let current = shunt_voltage.as_volts() / shut_value.as_ohms();
Current::from_amperes(current)
})?;
Ok(v)
}
}
}
async fn fault(&mut self, plant: usize, enable: bool) -> FatResult<()> {
if enable {
self.pump_expander
.pin_set_high(GPIOBank::Bank1, plant.try_into()?)?
.pin_set_high(GPIOBank::Bank1, plant as u8)?;
} else {
self.pump_expander
.pin_set_low(GPIOBank::Bank1, plant.try_into()?)?
.pin_set_low(GPIOBank::Bank1, plant as u8)?;
}
anyhow::Ok(())
Ok(())
}
fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> anyhow::Result<f32> {
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE {
self.signal_counter.counter_pause()?;
self.signal_counter.counter_clear()?;
//Disable all
self.sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
let sensor_channel = match sensor {
Sensor::A => plant as u32,
Sensor::B => (15 - plant) as u32,
};
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
if is_bit_set(0) {
self.sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?;
} else {
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
}
if is_bit_set(1) {
self.sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?;
} else {
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
}
if is_bit_set(2) {
self.sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?;
} else {
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
}
if is_bit_set(3) {
self.sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?;
} else {
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
}
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?;
self.sensor_expander
.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?;
let delay = Delay::new_default();
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000f32 / measurement as f32;
//give some time to stabilize
delay.delay_ms(10);
self.signal_counter.counter_resume()?;
delay.delay_ms(measurement);
self.signal_counter.counter_pause()?;
self.sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
self.sensor_expander
.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?;
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
self.sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
delay.delay_ms(10);
let unscaled = self.signal_counter.get_counter_value()? as i32;
let hz = unscaled as f32 * factor;
log(
LogMessage::RawMeasure,
unscaled as u32,
hz as u32,
&plant.to_string(),
&format!("{sensor:?}"),
);
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];
anyhow::Ok(median)
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> Result<f32, FatError> {
self.sensor.measure_moisture_hz(plant, sensor).await
}
fn general_fault(&mut self, enable: bool) {
unsafe { gpio_hold_dis(self.general_fault.pin()) };
self.general_fault.set_state(enable.into()).unwrap();
unsafe { gpio_hold_en(self.general_fault.pin()) };
async fn general_fault(&mut self, enable: bool) {
hold_disable(23);
self.general_fault.set_level(enable.into());
hold_enable(23);
}
fn factory_reset(&mut self) -> anyhow::Result<()> {
println!("factory resetting");
self.esp.delete_config()?;
//destroy backup header
let dummy: [u8; 0] = [];
self.backup_config(&dummy)?;
anyhow::Ok(())
}
fn get_rtc_time(&mut self) -> anyhow::Result<DateTime<Utc>> {
match self.rtc.datetime() {
OkStd(rtc_time) => anyhow::Ok(rtc_time.and_utc()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
}
}
fn set_rtc_time(&mut self, time: &DateTime<Utc>) -> anyhow::Result<()> {
let naive_time = time.naive_utc();
match self.rtc.set_datetime(&naive_time) {
OkStd(_) => anyhow::Ok(()),
Err(err) => {
bail!("Error getting rtc time {:?}", err)
}
}
}
fn test_pump(&mut self, plant: usize) -> anyhow::Result<()> {
self.pump(plant, true)?;
self.esp.delay.delay_ms(30000);
self.pump(plant, false)?;
anyhow::Ok(())
}
fn test(&mut self) -> anyhow::Result<()> {
self.general_fault(true);
self.esp.delay.delay_ms(100);
self.general_fault(false);
self.esp.delay.delay_ms(500);
self.light(true)?;
self.esp.delay.delay_ms(500);
self.light(false)?;
self.esp.delay.delay_ms(500);
async fn test(&mut self) -> Result<(), FatError> {
self.general_fault(true).await;
Timer::after_millis(100).await;
self.general_fault(false).await;
Timer::after_millis(500).await;
self.light(true).await?;
Timer::after_millis(500).await;
self.light(false).await?;
Timer::after_millis(500).await;
for i in 0..PLANT_COUNT {
self.fault(i, true)?;
self.esp.delay.delay_ms(500);
self.fault(i, false)?;
self.esp.delay.delay_ms(500);
self.fault(i, true).await?;
Timer::after_millis(500).await;
self.fault(i, false).await?;
Timer::after_millis(500).await;
}
for i in 0..PLANT_COUNT {
self.pump(i, true)?;
self.esp.delay.delay_ms(100);
self.pump(i, false)?;
self.esp.delay.delay_ms(100);
self.pump(i, true).await?;
Timer::after_millis(100).await;
self.pump(i, false).await?;
Timer::after_millis(100).await;
}
for plant in 0..PLANT_COUNT {
let a = self.measure_moisture_hz(plant, Sensor::A);
let b = self.measure_moisture_hz(plant, Sensor::B);
let a = self.measure_moisture_hz(plant, Sensor::A).await;
let b = self.measure_moisture_hz(plant, Sensor::B).await;
let aa = match a {
OkStd(a) => a as u32,
Ok(a) => a as u32,
Err(_) => u32::MAX,
};
let bb = match b {
OkStd(b) => b as u32,
Ok(b) => b as u32,
Err(_) => u32::MAX,
};
log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "");
LOG_ACCESS
.lock()
.await
.log(LogMessage::TestSensor, aa, bb, &plant.to_string(), "")
.await;
}
self.esp.delay.delay_ms(10);
anyhow::Ok(())
Timer::after_millis(10).await;
Ok(())
}
fn set_config(&mut self, config: PlantControllerConfig) -> anyhow::Result<()> {
fn set_config(&mut self, config: PlantControllerConfig) {
self.config = config;
self.esp.save_config(&self.config)?;
anyhow::Ok(())
}
fn get_mptt_voltage(&mut self) -> anyhow::Result<Voltage> {
async fn get_mptt_voltage(&mut self) -> Result<Voltage, FatError> {
self.charger.get_mptt_voltage()
}
fn get_mptt_current(&mut self) -> anyhow::Result<Current> {
self.charger.get_mptt_current()
async fn get_mptt_current(&mut self) -> Result<Current, FatError> {
self.charger.get_mppt_current()
}
}

130
rust/src/hal/v4_sensor.rs Normal file
View File

@@ -0,0 +1,130 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::Box;
use crate::hal::Sensor;
use crate::log::{LogMessage, LOG_ACCESS};
use alloc::format;
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::i2c::master::I2c;
use esp_hal::pcnt::unit::Unit;
use esp_hal::twai::Twai;
use esp_hal::Blocking;
use pca9535::{GPIOBank, Pca9535Immediate, StandardExpanderInterface};
const REPEAT_MOIST_MEASURE: usize = 10;
#[async_trait]
pub trait SensorInteraction {
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult<f32>;
}
const MS0: u8 = 1_u8;
const MS1: u8 = 0_u8;
const MS2: u8 = 3_u8;
const MS3: u8 = 4_u8;
const MS4: u8 = 2_u8;
const SENSOR_ON: u8 = 5_u8;
pub enum SensorImpl {
PulseCounter {
signal_counter: Unit<'static, 0>,
sensor_expander:
Pca9535Immediate<I2cDevice<'static, CriticalSectionRawMutex, I2c<'static, Blocking>>>,
},
CanBus {
twai: Twai<'static, Blocking>,
},
}
#[async_trait]
impl SensorInteraction for SensorImpl {
async fn measure_moisture_hz(&mut self, plant: usize, sensor: Sensor) -> FatResult<f32> {
match self {
SensorImpl::PulseCounter {
signal_counter,
sensor_expander,
..
} => {
let mut results = [0_f32; REPEAT_MOIST_MEASURE];
for repeat in 0..REPEAT_MOIST_MEASURE {
signal_counter.pause();
signal_counter.clear();
//Disable all
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
let sensor_channel = match sensor {
Sensor::A => plant as u32,
Sensor::B => (15 - plant) as u32,
};
let is_bit_set = |b: u8| -> bool { sensor_channel & (1 << b) != 0 };
if is_bit_set(0) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS0)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
}
if is_bit_set(1) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS1)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
}
if is_bit_set(2) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS2)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
}
if is_bit_set(3) {
sensor_expander.pin_set_high(GPIOBank::Bank0, MS3)?;
} else {
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
}
sensor_expander.pin_set_low(GPIOBank::Bank0, MS4)?;
sensor_expander.pin_set_high(GPIOBank::Bank0, SENSOR_ON)?;
let measurement = 100; // TODO what is this scaling factor? what is its purpose?
let factor = 1000f32 / measurement as f32;
//give some time to stabilize
Timer::after_millis(10).await;
signal_counter.resume();
Timer::after_millis(measurement).await;
signal_counter.pause();
sensor_expander.pin_set_high(GPIOBank::Bank0, MS4)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, SENSOR_ON)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS0)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS1)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS2)?;
sensor_expander.pin_set_low(GPIOBank::Bank0, MS3)?;
Timer::after_millis(10).await;
let unscaled = 1337; //signal_counter.get_counter_value()? as i32;
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)
}
SensorImpl::CanBus { twai } => Err(FatError::String {
error: "Not yet implemented".to_string(),
}),
}
}
}

180
rust/src/hal/water.rs Normal file
View File

@@ -0,0 +1,180 @@
use crate::bail;
use crate::fat_error::FatError;
use crate::hal::{ADC1, TANK_MULTI_SAMPLE};
use embassy_time::Timer;
use esp_hal::analog::adc::{Adc, AdcCalLine, AdcConfig, AdcPin, Attenuation};
use esp_hal::delay::Delay;
use esp_hal::gpio::{DriveMode, Flex, Input, InputConfig, 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::Async;
use esp_println::println;
use onewire::{ds18b20, Device, DeviceSearch, OneWire, DS18B20};
unsafe impl Send for TankSensor<'_> {}
pub struct TankSensor<'a> {
one_wire_bus: OneWire<Flex<'a>>,
tank_channel: Adc<'a, ADC1<'a>, Async>,
tank_power: Output<'a>,
tank_pin: AdcPin<GPIO5<'a>, ADC1<'a>, AdcCalLine<ADC1<'a>>>,
flow_counter: Unit<'a, 1>,
}
impl<'a> TankSensor<'a> {
pub(crate) fn create(
mut one_wire_pin: Flex<'a>,
adc1: ADC1<'a>,
gpio5: GPIO5<'a>,
tank_power: Output<'a>,
flow_sensor: Input,
pcnt1: Unit<'a, 1>,
) -> Result<TankSensor<'a>, FatError> {
one_wire_pin.apply_output_config(
&OutputConfig::default()
.with_drive_mode(DriveMode::OpenDrain)
.with_pull(Pull::None),
);
one_wire_pin.apply_input_config(&InputConfig::default().with_pull(Pull::None));
one_wire_pin.set_high();
one_wire_pin.set_input_enable(true);
one_wire_pin.set_output_enable(true);
let mut adc1_config = AdcConfig::new();
let tank_pin =
adc1_config.enable_pin_with_cal::<_, AdcCalLine<_>>(gpio5, Attenuation::_11dB);
let tank_channel = Adc::new(adc1, adc1_config).into_async();
let one_wire_bus = OneWire::new(one_wire_pin, false);
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: pcnt1,
})
}
pub fn reset_flow_meter(&mut self) {
self.flow_counter.pause();
self.flow_counter.clear();
}
pub fn start_flow_meter(&mut self) {
self.flow_counter.resume();
}
pub fn get_flow_meter_value(&mut self) -> i16 {
self.flow_counter.value()
}
pub fn stop_flow_meter(&mut self) -> i16 {
self.flow_counter.pause();
self.get_flow_meter_value()
}
pub async fn water_temperature_c(&mut self) -> Result<f32, FatError> {
//multisample should be moved to water_temperature_c
let mut attempt = 1;
let mut delay = Delay::new();
let presence = self.one_wire_bus.reset(&mut delay)?;
println!("OneWire: reset presence pulse = {}", presence);
if !presence {
println!("OneWire: no device responded to reset — check pull-up resistor and wiring");
}
let mut search = DeviceSearch::new();
let mut water_temp_sensor: Option<Device> = None;
let mut devices_found = 0u8;
while let Some(device) = self.one_wire_bus.search_next(&mut search, &mut delay)? {
devices_found += 1;
println!(
"OneWire: found device #{} family=0x{:02X} addr={:02X?}",
devices_found, device.address[0], device.address
);
if device.address[0] == ds18b20::FAMILY_CODE {
water_temp_sensor = Some(device);
break;
} else {
println!("OneWire: skipping device — not a DS18B20 (family 0x{:02X} != 0x{:02X})", device.address[0], ds18b20::FAMILY_CODE);
}
}
if devices_found == 0 {
println!("OneWire: search found zero devices on the bus");
}
match water_temp_sensor {
Some(device) => {
println!("Found one wire device: {:?}", device);
let mut water_temp_sensor = DS18B20::new(device)?;
let water_temp: Result<f32, FatError> = loop {
let temp = self
.single_temperature_c(&mut water_temp_sensor, &mut delay)
.await;
match &temp {
Ok(res) => {
println!("Water temp is {}", res);
break temp;
}
Err(err) => {
println!("Could not get water temp {} attempt {}", err, attempt)
}
}
if attempt == 5 {
break temp;
}
attempt += 1;
};
water_temp
}
None => {
bail!("Not found any one wire Ds18b20");
}
}
}
async fn single_temperature_c(
&mut self,
sensor: &mut DS18B20,
delay: &mut Delay,
) -> Result<f32, FatError> {
let resolution = sensor.measure_temperature(&mut self.one_wire_bus, delay)?;
Timer::after_millis(resolution.time_ms() as u64).await;
let temperature = sensor.read_temperature(&mut self.one_wire_bus, delay)? as f32;
if temperature == 85_f32 {
bail!("Ds18b20 dummy temperature returned");
}
Ok(temperature / 10_f32)
}
pub async fn tank_sensor_voltage(&mut self) -> Result<f32, FatError> {
self.tank_power.set_high();
//let stabilize
Timer::after_millis(100).await;
let mut store = [0_u16; TANK_MULTI_SAMPLE];
for sample in store.iter_mut() {
*sample = self.tank_channel.read_oneshot(&mut self.tank_pin).await;
//force yield between successful samples
Timer::after_millis(10).await;
}
self.tank_power.set_low();
store.sort();
let median_mv = store[TANK_MULTI_SAMPLE / 2] as f32;
Ok(median_mv / 1000.0)
}
}

View File

@@ -1,4 +0,0 @@
#![allow(dead_code)]
extern crate embedded_hal as hal;
pub mod sipo;

108
rust/src/log/interceptor.rs Normal file
View File

@@ -0,0 +1,108 @@
use alloc::string::String;
use alloc::vec::Vec;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::blocking_mutex::Mutex as BlockingMutex;
use log::{LevelFilter, Log, Metadata, Record};
const MAX_LIVE_LOG_ENTRIES: usize = 64;
struct LiveLogBuffer {
entries: Vec<(u64, String)>,
next_seq: u64,
}
impl LiveLogBuffer {
const fn new() -> Self {
Self {
entries: Vec::new(),
next_seq: 0,
}
}
fn push(&mut self, text: String) {
if self.entries.len() >= MAX_LIVE_LOG_ENTRIES {
self.entries.remove(0);
}
self.entries.push((self.next_seq, text));
self.next_seq += 1;
}
fn get_after(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
let next_seq = self.next_seq;
match after {
None => (self.entries.clone(), false, next_seq),
Some(after_seq) => {
let result: Vec<_> = self.entries
.iter()
.filter(|(seq, _)| *seq > after_seq)
.cloned()
.collect();
// Dropped if there are entries that should exist (seq > after_seq) but
// the oldest retained entry has a higher seq than after_seq + 1.
let dropped = if next_seq > after_seq.saturating_add(1) {
if let Some((oldest_seq, _)) = self.entries.first() {
*oldest_seq > after_seq.saturating_add(1)
} else {
// Buffer empty but entries were written — all dropped
true
}
} else {
false
};
(result, dropped, next_seq)
}
}
}
}
pub struct InterceptorLogger {
live_log: BlockingMutex<CriticalSectionRawMutex, core::cell::RefCell<LiveLogBuffer>>,
}
impl InterceptorLogger {
pub const fn new() -> Self {
Self {
live_log: BlockingMutex::new(core::cell::RefCell::new(LiveLogBuffer::new())),
}
}
/// Returns (entries_after, dropped, next_seq).
/// Pass `after = None` to retrieve the entire current buffer.
/// Pass `after = Some(seq)` to retrieve only entries with seq > that value.
pub fn get_live_logs(&self, after: Option<u64>) -> (Vec<(u64, String)>, bool, u64) {
self.live_log.lock(|buf| buf.borrow().get_after(after))
}
pub fn init(&'static self) {
match log::set_logger(self).map(|()| log::set_max_level(LevelFilter::Info)) {
Ok(()) => {}
Err(_e) => {
esp_println::println!("ERROR: Logger already set");
}
}
}
}
impl Log for InterceptorLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::Level::Info
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let message = alloc::format!("{}: {}", record.level(), record.args());
// Print to serial
esp_println::println!("{}", message);
// Store in live log ring buffer
self.live_log.lock(|buf| {
buf.borrow_mut().push(message);
});
}
}
fn flush(&self) {}
}

View File

@@ -1,73 +1,120 @@
use crate::vec;
use crate::BOARD_ACCESS;
use alloc::string::ToString;
use alloc::vec::Vec;
use bytemuck::{AnyBitPattern, Pod, Zeroable};
use deranged::RangedU8;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel;
use embassy_sync::mutex::Mutex;
use esp_hal::Persistable;
use log::{info, warn};
use serde::Serialize;
use std::{collections::HashMap, sync::Mutex};
use strum::EnumIter;
use strum_macros::IntoStaticStr;
use esp_idf_svc::systime::EspSystemTime;
use once_cell::sync::Lazy;
use ringbuffer::{ConstGenericRingBuffer, RingBuffer};
use text_template::Template;
use unit_enum::UnitEnum;
const LOG_ARRAY_SIZE: u8 = 220;
const MAX_LOG_ARRAY_INDEX: u8 = LOG_ARRAY_SIZE - 1;
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
static mut LOG_ARRAY: LogArray = LogArray {
buffer: [LogEntryInner {
timestamp: 0,
message_id: 0,
a: 0,
b: 0,
txt_short: [0; TXT_SHORT_LENGTH],
txt_long: [0; TXT_LONG_LENGTH],
}; LOG_ARRAY_SIZE as usize],
head: 0,
};
// this is the only reference created for LOG_ARRAY and the only way to access it
#[allow(static_mut_refs)]
pub static LOG_ACCESS: Mutex<CriticalSectionRawMutex, &'static mut LogArray> =
unsafe { Mutex::new(&mut LOG_ARRAY) };
mod interceptor;
pub use interceptor::InterceptorLogger;
pub static INTERCEPTOR: InterceptorLogger = InterceptorLogger::new();
pub struct LogRequest {
pub message_key: LogMessage,
pub number_a: u32,
pub number_b: u32,
pub txt_short: heapless::String<TXT_SHORT_LENGTH>,
pub txt_long: heapless::String<TXT_LONG_LENGTH>,
}
static LOG_CHANNEL: Channel<CriticalSectionRawMutex, LogRequest, 16> = Channel::new();
#[embassy_executor::task]
pub async fn log_task() {
loop {
let request = LOG_CHANNEL.receive().await;
LOG_ACCESS
.lock()
.await
.log(
request.message_key,
request.number_a,
request.number_b,
request.txt_short.as_str(),
request.txt_long.as_str(),
)
.await;
}
}
const TXT_SHORT_LENGTH: usize = 8;
const TXT_LONG_LENGTH: usize = 32;
const BUFFER_SIZE: usize = 220;
#[derive(Debug, Clone, Copy, AnyBitPattern)]
#[repr(C)]
pub struct LogArray {
buffer: [LogEntryInner; LOG_ARRAY_SIZE as usize],
head: u8,
}
#[link_section = ".rtc.data"]
static mut BUFFER: ConstGenericRingBuffer<LogEntry, BUFFER_SIZE> =
ConstGenericRingBuffer::<LogEntry, BUFFER_SIZE>::new();
#[allow(static_mut_refs)]
static BUFFER_ACCESS: Lazy<Mutex<&mut ConstGenericRingBuffer<LogEntry, BUFFER_SIZE>>> =
Lazy::new(|| unsafe { Mutex::new(&mut BUFFER) });
unsafe impl Persistable for LogArray {}
unsafe impl Zeroable for LogEntryInner {}
#[derive(Serialize, Debug, Clone)]
unsafe impl Pod for LogEntryInner {}
#[derive(Debug, Clone, Copy)]
struct LogEntryInner {
pub timestamp: u64,
pub message_id: u16,
pub a: u32,
pub b: u32,
pub txt_short: [u8; TXT_SHORT_LENGTH],
pub txt_long: [u8; TXT_LONG_LENGTH],
}
#[derive(Serialize)]
pub struct LogEntry {
pub timestamp: u64,
pub message_id: u16,
pub a: u32,
pub b: u32,
pub txt_short: heapless::String<TXT_SHORT_LENGTH>,
pub txt_long: heapless::String<TXT_LONG_LENGTH>,
pub txt_short: alloc::string::String,
pub txt_long: alloc::string::String,
}
pub fn init() {
unsafe {
BUFFER = ConstGenericRingBuffer::<LogEntry, BUFFER_SIZE>::new();
};
let mut access = BUFFER_ACCESS.lock().unwrap();
access.drain().for_each(|_| {});
}
fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<LIMIT>) {
for char in input.chars() {
match target.push(char) {
Ok(_) => {} //continue adding chars
Err(_) => {
//clear space for two asci chars
while target.len() + 2 >= LIMIT {
target.pop().unwrap();
}
//add .. to shortened strings
target.push('.').unwrap();
target.push('.').unwrap();
return;
}
impl From<LogEntryInner> for LogEntry {
fn from(value: LogEntryInner) -> Self {
LogEntry {
timestamp: value.timestamp,
message_id: value.message_id,
a: value.a,
b: value.b,
txt_short: alloc::string::String::from_utf8_lossy_owned(value.txt_short.to_vec()),
txt_long: alloc::string::String::from_utf8_lossy_owned(value.txt_long.to_vec()),
}
}
}
pub fn get_log() -> Vec<LogEntry> {
let buffer = BUFFER_ACCESS.lock().unwrap();
let mut read_copy = Vec::new();
for entry in buffer.iter() {
let copy = entry.clone();
read_copy.push(copy);
}
drop(buffer);
return read_copy;
}
pub fn log(message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &str, txt_long: &str) {
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
@@ -75,58 +122,119 @@ pub fn log(message_key: LogMessage, number_a: u32, number_b: u32, txt_short: &st
limit_length(txt_short, &mut txt_short_stack);
limit_length(txt_long, &mut txt_long_stack);
let time = EspSystemTime {}.now().as_millis() as u64;
let ordinal = message_key.ordinal() as u16;
let template_string: &str = message_key.into();
let mut values: HashMap<&str, &str> = HashMap::new();
let number_a_str = number_a.to_string();
let number_b_str = number_b.to_string();
values.insert("number_a", &number_a_str);
values.insert("number_b", &number_b_str);
values.insert("txt_short", txt_short);
values.insert("txt_long", txt_long);
let template = Template::from(template_string);
let serial_entry = template.fill_in(&values);
println!("{serial_entry}");
let entry = LogEntry {
timestamp: time,
message_id: ordinal,
a: number_a,
b: number_b,
match LOG_CHANNEL.try_send(LogRequest {
message_key,
number_a,
number_b,
txt_short: txt_short_stack,
txt_long: txt_long_stack,
};
let mut buffer = BUFFER_ACCESS.lock().unwrap();
buffer.push(entry);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn within_limit() {
let test = "12345678";
let mut txt_short_stack: heapless::String<TXT_SHORT_LENGTH> = heapless::String::new();
let mut txt_long_stack: heapless::String<TXT_LONG_LENGTH> = heapless::String::new();
limit_length(test, &mut txt_short_stack);
limit_length(test, &mut txt_long_stack);
assert_eq!(txt_short_stack.as_str(), test);
assert_eq!(txt_long_stack.as_str(), test);
}) {
Ok(_) => {}
Err(_) => {
warn!("Log channel full, dropping log entry");
}
}
}
#[derive(IntoStaticStr, EnumIter, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, UnitEnum)]
impl LogArray {
pub fn get(&mut self) -> Vec<LogEntry> {
let head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
RangedU8::new(self.head).unwrap_or(RangedU8::new_saturating(0));
let mut rv: Vec<LogEntry> = Vec::new();
let mut index = head.wrapping_sub(1);
for _ in 0..self.buffer.len() {
let entry = self.buffer[index.get() as usize];
if (entry.message_id as usize) != LogMessage::Empty.ordinal() {
rv.push(entry.into());
}
index = index.wrapping_sub(1);
}
rv
}
pub async fn log(
&mut self,
message_key: LogMessage,
number_a: u32,
number_b: u32,
txt_short: &str,
txt_long: &str,
) {
let mut head: RangedU8<0, MAX_LOG_ARRAY_INDEX> =
RangedU8::new(self.head).unwrap_or(RangedU8::new_saturating(0));
let time = {
let mut guard = BOARD_ACCESS.get().await.lock().await;
guard.board_hal.get_esp().rtc.current_time_us()
} / 1000;
let ordinal = message_key.ordinal() as u16;
let template: &str = message_key.into();
let mut template_string = template.to_string();
template_string = template_string.replace("${number_a}", number_a.to_string().as_str());
template_string = template_string.replace("${number_b}", number_b.to_string().as_str());
template_string = template_string.replace("${txt_long}", txt_long);
template_string = template_string.replace("${txt_short}", txt_short);
info!("{template_string}");
let to_modify = &mut self.buffer[head.get() as usize];
to_modify.timestamp = time;
to_modify.message_id = ordinal;
to_modify.a = number_a;
to_modify.b = number_b;
to_modify.txt_short.clone_from_slice(txt_short.as_bytes());
to_modify.txt_long.clone_from_slice(txt_long.as_bytes());
head = head.wrapping_add(1);
self.head = head.get();
}
}
fn limit_length<const LIMIT: usize>(input: &str, target: &mut heapless::String<LIMIT>) {
for char in input.chars() {
match target.push(char) {
Ok(_) => {} //continue adding chars
Err(_) => {
//clear space for two asci chars
info!("pushing char {char} to limit {LIMIT} current value {target} input {input}");
while target.len() + 2 >= LIMIT {
target.pop();
}
//add .. to shortened strings
match target.push('.') {
Ok(_) => {}
Err(_) => {
warn!(
"Error pushin . to limit {LIMIT} current value {target} input {input}"
)
}
}
match target.push('.') {
Ok(_) => {}
Err(_) => {
warn!(
"Error pushin . to limit {LIMIT} current value {target} input {input}"
)
}
}
}
}
}
while target.len() < LIMIT {
match target.push(' ') {
Ok(_) => {}
Err(_) => {
warn!("Error pushing space to limit {LIMIT} current value {target} input {input}")
}
}
}
}
#[derive(IntoStaticStr, Serialize, PartialEq, Eq, PartialOrd, Ord, Clone, UnitEnum)]
pub enum LogMessage {
#[strum(serialize = "")]
Empty,
#[strum(
serialize = "Reset due to ${txt_long} requires rtc clear ${number_a} and force config mode ${number_b}"
)]
@@ -159,7 +267,7 @@ pub enum LogMessage {
StayAlive,
#[strum(serialize = "Connecting mqtt ${txt_short} with id ${txt_long}")]
MqttInfo,
#[strum(serialize = "Received stay alive with value ${txt_short}")]
#[strum(serialize = "Received stay alive with value ${number_a}")]
MqttStayAliveRec,
#[strum(serialize = "Unknown topic recieved ${txt_long}")]
UnknownTopic,
@@ -193,6 +301,30 @@ pub enum LogMessage {
serialize = "Pumped multiple times, but plant is still to try attempt: ${number_a} limit :: ${number_b} plant: ${txt_short}"
)]
ConsecutivePumpCountLimit,
#[strum(
serialize = "Pump Overcurrent error, pump: ${number_a} tripped overcurrent ${number_b} limit was ${txt_short} @s ${txt_long}"
)]
PumpOverCurrent,
#[strum(
serialize = "Pump Open loop error, pump: ${number_a} is low, ${number_b} limit was ${txt_short} @s ${txt_long}"
)]
PumpOpenLoopCurrent,
#[strum(serialize = "Pump Open current sensor required but did not work: ${number_a}")]
PumpMissingSensorCurrent,
#[strum(
serialize = "Fertilizer applied for ${number_a}s on plant ${number_b} (last application ${txt_short} minutes ago)"
)]
FertilizerApplied,
#[strum(serialize = "MPPT Current sensor could not be reached")]
MPPTError,
#[strum(
serialize = "Trace: a: ${number_a} b: ${number_b} txt_s ${txt_short} long ${txt_long}"
)]
Trace,
#[strum(serialize = "Parsing error reading message")]
UnknownMessage,
#[strum(serialize = "Going to deep sleep for ${number_a} minutes")]
DeepSleep,
}
#[derive(Serialize)]
@@ -211,9 +343,9 @@ impl From<&LogMessage> for MessageTranslation {
}
impl LogMessage {
pub fn to_log_localisation_config() -> Vec<MessageTranslation> {
pub fn log_localisation_config() -> Vec<MessageTranslation> {
Vec::from_iter((0..LogMessage::len()).map(|i| {
let msg_type = LogMessage::from_ordinal(i).unwrap();
let msg_type = LogMessage::from_ordinal(i).unwrap_or(LogMessage::UnknownMessage);
(&msg_type).into()
}))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
[package]
name = "mcutie"
version = "3.0.0"
edition = "2021"
[lib]
path = "lib.rs"
[features]
default = []
homeassistant = []
serde = ["dep:serde", "heapless/serde"]
defmt = []
log = ["dep:log"]
[dependencies]
embassy-net = { version = "0.8.0", default-features = false, features = ["tcp", "dns", "proto-ipv4", "proto-ipv6", "medium-ethernet"] }
embassy-sync = { version = "0.8.0", default-features = false }
embassy-time = { version = "0.5.1", default-features = false }
embassy-futures = { version = "0.1.2", default-features = false }
embedded-io = { version = "0.7.1", default-features = false }
embedded-io-async = { version = "0.7.0", default-features = false }
heapless = { version = "0.7.17", default-features = false }
mqttrs = { version = "0.4.1", default-features = false }
once_cell = { version = "1.21.3", default-features = false, features = ["critical-section"] }
pin-project = { version = "1.1.10", default-features = false }
hex = { version = "0.4.3", default-features = false }
serde = { version = "1.0.228", default-features = false, features = ["derive"], optional = true }
log = { version = "0.4.28", default-features = false, optional = true }
[dev-dependencies]
futures-executor = "0.3.31"
futures-timer = "3.0.3"
futures-util = "0.3.31"

View File

@@ -0,0 +1,124 @@
use core::{cmp, fmt, ops::Deref};
use embedded_io::{SliceWriteError, Write};
use mqttrs::{encode_slice, Packet};
use crate::Error;
/// A stack allocated buffer that can be written to and then read back from.
/// Dereferencing as a [`u8`] slice allows access to previously written data.
///
/// Can be written to with [`write!`] and supports [`embedded_io::Write`] and
/// [`embedded_io_async::Write`].
pub struct Buffer<const N: usize> {
bytes: [u8; N],
cursor: usize,
}
impl<const N: usize> Default for Buffer<N> {
fn default() -> Self {
Self::new()
}
}
impl<const N: usize> Buffer<N> {
/// Creates a new buffer.
pub(crate) const fn new() -> Self {
Self {
bytes: [0; N],
cursor: 0,
}
}
/// Creates a new buffer and writes the given data into it.
pub(crate) fn from(buf: &[u8]) -> Result<Self, Error> {
let mut buffer = Self::new();
match buffer.write_all(buf) {
Ok(()) => Ok(buffer),
Err(_) => Err(Error::TooLarge),
}
}
pub(crate) fn encode_packet(&mut self, packet: &Packet<'_>) -> Result<(), mqttrs::Error> {
let len = encode_slice(packet, &mut self.bytes[self.cursor..])?;
self.cursor += len;
Ok(())
}
#[cfg(feature = "serde")]
/// Serializes a value into this buffer using JSON.
pub(crate) fn serialize_json<T: serde::Serialize>(
&mut self,
value: &T,
) -> Result<(), serde_json_core::ser::Error> {
let len = serde_json_core::to_slice(value, &mut self.bytes[self.cursor..])?;
self.cursor += len;
Ok(())
}
#[cfg(feature = "serde")]
/// Deserializes this buffer using JSON into the given type.
pub fn deserialize_json<'a, T: serde::Deserialize<'a>>(
&'a self,
) -> Result<T, serde_json_core::de::Error> {
let (result, _) = serde_json_core::from_slice(self)?;
Ok(result)
}
/// The number of bytes available for writing into this buffer.
pub fn available(&self) -> usize {
N - self.cursor
}
}
impl<const N: usize> Deref for Buffer<N> {
type Target = [u8];
fn deref(&self) -> &Self::Target {
&self.bytes[0..self.cursor]
}
}
impl<const N: usize> fmt::Write for Buffer<N> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_all(s.as_bytes()).map_err(|_| fmt::Error)
}
}
impl<const N: usize> embedded_io::ErrorType for Buffer<N> {
type Error = SliceWriteError;
}
impl<const N: usize> embedded_io::Write for Buffer<N> {
fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
if buf.is_empty() {
return Ok(0);
}
let writable = cmp::min(self.available(), buf.len());
if writable == 0 {
Err(SliceWriteError::Full)
} else {
self.bytes[self.cursor..self.cursor + writable].copy_from_slice(buf);
self.cursor += writable;
Ok(writable)
}
}
fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
impl<const N: usize> embedded_io_async::Write for Buffer<N> {
async fn write(&mut self, buf: &[u8]) -> Result<usize, Self::Error> {
<Self as embedded_io::Write>::write(self, buf)
}
async fn flush(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}

View File

@@ -0,0 +1,80 @@
#![macro_use]
#[cfg(all(feature = "defmt", feature = "log"))]
compile_error!("The `defmt` and `log` features cannot both be enabled at the same time.");
#[cfg(not(feature = "defmt"))]
use core::fmt;
#[cfg(feature = "defmt")]
pub(crate) use ::defmt::Debug2Format;
#[cfg(not(feature = "defmt"))]
pub(crate) struct Debug2Format<D: fmt::Debug>(pub(crate) D);
#[cfg(feature = "log")]
impl<D: fmt::Debug> fmt::Debug for Debug2Format<D> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
#[collapse_debuginfo(yes)]
macro_rules! trace {
($s:literal $(, $x:expr)* $(,)?) => {
#[cfg(feature = "defmt")]
::defmt::trace!($s $(, $x)*);
#[cfg(feature = "log")]
::log::trace!($s $(, $x)*);
#[cfg(not(any(feature="defmt", feature="log")))]
let _ = ($( & $x ),*);
};
}
#[collapse_debuginfo(yes)]
macro_rules! debug {
($s:literal $(, $x:expr)* $(,)?) => {
#[cfg(feature = "defmt")]
::defmt::debug!($s $(, $x)*);
#[cfg(feature = "log")]
::log::debug!($s $(, $x)*);
#[cfg(not(any(feature="defmt", feature="log")))]
let _ = ($( & $x ),*);
};
}
#[collapse_debuginfo(yes)]
macro_rules! info {
($s:literal $(, $x:expr)* $(,)?) => {
#[cfg(feature = "defmt")]
::defmt::info!($s $(, $x)*);
#[cfg(feature = "log")]
::log::info!($s $(, $x)*);
#[cfg(not(any(feature="defmt", feature="log")))]
let _ = ($( & $x ),*);
};
}
#[collapse_debuginfo(yes)]
macro_rules! warn {
($s:literal $(, $x:expr)* $(,)?) => {
#[cfg(feature = "defmt")]
::defmt::warn!($s $(, $x)*);
#[cfg(feature = "log")]
::log::warn!($s $(, $x)*);
#[cfg(not(any(feature="defmt", feature="log")))]
let _ = ($( & $x ),*);
};
}
#[collapse_debuginfo(yes)]
macro_rules! error {
($s:literal $(, $x:expr)* $(,)?) => {
#[cfg(feature = "defmt")]
::defmt::error!($s $(, $x)*);
#[cfg(feature = "log")]
::log::error!($s $(, $x)*);
#[cfg(not(any(feature="defmt", feature="log")))]
let _ = ($( & $x ),*);
};
}

View File

@@ -0,0 +1,120 @@
//! Tools for publishing a [Home Assistant binary sensor](https://www.home-assistant.io/integrations/binary_sensor.mqtt/).
use core::ops::Deref;
use serde::{Deserialize, Serialize};
use crate::{homeassistant::Component, Error, Publishable, Topic};
/// The state of the sensor. Can be easily converted to or from a [`bool`].
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(from = "&str", into = "&'static str")]
#[allow(missing_docs)]
pub enum BinarySensorState {
On,
Off,
}
impl From<BinarySensorState> for &'static str {
fn from(state: BinarySensorState) -> Self {
match state {
BinarySensorState::On => "ON",
BinarySensorState::Off => "OFF",
}
}
}
impl<'a> From<&'a str> for BinarySensorState {
fn from(st: &'a str) -> Self {
if st == "ON" {
Self::On
} else {
Self::Off
}
}
}
impl From<bool> for BinarySensorState {
fn from(val: bool) -> Self {
if val {
BinarySensorState::On
} else {
BinarySensorState::Off
}
}
}
impl From<BinarySensorState> for bool {
fn from(val: BinarySensorState) -> Self {
match val {
BinarySensorState::On => true,
BinarySensorState::Off => true,
}
}
}
impl AsRef<[u8]> for BinarySensorState {
fn as_ref(&self) -> &'static [u8] {
match self {
Self::On => "ON".as_bytes(),
Self::Off => "OFF".as_bytes(),
}
}
}
/// The type of sensor.
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum BinarySensorClass {
Battery,
BatteryCharging,
CarbonMonoxide,
Cold,
Connectivity,
Door,
GarageDoor,
Gas,
Heat,
Light,
Lock,
Moisture,
Motion,
Moving,
Occupancy,
Opening,
Plug,
Power,
Presence,
Problem,
Running,
Safety,
Smoke,
Sound,
Tamper,
Update,
Vibration,
Window,
}
/// A binary sensor that can publish a [`BinarySensorState`] status.
#[derive(Serialize)]
pub struct BinarySensor {
/// The type of sensor
pub device_class: Option<BinarySensorClass>,
}
impl Component for BinarySensor {
type State = BinarySensorState;
fn platform() -> &'static str {
"binary_sensor"
}
async fn publish_state<T: Deref<Target = str>>(
&self,
topic: &Topic<T>,
state: Self::State,
) -> Result<(), Error> {
topic.with_bytes(state).publish().await
}
}

View File

@@ -0,0 +1,40 @@
//! Tools for publishing a [Home Assistant button](https://www.home-assistant.io/integrations/button.mqtt/).
use core::ops::Deref;
use serde::Serialize;
use crate::{homeassistant::Component, Error, Topic};
/// The type of button.
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum ButtonClass {
Identify,
Restart,
Update,
}
/// A button that can be pressed.
#[derive(Serialize)]
pub struct Button {
/// The type of button.
pub device_class: Option<ButtonClass>,
}
impl Component for Button {
type State = ();
fn platform() -> &'static str {
"button"
}
async fn publish_state<T: Deref<Target = str>>(
&self,
_topic: &Topic<T>,
_state: Self::State,
) -> Result<(), Error> {
// Buttons don't have a state
Err(Error::Invalid)
}
}

View File

@@ -0,0 +1,384 @@
//! Tools for publishing a [Home Assistant light](https://www.home-assistant.io/integrations/light.mqtt/).
use core::{ops::Deref, str};
use serde::{ser::SerializeStruct, Deserialize, Serialize, Serializer};
use crate::{
fmt::Debug2Format,
homeassistant::{binary_sensor::BinarySensorState, ser::List, Component},
Error, Payload, Publishable, Topic,
};
#[derive(Serialize)]
#[serde(rename_all = "lowercase")]
#[allow(missing_docs)]
pub enum SupportedColorMode {
OnOff,
Brightness,
#[serde(rename = "color_temp")]
ColorTemp,
Hs,
Xy,
Rgb,
Rgbw,
Rgbww,
White,
}
#[derive(Serialize, Deserialize, Default)]
struct SerializedColor {
#[serde(default, skip_serializing_if = "Option::is_none")]
h: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
s: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
x: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
y: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
r: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
g: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
b: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
w: Option<u8>,
#[serde(default, skip_serializing_if = "Option::is_none")]
c: Option<u8>,
}
#[derive(Deserialize)]
struct LedPayload<'a> {
state: BinarySensorState,
#[serde(default)]
brightness: Option<u8>,
#[serde(default)]
color_temp: Option<u32>,
#[serde(default)]
color: Option<SerializedColor>,
#[serde(default)]
effect: Option<&'a str>,
}
/// The color of the light in various forms.
#[derive(Serialize)]
#[serde(rename_all = "lowercase", tag = "color_mode", content = "color")]
#[allow(missing_docs)]
pub enum Color {
None,
Brightness(u8),
ColorTemp(u32),
Hs {
#[serde(rename = "h")]
hue: f32,
#[serde(rename = "s")]
saturation: f32,
},
Xy {
x: f32,
y: f32,
},
Rgb {
#[serde(rename = "r")]
red: u8,
#[serde(rename = "g")]
green: u8,
#[serde(rename = "b")]
blue: u8,
},
Rgbw {
#[serde(rename = "r")]
red: u8,
#[serde(rename = "g")]
green: u8,
#[serde(rename = "b")]
blue: u8,
#[serde(rename = "w")]
white: u8,
},
Rgbww {
#[serde(rename = "r")]
red: u8,
#[serde(rename = "g")]
green: u8,
#[serde(rename = "b")]
blue: u8,
#[serde(rename = "c")]
cool_white: u8,
#[serde(rename = "w")]
warm_white: u8,
},
}
/// The state of the light. This can be sent to the broker and received as a
/// command from Home Assistant.
pub struct LightState<'a> {
/// Whether the light is on or off.
pub state: BinarySensorState,
/// The color of the light.
pub color: Color,
/// Any effect that is applied.
pub effect: Option<&'a str>,
}
impl<'a> LightState<'a> {
/// Parses the state from a command payload.
pub fn from_payload(payload: &'a Payload) -> Result<Self, Error> {
let parsed: LedPayload<'a> = match payload.deserialize_json() {
Ok(p) => p,
Err(e) => {
warn!("Failed to deserialize packet: {:?}", Debug2Format(&e));
if let Ok(s) = str::from_utf8(payload) {
trace!("{}", s);
}
return Err(Error::PacketError);
}
};
let color = if let Some(color) = parsed.color {
if let Some(x) = color.x {
Color::Xy {
x,
y: color.y.unwrap_or_default(),
}
} else if let Some(h) = color.h {
Color::Hs {
hue: h,
saturation: color.s.unwrap_or_default(),
}
} else if let Some(c) = color.c {
Color::Rgbww {
red: color.r.unwrap_or_default(),
green: color.g.unwrap_or_default(),
blue: color.b.unwrap_or_default(),
cool_white: c,
warm_white: color.w.unwrap_or_default(),
}
} else if let Some(w) = color.w {
Color::Rgbw {
red: color.r.unwrap_or_default(),
green: color.g.unwrap_or_default(),
blue: color.b.unwrap_or_default(),
white: w,
}
} else {
Color::Rgb {
red: color.r.unwrap_or_default(),
green: color.g.unwrap_or_default(),
blue: color.b.unwrap_or_default(),
}
}
} else if let Some(color_temp) = parsed.color_temp {
Color::ColorTemp(color_temp)
} else if let Some(brightness) = parsed.brightness {
Color::Brightness(brightness)
} else {
Color::None
};
Ok(LightState {
state: parsed.state,
color,
effect: parsed.effect,
})
}
}
impl Serialize for LightState<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 1;
if self.effect.is_some() {
len += 1;
}
match self.color {
Color::None => {}
Color::Brightness(_) | Color::ColorTemp(_) => len += 1,
_ => len += 2,
}
let mut serializer = serializer.serialize_struct("LightState", len)?;
serializer.serialize_field("state", &self.state)?;
if let Some(effect) = self.effect {
serializer.serialize_field("effect", effect)?;
} else {
serializer.skip_field("effect")?;
}
match self.color {
Color::None => {
serializer.skip_field("brightness")?;
serializer.skip_field("color_temp")?;
serializer.skip_field("color")?;
}
Color::Brightness(b) => {
serializer.skip_field("color_temp")?;
serializer.skip_field("color")?;
serializer.serialize_field("brightness", &b)?
}
Color::ColorTemp(c) => {
serializer.skip_field("brightness")?;
serializer.skip_field("color")?;
serializer.serialize_field("color_temp", &c)?
}
Color::Hs { hue, saturation } => {
serializer.skip_field("brightness")?;
serializer.skip_field("color_temp")?;
serializer.serialize_field("color_mode", "hs")?;
let color = SerializedColor {
h: Some(hue),
s: Some(saturation),
..Default::default()
};
serializer.serialize_field("color", &color)?
}
Color::Xy { x, y } => {
serializer.skip_field("brightness")?;
serializer.skip_field("color_temp")?;
serializer.serialize_field("color_mode", "xy")?;
let color = SerializedColor {
x: Some(x),
y: Some(y),
..Default::default()
};
serializer.serialize_field("color", &color)?
}
Color::Rgb { red, green, blue } => {
serializer.skip_field("brightness")?;
serializer.skip_field("color_temp")?;
serializer.serialize_field("color_mode", "rgb")?;
let color = SerializedColor {
r: Some(red),
g: Some(green),
b: Some(blue),
..Default::default()
};
serializer.serialize_field("color", &color)?
}
Color::Rgbw {
red,
green,
blue,
white,
} => {
serializer.skip_field("brightness")?;
serializer.skip_field("color_temp")?;
serializer.serialize_field("color_mode", "rgbw")?;
let color = SerializedColor {
r: Some(red),
g: Some(green),
b: Some(blue),
w: Some(white),
..Default::default()
};
serializer.serialize_field("color", &color)?
}
Color::Rgbww {
red,
green,
blue,
cool_white,
warm_white,
} => {
serializer.skip_field("brightness")?;
serializer.skip_field("color_temp")?;
serializer.serialize_field("color_mode", "rgbww")?;
let color = SerializedColor {
r: Some(red),
g: Some(green),
b: Some(blue),
c: Some(cool_white),
w: Some(warm_white),
..Default::default()
};
serializer.serialize_field("color", &color)?
}
}
serializer.end()
}
}
/// A light entity
pub struct Light<'a, const C: usize, const E: usize> {
/// The color modes supported by the light.
pub supported_color_modes: [SupportedColorMode; C],
/// Any effects that can be used.
pub effects: [&'a str; E],
}
impl<const C: usize, const E: usize> Serialize for Light<'_, C, E> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if C > 0 {
len += 1;
}
if E > 0 {
len += 2;
}
let mut serializer = serializer.serialize_struct("Light", len)?;
serializer.serialize_field("schema", "json")?;
if C > 0 {
serializer.serialize_field("sup_clrm", &List::new(&self.supported_color_modes))?;
} else {
serializer.skip_field("sup_clrm")?;
}
if E > 0 {
serializer.serialize_field("effect", &true)?;
serializer.serialize_field("fx_list", &List::new(&self.effects))?;
} else {
serializer.skip_field("effect")?;
serializer.skip_field("fx_list")?;
}
serializer.end()
}
}
impl<const C: usize, const E: usize> Component for Light<'_, C, E> {
type State = LightState<'static>;
fn platform() -> &'static str {
"light"
}
async fn publish_state<T: Deref<Target = str>>(
&self,
topic: &Topic<T>,
state: Self::State,
) -> Result<(), Error> {
topic.with_json(state).publish().await
}
}

View File

@@ -0,0 +1,295 @@
//! Home Assistant auto-discovery and related messages.
//!
//! Normally you would declare your entities statically in your binary. It is
//! then trivial to send out discovery messages or state changes.
//!
//! ```
//! # use mcutie::{Publishable, Topic};
//! # use mcutie::homeassistant::{Entity, Device, Origin, AvailabilityState, AvailabilityTopics};
//! # use mcutie::homeassistant::binary_sensor::{BinarySensor, BinarySensorClass, BinarySensorState};
//! const DEVICE_AVAILABILITY_TOPIC: Topic<&'static str> = Topic::Device("status");
//! const MOTION_STATE_TOPIC: Topic<&'static str> = Topic::Device("motion/status");
//!
//! const DEVICE: Device<'static> = Device::new();
//! const ORIGIN: Origin<'static> = Origin::new();
//!
//! const MOTION_SENSOR: Entity<'static, 1, BinarySensor> = Entity {
//! device: DEVICE,
//! origin: ORIGIN,
//! object_id: "motion",
//! unique_id: Some("motion"),
//! name: "Motion",
//! availability: AvailabilityTopics::All([DEVICE_AVAILABILITY_TOPIC]),
//! state_topic: Some(MOTION_STATE_TOPIC),
//! command_topic: None,
//! component: BinarySensor {
//! device_class: Some(BinarySensorClass::Motion),
//! },
//! };
//!
//! async fn send_discovery_messages() {
//! MOTION_SENSOR.publish_discovery().await.unwrap();
//! DEVICE_AVAILABILITY_TOPIC.with_bytes(AvailabilityState::Online).publish().await.unwrap();
//! }
//!
//! async fn send_state(state: BinarySensorState) {
//! MOTION_SENSOR.publish_state(state).await.unwrap();
//! }
//! ```
use core::{future::Future, ops::Deref};
use mqttrs::QoS;
use serde::{
ser::{Error as _, SerializeStruct},
Serialize, Serializer,
};
use crate::{
device_id, device_type, homeassistant::ser::DiscoverySerializer, io::publish, Error,
McutieTask, MqttMessage, Payload, Publishable, Topic, TopicString, DATA_CHANNEL,
};
pub mod binary_sensor;
pub mod button;
pub mod light;
pub mod sensor;
mod ser;
const HA_STATUS_TOPIC: Topic<&'static str> = Topic::General("homeassistant/status");
const STATE_ONLINE: &str = "online";
const STATE_OFFLINE: &str = "offline";
/// A trait representing a specific type of entity in Home Assistant
pub trait Component: Serialize {
/// The state to publish.
type State;
/// The platform identifier for this entity. Internal.
fn platform() -> &'static str;
/// Publishes this entity's state to the MQTT broker.
fn publish_state<T: Deref<Target = str>>(
&self,
topic: &Topic<T>,
state: Self::State,
) -> impl Future<Output = Result<(), Error>>;
}
impl<'t, T, L, const S: usize> McutieTask<'t, T, L, S>
where
T: Deref<Target = str> + 't,
L: Publishable + 't,
{
pub(super) async fn ha_after_connected(&self) {
let _ = HA_STATUS_TOPIC.subscribe(false).await;
}
pub(super) async fn ha_handle_update(
&self,
topic: &Topic<TopicString>,
payload: &Payload,
) -> bool {
if topic == &HA_STATUS_TOPIC {
if payload.as_ref() == STATE_ONLINE.as_bytes() {
DATA_CHANNEL.send(MqttMessage::HomeAssistantOnline).await;
}
true
} else {
false
}
}
}
impl<T: Deref<Target = str>> Serialize for Topic<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut topic = TopicString::new();
self.to_string(&mut topic)
.map_err(|_| S::Error::custom("topic was too large to serialize"))?;
serializer.serialize_str(&topic)
}
}
fn name_or_device<S>(name: &Option<&str>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(name.unwrap_or_else(|| device_type()))
}
/// Represents the device in Home Assistant.
///
/// Can just be the default in which case useful properties such as the ID are
/// automatically included.
#[derive(Clone, Copy, Default)]
pub struct Device<'a> {
/// A name to identify the device. If not provided the default device type is
/// used.
pub name: Option<&'a str>,
/// An optional configuration URL for the device.
pub configuration_url: Option<&'a str>,
}
impl Device<'_> {
/// Creates a new default device.
pub const fn new() -> Self {
Self {
name: None,
configuration_url: None,
}
}
}
impl Serialize for Device<'_> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut len = 2;
if self.configuration_url.is_some() {
len += 1;
}
let mut serializer = serializer.serialize_struct("Device", len)?;
serializer.serialize_field("name", self.name.unwrap_or_else(|| device_type()))?;
serializer.serialize_field("ids", device_id())?;
if let Some(cu) = self.configuration_url {
serializer.serialize_field("cu", cu)?;
} else {
serializer.skip_field("cu")?;
}
serializer.end()
}
}
/// Represents the device's origin in Home Assistant.
///
/// Can just be the default in which case useful properties are automatically
/// included.
#[derive(Clone, Copy, Default, Serialize)]
pub struct Origin<'a> {
/// A name to identify the device's origin. If not provided the default
/// device type is used.
#[serde(serialize_with = "name_or_device")]
pub name: Option<&'a str>,
}
impl Origin<'_> {
/// Creates a new default origin.
pub const fn new() -> Self {
Self { name: None }
}
}
/// A single entity for Home Assistant.
///
/// Calling [`Entity::publish_discovery`] will publish the discovery message to
/// allow Home Assistant to detect this entity. Read the
/// [Home Assistant MQTT docs](https://www.home-assistant.io/integrations/mqtt/)
/// for information on what some of these properties mean.
pub struct Entity<'a, const A: usize, C: Component> {
/// The device this entity is a part of.
pub device: Device<'a>,
/// The origin of the device.
pub origin: Origin<'a>,
/// An object identifier to allow for entity ID customisation in Home Assistant.
pub object_id: &'a str,
/// An optional unique identifier for the entity.
pub unique_id: Option<&'a str>,
/// A friendly name for the entity.
pub name: &'a str,
/// Specifies the availability topics that Home Assistant will listen to to
/// determine this entity's availability.
pub availability: AvailabilityTopics<'a, A>,
/// The state topic that this entity's state is published to.
pub state_topic: Option<Topic<&'a str>>,
/// The command topic that this entity receives commands from.
pub command_topic: Option<Topic<&'a str>>,
/// The specific entity.
pub component: C,
}
impl<const A: usize, C: Component> Entity<'_, A, C> {
/// Publishes the discovery message for this entity to the broker.
pub async fn publish_discovery(&self) -> Result<(), Error> {
let mut topic = TopicString::new();
topic
.push_str(option_env!("HA_DISCOVERY_PREFIX").unwrap_or("homeassistant"))
.map_err(|_| Error::TooLarge)?;
topic.push('/').map_err(|_| Error::TooLarge)?;
topic.push_str(C::platform()).map_err(|_| Error::TooLarge)?;
topic.push('/').map_err(|_| Error::TooLarge)?;
topic
.push_str(self.object_id)
.map_err(|_| Error::TooLarge)?;
topic.push_str("/config").map_err(|_| Error::TooLarge)?;
let mut payload = Payload::new();
payload.serialize_json(self).map_err(|_| Error::TooLarge)?;
publish(&topic, &payload, QoS::AtMostOnce, false).await
}
/// Publishes this entity's state to the broker.
///
/// # Errors
///
/// - [`Error::Invalid`] if the entity doesn't have a state topic.
pub async fn publish_state(&self, state: C::State) -> Result<(), Error> {
if let Some(topic) = self.state_topic {
self.component.publish_state(&topic, state).await
} else {
Err(Error::Invalid)
}
}
}
/// A payload representing a device or entity's availability.
#[allow(missing_docs)]
pub enum AvailabilityState {
Online,
Offline,
}
impl AsRef<[u8]> for AvailabilityState {
fn as_ref(&self) -> &'static [u8] {
match self {
Self::Online => STATE_ONLINE.as_bytes(),
Self::Offline => STATE_OFFLINE.as_bytes(),
}
}
}
/// The availiabity topics that home assistant will use to determine an entity's
/// availability.
pub enum AvailabilityTopics<'a, const A: usize> {
/// The entity is always available.
None,
/// The entity is available if all of the topics are publishes as online.
All([Topic<&'a str>; A]),
/// The entity is available if any of the topics are publishes as online.
Any([Topic<&'a str>; A]),
/// The entity is available based on the most recent of the topics to
/// publish state.
Latest([Topic<&'a str>; A]),
}
impl<const A: usize, C: Component> Serialize for Entity<'_, A, C> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let outer = DiscoverySerializer {
discovery: self,
inner: serializer,
};
self.component.serialize(outer)
}
}

View File

@@ -0,0 +1,103 @@
//! Tools for publishing a [Home Assistant sensor](https://www.home-assistant.io/integrations/sensor.mqtt/).
use core::ops::Deref;
use serde::Serialize;
use crate::{homeassistant::Component, Error, Publishable, Topic};
/// The type of sensor.
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
#[allow(missing_docs)]
pub enum SensorClass {
ApparentPower,
Aqi,
AtmosphericPressure,
Battery,
CarbonDioxide,
CarbonMonoxide,
Current,
DataRate,
DataSize,
Date,
Distance,
Duration,
Energy,
EnergyStorage,
Enum,
Frequency,
Gas,
Humidity,
Illuminance,
Irradiance,
Moisture,
Monetary,
NitrogenDioxide,
NitrogenMonoxide,
NitrousOxide,
Ozone,
Ph,
Pm1,
Pm25,
Pm10,
PowerFactor,
Power,
Precipitation,
PrecipitationIntensity,
Pressure,
ReactivePower,
SignalStrength,
SoundPressure,
Speed,
SulphurDioxide,
Temperature,
Timestamp,
VolatileOrganicCompounds,
VolatileOrganicCompoundsParts,
Voltage,
Volume,
VolumeFlowRate,
VolumeStorage,
Water,
Weight,
WindSpeed,
}
/// The type of measurement that this entity publishes.
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SensorStateClass {
/// A measurement at a singe point in time.
Measurement,
/// A cumulative total that can increase or decrease over time.
Total,
/// A cumulative total that can only increase.
TotalIncreasing,
}
/// A binary sensor that can publish a [`f32`] value.
#[derive(Serialize)]
pub struct Sensor<'u> {
/// The type of sensor.
pub device_class: Option<SensorClass>,
/// The type of measurement that this sensor reports.
pub state_class: Option<SensorStateClass>,
/// The unit of measurement for this sensor.
pub unit_of_measurement: Option<&'u str>,
}
impl Component for Sensor<'_> {
type State = f32;
fn platform() -> &'static str {
"sensor"
}
async fn publish_state<T: Deref<Target = str>>(
&self,
topic: &Topic<T>,
state: Self::State,
) -> Result<(), Error> {
topic.with_display(state).publish().await
}
}

View File

@@ -0,0 +1,333 @@
use core::ops::Deref;
use serde::{
ser::{SerializeSeq, SerializeStruct},
Serialize, Serializer,
};
use crate::{
homeassistant::{AvailabilityTopics, Component, Entity},
Topic,
};
#[derive(Serialize)]
pub(super) struct AvailabilityTopicItem<'a> {
topic: Topic<&'a str>,
}
struct AvailabilityTopicList<'a, T: Deref<Target = str>, const N: usize> {
list: &'a [Topic<T>; N],
}
impl<'a, const N: usize, T: Deref<Target = str>> AvailabilityTopicList<'a, T, N> {
pub(super) fn new(list: &'a [Topic<T>; N]) -> Self {
Self { list }
}
}
impl<T: Deref<Target = str>, const N: usize> Serialize for AvailabilityTopicList<'_, T, N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut serializer = serializer.serialize_seq(Some(N))?;
for topic in self.list {
serializer.serialize_element(&AvailabilityTopicItem {
topic: topic.as_ref(),
})?;
}
serializer.end()
}
}
pub(super) struct List<'a, T: Serialize, const N: usize> {
list: &'a [T; N],
}
impl<'a, T: Serialize, const N: usize> List<'a, T, N> {
pub(super) fn new(list: &'a [T; N]) -> Self {
Self { list }
}
}
impl<T: Serialize, const N: usize> Serialize for List<'_, T, N> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut serializer = serializer.serialize_seq(Some(N))?;
for item in self.list {
serializer.serialize_element(item)?;
}
serializer.end()
}
}
pub(super) struct DiscoverySerializer<'a, const A: usize, C: Component, S: Serializer> {
pub(super) discovery: &'a Entity<'a, A, C>,
pub(super) inner: S,
}
impl<const A: usize, C: Component, S: Serializer> Serializer for DiscoverySerializer<'_, A, C, S> {
type Ok = S::Ok;
type Error = S::Error;
type SerializeSeq = S::SerializeSeq;
type SerializeTuple = S::SerializeTuple;
type SerializeTupleStruct = S::SerializeTupleStruct;
type SerializeTupleVariant = S::SerializeTupleVariant;
type SerializeMap = S::SerializeMap;
type SerializeStruct = S::SerializeStruct;
type SerializeStructVariant = S::SerializeStructVariant;
fn serialize_struct(
self,
name: &'static str,
mut len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
len += 5;
if self.discovery.state_topic.is_some() {
len += 1;
}
if self.discovery.command_topic.is_some() {
len += 1;
}
if self.discovery.unique_id.is_some() {
len += 1;
}
if !matches!(self.discovery.availability, AvailabilityTopics::None) {
len += 2;
}
let mut serializer = self.inner.serialize_struct(name, len)?;
serializer.serialize_field("dev", &self.discovery.device)?;
serializer.serialize_field("o", &self.discovery.origin)?;
serializer.serialize_field("p", C::platform())?;
serializer.serialize_field("obj_id", self.discovery.object_id)?;
serializer.serialize_field("name", self.discovery.name)?;
if let Some(t) = self.discovery.state_topic {
serializer.serialize_field("stat_t", &t)?;
} else {
serializer.skip_field("stat_t")?;
}
if let Some(t) = self.discovery.command_topic {
serializer.serialize_field("cmd_t", &t)?;
} else {
serializer.skip_field("cmd_t")?;
}
match &self.discovery.availability {
AvailabilityTopics::None => {
serializer.skip_field("avty")?;
serializer.skip_field("avty_mode")?;
}
AvailabilityTopics::All(topics) => {
serializer.serialize_field("avty_mode", "all")?;
serializer.serialize_field("avty", &AvailabilityTopicList::new(topics))?;
}
AvailabilityTopics::Any(topics) => {
serializer.serialize_field("avty_mode", "any")?;
serializer.serialize_field("avty", &AvailabilityTopicList::new(topics))?;
}
AvailabilityTopics::Latest(topics) => {
serializer.serialize_field("avty_mode", "latest")?;
serializer.serialize_field("avty", &AvailabilityTopicList::new(topics))?;
}
}
if let Some(v) = self.discovery.unique_id {
serializer.serialize_field("uniq_id", v)?;
} else {
serializer.skip_field("uniq_id")?;
}
Ok(serializer)
}
fn serialize_bool(self, _: bool) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_i8(self, _: i8) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_i16(self, _: i16) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_i32(self, _: i32) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_i64(self, _: i64) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_u8(self, _: u8) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_u16(self, _: u16) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_u32(self, _: u32) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_u64(self, _: u64) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_f32(self, _: f32) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_f64(self, _: f64) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_char(self, _: char) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_str(self, _: &str) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_some<T>(self, _: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
unimplemented!()
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_unit_struct(self, _: &'static str) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_unit_variant(
self,
_: &'static str,
_: u32,
_: &'static str,
) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_newtype_struct<T>(self, _: &'static str, _: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
unimplemented!()
}
fn serialize_newtype_variant<T>(
self,
_: &'static str,
_: u32,
_: &'static str,
_: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
unimplemented!()
}
fn serialize_seq(self, _: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
unimplemented!()
}
fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple, Self::Error> {
unimplemented!()
}
fn serialize_tuple_struct(
self,
_: &'static str,
_: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
unimplemented!()
}
fn serialize_tuple_variant(
self,
_: &'static str,
_: u32,
_: &'static str,
_: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
unimplemented!()
}
fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
unimplemented!()
}
fn serialize_struct_variant(
self,
_: &'static str,
_: u32,
_: &'static str,
_: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
unimplemented!()
}
fn serialize_i128(self, _: i128) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn serialize_u128(self, _: u128) -> Result<Self::Ok, Self::Error> {
unimplemented!()
}
fn collect_seq<I>(self, _: I) -> Result<Self::Ok, Self::Error>
where
I: IntoIterator,
<I as IntoIterator>::Item: Serialize,
{
unimplemented!()
}
fn collect_map<K, V, I>(self, _: I) -> Result<Self::Ok, Self::Error>
where
K: Serialize,
V: Serialize,
I: IntoIterator<Item = (K, V)>,
{
unimplemented!()
}
fn collect_str<T>(self, _: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + core::fmt::Display,
{
unimplemented!()
}
fn is_human_readable(&self) -> bool {
unimplemented!()
}
}

483
rust/src/mcutie_3_0_0/io.rs Normal file
View File

@@ -0,0 +1,483 @@
use core::ops::Deref;
pub(crate) use atomic16::assign_pid;
use embassy_futures::select::{select, select4, Either};
use embassy_net::{
dns::DnsQueryType,
tcp::{TcpReader, TcpSocket, TcpWriter},
Stack,
};
use embassy_sync::{
blocking_mutex::raw::CriticalSectionRawMutex,
pubsub::{PubSubChannel, Subscriber, WaitResult},
};
use embassy_time::Timer;
use embedded_io_async::Write;
use mqttrs::{
decode_slice, Connect, ConnectReturnCode, LastWill, Packet, Pid, Protocol, Publish, QoS, QosPid,
};
use crate::{
device_id, fmt::Debug2Format, pipe::ConnectedPipe, ControlMessage, Error, MqttMessage, Payload,
Publishable, Topic, TopicString, CONFIRMATION_TIMEOUT, DATA_CHANNEL, DEFAULT_BACKOFF,
RESET_BACKOFF,
};
static SEND_QUEUE: ConnectedPipe<CriticalSectionRawMutex, Payload, 10> = ConnectedPipe::new();
pub(crate) static CONTROL_CHANNEL: PubSubChannel<CriticalSectionRawMutex, ControlMessage, 2, 5, 0> =
PubSubChannel::new();
type ControlSubscriber = Subscriber<'static, CriticalSectionRawMutex, ControlMessage, 2, 5, 0>;
pub(crate) async fn subscribe() -> ControlSubscriber {
loop {
if let Ok(sub) = CONTROL_CHANNEL.subscriber() {
return sub;
}
Timer::after_millis(50).await;
}
}
#[cfg(target_has_atomic = "16")]
mod atomic16 {
use core::sync::atomic::{AtomicU16, Ordering};
use mqttrs::Pid;
static PID: AtomicU16 = AtomicU16::new(0);
pub(crate) async fn assign_pid() -> Pid {
Pid::new() + PID.fetch_add(1, Ordering::SeqCst)
}
}
#[cfg(not(target_has_atomic = "16"))]
mod atomic16 {
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, mutex::Mutex};
use mqttrs::Pid;
static PID_MUTEX: Mutex<CriticalSectionRawMutex, u16> = Mutex::new(0);
pub(crate) async fn assign_pid() -> Pid {
let mut locked = PID_MUTEX.lock().await;
*locked += 1;
Pid::new() + *locked
}
}
pub(crate) async fn send_packet(packet: Packet<'_>) -> Result<(), Error> {
let mut buffer = Payload::new();
match buffer.encode_packet(&packet) {
Ok(()) => {
debug!(
"Sending packet to broker: {:?}",
Debug2Format(&packet.get_type())
);
SEND_QUEUE.push(buffer).await;
Ok(())
}
Err(_) => {
error!("Failed to send packet");
Err(Error::PacketError)
}
}
}
pub(crate) async fn wait_for_publish(
mut subscriber: ControlSubscriber,
expected_pid: Pid,
) -> Result<(), Error> {
match select(
async {
loop {
match subscriber.next_message().await {
WaitResult::Lagged(_) => {
// Maybe we missed the message?
}
WaitResult::Message(ControlMessage::Published(published_pid)) => {
if published_pid == expected_pid {
return Ok(());
}
}
_ => {}
}
}
},
Timer::after_millis(CONFIRMATION_TIMEOUT),
)
.await
{
Either::First(r) => r,
Either::Second(_) => Err(Error::TimedOut),
}
}
pub(crate) async fn publish(
topic_name: &str,
payload: &[u8],
qos: QoS,
retain: bool,
) -> Result<(), Error> {
let subscriber = subscribe().await;
let (qospid, pid) = match qos {
QoS::AtMostOnce => (QosPid::AtMostOnce, None),
QoS::AtLeastOnce => {
let pid = assign_pid().await;
(QosPid::AtLeastOnce(pid), Some(pid))
}
QoS::ExactlyOnce => {
let pid = assign_pid().await;
(QosPid::ExactlyOnce(pid), Some(pid))
}
};
let packet = Packet::Publish(Publish {
dup: false,
qospid,
retain,
topic_name,
payload,
});
send_packet(packet).await?;
if let Some(expected_pid) = pid {
wait_for_publish(subscriber, expected_pid).await
} else {
Ok(())
}
}
fn packet_size(buffer: &[u8]) -> Option<usize> {
let mut pos = 1;
let mut multiplier = 1;
let mut value = 0;
while pos < buffer.len() {
value += (buffer[pos] & 127) as usize * multiplier;
multiplier *= 128;
if (buffer[pos] & 128) == 0 {
return Some(value + pos + 1);
}
pos += 1;
if pos == 5 {
return Some(0);
}
}
None
}
/// The MQTT task that must be run in order for the stack to operate.
pub struct McutieTask<'t, T, L, const S: usize>
where
T: Deref<Target = str> + 't,
L: Publishable + 't,
{
pub(crate) network: Stack<'t>,
pub(crate) broker: &'t str,
pub(crate) last_will: Option<L>,
pub(crate) username: Option<&'t str>,
pub(crate) password: Option<&'t str>,
pub(crate) subscriptions: [Topic<T>; S],
pub(crate) keep_alive: u16
}
impl<'t, T, L, const S: usize> McutieTask<'t, T, L, S>
where
T: Deref<Target = str> + 't,
L: Publishable + 't,
{
#[cfg(not(feature = "homeassistant"))]
async fn ha_handle_update(&self, _topic: &Topic<TopicString>, _payload: &Payload) -> bool {
false
}
async fn recv_loop(&self, mut reader: TcpReader<'_>) -> Result<(), Error> {
let mut buffer = [0_u8; 4096];
let mut cursor: usize = 0;
let controller = CONTROL_CHANNEL.immediate_publisher();
loop {
match reader.read(&mut buffer[cursor..]).await {
Ok(0) => {
error!("Receive socket closed");
return Ok(());
}
Ok(len) => {
cursor += len;
}
Err(_) => {
error!("I/O failure reading packet");
return Err(Error::IOError);
}
}
let mut start_pos = 0;
loop {
let packet_length = match packet_size(&buffer[start_pos..cursor]) {
Some(0) => {
error!("Invalid MQTT packet");
return Err(Error::PacketError);
}
Some(len) => len,
None => {
// None is returned when there is not yet enough data to decode a packet.
if start_pos != 0 {
// Adjust the buffer to reclaim any unused data
buffer.copy_within(start_pos..cursor, 0);
cursor -= start_pos;
}
break;
}
};
let packet = match decode_slice(&buffer[start_pos..(start_pos + packet_length)]) {
Ok(Some(p)) => p,
Ok(None) => {
error!("Packet length calculation failed.");
return Err(Error::PacketError);
}
Err(_) => {
error!("Invalid MQTT packet");
return Err(Error::PacketError);
}
};
debug!(
"Received packet from broker: {:?}",
Debug2Format(&packet.get_type())
);
match packet {
Packet::Connack(connack) => match connack.code {
ConnectReturnCode::Accepted => {
#[cfg(feature = "homeassistant")]
self.ha_after_connected().await;
for topic in &self.subscriptions {
let _ = topic.subscribe(false).await;
}
DATA_CHANNEL.send(MqttMessage::Connected).await;
}
_ => {
error!("Connection request to broker was not accepted");
return Err(Error::IOError);
}
},
Packet::Pingresp => {}
Packet::Publish(publish) => {
match (
Topic::from_str(publish.topic_name),
Payload::from(publish.payload),
) {
(Ok(topic), Ok(payload)) => {
if !self.ha_handle_update(&topic, &payload).await {
DATA_CHANNEL
.send(MqttMessage::Publish(topic, payload))
.await;
}
}
_ => {
error!("Unable to process publish data as it was too large");
}
}
match publish.qospid {
mqttrs::QosPid::AtMostOnce => {}
mqttrs::QosPid::AtLeastOnce(pid) => {
send_packet(Packet::Puback(pid)).await?;
}
mqttrs::QosPid::ExactlyOnce(pid) => {
send_packet(Packet::Pubrec(pid)).await?;
}
}
}
Packet::Puback(pid) => {
controller.publish_immediate(ControlMessage::Published(pid));
}
Packet::Pubrec(pid) => {
controller.publish_immediate(ControlMessage::Published(pid));
send_packet(Packet::Pubrel(pid)).await?;
}
Packet::Pubrel(pid) => send_packet(Packet::Pubrel(pid)).await?,
Packet::Pubcomp(_) => {}
Packet::Suback(suback) => {
if let Some(return_code) = suback.return_codes.first() {
controller.publish_immediate(ControlMessage::Subscribed(
suback.pid,
*return_code,
));
} else {
warn!("Unexpected suback with no return codes");
}
}
Packet::Unsuback(pid) => {
controller.publish_immediate(ControlMessage::Unsubscribed(pid));
}
Packet::Connect(_)
| Packet::Subscribe(_)
| Packet::Pingreq
| Packet::Unsubscribe(_)
| Packet::Disconnect => {
debug!(
"Unexpected packet from broker: {:?}",
Debug2Format(&packet.get_type())
);
}
}
start_pos += packet_length;
if start_pos == cursor {
cursor = 0;
break;
}
}
}
}
async fn write_loop(&self, mut writer: TcpWriter<'_>) {
let mut buffer = Payload::new();
let mut last_will_topic = TopicString::new();
let mut last_will_payload = Payload::new();
let last_will = self.last_will.as_ref().and_then(|p| {
if p.write_topic(&mut last_will_topic).is_ok()
&& p.write_payload(&mut last_will_payload).is_ok()
{
Some(LastWill {
topic: &last_will_topic,
message: &last_will_payload,
qos: p.qos(),
retain: p.retain(),
})
} else {
None
}
});
// Send our connection request.
if buffer
.encode_packet(&Packet::Connect(Connect {
protocol: Protocol::MQTT311,
keep_alive: self.keep_alive,
client_id: device_id(),
clean_session: true,
last_will,
username: self.username,
password: self.password.map(|s| s.as_bytes()),
}))
.is_err()
{
error!("Failed to encode connection packet");
return;
}
if let Err(e) = writer.write(&buffer).await {
error!("Failed to send connection packet: {:?}", e);
return;
}
let reader = SEND_QUEUE.reader();
loop {
let buffer = reader.receive().await;
trace!("Writer sending packet");
if let Err(e) = writer.write(&buffer).await {
error!("Failed to send data: {:?}", e);
return;
}
}
}
/// Runs the MQTT stack. The future returned from this must be awaited for everything to work.
pub async fn run(self) {
let mut timeout: Option<u64> = None;
let mut rx_buffer = [0; 4096];
let mut tx_buffer = [0; 4096];
loop {
if let Some(millis) = timeout.replace(DEFAULT_BACKOFF) {
Timer::after_millis(millis).await;
}
if !self.network.is_config_up() {
debug!("Waiting for network to configure.");
self.network.wait_config_up().await;
debug!("Network configured.");
}
let ip_addrs = match self.network.dns_query(self.broker, DnsQueryType::A).await {
Ok(v) => v,
Err(e) => {
error!("Failed to lookup '{}' for broker: {:?}", self.broker, e);
continue;
}
};
let ip = match ip_addrs.first() {
Some(i) => *i,
None => {
error!("No IP address found for broker '{}'", self.broker);
continue;
}
};
debug!("Connecting to {}:1883", ip);
let mut socket = TcpSocket::new(self.network, &mut rx_buffer, &mut tx_buffer);
if let Err(e) = socket.connect((ip, 1883)).await {
error!("Failed to connect to {}:1883: {:?}", ip, e);
continue;
}
info!("Connected to {}", self.broker);
timeout = Some(RESET_BACKOFF);
let (reader, writer) = socket.split();
let recv_loop = self.recv_loop(reader);
let send_loop = self.write_loop(writer);
let ping_loop = async {
loop {
Timer::after_secs(45).await;
let _ = send_packet(Packet::Pingreq).await;
}
};
let link_down = async {
self.network.wait_link_down().await;
warn!("Network link lost");
};
let ip_down = async {
self.network.wait_config_down().await;
warn!("Network config lost");
};
select4(send_loop, ping_loop, recv_loop, select(link_down, ip_down)).await;
socket.close();
warn!("Lost connection with broker");
DATA_CHANNEL.send(MqttMessage::Disconnected).await;
}
}
}

View File

@@ -0,0 +1,227 @@
#![no_std]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
//! MQTT client support crate vendored into this repository.
use core::{ops::Deref, str};
pub use buffer::Buffer;
use embassy_net::{HardwareAddress, Stack};
use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, channel::Channel};
use heapless::String;
pub use io::McutieTask;
pub use mqttrs::QoS;
use mqttrs::{Pid, SubscribeReturnCodes};
use once_cell::sync::OnceCell;
pub use publish::*;
pub use topic::Topic;
// This must come first so the macros are visible
pub(crate) mod fmt;
mod buffer;
#[cfg(feature = "homeassistant")]
pub mod homeassistant;
mod io;
mod pipe;
mod publish;
mod topic;
// This really needs to match that used by mqttrs.
const TOPIC_LENGTH: usize = 256;
const PAYLOAD_LENGTH: usize = 2048;
/// A fixed length stack allocated string. The length is fixed by the mqttrs crate.
pub type TopicString = String<TOPIC_LENGTH>;
/// A fixed length buffer of 2048 bytes.
pub type Payload = Buffer<PAYLOAD_LENGTH>;
// By default in the event of an error connecting to the broker we will wait for 5s.
const DEFAULT_BACKOFF: u64 = 5000;
// If the connection dropped then re-connect more quickly.
const RESET_BACKOFF: u64 = 200;
// How long to wait for the broker to confirm actions.
const CONFIRMATION_TIMEOUT: u64 = 2000;
static DATA_CHANNEL: Channel<CriticalSectionRawMutex, MqttMessage, 10> = Channel::new();
static DEVICE_TYPE: OnceCell<String<32>> = OnceCell::new();
static DEVICE_ID: OnceCell<String<32>> = OnceCell::new();
fn device_id() -> &'static str {
DEVICE_ID.get().unwrap()
}
fn device_type() -> &'static str {
DEVICE_TYPE.get().unwrap()
}
/// Various errors
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Error {
/// An IO error occured.
IOError,
/// The operation timed out.
TimedOut,
/// An attempt was made to encode something too large.
TooLarge,
/// A packet or payload could not be decoded or encoded.
PacketError,
/// An invalid or unsupported operation was attempted.
Invalid,
/// A value was rejected.
Rejected,
}
#[allow(clippy::large_enum_variant)]
/// A message from the MQTT broker.
pub enum MqttMessage {
/// The broker has been connected to successfully. Generally in response to this message a
/// device should subscribe to topics of interest and send out any device state.
Connected,
/// New data received from the broker.
Publish(Topic<TopicString>, Payload),
/// The connection to the broker has been dropped.
Disconnected,
/// Home Assistant has come online and you should send any discovery messages.
#[cfg(feature = "homeassistant")]
HomeAssistantOnline,
}
#[derive(Clone)]
enum ControlMessage {
Published(Pid),
Subscribed(Pid, SubscribeReturnCodes),
Unsubscribed(Pid),
}
/// Receives messages from the broker.
pub struct McutieReceiver;
impl McutieReceiver {
/// Waits for the next message from the broker.
pub async fn receive(&self) -> MqttMessage {
DATA_CHANNEL.receive().await
}
}
/// A builder to configure the MQTT stack.
pub struct McutieBuilder<'t, T, L, const S: usize>
where
T: Deref<Target = str> + 't,
L: Publishable + 't,
{
network: Stack<'t>,
device_type: &'t str,
device_id: Option<&'t str>,
broker: &'t str,
last_will: Option<L>,
username: Option<&'t str>,
password: Option<&'t str>,
subscriptions: [Topic<T>; S],
}
impl<'t, T: Deref<Target = str> + 't, L: Publishable + 't> McutieBuilder<'t, T, L, 0> {
/// Creates a new builder with the initial required configuration.
///
/// `device_type` is expected to be the same for all devices of the same type.
/// `broker` may be an IP address or a DNS name for the broker to connect to.
pub fn new(network: Stack<'t>, device_type: &'t str, broker: &'t str) -> Self {
Self {
network,
device_type,
broker,
device_id: None,
last_will: None,
username: None,
password: None,
subscriptions: [],
}
}
}
impl<'t, T: Deref<Target = str> + 't, L: Publishable + 't, const S: usize>
McutieBuilder<'t, T, L, S>
{
/// Add some default topics to subscribe to.
pub fn with_subscriptions<const N: usize>(
self,
subscriptions: [Topic<T>; N],
) -> McutieBuilder<'t, T, L, N> {
McutieBuilder {
network: self.network,
device_type: self.device_type,
broker: self.broker,
device_id: self.device_id,
last_will: self.last_will,
username: self.username,
password: self.password,
subscriptions,
}
}
}
impl<'t, T: Deref<Target = str> + 't, L: Publishable + 't, const S: usize>
McutieBuilder<'t, T, L, S>
{
/// Adds authentication for the broker.
pub fn with_authentication(self, username: &'t str, password: &'t str) -> Self {
Self {
username: Some(username),
password: Some(password),
..self
}
}
/// Sets a last will message to be published in the event of disconnection.
pub fn with_last_will(self, last_will: L) -> Self {
Self {
last_will: Some(last_will),
..self
}
}
/// Sets a custom unique device identifier. If none is set then the network
/// MAC address is used.
pub fn with_device_id(self, device_id: &'t str) -> Self {
Self {
device_id: Some(device_id),
..self
}
}
/// Initialises the MQTT stack returning a receiver for listening to
/// messages from the broker and a future that must be run in order for the
/// stack to operate.
pub fn build(self, keep_alive: u16) -> (McutieReceiver, McutieTask<'t, T, L, S>) {
let mut dtype = String::<32>::new();
dtype.push_str(self.device_type).unwrap();
DEVICE_TYPE.set(dtype).unwrap();
let mut did = String::<32>::new();
if let Some(device_id) = self.device_id {
did.push_str(device_id).unwrap();
} else if let HardwareAddress::Ethernet(address) = self.network.hardware_address() {
let mut buffer = [0_u8; 12];
hex::encode_to_slice(address.as_bytes(), &mut buffer).unwrap();
did.push_str(str::from_utf8(&buffer).unwrap()).unwrap();
}
DEVICE_ID.set(did).unwrap();
(
McutieReceiver {},
McutieTask {
network: self.network,
broker: self.broker,
last_will: self.last_will,
username: self.username,
password: self.password,
subscriptions: self.subscriptions,
keep_alive
},
)
}
}

View File

@@ -0,0 +1,267 @@
use core::{
cell::RefCell,
future::Future,
pin::Pin,
task::{Context, Poll, Waker},
};
use embassy_sync::blocking_mutex::{raw::RawMutex, Mutex};
use pin_project::pin_project;
struct PipeData<T, const N: usize> {
connect_count: usize,
receiver_waker: Option<Waker>,
sender_waker: Option<Waker>,
pending: Option<T>,
}
fn swap_wakers(waker: &mut Option<Waker>, new_waker: &Waker) {
if let Some(old_waker) = waker.take() {
if old_waker.will_wake(new_waker) {
*waker = Some(old_waker)
} else {
if !new_waker.will_wake(&old_waker) {
old_waker.wake();
}
*waker = Some(new_waker.clone());
}
} else {
*waker = Some(new_waker.clone())
}
}
pub(crate) struct ReceiveFuture<'a, M: RawMutex, T, const N: usize> {
pipe: &'a ConnectedPipe<M, T, N>,
}
impl<M: RawMutex, T, const N: usize> Future for ReceiveFuture<'_, M, T, N> {
type Output = T;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.pipe.inner.lock(|cell| {
let mut inner = cell.borrow_mut();
if let Some(waker) = inner.sender_waker.take() {
waker.wake();
}
if let Some(item) = inner.pending.take() {
if let Some(old_waker) = inner.receiver_waker.take() {
old_waker.wake();
}
Poll::Ready(item)
} else {
swap_wakers(&mut inner.receiver_waker, cx.waker());
Poll::Pending
}
})
}
}
pub(crate) struct PipeReader<'a, M: RawMutex, T, const N: usize> {
pipe: &'a ConnectedPipe<M, T, N>,
}
impl<M: RawMutex, T, const N: usize> PipeReader<'_, M, T, N> {
#[must_use]
pub(crate) fn receive(&self) -> ReceiveFuture<'_, M, T, N> {
ReceiveFuture { pipe: self.pipe }
}
}
impl<M: RawMutex, T, const N: usize> Drop for PipeReader<'_, M, T, N> {
fn drop(&mut self) {
self.pipe.inner.lock(|cell| {
let mut inner = cell.borrow_mut();
inner.connect_count -= 1;
if inner.connect_count == 0 {
inner.pending = None;
}
if let Some(waker) = inner.sender_waker.take() {
waker.wake();
}
})
}
}
#[pin_project]
pub(crate) struct PushFuture<'a, M: RawMutex, T, const N: usize> {
data: Option<T>,
pipe: &'a ConnectedPipe<M, T, N>,
}
impl<M: RawMutex, T, const N: usize> Future for PushFuture<'_, M, T, N> {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.pipe.inner.lock(|cell| {
let project = self.project();
let mut inner = cell.borrow_mut();
if let Some(receiver) = inner.receiver_waker.take() {
receiver.wake();
}
if project.data.is_none() || inner.connect_count == 0 {
trace!("Dropping packet");
Poll::Ready(())
} else if inner.pending.is_some() {
swap_wakers(&mut inner.sender_waker, cx.waker());
Poll::Pending
} else {
inner.pending = project.data.take();
Poll::Ready(())
}
})
}
}
/// A pipe that knows whether a receiver is connected. If so pushing to the
/// queue waits until there is space in the queue, otherwise data is simply
/// dropped.
pub(crate) struct ConnectedPipe<M: RawMutex, T, const N: usize> {
inner: Mutex<M, RefCell<PipeData<T, N>>>,
}
impl<M: RawMutex, T, const N: usize> ConnectedPipe<M, T, N> {
pub(crate) const fn new() -> Self {
Self {
inner: Mutex::new(RefCell::new(PipeData {
connect_count: 0,
receiver_waker: None,
sender_waker: None,
pending: None,
})),
}
}
/// A future that waits for a new item to be available.
pub(crate) fn reader(&self) -> PipeReader<'_, M, T, N> {
self.inner.lock(|cell| {
let mut inner = cell.borrow_mut();
inner.connect_count += 1;
PipeReader { pipe: self }
})
}
/// Pushes an item to the reader, waiting for a slot to become available if
/// connected.
#[must_use]
pub(crate) fn push(&self, data: T) -> PushFuture<'_, M, T, N> {
PushFuture {
data: Some(data),
pipe: self,
}
}
}
#[cfg(test)]
mod tests {
use core::time::Duration;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use futures_executor::{LocalPool, ThreadPool};
use futures_timer::Delay;
use futures_util::{future::select, pin_mut, task::SpawnExt, FutureExt};
use super::ConnectedPipe;
async fn wait_milis(milis: u64) {
Delay::new(Duration::from_millis(milis)).await;
}
// #[futures_test::test]
#[test]
fn test_send_receive() {
let mut executor = LocalPool::new();
let spawner = executor.spawner();
static PIPE: ConnectedPipe<CriticalSectionRawMutex, usize, 5> = ConnectedPipe::new();
// Task that sends
spawner
.spawn(async {
wait_milis(10).await;
PIPE.push(23).await;
PIPE.push(56).await;
PIPE.push(67).await;
})
.unwrap();
// Task that receives
spawner
.spawn(async {
let reader = PIPE.reader();
let value = reader.receive().await;
assert_eq!(value, 23);
let value = reader.receive().await;
assert_eq!(value, 56);
let value = reader.receive().await;
assert_eq!(value, 67);
})
.unwrap();
executor.run();
}
#[futures_test::test]
async fn test_send_drop() {
static PIPE: ConnectedPipe<CriticalSectionRawMutex, usize, 5> = ConnectedPipe::new();
PIPE.push(23).await;
PIPE.push(56).await;
PIPE.push(67).await;
// Create a reader after sending
let reader = PIPE.reader();
let receive = reader.receive().fuse();
pin_mut!(receive);
let timeout = wait_milis(50).fuse();
pin_mut!(timeout);
let either = select(receive, timeout).await;
match either {
futures_util::future::Either::Left(_) => {
panic!("There should be nothing to receive!");
}
futures_util::future::Either::Right(_) => {}
}
}
#[futures_test::test]
async fn test_bulk_send_publish() {
static PIPE: ConnectedPipe<CriticalSectionRawMutex, usize, 5> = ConnectedPipe::new();
let executor = ThreadPool::new().unwrap();
executor
.spawn(async {
for i in 0..1000 {
PIPE.push(i).await;
}
})
.unwrap();
executor
.spawn(async {
for i in 1000..2000 {
PIPE.push(i).await;
}
})
.unwrap();
let reader = PIPE.reader();
for _ in 0..800 {
reader.receive().await;
}
}
}

View File

@@ -0,0 +1,173 @@
use core::{fmt::Display, future::Future, ops::Deref};
use embedded_io::Write;
use mqttrs::QoS;
use crate::{io::publish, Error, Payload, Topic, TopicString};
/// A message that can be published to an MQTT broker.
pub trait Publishable {
/// Write this message's topic into the supplied buffer.
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error>;
/// Write this message's payload into the supplied buffer.
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error>;
/// Get this message's QoS level.
fn qos(&self) -> QoS {
QoS::AtMostOnce
}
/// Whether the broker should retain this message.
fn retain(&self) -> bool {
false
}
/// Publishes this message to the broker. If the stack has not yet been
/// initialized this is likely to panic.
fn publish(&self) -> impl Future<Output = Result<(), Error>> {
async {
let mut topic = TopicString::new();
self.write_topic(&mut topic)?;
let mut payload = Payload::new();
self.write_payload(&mut payload)?;
publish(&topic, &payload, self.qos(), self.retain()).await
}
}
}
/// A [`Publishable`] with a raw byte payload.
pub struct PublishBytes<'a, T, B: AsRef<[u8]>> {
pub(crate) topic: &'a Topic<T>,
pub(crate) data: B,
pub(crate) qos: QoS,
pub(crate) retain: bool,
}
impl<T, B: AsRef<[u8]>> PublishBytes<'_, T, B> {
/// Sets the QoS level for this message.
pub fn qos(mut self, qos: QoS) -> Self {
self.qos = qos;
self
}
/// Sets whether the broker should retain this message.
pub fn retain(mut self, retain: bool) -> Self {
self.retain = retain;
self
}
}
impl<'a, T: Deref<Target = str> + 'a, B: AsRef<[u8]>> Publishable for PublishBytes<'a, T, B> {
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error> {
self.topic.to_string(buffer)
}
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error> {
buffer
.write_all(self.data.as_ref())
.map_err(|_| Error::TooLarge)
}
fn qos(&self) -> QoS {
self.qos
}
fn retain(&self) -> bool {
self.retain
}
async fn publish(&self) -> Result<(), Error> {
let mut topic = TopicString::new();
self.write_topic(&mut topic)?;
publish(&topic, self.data.as_ref(), self.qos(), self.retain()).await
}
}
/// A [`Publishable`] with a payload that implements [`Display`].
pub struct PublishDisplay<'a, T, D: Display> {
pub(crate) topic: &'a Topic<T>,
pub(crate) data: D,
pub(crate) qos: QoS,
pub(crate) retain: bool,
}
impl<T, D: Display> PublishDisplay<'_, T, D> {
/// Sets the QoS level for this message.
pub fn qos(mut self, qos: QoS) -> Self {
self.qos = qos;
self
}
/// Sets whether the broker should retain this message.
pub fn retain(mut self, retain: bool) -> Self {
self.retain = retain;
self
}
}
impl<'a, T: Deref<Target = str> + 'a, D: Display> Publishable for PublishDisplay<'a, T, D> {
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error> {
self.topic.to_string(buffer)
}
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error> {
write!(buffer, "{}", self.data).map_err(|_| Error::TooLarge)
}
fn qos(&self) -> QoS {
self.qos
}
fn retain(&self) -> bool {
self.retain
}
}
#[cfg(feature = "serde")]
/// A [`Publishable`] with that serializes a JSON payload.
pub struct PublishJson<'a, T, D: serde::Serialize> {
pub(crate) topic: &'a Topic<T>,
pub(crate) data: D,
pub(crate) qos: QoS,
pub(crate) retain: bool,
}
#[cfg(feature = "serde")]
impl<T, D: serde::Serialize> PublishJson<'_, T, D> {
/// Sets the QoS level for this message.
pub fn qos(mut self, qos: QoS) -> Self {
self.qos = qos;
self
}
/// Sets whether the broker should retain this message.
pub fn retain(mut self, retain: bool) -> Self {
self.retain = retain;
self
}
}
#[cfg(feature = "serde")]
impl<'a, T: Deref<Target = str> + 'a, D: serde::Serialize> Publishable for PublishJson<'a, T, D> {
fn write_topic(&self, buffer: &mut TopicString) -> Result<(), Error> {
self.topic.to_string(buffer)
}
fn write_payload(&self, buffer: &mut Payload) -> Result<(), Error> {
buffer
.serialize_json(&self.data)
.map_err(|_| Error::TooLarge)
}
fn qos(&self) -> QoS {
self.qos
}
fn retain(&self) -> bool {
self.retain
}
}

View File

@@ -0,0 +1,284 @@
use core::{fmt::Display, ops::Deref};
use embassy_futures::select::{select, Either};
use embassy_sync::pubsub::WaitResult;
use embassy_time::Timer;
use heapless::{String, Vec};
use mqttrs::{Packet, QoS, Subscribe, SubscribeReturnCodes, SubscribeTopic, Unsubscribe};
#[cfg(feature = "serde")]
use crate::publish::PublishJson;
use crate::{
device_id, device_type,
io::{assign_pid, send_packet, subscribe},
publish::{PublishBytes, PublishDisplay},
ControlMessage, Error, TopicString, CONFIRMATION_TIMEOUT,
};
/// An MQTT topic that is optionally prefixed with the device type and unique ID.
/// Normally you will define all your application's topics as consts with static
/// lifetimes.
///
/// A [`Topic`] is the main entry to publishing messages to the broker.
///
/// ```
/// # use mcutie::{Publishable, Topic};
/// const DEVICE_AVAILABILITY: Topic<&'static str> = Topic::Device("state");
///
/// async fn send_status(status: &'static str) {
/// let _ = DEVICE_AVAILABILITY.with_bytes(status.as_bytes()).publish().await;
/// }
/// ```
#[derive(Clone, Copy)]
pub enum Topic<T> {
/// A topic that is prefixed with the device type.
DeviceType(T),
/// A topic that is prefixed with the device type and unique ID.
Device(T),
/// Any topic.
General(T),
}
impl<A, B> PartialEq<Topic<A>> for Topic<B>
where
B: PartialEq<A>,
{
fn eq(&self, other: &Topic<A>) -> bool {
match (self, other) {
(Topic::DeviceType(l0), Topic::DeviceType(r0)) => l0 == r0,
(Topic::Device(l0), Topic::Device(r0)) => l0 == r0,
(Topic::General(l0), Topic::General(r0)) => l0 == r0,
_ => false,
}
}
}
impl<T> Topic<T> {
/// Creates a publishable message with something that can return a reference
/// to the payload in bytes.
///
/// Defaults to non-retained with QoS of 0 (AtMostOnce).
pub fn with_bytes<B: AsRef<[u8]>>(&self, data: B) -> PublishBytes<'_, T, B> {
PublishBytes {
topic: self,
data,
qos: QoS::AtMostOnce,
retain: false,
}
}
/// Creates a publishable message with something that implements [`Display`].
///
/// Defaults to non-retained with QoS of 0 (AtMostOnce).
pub fn with_display<D: Display>(&self, data: D) -> PublishDisplay<'_, T, D> {
PublishDisplay {
topic: self,
data,
qos: QoS::AtMostOnce,
retain: false,
}
}
#[cfg(feature = "serde")]
/// Creates a publishable message with something that can be serialized to
/// JSON.
///
/// Defaults to non-retained with QoS of 0 (AtMostOnce).
pub fn with_json<D: serde::Serialize>(&self, data: D) -> PublishJson<'_, T, D> {
PublishJson {
topic: self,
data,
qos: QoS::AtMostOnce,
retain: false,
}
}
}
impl Topic<TopicString> {
pub(crate) fn from_str(mut st: &str) -> Result<Self, Error> {
let mut strip_prefix = |pr: &str| -> bool {
if st.starts_with(pr) && st.len() > pr.len() && &st[pr.len()..pr.len() + 1] == "/" {
st = &st[pr.len() + 1..];
true
} else {
false
}
};
if strip_prefix(device_type()) {
if strip_prefix(device_id()) {
let mut topic = TopicString::new();
topic.push_str(st).map_err(|_| Error::TooLarge)?;
Ok(Topic::Device(topic))
} else {
let mut topic = TopicString::new();
topic.push_str(st).map_err(|_| Error::TooLarge)?;
Ok(Topic::DeviceType(topic))
}
} else {
let mut topic = TopicString::new();
topic.push_str(st).map_err(|_| Error::TooLarge)?;
Ok(Topic::General(topic))
}
}
}
impl<T: Deref<Target = str>> Topic<T> {
pub(crate) fn to_string<const N: usize>(&self, result: &mut String<N>) -> Result<(), Error> {
match self {
Topic::Device(st) => {
result
.push_str(device_type())
.map_err(|_| Error::TooLarge)?;
result.push_str("/").map_err(|_| Error::TooLarge)?;
result.push_str(device_id()).map_err(|_| Error::TooLarge)?;
result.push_str("/").map_err(|_| Error::TooLarge)?;
result.push_str(st.as_ref()).map_err(|_| Error::TooLarge)?;
}
Topic::DeviceType(st) => {
result
.push_str(device_type())
.map_err(|_| Error::TooLarge)?;
result.push_str("/").map_err(|_| Error::TooLarge)?;
result.push_str(st.as_ref()).map_err(|_| Error::TooLarge)?;
}
Topic::General(st) => {
result.push_str(st.as_ref()).map_err(|_| Error::TooLarge)?;
}
}
Ok(())
}
/// Converts to a topic containing an [`str`]. Particularly useful for converting from an owned
/// string for match patterns.
pub fn as_ref(&self) -> Topic<&str> {
match self {
Topic::DeviceType(st) => Topic::DeviceType(st.as_ref()),
Topic::Device(st) => Topic::Device(st.as_ref()),
Topic::General(st) => Topic::General(st.as_ref()),
}
}
/// Subscribes to this topic. If `wait_for_ack` is true then this will wait until confirmation
/// is received from the broker before returning.
pub async fn subscribe(&self, wait_for_ack: bool) -> Result<(), Error> {
let mut subscriber = subscribe().await;
let mut topic_path = TopicString::new();
if self.to_string(&mut topic_path).is_err() {
return Err(Error::TooLarge);
}
let pid = assign_pid().await;
let mut subscribe_topic_path = String::<256>::new();
subscribe_topic_path
.push_str(topic_path.as_str())
.map_err(|_| Error::TooLarge)?;
let subscribe_topic = SubscribeTopic {
topic_path: subscribe_topic_path,
qos: QoS::AtLeastOnce,
};
// The size of this vec must match that used by mqttrs.
let topics = match Vec::<SubscribeTopic, 5>::from_slice(&[subscribe_topic]) {
Ok(t) => t,
Err(_) => return Err(Error::TooLarge),
};
let packet = Packet::Subscribe(Subscribe { pid, topics });
send_packet(packet).await?;
if wait_for_ack {
match select(
async {
loop {
match subscriber.next_message().await {
WaitResult::Lagged(_) => {
// Maybe we missed the message?
}
WaitResult::Message(ControlMessage::Subscribed(
subscribed_pid,
return_code,
)) => {
if subscribed_pid == pid {
if matches!(return_code, SubscribeReturnCodes::Success(_)) {
return Ok(());
} else {
return Err(Error::IOError);
}
}
}
_ => {}
}
}
},
Timer::after_millis(CONFIRMATION_TIMEOUT),
)
.await
{
Either::First(r) => r,
Either::Second(_) => Err(Error::TimedOut),
}
} else {
Ok(())
}
}
/// Unsubscribes from a topic. If `wait_for_ack` is true then this will wait until confirmation is
/// received from the broker before returning.
pub async fn unsubscribe(&self, wait_for_ack: bool) -> Result<(), Error> {
let mut subscriber = subscribe().await;
let mut topic_path = TopicString::new();
if self.to_string(&mut topic_path).is_err() {
return Err(Error::TooLarge);
}
let pid = assign_pid().await;
// The size of this vec must match that used by mqttrs.
let mut unsubscribe_topic_path = String::<256>::new();
unsubscribe_topic_path
.push_str(topic_path.as_str())
.map_err(|_| Error::TooLarge)?;
let topics = match Vec::<String<256>, 5>::from_slice(&[unsubscribe_topic_path]) {
Ok(t) => t,
Err(_) => return Err(Error::TooLarge),
};
let packet = Packet::Unsubscribe(Unsubscribe { pid, topics });
send_packet(packet).await?;
if wait_for_ack {
match select(
async {
loop {
match subscriber.next_message().await {
WaitResult::Lagged(_) => {
// Maybe we missed the message?
}
WaitResult::Message(ControlMessage::Unsubscribed(subscribed_pid)) => {
if subscribed_pid == pid {
return Ok(());
}
}
_ => {}
}
}
},
Timer::after_millis(CONFIRMATION_TIMEOUT),
)
.await
{
Either::First(r) => r,
Either::Second(_) => Err(Error::TimedOut),
}
} else {
Ok(())
}
}
}

398
rust/src/mqtt.rs Normal file
View File

@@ -0,0 +1,398 @@
use crate::config::NetworkConfig;
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::battery::{BatteryError, BatteryInfo, BatteryState};
use crate::hal::{PlantHal, HAL};
use crate::log::{log, LogMessage};
use crate::plant_state::PlantState;
use crate::tank::TankState;
use crate::{bail, VersionInfo};
use alloc::string::String;
use alloc::{format, string::ToString};
use chrono::DateTime;
use chrono_tz::Tz;
use core::sync::atomic::Ordering;
use embassy_executor::Spawner;
use embassy_net::Stack;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::MutexGuard;
use embassy_sync::once_lock::OnceLock;
use embassy_time::{Duration, Timer, WithTimeout};
use log::{info, warn};
use mcutie::{
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
QoS, Topic,
};
use portable_atomic::AtomicBool;
use serde::{Deserialize, Serialize};
static MQTT_CONNECTED_EVENT_RECEIVED: AtomicBool = AtomicBool::new(false);
static MQTT_ROUND_TRIP_RECEIVED: AtomicBool = AtomicBool::new(false);
pub static MQTT_STAY_ALIVE: AtomicBool = AtomicBool::new(false);
static MQTT_BASE_TOPIC: OnceLock<String> = OnceLock::new();
pub fn is_stay_alive() -> bool {
MQTT_STAY_ALIVE.load(Ordering::Relaxed)
}
pub async fn publish(subtopic: &str, message: &str) {
let online = MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed);
if !online {
return;
}
let roundtrip_ok = MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed);
if !roundtrip_ok {
info!("MQTT roundtrip not received yet, dropping message");
return;
}
match publish_inner(subtopic, message).await {
Ok(()) => {}
Err(err) => {
info!(
"Error during mqtt send on topic {subtopic} with message {message:#?} error is {err:?}"
);
}
};
}
async fn publish_inner(subtopic: &str, message: &str) -> FatResult<()> {
if !subtopic.starts_with("/") {
bail!("Subtopic without / at start {}", subtopic);
}
if subtopic.len() > 192 {
bail!("Subtopic exceeds 192 chars {}", subtopic);
}
let base_topic = MQTT_BASE_TOPIC
.try_get()
.context("missing base topic in static!")?;
let full_topic = format!("{base_topic}{subtopic}");
loop {
let result = Topic::General(full_topic.as_str())
.with_display(message)
.retain(true)
.publish()
.await;
match result {
Ok(()) => return Ok(()),
Err(err) => {
let retry = match err {
Error::IOError => false,
Error::TimedOut => true,
Error::TooLarge => false,
Error::PacketError => false,
Error::Invalid => false,
Error::Rejected => false,
};
if !retry {
bail!(
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
&full_topic,
message,
err
);
}
info!(
"Retransmit for {} with message {:#?} error is {:?} retrying {}",
&full_topic, message, err, retry
);
Timer::after(Duration::from_millis(100)).await;
}
}
}
}
use crate::util::mk_static;
pub async fn mqtt_init(
network_config: &'static NetworkConfig,
stack: Stack<'static>,
spawner: Spawner,
) -> FatResult<()> {
let base_topic = network_config
.base_topic
.as_ref()
.context("missing base topic")?;
if base_topic.is_empty() {
bail!("Mqtt base_topic was empty")
}
MQTT_BASE_TOPIC
.init(base_topic.to_string())
.map_err(|_| FatError::String {
error: "Error setting basetopic".to_string(),
})?;
let mqtt_url = network_config
.mqtt_url
.as_ref()
.context("missing mqtt url")?;
if mqtt_url.is_empty() {
bail!("Mqtt url was empty")
}
let last_will_topic = format!("{base_topic}/state");
let round_trip_topic = format!("{base_topic}/internal/roundtrip");
let stay_alive_topic = format!("{base_topic}/stay_alive");
let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
McutieBuilder::new(stack, "plant ctrl", mqtt_url);
if let (Some(mqtt_user), Some(mqtt_password)) = (
network_config.mqtt_user.as_ref(),
network_config.mqtt_password.as_ref(),
) {
builder = builder.with_authentication(mqtt_user, mqtt_password);
info!("With authentification");
}
let lwt = Topic::General(last_will_topic);
let lwt = mk_static!(Topic<String>, lwt);
let lwt = lwt.with_display("lost").retain(true).qos(QoS::AtLeastOnce);
builder = builder.with_last_will(lwt);
//TODO make configurable
builder = builder.with_device_id("plantctrl");
let builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 2> = builder
.with_subscriptions([
Topic::General(round_trip_topic.clone()),
Topic::General(stay_alive_topic.clone()),
]);
let keep_alive = Duration::from_secs(60 * 60 * 2).as_secs() as u16;
let (receiver, task) = builder.build(keep_alive);
spawner.spawn(mqtt_incoming_task(
receiver,
round_trip_topic.clone(),
stay_alive_topic.clone(),
)?);
spawner.spawn(mqtt_runner(task)?);
log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic);
log(LogMessage::MqttInfo, 0, 0, "", mqtt_url);
let mqtt_timeout = 15000;
let res = async {
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
PlantHal::feed_watchdog();
Timer::after(Duration::from_millis(100)).await;
}
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
.await;
if res.is_err() {
bail!("Timeout waiting MQTT connect event")
}
let _ = Topic::General(round_trip_topic.clone())
.with_display("online_text")
.publish()
.await;
let res = async {
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
PlantHal::feed_watchdog();
Timer::after(Duration::from_millis(100)).await;
}
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(mqtt_timeout as u64))
.await;
if res.is_err() {
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
bail!("Timeout waiting MQTT roundtrip")
}
Ok(())
}
#[embassy_executor::task]
async fn mqtt_runner(
task: McutieTask<'static, String, PublishDisplay<'static, String, &'static str>, 2>,
) {
task.run().await;
}
#[embassy_executor::task]
async fn mqtt_incoming_task(
receiver: McutieReceiver,
round_trip_topic: String,
stay_alive_topic: String,
) {
loop {
let message = receiver.receive().await;
match message {
MqttMessage::Connected => {
info!("Mqtt connected");
MQTT_CONNECTED_EVENT_RECEIVED.store(true, Ordering::Relaxed);
}
MqttMessage::Publish(topic, payload) => match topic {
Topic::DeviceType(_type_topic) => {}
Topic::Device(_device_topic) => {}
Topic::General(topic) => {
let subtopic = topic.as_str();
if subtopic.eq(round_trip_topic.as_str()) {
MQTT_ROUND_TRIP_RECEIVED.store(true, Ordering::Relaxed);
} else if subtopic.eq(stay_alive_topic.as_str()) {
let value = payload.eq_ignore_ascii_case("true".as_ref())
|| payload.eq_ignore_ascii_case("1".as_ref());
let a = match value {
true => 1,
false => 0,
};
log(LogMessage::MqttStayAliveRec, a, 0, "", "");
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
} else {
log(LogMessage::UnknownTopic, 0, 0, "", &topic);
}
}
},
MqttMessage::Disconnected => {
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
info!("Mqtt disconnected");
}
}
}
}
pub async fn publish_tank_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
tank_state: &TankState,
water_temp: FatResult<f32>,
) {
let state = serde_json::to_string(
&tank_state.as_mqtt_info(&board.board_hal.get_config().tank, &water_temp),
)
.unwrap();
let _ = publish("/water", &*state).await;
}
pub async fn publish_plant_states(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
timezone_time: &DateTime<Tz>,
plantstate: &[PlantState; 8],
) {
for (plant_id, (plant_state, plant_conf)) in plantstate
.iter()
.zip(&board.board_hal.get_config().plants.clone())
.enumerate()
{
let state =
serde_json::to_string(&plant_state.to_mqtt_info(plant_conf, timezone_time)).unwrap();
let plant_topic = format!("/plant{}", plant_id + 1);
let _ = publish(&plant_topic, &state).await;
}
}
pub async fn publish_firmware_info(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
version: VersionInfo,
ip_address: &str,
timezone_time: &str,
) {
publish("/firmware/address", ip_address).await;
publish("/firmware/state", &serde_json::to_string(&version).unwrap()).await;
publish("/firmware/last_online", timezone_time).await;
publish("/state", "online").await;
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Default)]
struct PumpInfo {
pub enabled: bool,
pub pump_ineffective: bool,
pub median_current_ma: u16,
pub max_current_ma: u16,
pub min_current_ma: u16,
}
pub async fn pump_info(
plant_id: usize,
pump_active: bool,
pump_ineffective: bool,
median_current_ma: u16,
max_current_ma: u16,
min_current_ma: u16,
_error: bool,
) {
let pump_info = PumpInfo {
enabled: pump_active,
pump_ineffective,
median_current_ma,
max_current_ma,
min_current_ma,
};
let pump_topic = format!("/pump{}", plant_id + 1);
match serde_json::to_string(&pump_info) {
Ok(state) => {
let _ = publish(&pump_topic, &state).await;
}
Err(err) => {
warn!("Error publishing pump state {}", err);
}
};
}
#[derive(Serialize, Debug, PartialEq)]
pub struct Solar {
pub current_ma: u32,
pub voltage_ma: u32,
}
pub async fn publish_mppt_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
) -> FatResult<()> {
let current = board.board_hal.get_mptt_current().await?;
let voltage = board.board_hal.get_mptt_voltage().await?;
let solar_state = Solar {
current_ma: current.as_milliamperes() as u32,
voltage_ma: voltage.as_millivolts() as u32,
};
if let Ok(serialized_solar_state_bytes) = serde_json::to_string(&solar_state) {
let _ = publish("/mppt", &serialized_solar_state_bytes).await;
}
Ok(())
}
pub async fn publish_battery_state(
board: &mut MutexGuard<'_, CriticalSectionRawMutex, HAL<'static>>,
) -> () {
let telemetry = match board
.board_hal
.get_battery_monitor()
.get_battery_state()
.await
{
Ok(BatteryState::Info(info)) => info,
Ok(BatteryState::Unknown) => BatteryInfo {
voltage_mv: None,
avg_current_ma: None,
soc_pct: None,
soh_pct: None,
temperature_c: None,
cycle_count: None,
remaining_mah: None,
design_mah: None,
error: Some(BatteryError::NoBatteryMonitor),
},
Err(e) => BatteryInfo {
voltage_mv: None,
avg_current_ma: None,
soc_pct: None,
soh_pct: None,
temperature_c: None,
cycle_count: None,
remaining_mah: None,
design_mah: None,
error: Some(BatteryError::CommunicationError {
message: alloc::format!("{:?}", e),
}),
},
};
if let Ok(json) = serde_json::to_string(&telemetry) {
let _ = publish("/battery", &json).await;
}
}

419
rust/src/network.rs Normal file
View File

@@ -0,0 +1,419 @@
use crate::bail;
use crate::config::NetworkConfig;
use crate::fat_error::{ContextExt, FatError, FatResult};
use crate::hal::{PlantHal, HAL};
use crate::mqtt;
use crate::util::mk_static;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use chrono::{DateTime, Utc};
use core::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4};
use embassy_executor::Spawner;
use embassy_net::dns::DnsQueryType;
use embassy_net::udp::{PacketMetadata, UdpSocket};
use embassy_net::{DhcpConfig, Runner, Stack, StackResources, StaticConfigV4};
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::{Mutex, MutexGuard};
use embassy_time::{Duration, Timer, WithTimeout};
use option_lock::OptionLock;
use edge_dhcp::{
io::{self, DEFAULT_SERVER_PORT},
server::{Server, ServerOptions},
};
use edge_nal::UdpBind;
use edge_nal_embassy::{Udp, UdpBuffers};
use esp_hal::rng::Rng;
use esp_println::println;
use esp_radio::wifi::ap::AccessPointConfig;
use esp_radio::wifi::sta::StationConfig;
use esp_radio::wifi::{AuthenticationMethod, Config, Interface};
use log::{info, warn, error};
use serde::Serialize;
use sntpc::{NtpContext, NtpTimestampGenerator, NtpUdpSocket, get_time};
const NTP_SERVER: &str = "pool.ntp.org";
#[derive(Copy, Clone, Default)]
struct Timestamp {
stamp: DateTime<Utc>,
}
impl NtpTimestampGenerator for Timestamp {
fn init(&mut self) {
self.stamp = DateTime::default();
}
fn timestamp_sec(&self) -> u64 {
self.stamp.timestamp() as u64
}
fn timestamp_subsec_micros(&self) -> u32 {
self.stamp.timestamp_subsec_micros()
}
}
struct EmbassyNtpSocket<'a, 'b> {
socket: &'a UdpSocket<'b>,
}
impl<'a, 'b> EmbassyNtpSocket<'a, 'b> {
fn new(socket: &'a UdpSocket<'b>) -> Self {
Self { socket }
}
}
impl NtpUdpSocket for EmbassyNtpSocket<'_, '_> {
async fn send_to(&self, buf: &[u8], addr: SocketAddr) -> sntpc::Result<usize> {
self.socket
.send_to(buf, addr)
.await
.map_err(|_| sntpc::Error::Network)?;
Ok(buf.len())
}
async fn recv_from(&self, buf: &mut [u8]) -> sntpc::Result<(usize, SocketAddr)> {
let (len, metadata) = self
.socket
.recv_from(buf)
.await
.map_err(|_| sntpc::Error::Network)?;
let addr = match metadata.endpoint.addr {
embassy_net::IpAddress::Ipv4(ip) => IpAddr::V4(ip),
embassy_net::IpAddress::Ipv6(ip) => IpAddr::V6(ip),
};
Ok((len, SocketAddr::new(addr, metadata.endpoint.port)))
}
}
pub async fn sntp(max_wait_ms: u32, stack: Stack<'_>) -> FatResult<DateTime<Utc>> {
println!("start sntp");
let mut rx_meta = [PacketMetadata::EMPTY; 16];
let mut rx_buffer = [0; 4096];
let mut tx_meta = [PacketMetadata::EMPTY; 16];
let mut tx_buffer = [0; 4096];
let mut socket = UdpSocket::new(
stack,
&mut rx_meta,
&mut rx_buffer,
&mut tx_meta,
&mut tx_buffer,
);
socket.bind(123).context("Could not bind UDP socket")?;
let context = NtpContext::new(Timestamp::default());
let ntp_socket = EmbassyNtpSocket::new(&socket);
let ntp_addrs = stack
.dns_query(NTP_SERVER, DnsQueryType::A)
.await
.context("Failed to resolve DNS")?;
if ntp_addrs.is_empty() {
bail!("No IP addresses found for NTP server");
}
let ntp = ntp_addrs[0];
info!("NTP server: {ntp:?}");
let mut counter = 0;
loop {
let addr: IpAddr = ntp.into();
let timeout = get_time(SocketAddr::from((addr, 123)), &ntp_socket, context)
.with_timeout(Duration::from_millis((max_wait_ms / 10) as u64))
.await;
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(err) => {
warn!("sntp timeout, retry: {err:?}");
counter += 1;
if counter > 10 {
bail!("Failed to get time from NTP server");
}
Timer::after(Duration::from_millis(100)).await;
}
}
}
}
#[derive(Serialize, Debug, PartialEq)]
pub enum SntpMode {
OFFLINE,
SYNC { current: DateTime<Utc> },
}
#[derive(Serialize, Debug, PartialEq)]
pub enum NetworkMode {
WIFI {
sntp: SntpMode,
mqtt: bool,
ip_address: String,
},
OFFLINE,
}
#[embassy_executor::task(pool_size = 2)]
pub(crate) async fn net_task(mut runner: Runner<'static, Interface<'static>>) {
runner.run().await;
}
#[embassy_executor::task]
pub(crate) async fn run_dhcp(stack: Stack<'static>, ip: Ipv4Addr) {
let mut buf = [0u8; 1500];
let mut gw_buf = [Ipv4Addr::UNSPECIFIED];
let buffers = UdpBuffers::<3, 1024, 1024, 10>::new();
let unbound_socket = Udp::new(stack, &buffers);
let mut bound_socket = match unbound_socket
.bind(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::UNSPECIFIED,
DEFAULT_SERVER_PORT,
)))
.await
{
Ok(s) => s,
Err(e) => {
error!("dhcp task failed to bind socket: {:?}", e);
return;
}
};
loop {
_ = io::server::run(
&mut Server::<_, 64>::new_with_et(ip),
&ServerOptions::new(ip, Some(&mut gw_buf)),
&mut bound_socket,
&mut buf,
)
.await
.inspect_err(|e| warn!("DHCP server error: {e:?}"));
Timer::after(Duration::from_millis(500)).await;
}
}
pub async fn wifi_ap(
ssid: String,
interface_ap: Interface<'static>,
controller: &Arc<Mutex<CriticalSectionRawMutex, esp_radio::wifi::WifiController<'static>>>,
rng: &mut Rng,
spawner: Spawner,
) -> FatResult<Stack<'static>> {
let gw_ip_addr = Ipv4Addr::new(192, 168, 71, 1);
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
address: embassy_net::Ipv4Cidr::new(gw_ip_addr, 24),
gateway: Some(gw_ip_addr),
dns_servers: Default::default(),
});
let seed = (rng.random() as u64) << 32 | rng.random() as u64;
println!("init secondary stack");
let (stack, runner) = embassy_net::new(
interface_ap,
config,
mk_static!(StackResources<4>, StackResources::<4>::new()),
seed,
);
let stack = mk_static!(Stack, stack);
let client_config =
Config::AccessPoint(AccessPointConfig::default().with_ssid(ssid.clone()));
controller.lock().await.set_config(&client_config)?;
println!("start net task");
spawner.spawn(net_task(runner)?);
println!("run dhcp");
spawner.spawn(run_dhcp(*stack, gw_ip_addr)?);
loop {
if stack.is_link_up() {
break;
}
Timer::after(Duration::from_millis(500)).await;
}
while !stack.is_config_up() {
Timer::after(Duration::from_millis(100)).await
}
println!("Connect to the AP `${ssid}` and point your browser to http://{gw_ip_addr}/");
stack
.config_v4()
.inspect(|c| println!("ipv4 config: {c:?}"));
Ok(*stack)
}
pub async fn wifi(
network_config: &NetworkConfig,
interface_sta: Interface<'static>,
controller: &Arc<Mutex<CriticalSectionRawMutex, esp_radio::wifi::WifiController<'static>>>,
rng: &mut Rng,
spawner: Spawner,
) -> FatResult<Stack<'static>> {
esp_radio::wifi_set_log_verbose();
let ssid = match &network_config.ssid {
Some(ssid) => {
if ssid.is_empty() {
bail!("Wifi ssid was empty")
}
ssid.as_str().to_string()
}
None => {
bail!("Wifi ssid was empty")
}
};
info!("attempting to connect wifi {ssid}");
let password = match network_config.password {
Some(ref password) => password.as_str().to_string(),
None => "".to_string(),
};
let max_wait = network_config.max_wait;
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
let seed = (rng.random() as u64) << 32 | rng.random() as u64;
let (stack, runner) = embassy_net::new(
interface_sta,
config,
mk_static!(StackResources<8>, StackResources::<8>::new()),
seed,
);
let stack = mk_static!(Stack, stack);
let auth_method = if password.is_empty() {
AuthenticationMethod::None
} else {
AuthenticationMethod::Wpa2Personal
};
let client_config = StationConfig::default()
.with_ssid(ssid)
.with_auth_method(auth_method)
.with_scan_method(esp_radio::wifi::sta::ScanMethod::AllChannels)
.with_listen_interval(10)
.with_beacon_timeout(10)
.with_failure_retry_cnt(3)
.with_password(password);
controller
.lock()
.await
.set_config(&Config::Station(client_config))?;
spawner.spawn(net_task(runner)?);
controller
.lock()
.await
.connect_async()
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
.await
.context("Timeout waiting for wifi sta connected")??;
let res = async {
while !stack.is_link_up() {
Timer::after(Duration::from_millis(500)).await;
}
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
.await;
if res.is_err() {
bail!("Timeout waiting for wifi link up")
}
let res = async {
while !stack.is_config_up() {
Timer::after(Duration::from_millis(100)).await
}
Ok::<(), FatError>(())
}
.with_timeout(Duration::from_millis(max_wait as u64 * 1000))
.await;
if res.is_err() {
bail!("Timeout waiting for wifi config up")
}
info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
Ok(*stack)
}
pub async fn try_connect_wifi_sntp_mqtt(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
stack_store: &mut OptionLock<Stack<'static>>,
spawner: Spawner,
) -> NetworkMode {
let nw_conf = &board.board_hal.get_config().network.clone();
let esp = board.board_hal.get_esp();
let device = match esp.interface_sta.take() {
Some(d) => d,
None => {
info!("Offline mode due to STA interface already taken");
board.board_hal.general_fault(true).await;
return NetworkMode::OFFLINE;
}
};
match wifi(nw_conf, device, &esp.controller, &mut esp.rng, spawner).await {
Ok(stack) => {
stack_store.replace(stack);
let sntp_mode: SntpMode = match sntp(1000 * 10, stack).await {
Ok(new_time) => {
info!("Using time from sntp {}", new_time.to_rfc3339());
let _ = board
.board_hal
.get_rtc_module()
.set_rtc_time(&new_time)
.await;
SntpMode::SYNC { current: new_time }
}
Err(err) => {
warn!("sntp error: {err}");
board.board_hal.general_fault(true).await;
SntpMode::OFFLINE
}
};
let mqtt_connected = if board.board_hal.get_config().network.mqtt_url.is_some() {
let nw_config = board.board_hal.get_config().network.clone();
let nw_config = mk_static!(NetworkConfig, nw_config);
match mqtt::mqtt_init(nw_config, stack, spawner).await {
Ok(_) => {
info!("Mqtt connection ready");
true
}
Err(err) => {
warn!("Could not connect mqtt due to {err}");
false
}
}
} else {
false
};
let ip = match stack.config_v4() {
Some(config) => config.address.address().to_string(),
None => match stack.config_v6() {
Some(config) => config.address.address().to_string(),
None => String::from("No IP"),
},
};
NetworkMode::WIFI {
sntp: sntp_mode,
mqtt: mqtt_connected,
ip_address: ip,
}
}
Err(err) => {
info!("Offline mode due to {err}");
board.board_hal.general_fault(true).await;
NetworkMode::OFFLINE
}
}
}

View File

@@ -1,18 +1,22 @@
use crate::{
config::PlantConfig,
hal::{Sensor, HAL},
in_time_range,
};
use alloc::string::{String, ToString};
use chrono::{DateTime, TimeDelta, Utc};
use chrono_tz::Tz;
use serde::{Deserialize, Serialize};
use crate::hal::{BoardInteraction, Sensor, HAL};
use crate::{config::PlantConfig, in_time_range};
const MOIST_SENSOR_MAX_FREQUENCY: f32 = 7500.; // 60kHz (500Hz margin)
const MOIST_SENSOR_MIN_FREQUENCY: f32 = 150.; // this is really, really dry, think like cactus levels
#[derive(Debug, PartialEq, Serialize)]
#[derive(Debug, PartialEq, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum MoistureSensorError {
ShortCircuit { hz: f32, max: f32 },
OpenLoop { hz: f32, min: f32 },
BoardError(String),
BoardError { message: String },
}
#[derive(Debug, PartialEq, Serialize)]
@@ -46,6 +50,14 @@ impl MoistureSensorState {
impl MoistureSensorState {}
#[derive(Debug, PartialEq, Serialize)]
pub struct SensorTelemetry {
pub moisture_pct: Option<f32>,
pub raw_hz: Option<f32>,
pub error: Option<MoistureSensorError>,
}
#[derive(Debug, PartialEq, Serialize)]
#[serde(tag = "kind")]
pub enum PumpError {
PumpNotWorking {
failed_attempts: usize,
@@ -76,6 +88,7 @@ impl PumpState {
pub enum PlantWateringMode {
OFF,
TargetMoisture,
MinMoisture,
TimerOnly,
}
@@ -112,9 +125,13 @@ fn map_range_moisture(
}
impl PlantState {
pub fn read_hardware_state(plant_id: usize, board: &mut HAL) -> Self {
pub async fn read_hardware_state(plant_id: usize, board: &mut HAL<'_>) -> Self {
let sensor_a = if board.board_hal.get_config().plants[plant_id].sensor_a {
match board.board_hal.measure_moisture_hz(plant_id, Sensor::A) {
match board
.board_hal
.measure_moisture_hz(plant_id, Sensor::A)
.await
{
Ok(raw) => match map_range_moisture(
raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
@@ -126,16 +143,20 @@ impl PlantState {
},
Err(err) => MoistureSensorState::SensorError(err),
},
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
err.to_string(),
)),
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError {
message: err.to_string(),
})
}
} else {
MoistureSensorState::Disabled
};
let sensor_b = if board.board_hal.get_config().plants[plant_id].sensor_b {
match board.board_hal.measure_moisture_hz(plant_id, Sensor::B) {
match board
.board_hal
.measure_moisture_hz(plant_id, Sensor::B)
.await
{
Ok(raw) => match map_range_moisture(
raw,
board.board_hal.get_config().plants[plant_id].moisture_sensor_min_frequency,
@@ -147,9 +168,9 @@ impl PlantState {
},
Err(err) => MoistureSensorState::SensorError(err),
},
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError(
err.to_string(),
)),
Err(err) => MoistureSensorState::SensorError(MoistureSensorError::BoardError {
message: err.to_string(),
})
}
} else {
MoistureSensorState::Disabled
@@ -233,23 +254,53 @@ impl PlantState {
false
}
}
PlantWateringMode::MinMoisture => {
let (moisture_percent, _) = self.plant_moisture();
if let Some(_moisture_percent) = moisture_percent {
if self.pump_in_timeout(plant_conf, current_time) {
false
} else if !in_time_range(
current_time,
plant_conf.pump_hour_start,
plant_conf.pump_hour_end,
) {
false
} else if true {
//if not cooldown min and below max
true
} else if true {
//if below min disable cooldown min
true
} else {
false
}
} else {
false
}
}
PlantWateringMode::TimerOnly => !self.pump_in_timeout(plant_conf, current_time),
}
}
pub fn to_mqtt_info(&self, plant_conf: &PlantConfig, current_time: &DateTime<Tz>) -> PlantInfo {
pub fn to_mqtt_info(
&self,
plant_conf: &PlantConfig,
current_time: &DateTime<Tz>,
) -> PlantInfo {
let (moisture_pct, _) = self.plant_moisture();
PlantInfo {
sensor_a: &self.sensor_a,
sensor_b: &self.sensor_b,
moisture_pct,
sensor_a: Self::sensor_to_telemetry(&self.sensor_a),
sensor_b: Self::sensor_to_telemetry(&self.sensor_b),
mode: plant_conf.mode,
do_water: self.needs_to_be_watered(plant_conf, current_time),
dry: if let Some(moisture_percent) = self.plant_moisture().0 {
dry: if let Some(moisture_percent) = moisture_pct {
moisture_percent < plant_conf.target_moisture
} else {
false
},
cooldown: self.pump_in_timeout(plant_conf, current_time),
out_of_work_hour: in_time_range(
out_of_work_hour: !in_time_range(
current_time,
plant_conf.pump_hour_start,
plant_conf.pump_hour_end,
@@ -262,7 +313,9 @@ impl PlantState {
.map(|t| t.with_timezone(&current_time.timezone())),
next_pump: if matches!(
plant_conf.mode,
PlantWateringMode::TimerOnly | PlantWateringMode::TargetMoisture
PlantWateringMode::TimerOnly
| PlantWateringMode::TargetMoisture
| PlantWateringMode::MinMoisture
) {
self.pump.previous_pump.and_then(|last_pump| {
last_pump
@@ -274,15 +327,40 @@ impl PlantState {
},
}
}
fn sensor_to_telemetry(sensor: &MoistureSensorState) -> SensorTelemetry {
match sensor {
MoistureSensorState::Disabled => SensorTelemetry {
moisture_pct: None,
raw_hz: None,
error: None,
},
MoistureSensorState::MoistureValue {
raw_hz,
moisture_percent,
} => SensorTelemetry {
moisture_pct: Some(*moisture_percent),
raw_hz: Some(*raw_hz),
error: None,
},
MoistureSensorState::SensorError(err) => SensorTelemetry {
moisture_pct: None,
raw_hz: None,
error: Some(err.clone()),
},
}
}
}
#[derive(Debug, PartialEq, Serialize)]
/// State of a single plant to be tracked
pub struct PlantInfo<'a> {
pub struct PlantInfo {
/// combined plant moisture from available sensors
moisture_pct: Option<f32>,
/// state of humidity sensor on bank a
sensor_a: &'a MoistureSensorState,
sensor_a: SensorTelemetry,
/// state of humidity sensor on bank b
sensor_b: &'a MoistureSensorState,
sensor_b: SensorTelemetry,
/// configured plant watering mode
mode: PlantWateringMode,
/// the plant needs to be watered

View File

@@ -1,17 +1,21 @@
use serde::Serialize;
use crate::alloc::string::{String, ToString};
use crate::config::TankConfig;
use crate::hal::{BoardInteraction, HAL};
use crate::hal::HAL;
use crate::fat_error::FatResult;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::mutex::MutexGuard;
use serde::Serialize;
const OPEN_TANK_VOLTAGE: f32 = 3.0;
pub const WATER_FROZEN_THRESH: f32 = 4.0;
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum TankError {
SensorDisabled,
SensorMissing(f32),
SensorMissing { raw_mv: f32 },
SensorValueError { value: f32, min: f32, max: f32 },
BoardError(String),
BoardError { message: String },
}
pub enum TankState {
@@ -22,7 +26,7 @@ pub enum TankState {
fn raw_voltage_to_divider_percent(raw_value_mv: f32) -> Result<f32, TankError> {
if raw_value_mv > OPEN_TANK_VOLTAGE {
return Err(TankError::SensorMissing(raw_value_mv));
return Err(TankError::SensorMissing { raw_mv: raw_value_mv });
}
let r2 = raw_value_mv * 50.0 / (3.3 - raw_value_mv);
@@ -114,7 +118,7 @@ impl TankState {
}
}
pub fn as_mqtt_info(&self, config: &TankConfig, water_temp: &anyhow::Result<f32>) -> TankInfo {
pub fn as_mqtt_info(&self, config: &TankConfig, water_temp: &FatResult<f32>) -> TankInfo {
let mut tank_err: Option<TankError> = None;
let left_ml = match self.left_ml(config) {
Err(err) => {
@@ -138,24 +142,30 @@ impl TankState {
TankInfo {
enough_water,
warn_level,
left_ml,
volume_ml: left_ml,
sensor_error: tank_err,
raw,
fill_raw_v: raw,
water_frozen: water_temp
.as_ref()
.is_ok_and(|temp| *temp < WATER_FROZEN_THRESH),
water_temp: water_temp.as_ref().copied().ok(),
water_temp_c: water_temp.as_ref().copied().ok(),
temp_sensor_error: water_temp.as_ref().err().map(|err| err.to_string()),
percent,
fill_pct: percent,
}
}
}
pub fn determine_tank_state(board: &mut std::sync::MutexGuard<'_, HAL<'_>>) -> TankState {
pub async fn determine_tank_state(
board: &mut MutexGuard<'static, CriticalSectionRawMutex, HAL<'static>>,
) -> TankState {
if board.board_hal.get_config().tank.tank_sensor_enabled {
match board.board_hal.tank_sensor_voltage() {
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv),
Err(err) => TankState::Error(TankError::BoardError(err.to_string())),
match board
.board_hal
.get_tank_sensor()
.and_then(|f| core::prelude::v1::Ok(f.tank_sensor_voltage()))
{
Ok(raw_sensor_value_mv) => TankState::Present(raw_sensor_value_mv.await.unwrap()),
Err(err) => TankState::Error(TankError::BoardError { message: err.to_string() }),
}
} else {
TankState::Disabled
@@ -166,20 +176,20 @@ pub fn determine_tank_state(board: &mut std::sync::MutexGuard<'_, HAL<'_>>) -> T
/// Information structure send to mqtt for monitoring purposes
pub struct TankInfo {
/// there is enough water in the tank
enough_water: bool,
pub(crate) enough_water: bool,
/// warning that water needs to be refilled soon
warn_level: bool,
pub(crate) warn_level: bool,
/// estimation how many ml are still in the tank
left_ml: Option<f32>,
pub(crate) volume_ml: Option<f32>,
/// if there is an issue with the water level sensor
sensor_error: Option<TankError>,
pub(crate) sensor_error: Option<TankError>,
/// raw water sensor value
raw: Option<f32>,
pub(crate) fill_raw_v: Option<f32>,
/// percent value
percent: Option<f32>,
pub(crate) fill_pct: Option<f32>,
/// water in the tank might be frozen
water_frozen: bool,
pub(crate) water_frozen: bool,
/// water temperature
water_temp: Option<f32>,
temp_sensor_error: Option<String>,
pub(crate) water_temp_c: Option<f32>,
pub(crate) temp_sensor_error: Option<String>,
}

10
rust/src/util.rs Normal file
View File

@@ -0,0 +1,10 @@
macro_rules! mk_static {
($t:ty,$val:expr) => {{
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
#[deny(unused_attributes)]
let x = STATIC_CELL.uninit().write(($val));
x
}};
}
pub(crate) use mk_static;

View File

@@ -0,0 +1,191 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::rtc::X25;
use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::{String, ToString};
use chrono::DateTime;
use edge_http::io::server::Connection;
use edge_nal::io::{Read, Write};
use log::info;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
pub struct WebBackupHeader {
timestamp: String,
size: u16,
}
pub(crate) async fn get_backup_config<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> FatResult<Option<u32>>
where
T: Read + Write,
{
// First pass: verify checksum without sending data
let mut checksum = X25.digest();
let mut chunk = 0_usize;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
let (buf, len, expected_crc) = board
.board_hal
.get_rtc_module()
.get_backup_config(chunk)
.await?;
// Update checksum with the actual data bytes of this chunk
checksum.update(&buf[..len]);
let is_last = len == 0 || len < buf.len();
if is_last {
let actual_crc = checksum.finalize();
if actual_crc != expected_crc {
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
409,
Some(
format!(
"Checksum mismatch expected {} got {}",
expected_crc, actual_crc
)
.as_str(),
),
&[],
)
.await?;
return Ok(Some(409));
}
break;
}
chunk += 1;
}
// Second pass: stream data
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
let mut chunk = 0_usize;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
let (buf, len, _expected_crc) = board
.board_hal
.get_rtc_module()
.get_backup_config(chunk)
.await?;
if len == 0 {
break;
}
conn.write_all(&buf[..len]).await?;
if len < buf.len() {
break;
}
chunk += 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Ok(Some(200))
}
pub(crate) async fn backup_config<T, const N: usize>(
conn: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let mut offset = 0_usize;
let mut buf = [0_u8; 32];
let mut checksum = X25.digest();
let mut counter = 0;
loop {
let to_write = conn.read(&mut buf).await?;
if to_write == 0 {
info!("backup finished");
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(counter).await;
counter = counter + 1;
board
.board_hal
.get_rtc_module()
.backup_config(offset, &buf[0..to_write])
.await?;
checksum.update(&buf[0..to_write]);
}
offset = offset + to_write;
}
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_rtc_module()
.backup_config_finalize(checksum.finalize(), offset)
.await?;
board.board_hal.clear_progress().await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Ok(Some("saved".to_owned()))
}
pub(crate) async fn backup_info<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, FatError>
where
T: Read + Write,
{
let mut board = BOARD_ACCESS.get().await.lock().await;
let header = board.board_hal.get_rtc_module().get_backup_info().await;
let json = match header {
Ok(h) => {
let timestamp = DateTime::from_timestamp_millis(h.timestamp).unwrap();
let wbh = WebBackupHeader {
timestamp: timestamp.to_rfc3339(),
size: h.size,
};
serde_json::to_string(&wbh)?
}
Err(err) => {
let wbh = WebBackupHeader {
timestamp: err.to_string(),
size: 0,
};
serde_json::to_string(&wbh)?
}
};
Ok(Some(json))
}

View File

@@ -0,0 +1,160 @@
use crate::fat_error::{FatError, FatResult};
use crate::BOARD_ACCESS;
use alloc::borrow::ToOwned;
use alloc::format;
use alloc::string::String;
use edge_http::io::server::Connection;
use edge_http::Method;
use edge_nal::io::{Read, Write};
use log::info;
pub(crate) async fn list_files<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let result = board.board_hal.get_esp().list_files().await?;
let file_list_json = serde_json::to_string(&result)?;
Ok(Some(file_list_json))
}
pub(crate) async fn file_operations<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
method: Method,
path: &&str,
prefix: &&str,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
let filename = &path[prefix.len()..];
info!("file request for {} with method {}", filename, method);
Ok(match method {
Method::Delete => {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await?;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
Method::Get => {
let disposition = format!("attachment; filename=\"{filename}\"");
let size = {
let mut board = BOARD_ACCESS.get().await.lock().await;
board
.board_hal
.get_esp()
.get_size(filename.to_owned())
.await?
};
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "application/octet-stream"),
("Content-Disposition", disposition.as_str()),
("Content-Length", &format!("{}", size)),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
let mut chunk = 0;
loop {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk).await;
let read_chunk = board
.board_hal
.get_esp()
.get_file(filename.to_owned(), chunk)
.await?;
let length = read_chunk.1;
if length == 0 {
info!("file request for {} finished", filename);
break;
}
let data = &read_chunk.0[0..length];
conn.write_all(data).await?;
if length < read_chunk.0.len() {
info!("file request for {} finished", filename);
break;
}
chunk = chunk + 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
Some(200)
}
Method::Post => {
{
let mut board = BOARD_ACCESS.get().await.lock().await;
//ensure the file is deleted first; otherwise we would need to truncate the file which will not work with streaming
let _ = board
.board_hal
.get_esp()
.delete_file(filename.to_owned())
.await;
}
let mut offset = 0_usize;
let mut chunk = 0;
loop {
let mut buf = [0_u8; 1024];
let to_write = conn.read(&mut buf).await?;
if to_write == 0 {
info!("file request for {} finished", filename);
break;
} else {
let mut board = BOARD_ACCESS.get().await.lock().await;
board.board_hal.progress(chunk as u32).await;
board
.board_hal
.get_esp()
.write_file(filename.to_owned(), offset as u32, &buf[0..to_write])
.await?;
}
offset = offset + to_write;
chunk = chunk + 1;
}
BOARD_ACCESS
.get()
.await
.lock()
.await
.board_hal
.clear_progress()
.await;
conn.initiate_response(
200,
Some("OK"),
&[
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
Some(200)
}
_ => None,
})
}

View File

@@ -0,0 +1,184 @@
use crate::fat_error::{FatError, FatResult};
use crate::hal::PLANT_COUNT;
use crate::log::LogMessage;
use crate::plant_state::{MoistureSensorState, PlantState};
use crate::tank::determine_tank_state;
use crate::{get_version, BOARD_ACCESS};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use chrono_tz::Tz;
use core::str::FromStr;
use edge_http::io::server::Connection;
use edge_nal::io::{Read, Write};
use log::info;
use serde::Serialize;
#[derive(Serialize, Debug)]
struct LoadData<'a> {
rtc: &'a str,
native: &'a str,
}
#[derive(Serialize, Debug)]
struct Moistures {
moisture_a: Vec<String>,
moisture_b: Vec<String>,
}
#[derive(Serialize, Debug)]
struct SolarState {
mppt_voltage: f32,
mppt_current: f32,
is_day: bool,
}
pub(crate) async fn get_live_moisture<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>>
where
T: Read + Write,
{
let mut board = BOARD_ACCESS.get().await.lock().await;
let mut plant_state = Vec::new();
for i in 0..PLANT_COUNT {
plant_state.push(PlantState::read_hardware_state(i, &mut board).await);
}
let a = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_a {
MoistureSensorState::Disabled => "disabled".to_string(),
MoistureSensorState::MoistureValue {
raw_hz,
moisture_percent,
} => {
format!("{moisture_percent:.2}% {raw_hz}hz",)
}
MoistureSensorState::SensorError(err) => format!("{err:?}"),
}));
let b = Vec::from_iter(plant_state.iter().map(|s| match &s.sensor_b {
MoistureSensorState::Disabled => "disabled".to_string(),
MoistureSensorState::MoistureValue {
raw_hz,
moisture_percent,
} => {
format!("{moisture_percent:.2}% {raw_hz}hz",)
}
MoistureSensorState::SensorError(err) => format!("{err:?}"),
}));
let data = Moistures {
moisture_a: a,
moisture_b: b,
};
let json = serde_json::to_string(&data)?;
Ok(Some(json))
}
pub(crate) async fn tank_info<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> Result<Option<String>, FatError>
where
T: Read + Write,
{
let mut board = BOARD_ACCESS.get().await.lock().await;
let tank_state = determine_tank_state(&mut board).await;
//should be multisampled
let sensor = board.board_hal.get_tank_sensor()?;
let water_temp: FatResult<f32> = sensor.water_temperature_c().await;
Ok(Some(serde_json::to_string(&tank_state.as_mqtt_info(
&board.board_hal.get_config().tank,
&water_temp,
))?))
}
pub(crate) async fn get_timezones() -> FatResult<Option<String>> {
// Get all timezones compiled into the binary from chrono-tz
let timezones: Vec<&'static str> = chrono_tz::TZ_VARIANTS.iter().map(|tz| tz.name()).collect();
let json = serde_json::to_string(&timezones)?;
Ok(Some(json))
}
pub(crate) async fn get_solar_state<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let state = SolarState {
mppt_voltage: board.board_hal.get_mptt_voltage().await?.as_millivolts() as f32,
mppt_current: board.board_hal.get_mptt_current().await?.as_milliamperes() as f32,
is_day: board.board_hal.is_day(),
};
Ok(Some(serde_json::to_string(&state)?))
}
pub(crate) async fn get_version_web<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
Ok(Some(serde_json::to_string(&get_version(&mut board).await)?))
}
pub(crate) async fn get_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let json = serde_json::to_string(&board.board_hal.get_config())?;
Ok(Some(json))
}
pub(crate) async fn get_battery_state<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let battery_state = board
.board_hal
.get_battery_monitor()
.get_battery_state()
.await?;
Ok(Some(serde_json::to_string(&battery_state)?))
}
pub(crate) async fn get_time<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
let mut board = BOARD_ACCESS.get().await.lock().await;
let conf = board.board_hal.get_config();
let tz: Tz = match conf.timezone.as_ref() {
None => Tz::UTC,
Some(tz_string) => match Tz::from_str(tz_string) {
Ok(tz) => tz,
Err(err) => {
info!("failed parsing timezone {err}");
Tz::UTC
}
},
};
let native = board
.board_hal
.get_time()
.await
.with_timezone(&tz)
.to_rfc3339();
let rtc = match board.board_hal.get_rtc_module().get_rtc_time().await {
Ok(time) => time.with_timezone(&tz).to_rfc3339(),
Err(err) => {
format!("Error getting time: {err}")
}
};
let data = LoadData {
rtc: rtc.as_str(),
native: native.as_str(),
};
let json = serde_json::to_string(&data)?;
Ok(Some(json))
}
pub(crate) async fn get_log_localization_config<T, const N: usize>(
_request: &mut Connection<'_, T, N>,
) -> FatResult<Option<String>> {
Ok(Some(serde_json::to_string(
&LogMessage::log_localisation_config(),
)?))
}

View File

@@ -0,0 +1,36 @@
use crate::fat_error::FatResult;
use crate::log::LOG_ACCESS;
use edge_http::io::server::Connection;
use edge_nal::io::{Read, Write};
pub(crate) async fn get_log<T, const N: usize>(
conn: &mut Connection<'_, T, N>,
) -> FatResult<Option<u32>>
where
T: Read + Write,
{
let log = LOG_ACCESS.lock().await.get();
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "text/javascript"),
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Headers", "*"),
("Access-Control-Allow-Methods", "*"),
],
)
.await?;
conn.write_all("[".as_bytes()).await?;
let mut append = false;
for entry in log {
if append {
conn.write_all(",".as_bytes()).await?;
}
append = true;
let json = serde_json::to_string(&entry)?;
conn.write_all(json.as_bytes()).await?;
}
conn.write_all("]".as_bytes()).await?;
Ok(Some(200))
}

View File

@@ -0,0 +1,50 @@
use crate::fat_error::FatError;
use edge_http::io::server::Connection;
use edge_nal::io::{Read, Write};
pub(crate) async fn serve_favicon<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
conn.initiate_response(200, Some("OK"), &[("Content-Type", "image/x-icon")])
.await?;
conn.write_all(include_bytes!("favicon.ico")).await?;
Ok(Some(200))
}
pub(crate) async fn serve_index<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
conn.initiate_response(
200,
Some("OK"),
&[("Content-Type", "text/html"), ("Content-Encoding", "gzip")],
)
.await?;
conn.write_all(include_bytes!("index.html.gz")).await?;
Ok(Some(200))
}
pub(crate) async fn serve_bundle<T, const N: usize>(
conn: &mut Connection<'_, T, { N }>,
) -> Result<Option<u32>, FatError>
where
T: Read + Write,
{
conn.initiate_response(
200,
Some("OK"),
&[
("Content-Type", "text/javascript"),
("Content-Encoding", "gzip"),
],
)
.await?;
conn.write_all(include_bytes!("bundle.js.gz")).await?;
Ok(Some(200))
}

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