Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd257b89f0 | |||
| 4debbfb39e | |||
| d83189417a | |||
| 3c92cf0c26 | |||
|
96023c8dc3
|
|||
|
7497a8c05d
|
|||
|
d3d8d829be
|
|||
|
6889ba4561
|
|||
|
18095349f3
|
|||
|
3d8fd893f5
|
|||
|
1bea7ef2f4
|
|||
|
f5b9674840
|
|||
|
6f22881007
|
|||
|
1d8af1b6c4
|
|||
|
2c532359fc
|
|||
|
53819484fb
|
|||
|
1151d099cf
|
|||
|
3feaacd460
|
|||
|
e15e78cc26
|
|||
|
d9aa96a3cb
|
|||
|
ecf989b859
|
|||
|
db401aac55
|
|||
| ecb7707357 | |||
|
4cf7a1c94f
|
|||
|
9155676e06
|
|||
|
e05f3d768f
|
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CONTAINER_NAME="localhost/wch-dev-tools:latest"
|
CONTAINER_NAME="localhost/esp-plant-dev-tools:latest"
|
||||||
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
|
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
|
||||||
pushd "$CONTAINER_TOOLS_BASEDIR"
|
pushd "$CONTAINER_TOOLS_BASEDIR"
|
||||||
podman build -t "$CONTAINER_NAME" -f "../wch-tools.Containerfile" .
|
podman build -t "$CONTAINER_NAME" -f "esp-plant-dev-tools.Containerfile" .
|
||||||
popd
|
popd
|
||||||
@@ -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
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CONTAINER_IMAGE="localhost/wch-dev-tools:latest"
|
CONTAINER_IMAGE="localhost/esp-plant-dev-tools:latest"
|
||||||
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
|
CONTAINER_TOOLS_BASEDIR="$(dirname "$(readlink -f "$0")")"
|
||||||
|
PLANTCTL_PROJECT_DIR="$(readlink -f "$CONTAINER_TOOLS_BASEDIR/..")"
|
||||||
|
|
||||||
function _fatal {
|
function _fatal {
|
||||||
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
|
echo -e "\e[31mERROR\e[0m $(</dev/stdin)$*" 1>&2
|
||||||
@@ -12,8 +13,7 @@ function _fatal {
|
|||||||
|
|
||||||
declare -a PODMAN_ARGS=(
|
declare -a PODMAN_ARGS=(
|
||||||
"--rm" "-i" "--log-driver=none"
|
"--rm" "-i" "--log-driver=none"
|
||||||
"--network=host"
|
"-v" "$PLANTCTL_PROJECT_DIR:$PLANTCTL_PROJECT_DIR:rw"
|
||||||
"--pid=host"
|
|
||||||
"-v" "$PWD:$PWD:rw"
|
"-v" "$PWD:$PWD:rw"
|
||||||
"-w" "$PWD"
|
"-w" "$PWD"
|
||||||
)
|
)
|
||||||
@@ -22,8 +22,8 @@ declare -a PODMAN_ARGS=(
|
|||||||
|
|
||||||
if ! podman image exists "$CONTAINER_IMAGE"; then
|
if ! podman image exists "$CONTAINER_IMAGE"; then
|
||||||
#attempt to build container
|
#attempt to build container
|
||||||
"$CONTAINER_TOOLS_BASEDIR/build-wch-tools-container.sh" 1>&2 ||
|
"$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"
|
_fatal "faild to build local image, cannot continue! … please ensure you have an internet connection"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
podman run "${PODMAN_ARGS[@]}" --entrypoint riscv-none-elf-gdb-py3 "$CONTAINER_IMAGE" "$@"
|
podman run "${PODMAN_ARGS[@]}" --entrypoint npm "$CONTAINER_IMAGE" "$@"
|
||||||
@@ -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" "$@"
|
||||||
Executable
+29
@@ -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" "$@"
|
||||||
@@ -34,5 +34,3 @@ _autosave-*
|
|||||||
|
|
||||||
# Autorouter files (exported from Pcbnew)
|
# Autorouter files (exported from Pcbnew)
|
||||||
fp-info-cache
|
fp-info-cache
|
||||||
*.zip
|
|
||||||
netlist.ipc
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"EXTRA_LAYERS": "", "ALL_ACTIVE_LAYERS": false, "EXTEND_EDGE_CUT": false, "ALTERNATIVE_EDGE_CUT": false, "AUTO TRANSLATE": true, "AUTO FILL": true, "EXCLUDE DNP": false}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
(fp_lib_table
|
|
||||||
(version 7)
|
|
||||||
(lib (name "Sensor")(type "KiCad")(uri "/home/empire/workspace/PlantCtrl/board/modules/Sensors/Sensors/Sensor.pretty")(options "")(descr ""))
|
|
||||||
)
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
P CODE 00
|
|
||||||
P UNITS CUST 0
|
|
||||||
P arrayDim N
|
|
||||||
317GND VIA MD0118PA00X+040551Y-024902X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+054232Y-022933X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+053346Y-021949X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+040846Y-019980X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+041043Y-025787X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+040059Y-019980X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+044291Y-025787X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+046654Y-025000X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+041240Y-021949X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+043307Y-020768X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+039961Y-022539X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+049606Y-021260X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+043898Y-024803X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+044587Y-023917X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+041142Y-024016X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+052461Y-022835X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+053642Y-024016X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+047835Y-026378X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+042618Y-020669X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+049409Y-022047X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+042520Y-022441X0236Y0000R000S3
|
|
||||||
317GND VIA MD0118PA00X+048622Y-022835X0236Y0000R000S3
|
|
||||||
327GND R_slop-1 A01X+053967Y-020177X0315Y0374R180S2
|
|
||||||
327NET-(U1-RS) R_slop-2 A01X+053317Y-020177X0315Y0374R180S2
|
|
||||||
3173_3V U6 -1 D0394PA00X+039016Y-019370X0669Y0669R000S0
|
|
||||||
317(U6-VBAT-PAD2) U6 -2 D0394PA00X+039016Y-020370X0669Y0000R000S0
|
|
||||||
317-(U6-SDA-PAD3) U6 -3 D0394PA00X+039016Y-021370X0669Y0000R000S0
|
|
||||||
317-(U6-SCL-PAD4) U6 -4 D0394PA00X+039016Y-022370X0669Y0000R000S0
|
|
||||||
317ENABLE_CAN U6 -5 D0394PA00X+039016Y-023370X0669Y0000R000S0
|
|
||||||
317CAN+ U6 -6 D0394PA00X+039016Y-024370X0669Y0000R000S0
|
|
||||||
317CAN- U6 -7 D0394PA00X+039016Y-025370X0669Y0000R000S0
|
|
||||||
317GND U6 -8 D0394PA00X+039016Y-026370X0669Y0000R000S0
|
|
||||||
317GND U6 -9 D0394PA00X+071260Y-034252X0669Y0669R000S0
|
|
||||||
317GND U6 -10 D0394PA00X+055512Y-031299X0669Y0669R000S0
|
|
||||||
317GND U6 -11 D0394PA00X+038780Y-034252X0669Y0669R000S0
|
|
||||||
317GND U6 -12 D0394PA00X+055512Y-020669X0669Y0669R000S0
|
|
||||||
317GND U6 -13 D0394PA00X+071260Y-019094X0669Y0669R000S0
|
|
||||||
327NET-(QP_1-G) QP_1 -1 A01X+047835Y-021280X0354Y0315R000S2
|
|
||||||
3273_3V QP_1 -2 A01X+047835Y-022028X0354Y0315R000S2
|
|
||||||
327CAN_POWER QP_1 -3 A01X+048622Y-021654X0354Y0315R000S2
|
|
||||||
327CAN_POWER R5 -1 A01X+047835Y-022904X0315Y0374R090S2
|
|
||||||
327NET-(I1-A) R5 -2 A01X+047835Y-023553X0315Y0374R090S2
|
|
||||||
327NET-(QP_1-G) R6 -1 A01X+046654Y-021329X0315Y0374R090S2
|
|
||||||
3273_3V R6 -2 A01X+046654Y-021978X0315Y0374R090S2
|
|
||||||
327NET-(QP_1-G) R7 -1 A01X+045669Y-021329X0315Y0374R090S2
|
|
||||||
327NET-(Q1-D) R7 -2 A01X+045669Y-021978X0315Y0374R090S2
|
|
||||||
317CAN_POWER J1 -1 D0374PA00X+055197Y-025394X0669Y0768R270S0
|
|
||||||
317NET-(J1-PIN_2) J1 -2 D0374PA00X+055197Y-024409X0669Y0768R270S0
|
|
||||||
317NET-(J1-PIN_3) J1 -3 D0374PA00X+055197Y-023425X0669Y0768R270S0
|
|
||||||
317GND J1 -4 D0374PA00X+055197Y-022441X0669Y0768R270S0
|
|
||||||
327NET-(Q1-G) Q1 -1 A01X+043819Y-021280X0354Y0315R000S2
|
|
||||||
327GND Q1 -2 A01X+043819Y-022028X0354Y0315R000S2
|
|
||||||
327NET-(Q1-D) Q1 -3 A01X+044606Y-021654X0354Y0315R000S2
|
|
||||||
327CAN+ U1 -1 A01X+050295Y-020707X0768Y0236R000S2
|
|
||||||
327GND U1 -2 A01X+050295Y-021207X0768Y0236R000S2
|
|
||||||
327CAN_POWER U1 -3 A01X+050295Y-021707X0768Y0236R000S2
|
|
||||||
327CAN- U1 -4 A01X+050295Y-022207X0768Y0236R000S2
|
|
||||||
327(U1-VREF-PAD5) U1 -5 A01X+052244Y-022207X0768Y0236R000S2
|
|
||||||
327NET-(J1-PIN_2) U1 -6 A01X+052244Y-021707X0768Y0236R000S2
|
|
||||||
327NET-(J1-PIN_3) U1 -7 A01X+052244Y-021207X0768Y0236R000S2
|
|
||||||
327NET-(U1-RS) U1 -8 A01X+052244Y-020707X0768Y0236R000S2
|
|
||||||
327NET-(Q1-G) R8 -1 A01X+042520Y-021329X0315Y0374R090S2
|
|
||||||
327GND R8 -2 A01X+042520Y-021978X0315Y0374R090S2
|
|
||||||
327GND I1 -1 A01X+047835Y-025541X0384Y0551R270S2
|
|
||||||
327NET-(I1-A) I1 -2 A01X+047835Y-024803X0384Y0551R270S2
|
|
||||||
327NET-(Q1-G) R4 -1 A01X+040846Y-023346X0315Y0374R180S2
|
|
||||||
327ENABLE_CAN R4 -2 A01X+040197Y-023346X0315Y0374R180S2
|
|
||||||
999
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
;;; .doomrc --- doom runtime config -*- mode: emacs-lisp; lexical-binding: t; -*-
|
|
||||||
;;; Commentary:
|
|
||||||
;;; Code:
|
|
||||||
(require 'doom) ; be silent, byte-compiler
|
|
||||||
|
|
||||||
(after! dape
|
|
||||||
(add-to-list
|
|
||||||
'dape-configs
|
|
||||||
`(gdb-dap-openocd
|
|
||||||
ensure (lambda (config)
|
|
||||||
(dape-ensure-command config)
|
|
||||||
(let* ((default-directory
|
|
||||||
(or (dape-config-get config 'command-cwd)
|
|
||||||
default-directory))
|
|
||||||
(command (dape-config-get config 'command))
|
|
||||||
(output (shell-command-to-string (format "%s --version" command)))
|
|
||||||
(version (save-match-data
|
|
||||||
(when (string-match "GNU gdb \\(?:(.*) \\)?\\([0-9.]+\\)" output)
|
|
||||||
(string-to-number (match-string 1 output))))))
|
|
||||||
(unless (>= version 14.1)
|
|
||||||
(user-error "Requires gdb version >= 14.1"))))
|
|
||||||
modes ()
|
|
||||||
command-cwd dape-command-cwd
|
|
||||||
command "gdb"
|
|
||||||
command-args ("--interpreter=dap")
|
|
||||||
:request nil
|
|
||||||
:program nil
|
|
||||||
:args []
|
|
||||||
:stopAtBeginningOfMainSubprogram nil))
|
|
||||||
)
|
|
||||||
|
|
||||||
;;; .doomrc ends here
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="EMPTY_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# ESP-IDF build artifacts
|
|
||||||
build/
|
|
||||||
.sdkconfig*
|
|
||||||
CMakeFiles/
|
|
||||||
CMakeCache.txt
|
|
||||||
cmake-build-*/
|
|
||||||
*.log
|
|
||||||
*.bin
|
|
||||||
*.elf
|
|
||||||
*.map
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# Editor-based HTTP Client requests
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="EMPTY_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
Generated
-8
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/bootloader.iml" filepath="$PROJECT_DIR$/.idea/bootloader.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
Generated
-7
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
|
||||||
<mapping directory="$PROJECT_DIR$/../website/themes/blowfish" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
|
|
||||||
# Minimal ESP-IDF project to build only the bootloader
|
|
||||||
# You must have ESP-IDF installed and IDF_PATH exported.
|
|
||||||
|
|
||||||
# Pin the target to ESP32-C6 to ensure correct bootloader build
|
|
||||||
# (must be set before including project.cmake)
|
|
||||||
set(IDF_TARGET "esp32c6")
|
|
||||||
|
|
||||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
|
||||||
project(custom_bootloader)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
Custom ESP-IDF Bootloader (Rollback Enabled)
|
|
||||||
|
|
||||||
This minimal project builds a custom ESP-IDF bootloader with rollback support enabled.
|
|
||||||
You can flash it later alongside a Rust firmware using `espflash`.
|
|
||||||
|
|
||||||
What this provides
|
|
||||||
- A minimal ESP-IDF project (CMake) that can build just the bootloader.
|
|
||||||
- Rollback support enabled via sdkconfig.defaults (CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y).
|
|
||||||
- A sample OTA partition table (partitions.csv) suitable for OTA and rollback (otadata + two OTA slots).
|
|
||||||
- A convenience script to build the bootloader for the desired target.
|
|
||||||
|
|
||||||
Requirements
|
|
||||||
- ESP-IDF installed and set up (IDF_PATH exported, Python env activated).
|
|
||||||
- A selected target (esp32, esp32s3, esp32c3, etc.).
|
|
||||||
|
|
||||||
Build
|
|
||||||
1) Ensure ESP-IDF is set up:
|
|
||||||
source "$IDF_PATH/export.sh"
|
|
||||||
|
|
||||||
2) Pick a target (examples):
|
|
||||||
idf.py set-target esp32
|
|
||||||
# or use the script:
|
|
||||||
./build_bootloader.sh esp32
|
|
||||||
|
|
||||||
3) Build only the bootloader:
|
|
||||||
idf.py bootloader
|
|
||||||
# or using the script (which also supports setting target):
|
|
||||||
./build_bootloader.sh esp32
|
|
||||||
|
|
||||||
Artifacts
|
|
||||||
- build/bootloader/bootloader.bin
|
|
||||||
|
|
||||||
Using with espflash (Rust)
|
|
||||||
- For a no_std Rust firmware, you can pass this custom bootloader to espflash:
|
|
||||||
espflash flash --bootloader build/bootloader/bootloader.bin \
|
|
||||||
--partition-table partitions.csv \
|
|
||||||
<your-app-binary-or-elf>
|
|
||||||
|
|
||||||
Notes
|
|
||||||
- Rollback logic requires an OTA layout (otadata + at least two OTA app partitions). The provided partitions.csv is a starting point; adjust sizes/offsets to match your needs.
|
|
||||||
- This project doesn’t build an application; it exists solely to produce a bootloader with the right configuration.
|
|
||||||
- If you need different log verbosity or features, run `idf.py menuconfig` and then diff/port the changes back into sdkconfig.defaults.
|
|
||||||
- Targets supported depend on your ESP-IDF version. Use `idf.py set-target <chip>` or `./build_bootloader.sh <chip>`.
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Build script for custom ESP-IDF bootloader with rollback enabled.
|
|
||||||
# Requirements:
|
|
||||||
# - ESP-IDF installed
|
|
||||||
# - IDF_PATH exported
|
|
||||||
# - Python env prepared (the usual ESP-IDF setup)
|
|
||||||
# Usage:
|
|
||||||
# ./build_bootloader.sh [esp32|esp32s3|esp32c3|esp32s2|esp32c2|esp32c6|esp32h2]
|
|
||||||
# If target is omitted, the last configured target will be used.
|
|
||||||
|
|
||||||
TARGET=${1:-}
|
|
||||||
|
|
||||||
if [[ -z "${IDF_PATH:-}" ]]; then
|
|
||||||
echo "ERROR: IDF_PATH is not set. Please install ESP-IDF and export the environment (source export.sh)." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# shellcheck source=/dev/null
|
|
||||||
source "$IDF_PATH/export.sh"
|
|
||||||
|
|
||||||
if [[ -n "$TARGET" ]]; then
|
|
||||||
idf.py set-target "$TARGET"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure sdkconfig.defaults is considered (ESP-IDF does this automatically).
|
|
||||||
# Build only the bootloader.
|
|
||||||
idf.py bootloader
|
|
||||||
|
|
||||||
echo
|
|
||||||
BOOTLOADER_BIN="build/bootloader/bootloader.bin"
|
|
||||||
if [[ -f "$BOOTLOADER_BIN" ]]; then
|
|
||||||
echo "Bootloader built: $BOOTLOADER_BIN"
|
|
||||||
echo "You can use this with espflash via:"
|
|
||||||
echo " espflash flash --bootloader $BOOTLOADER_BIN [--partition-table partitions.csv] <your-app-binary>"
|
|
||||||
else
|
|
||||||
echo "ERROR: Bootloader binary not found. Check build logs above." >&2
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
cp build/bootloader/bootloader.bin ../rust/bootloader.bin
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
idf_component_register(SRCS "dummy.c" INCLUDE_DIRS ".")
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
// This file intentionally left almost empty.
|
|
||||||
// ESP-IDF expects at least one component; the bootloader build does not use this.
|
|
||||||
|
|
||||||
void __unused_dummy_symbol(void) {}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
nvs, data, nvs, , 16k,
|
|
||||||
otadata, data, ota, , 8k,
|
|
||||||
phy_init, data, phy, , 4k,
|
|
||||||
ota_0, app, ota_0, , 3968k,
|
|
||||||
ota_1, app, ota_1, , 3968k,
|
|
||||||
storage, data, littlefs,, 8M,
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,17 +0,0 @@
|
|||||||
# Target can be set with: idf.py set-target esp32|esp32s3|esp32c3|...
|
|
||||||
# If not set via idf.py, ESP-IDF may default to a target; it's recommended to set it explicitly.
|
|
||||||
|
|
||||||
# Explicitly pin target to ESP32-C6
|
|
||||||
CONFIG_IDF_TARGET="esp32c6"
|
|
||||||
CONFIG_IDF_TARGET_ESP32C6=y
|
|
||||||
CONFIG_IDF_TARGET_ARCH_RISCV=y
|
|
||||||
|
|
||||||
# Bootloader configuration
|
|
||||||
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
|
||||||
CONFIG_BOOTLOADER_LOG_LEVEL_INFO=y
|
|
||||||
# Slightly faster boot by skipping GPIO checks unless you need that feature
|
|
||||||
CONFIG_BOOTLOADER_SKIP_VALIDATE_IN_DEEP_SLEEP=y
|
|
||||||
|
|
||||||
# Partition table config is not required to build bootloader, but shown for clarity when you build full app later
|
|
||||||
# CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
|
||||||
# CONFIG_PARTITION_TABLE_FILENAME="partitions.csv"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,7 @@ rustflags = [
|
|||||||
target = "riscv32imac-unknown-none-elf"
|
target = "riscv32imac-unknown-none-elf"
|
||||||
|
|
||||||
[target.riscv32imac-unknown-none-elf]
|
[target.riscv32imac-unknown-none-elf]
|
||||||
#runner = "espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv"
|
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 --baud 921600 --partition-table partitions.csv -b no-reset" # Select this runner in case of usb ttl
|
||||||
#runner = "espflash flash --monitor"
|
#runner = "espflash flash --monitor"
|
||||||
#runner = "cargo runner"
|
#runner = "cargo runner"
|
||||||
|
|||||||
Generated
+2931
File diff suppressed because it is too large
Load Diff
+44
-78
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "plant-ctrl2"
|
name = "plant-ctrl2"
|
||||||
@@ -13,6 +14,19 @@ test = false
|
|||||||
bench = false
|
bench = false
|
||||||
doc = false
|
doc = false
|
||||||
|
|
||||||
|
[package.metadata.cargo_runner]
|
||||||
|
# The string `$TARGET_FILE` will be replaced with the path from cargo.
|
||||||
|
command = [
|
||||||
|
"cargo",
|
||||||
|
"espflash",
|
||||||
|
"save-image",
|
||||||
|
"--chip",
|
||||||
|
"esp32c6",
|
||||||
|
"image.bin",
|
||||||
|
"--partition-table",
|
||||||
|
"partitions.csv"
|
||||||
|
]
|
||||||
|
|
||||||
#this strips the bootloader, we need that tho
|
#this strips the bootloader, we need that tho
|
||||||
#strip = true
|
#strip = true
|
||||||
|
|
||||||
@@ -38,79 +52,43 @@ partition_table = "partitions.csv"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
#ESP stuff
|
#ESP stuff
|
||||||
esp-bootloader-esp-idf = { version = "0.2.0", features = ["esp32c6"] }
|
log = "0.4.28"
|
||||||
esp-hal = { version = "=1.0.0-rc.0", features = [
|
esp-bootloader-esp-idf = { version = "0.5.0", features = ["esp32c6", "log-04"] }
|
||||||
"esp32c6",
|
esp-hal = { version = "1.1.0", features = ["esp32c6", "log-04"] }
|
||||||
"log-04",
|
esp-rtos = { version = "0.3.0", features = ["esp32c6", "embassy", "esp-radio"] }
|
||||||
"unstable",
|
esp-backtrace = { version = "0.19.0", features = ["esp32c6", "panic-handler", "println", "colors", "custom-halt"] }
|
||||||
"rt"
|
esp-println = { version = "0.17.0", features = ["esp32c6", "log-04", "auto"] }
|
||||||
] }
|
esp-storage = { version = "0.9.0", features = ["esp32c6"] }
|
||||||
log = "0.4.27"
|
esp-radio = { version = "0.18.0", features = ["esp32c6", "log-04", "wifi", "unstable"] }
|
||||||
|
esp-alloc = { version = "0.10.0", features = ["esp32c6", "internal-heap-stats"] }
|
||||||
|
|
||||||
embassy-net = { version = "0.7.1", default-features = false, features = [
|
# Async runtime (Embassy core)
|
||||||
"dhcpv4",
|
embassy-executor = { version = "0.10.0", features = ["log", "nightly"] }
|
||||||
"log",
|
embassy-time = { version = "0.5.1", features = ["log"], default-features = false }
|
||||||
"medium-ethernet",
|
embassy-sync = { version = "0.8.0", features = ["log"] }
|
||||||
"tcp",
|
|
||||||
"udp",
|
|
||||||
"proto-ipv4",
|
|
||||||
"dns"
|
|
||||||
] }
|
|
||||||
embedded-io = "0.6.1"
|
|
||||||
embedded-io-async = "0.6.1"
|
|
||||||
esp-alloc = "0.8.0"
|
|
||||||
esp-backtrace = { version = "0.17.0", features = [
|
|
||||||
"esp32c6",
|
|
||||||
"exception-handler",
|
|
||||||
"panic-handler",
|
|
||||||
"println",
|
|
||||||
"colors",
|
|
||||||
"custom-halt"
|
|
||||||
] }
|
|
||||||
esp-println = { version = "0.15.0", features = ["esp32c6", "log-04"] }
|
|
||||||
# for more networking protocol support see https://crates.io/crates/edge-net
|
|
||||||
embassy-executor = { version = "0.7.0", features = [
|
|
||||||
"log",
|
|
||||||
"task-arena-size-64",
|
|
||||||
"nightly"
|
|
||||||
] }
|
|
||||||
embassy-time = { version = "0.5.0", features = ["log"], default-features = false }
|
|
||||||
esp-hal-embassy = { version = "0.9.0", features = ["esp32c6", "log-04"] }
|
|
||||||
esp-storage = { version = "0.7.0", features = ["esp32c6"] }
|
|
||||||
|
|
||||||
esp-wifi = { version = "0.15.0", features = [
|
# Networking and protocol stacks
|
||||||
"builtin-scheduler",
|
embassy-net = { version = "0.8.0", features = ["dhcpv4", "log", "medium-ethernet", "tcp", "udp", "proto-ipv4", "dns", "proto-ipv6"] }
|
||||||
"esp-alloc",
|
sntpc = { version = "0.6.1", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
|
||||||
"esp32c6",
|
edge-dhcp = "0.7.0"
|
||||||
"log-04",
|
edge-nal = "0.6.0"
|
||||||
"smoltcp",
|
edge-nal-embassy = "0.8.1"
|
||||||
"wifi",
|
edge-http = { version = "0.7.0", features = ["log"] }
|
||||||
] }
|
|
||||||
smoltcp = { version = "0.12.0", default-features = false, features = [
|
esp32c6 = { version = "0.23.2" }
|
||||||
"alloc",
|
|
||||||
"log",
|
# Hardware abstraction traits and HAL adapters
|
||||||
"medium-ethernet",
|
|
||||||
"multicast",
|
|
||||||
"proto-dhcpv4",
|
|
||||||
"proto-ipv6",
|
|
||||||
"proto-dns",
|
|
||||||
"proto-ipv4",
|
|
||||||
"socket-dns",
|
|
||||||
"socket-icmp",
|
|
||||||
"socket-raw",
|
|
||||||
"socket-tcp",
|
|
||||||
"socket-udp",
|
|
||||||
] }
|
|
||||||
#static_cell = "2.1.1"
|
|
||||||
embedded-hal = "1.0.0"
|
embedded-hal = "1.0.0"
|
||||||
embedded-hal-bus = { version = "0.3.0" }
|
embedded-storage = "0.3.1"
|
||||||
|
embassy-embedded-hal = "0.6.0"
|
||||||
|
nb = "1.1.0"
|
||||||
|
|
||||||
#Hardware additional driver
|
#Hardware additional driver
|
||||||
|
|
||||||
#bq34z100 = { version = "0.3.0", default-features = false }
|
#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"
|
onewire = "0.4.0"
|
||||||
#strum = { version = "0.27.0", default-feature = false, features = ["derive"] }
|
#strum = { version = "0.27.0", default-feature = false, features = ["derive"] }
|
||||||
measurements = "0.11.0"
|
|
||||||
ds323x = "0.6.0"
|
ds323x = "0.6.0"
|
||||||
|
|
||||||
#json
|
#json
|
||||||
@@ -125,35 +103,23 @@ strum_macros = "0.27.0"
|
|||||||
unit-enum = "1.4.1"
|
unit-enum = "1.4.1"
|
||||||
pca9535 = { version = "2.0.0" }
|
pca9535 = { version = "2.0.0" }
|
||||||
ina219 = { version = "0.2.0" }
|
ina219 = { version = "0.2.0" }
|
||||||
embedded-storage = "=0.3.1"
|
|
||||||
portable-atomic = "1.11.1"
|
portable-atomic = "1.11.1"
|
||||||
embassy-sync = { version = "0.7.2", features = ["log"] }
|
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
bq34z100 = { version = "0.4.0", default-features = false }
|
bq34z100 = { version = "0.4.0", default-features = false }
|
||||||
edge-dhcp = "0.6.0"
|
|
||||||
edge-nal = "0.5.0"
|
|
||||||
edge-nal-embassy = "0.6.0"
|
|
||||||
static_cell = "2.1.1"
|
static_cell = "2.1.1"
|
||||||
edge-http = { version = "0.6.1", features = ["log"] }
|
|
||||||
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
|
littlefs2 = { version = "0.6.1", features = ["c-stubs", "alloc"] }
|
||||||
littlefs2-core = "0.1.1"
|
littlefs2-core = "0.1.1"
|
||||||
bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
|
bytemuck = { version = "1.23.2", features = ["derive", "min_const_generics", "pod_saturating", "extern_crate_alloc"] }
|
||||||
deranged = "0.5.3"
|
deranged = "0.5.3"
|
||||||
embassy-embedded-hal = "0.5.0"
|
|
||||||
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
|
bincode = { version = "2.0.1", default-features = false, features = ["derive"] }
|
||||||
sntpc = { version = "0.6.0", default-features = false, features = ["log", "embassy-socket", "embassy-socket-ipv6"] }
|
|
||||||
option-lock = { version = "0.3.1", default-features = false }
|
option-lock = { version = "0.3.1", default-features = false }
|
||||||
|
measurements = "0.11.1"
|
||||||
#stay in sync with mcutie version here!
|
|
||||||
heapless = { version = "0.7.17", features = ["serde"] }
|
heapless = { version = "0.7.17", features = ["serde"] }
|
||||||
mcutie = { version = "0.3.0", default-features = false, features = ["log", "homeassistant"] }
|
mcutie = { path = "./src/mcutie_3_0_0/", default-features = false, features = ["log"] }
|
||||||
nb = "1.1.0"
|
|
||||||
embedded-can = "0.4.1"
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
mcutie = { git = 'https://github.com/empirephoenix/mcutie.git' }
|
|
||||||
#bq34z100 = { path = "../../bq34z100_rust" }
|
#bq34z100 = { path = "../../bq34z100_rust" }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
rm ./src/webserver/index.html.gz
|
|
||||||
rm ./src/webserver/bundle.js.gz
|
|
||||||
set -e
|
|
||||||
cd ./src_webpack/
|
|
||||||
npx webpack build
|
|
||||||
cp index.html.gz ../src/webserver/index.html.gz
|
|
||||||
cp bundle.js.gz ../src/webserver/bundle.js.gz
|
|
||||||
cd ../
|
|
||||||
|
|
||||||
cargo build --release
|
|
||||||
espflash save-image --bootloader bootloader.bin --partition-table partitions.csv --chip esp32c6 target/riscv32imac-unknown-none-elf/release/plant-ctrl2 image.bin
|
|
||||||
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
|
|
||||||
Binary file not shown.
+1
-1
@@ -50,7 +50,7 @@ fn linker_be_nice() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
//webpack();
|
webpack();
|
||||||
linker_be_nice();
|
linker_be_nice();
|
||||||
let _ = EmitBuilder::builder().all_git().all_build().emit();
|
let _ = EmitBuilder::builder().all_git().all_build().emit();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
rm ./src/webserver/index.html.gz
|
|
||||||
rm ./src/webserver/bundle.js.gz
|
|
||||||
set -e
|
|
||||||
cd ./src_webpack/
|
|
||||||
npx webpack build
|
|
||||||
cp index.html.gz ../src/webserver/index.html.gz
|
|
||||||
cp bundle.js.gz ../src/webserver/bundle.js.gz
|
|
||||||
cd ../
|
|
||||||
|
|
||||||
cargo build --release
|
|
||||||
espflash flash --monitor --bootloader bootloader.bin --chip esp32c6 --baud 921600 --partition-table partitions.csv target/riscv32imac-unknown-none-elf/release/plant-ctrl2
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
rm image.bin
|
|
||||||
rm ./src/webserver/index.html.gz
|
|
||||||
rm ./src/webserver/bundle.js.gz
|
|
||||||
set -e
|
|
||||||
cd ./src_webpack/
|
|
||||||
npx webpack build
|
|
||||||
cp index.html.gz ../src/webserver/index.html.gz
|
|
||||||
cp bundle.js.gz ../src/webserver/bundle.js.gz
|
|
||||||
cd ../
|
|
||||||
|
|
||||||
set -e
|
|
||||||
cargo build --release
|
|
||||||
espflash save-image --bootloader bootloader.bin --partition-table partitions.csv --chip esp32c6 target/riscv32imac-unknown-none-elf/release/plant-ctrl2 image.bin
|
|
||||||
+32
-29
@@ -6,11 +6,9 @@ use core::str::Utf8Error;
|
|||||||
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
use embassy_embedded_hal::shared_bus::I2cDeviceError;
|
||||||
use embassy_executor::SpawnError;
|
use embassy_executor::SpawnError;
|
||||||
use embassy_sync::mutex::TryLockError;
|
use embassy_sync::mutex::TryLockError;
|
||||||
use embedded_storage::nor_flash::NorFlashErrorKind;
|
|
||||||
use esp_hal::i2c::master::ConfigError;
|
use esp_hal::i2c::master::ConfigError;
|
||||||
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
|
use esp_hal::pcnt::unit::{InvalidHighLimit, InvalidLowLimit};
|
||||||
use esp_hal::twai::EspTwaiError;
|
use esp_radio::wifi::WifiError;
|
||||||
use esp_wifi::wifi::WifiError;
|
|
||||||
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
|
use ina219::errors::{BusVoltageReadError, ShuntVoltageReadError};
|
||||||
use littlefs2_core::PathError;
|
use littlefs2_core::PathError;
|
||||||
use onewire::Error;
|
use onewire::Error;
|
||||||
@@ -47,6 +45,7 @@ pub enum FatError {
|
|||||||
SpawnError {
|
SpawnError {
|
||||||
error: SpawnError,
|
error: SpawnError,
|
||||||
},
|
},
|
||||||
|
OTAError,
|
||||||
PartitionError {
|
PartitionError {
|
||||||
error: esp_bootloader_esp_idf::partitions::Error,
|
error: esp_bootloader_esp_idf::partitions::Error,
|
||||||
},
|
},
|
||||||
@@ -62,9 +61,6 @@ pub enum FatError {
|
|||||||
ExpanderError {
|
ExpanderError {
|
||||||
error: String,
|
error: String,
|
||||||
},
|
},
|
||||||
CanBusError {
|
|
||||||
error: EspTwaiError,
|
|
||||||
},
|
|
||||||
SNTPError {
|
SNTPError {
|
||||||
error: sntpc::Error,
|
error: sntpc::Error,
|
||||||
},
|
},
|
||||||
@@ -96,10 +92,10 @@ impl fmt::Display for FatError {
|
|||||||
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
|
FatError::DS323 { error } => write!(f, "DS323 {:?}", error),
|
||||||
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
|
FatError::Eeprom24x { error } => write!(f, "Eeprom24x {:?}", error),
|
||||||
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error),
|
FatError::ExpanderError { error } => write!(f, "ExpanderError {:?}", error),
|
||||||
FatError::CanBusError { error } => {
|
FatError::SNTPError { error } => write!(f, "SNTPError {error:?}"),
|
||||||
write!(f, "CanBusError {:?}", error)
|
FatError::OTAError => {
|
||||||
|
write!(f, "OTA missing partition")
|
||||||
}
|
}
|
||||||
FatError::SNTPError { error } => write!(f, "SNTPError {:?}", error),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +135,24 @@ impl<T> ContextExt<T> for Option<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl From<Error<Infallible>> for FatError {
|
||||||
fn from(error: Error<Infallible>) -> Self {
|
fn from(error: Error<Infallible>) -> Self {
|
||||||
FatError::OneWireError { error }
|
FatError::OneWireError { error }
|
||||||
@@ -180,6 +194,12 @@ impl From<SpawnError> for FatError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
impl From<esp_bootloader_esp_idf::partitions::Error> for FatError {
|
||||||
fn from(value: esp_bootloader_esp_idf::partitions::Error) -> Self {
|
fn from(value: esp_bootloader_esp_idf::partitions::Error) -> Self {
|
||||||
FatError::PartitionError { error: value }
|
FatError::PartitionError { error: value }
|
||||||
@@ -292,27 +312,10 @@ impl From<InvalidHighLimit> for FatError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<nb::Error<EspTwaiError>> for FatError {
|
impl From<chrono::format::ParseError> for FatError {
|
||||||
fn from(value: nb::Error<EspTwaiError>) -> Self {
|
fn from(value: chrono::format::ParseError) -> Self {
|
||||||
match value {
|
|
||||||
nb::Error::Other(can_error) => FatError::CanBusError { error: can_error },
|
|
||||||
nb::Error::WouldBlock => FatError::String {
|
|
||||||
error: "Would block".to_string(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NorFlashErrorKind> for FatError {
|
|
||||||
fn from(value: NorFlashErrorKind) -> Self {
|
|
||||||
FatError::String {
|
FatError::String {
|
||||||
error: value.to_string(),
|
error: format!("Parsing error: {value:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<sntpc::Error> for FatError {
|
|
||||||
fn from(value: sntpc::Error) -> Self {
|
|
||||||
FatError::SNTPError { error: value }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
use crate::hal::Sensor;
|
|
||||||
use bincode::{Decode, Encode};
|
|
||||||
|
|
||||||
pub(crate) const SENSOR_BASE_ADDRESS: u16 = 1000;
|
|
||||||
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
|
||||||
pub(crate) struct AutoDetectRequest {}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Encode, Decode)]
|
|
||||||
pub(crate) struct ResponseMoisture {
|
|
||||||
pub plant: u8,
|
|
||||||
pub sensor: Sensor,
|
|
||||||
pub hz: u32,
|
|
||||||
}
|
|
||||||
+122
-662
@@ -1,29 +1,30 @@
|
|||||||
use crate::bail;
|
use crate::bail;
|
||||||
use crate::config::{NetworkConfig, PlantControllerConfig};
|
use crate::config::{NetworkConfig, PlantControllerConfig};
|
||||||
use crate::hal::{get_current_slot_and_fix_ota_data, PLANT_COUNT, TIME_ACCESS};
|
use crate::hal::PLANT_COUNT;
|
||||||
use crate::log::{LogMessage, LOG_ACCESS};
|
use crate::log::{log, LogMessage};
|
||||||
|
use alloc::vec;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use esp_hal::Blocking;
|
||||||
|
use esp_hal::uart::Uart;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::fat_error::{ContextExt, FatError, FatResult};
|
use crate::fat_error::{ContextExt, FatError, FatResult};
|
||||||
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
use crate::hal::little_fs2storage_adapter::LittleFs2Filesystem;
|
||||||
use alloc::string::ToString;
|
use alloc::string::ToString;
|
||||||
use alloc::sync::Arc;
|
use alloc::sync::Arc;
|
||||||
use alloc::{format, string::String, vec, vec::Vec};
|
use alloc::{format, string::String, vec::Vec};
|
||||||
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use core::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
use core::str::FromStr;
|
use core::str::FromStr;
|
||||||
use core::sync::atomic::Ordering;
|
use core::sync::atomic::Ordering;
|
||||||
use embassy_executor::Spawner;
|
use embassy_executor::Spawner;
|
||||||
use embassy_net::udp::UdpSocket;
|
use embassy_net::{DhcpConfig, IpAddress, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
||||||
use embassy_net::{DhcpConfig, Ipv4Cidr, Runner, Stack, StackResources, StaticConfigV4};
|
|
||||||
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
|
||||||
use embassy_sync::mutex::{Mutex, MutexGuard};
|
use embassy_sync::mutex::{Mutex, MutexGuard};
|
||||||
use embassy_sync::once_lock::OnceLock;
|
|
||||||
use embassy_time::{Duration, Timer, WithTimeout};
|
use embassy_time::{Duration, Timer, WithTimeout};
|
||||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash, RmwNorFlashStorage};
|
||||||
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
use esp_bootloader_esp_idf::ota::OtaImageState::Valid;
|
||||||
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState, Slot};
|
use esp_bootloader_esp_idf::ota::{Ota, OtaImageState};
|
||||||
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
use esp_bootloader_esp_idf::partitions::{AppPartitionSubType, FlashRegion};
|
||||||
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
use esp_hal::gpio::{Input, RtcPinWithResistors};
|
||||||
use esp_hal::rng::Rng;
|
use esp_hal::rng::Rng;
|
||||||
use esp_hal::rtc_cntl::{
|
use esp_hal::rtc_cntl::{
|
||||||
@@ -32,39 +33,32 @@ use esp_hal::rtc_cntl::{
|
|||||||
};
|
};
|
||||||
use esp_hal::system::software_reset;
|
use esp_hal::system::software_reset;
|
||||||
use esp_println::println;
|
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 esp_storage::FlashStorage;
|
||||||
use esp_wifi::wifi::{
|
|
||||||
AccessPointConfiguration, AccessPointInfo, AuthMethod, ClientConfiguration, Configuration,
|
|
||||||
ScanConfig, ScanTypeConfig, WifiController, WifiDevice, WifiState,
|
|
||||||
};
|
|
||||||
use littlefs2::fs::Filesystem;
|
use littlefs2::fs::Filesystem;
|
||||||
use littlefs2_core::{FileType, PathBuf, SeekFrom};
|
use littlefs2_core::{FileType, PathBuf, SeekFrom};
|
||||||
use log::{info, warn};
|
use log::{info, warn, error};
|
||||||
use mcutie::{
|
|
||||||
Error, McutieBuilder, McutieReceiver, McutieTask, MqttMessage, PublishDisplay, Publishable,
|
|
||||||
QoS, Topic,
|
|
||||||
};
|
|
||||||
use portable_atomic::AtomicBool;
|
use portable_atomic::AtomicBool;
|
||||||
use smoltcp::socket::udp::PacketMetadata;
|
|
||||||
use smoltcp::wire::DnsQueryType;
|
|
||||||
use sntpc::{get_time, NtpContext, NtpTimestampGenerator};
|
|
||||||
|
|
||||||
#[esp_hal::ram(rtc_fast, persistent)]
|
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];
|
static mut LAST_WATERING_TIMESTAMP: [i64; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||||
#[esp_hal::ram(rtc_fast, persistent)]
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||||
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
|
static mut CONSECUTIVE_WATERING_PLANT: [u32; PLANT_COUNT] = [0; PLANT_COUNT];
|
||||||
#[esp_hal::ram(rtc_fast, persistent)]
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||||
static mut LOW_VOLTAGE_DETECTED: i8 = 0;
|
static mut LOW_VOLTAGE_DETECTED: i8 = 0;
|
||||||
#[esp_hal::ram(rtc_fast, persistent)]
|
#[esp_hal::ram(unstable(rtc_fast), unstable(persistent))]
|
||||||
static mut RESTART_TO_CONF: i8 = 0;
|
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";
|
const CONFIG_FILE: &str = "config.json";
|
||||||
const NTP_SERVER: &str = "pool.ntp.org";
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
#[derive(Serialize, Debug)]
|
#[derive(Serialize, Debug)]
|
||||||
pub struct FileInfo {
|
pub struct FileInfo {
|
||||||
@@ -79,11 +73,6 @@ pub struct FileList {
|
|||||||
files: Vec<FileInfo>,
|
files: Vec<FileInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
|
||||||
struct Timestamp {
|
|
||||||
stamp: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions
|
// Minimal esp-idf equivalent for gpio_hold on esp32c6 via ROM functions
|
||||||
extern "C" {
|
extern "C" {
|
||||||
fn gpio_pad_hold(gpio_num: u32);
|
fn gpio_pad_hold(gpio_num: u32);
|
||||||
@@ -100,36 +89,25 @@ pub fn hold_disable(gpio_num: u8) {
|
|||||||
unsafe { gpio_pad_unhold(gpio_num as u32) }
|
unsafe { gpio_pad_unhold(gpio_num as u32) }
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Esp<'a> {
|
pub struct Esp<'a> {
|
||||||
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
|
pub fs: Arc<Mutex<CriticalSectionRawMutex, Filesystem<'static, LittleFs2Filesystem>>>,
|
||||||
pub rng: Rng,
|
pub rng: Rng,
|
||||||
//first starter (ap or sta will take these)
|
//first starter (ap or sta will take these)
|
||||||
pub interface_sta: Option<WifiDevice<'static>>,
|
pub interface_sta: Option<Interface<'static>>,
|
||||||
pub interface_ap: Option<WifiDevice<'static>>,
|
pub interface_ap: Option<Interface<'static>>,
|
||||||
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
|
pub controller: Arc<Mutex<CriticalSectionRawMutex, WifiController<'static>>>,
|
||||||
|
|
||||||
pub boot_button: Input<'a>,
|
pub boot_button: Input<'a>,
|
||||||
|
|
||||||
// RTC-capable GPIO used as external wake source (store the raw peripheral)
|
// RTC-capable GPIO used as external wake source (store the raw peripheral)
|
||||||
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
pub wake_gpio1: esp_hal::peripherals::GPIO1<'static>,
|
||||||
|
pub uart0: Uart<'a, Blocking>,
|
||||||
|
|
||||||
pub ota: Ota<'static, FlashStorage>,
|
pub rtc: Rtc<'a>,
|
||||||
pub ota_target: &'static mut FlashRegion<'static, FlashStorage>,
|
|
||||||
pub current: Slot,
|
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 slot0_state: OtaImageState,
|
||||||
pub slot1_state: OtaImageState,
|
pub slot1_state: OtaImageState,
|
||||||
}
|
}
|
||||||
@@ -142,16 +120,48 @@ pub struct Esp<'a> {
|
|||||||
// CPU cores/threads, reconsider this.
|
// CPU cores/threads, reconsider this.
|
||||||
unsafe impl Send for Esp<'_> {}
|
unsafe impl Send for Esp<'_> {}
|
||||||
|
|
||||||
macro_rules! mk_static {
|
impl Esp<'_> {
|
||||||
($t:ty,$val:expr) => {{
|
pub fn get_time(&self) -> DateTime<Utc> {
|
||||||
static STATIC_CELL: static_cell::StaticCell<$t> = static_cell::StaticCell::new();
|
DateTime::from_timestamp_micros(self.rtc.current_time_us() as i64)
|
||||||
#[deny(unused_attributes)]
|
.unwrap_or(DateTime::UNIX_EPOCH)
|
||||||
let x = STATIC_CELL.uninit().write(($val));
|
|
||||||
x
|
|
||||||
}};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Esp<'_> {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
|
pub(crate) async fn delete_file(&self, filename: String) -> FatResult<()> {
|
||||||
let file = PathBuf::try_from(filename.as_str())?;
|
let file = PathBuf::try_from(filename.as_str())?;
|
||||||
let access = self.fs.lock().await;
|
let access = self.fs.lock().await;
|
||||||
@@ -218,6 +228,7 @@ impl Esp<'_> {
|
|||||||
|
|
||||||
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
pub(crate) async fn write_ota(&mut self, offset: u32, buf: &[u8]) -> Result<(), FatError> {
|
||||||
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
let _ = check_erase(self.ota_target, offset, offset + 4096);
|
||||||
|
info!("erasing and writing block 0x{offset:x}");
|
||||||
self.ota_target.erase(offset, offset + 4096)?;
|
self.ota_target.erase(offset, offset + 4096)?;
|
||||||
|
|
||||||
let mut temp = vec![0; buf.len()];
|
let mut temp = vec![0; buf.len()];
|
||||||
@@ -226,7 +237,7 @@ impl Esp<'_> {
|
|||||||
self.ota_target.write(offset, buf)?;
|
self.ota_target.write(offset, buf)?;
|
||||||
self.ota_target.read(offset, read_back)?;
|
self.ota_target.read(offset, read_back)?;
|
||||||
if buf != read_back {
|
if buf != read_back {
|
||||||
info!("Expected {:?} but got {:?}", buf, read_back);
|
info!("Expected {buf:?} but got {read_back:?}");
|
||||||
bail!(
|
bail!(
|
||||||
"Flash error, read back does not match write buffer at offset {:x}",
|
"Flash error, read back does not match write buffer at offset {:x}",
|
||||||
offset
|
offset
|
||||||
@@ -236,103 +247,49 @@ impl Esp<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
pub(crate) async fn finalize_ota(&mut self) -> Result<(), FatError> {
|
||||||
let current = self.ota.current_slot()?;
|
let current = self.ota.current_app_partition()?;
|
||||||
if self.ota.current_ota_state()? != OtaImageState::Valid {
|
if self.ota.current_ota_state()? != Valid {
|
||||||
info!(
|
info!("Validating current slot {current:?} as it was able to ota");
|
||||||
"Validating current slot {:?} as it was able to ota",
|
|
||||||
current
|
|
||||||
);
|
|
||||||
self.ota.set_current_ota_state(Valid)?;
|
self.ota.set_current_ota_state(Valid)?;
|
||||||
}
|
}
|
||||||
|
let next = match current {
|
||||||
self.ota.set_current_slot(current.next())?;
|
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");
|
info!("switched slot");
|
||||||
self.ota.set_current_ota_state(OtaImageState::New)?;
|
self.ota.set_current_ota_state(OtaImageState::New)?;
|
||||||
info!("switched state for new partition");
|
info!("switched state for new partition");
|
||||||
let state_new = self.ota.current_ota_state()?;
|
let state_new = self.ota.current_ota_state()?;
|
||||||
info!("state on new partition now {:?}", state_new);
|
info!("state on new partition now {state_new:?}");
|
||||||
//determine nextslot crc
|
|
||||||
|
|
||||||
self.set_restart_to_conf(true);
|
self.set_restart_to_conf(true);
|
||||||
Ok(())
|
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 {
|
pub(crate) fn mode_override_pressed(&mut self) -> bool {
|
||||||
self.boot_button.is_low()
|
self.boot_button.is_low()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn sntp(
|
|
||||||
&mut self,
|
|
||||||
_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).unwrap();
|
|
||||||
|
|
||||||
let context = NtpContext::new(Timestamp::default());
|
|
||||||
|
|
||||||
let ntp_addrs = stack
|
|
||||||
.dns_query(NTP_SERVER, DnsQueryType::A)
|
|
||||||
.await
|
|
||||||
.expect("Failed to resolve DNS");
|
|
||||||
if ntp_addrs.is_empty() {
|
|
||||||
bail!("Failed to resolve DNS");
|
|
||||||
}
|
|
||||||
info!("NTP server: {:?}", ntp_addrs);
|
|
||||||
|
|
||||||
let mut counter = 0;
|
|
||||||
loop {
|
|
||||||
let addr: IpAddr = ntp_addrs[0].into();
|
|
||||||
let timeout = get_time(SocketAddr::from((addr, 123)), &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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
|
pub(crate) async fn wifi_scan(&mut self) -> FatResult<Vec<AccessPointInfo>> {
|
||||||
info!("start wifi scan");
|
info!("start wifi scan");
|
||||||
let mut lock = self.controller.try_lock()?;
|
let mut lock = self.controller.try_lock()?;
|
||||||
info!("start wifi scan lock");
|
info!("start wifi scan lock");
|
||||||
let scan_config = ScanConfig {
|
let scan_config = ScanConfig::default().with_scan_type(ScanTypeConfig::Active {
|
||||||
ssid: None,
|
min: esp_hal::time::Duration::from_millis(0),
|
||||||
bssid: None,
|
max: esp_hal::time::Duration::from_millis(0),
|
||||||
channel: None,
|
});
|
||||||
show_hidden: false,
|
let rv = lock.scan_async(&scan_config).await?;
|
||||||
scan_type: ScanTypeConfig::Active {
|
|
||||||
min: Default::default(),
|
|
||||||
max: Default::default(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let rv = lock.scan_with_config_async(scan_config).await?;
|
|
||||||
info!("end wifi scan lock");
|
info!("end wifi scan lock");
|
||||||
Ok(rv)
|
Ok(rv)
|
||||||
}
|
}
|
||||||
@@ -381,235 +338,32 @@ impl Esp<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn wifi_ap(&mut self) -> FatResult<Stack<'static>> {
|
pub fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
|
||||||
let ssid = match self.load_config().await {
|
|
||||||
Ok(config) => config.network.ap_ssid.as_str().to_string(),
|
|
||||||
Err(_) => "PlantCtrl Emergency Mode".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let spawner = Spawner::for_current_executor().await;
|
|
||||||
|
|
||||||
let device = self.interface_ap.take().unwrap();
|
|
||||||
let gw_ip_addr_str = "192.168.71.1";
|
|
||||||
let gw_ip_addr = Ipv4Addr::from_str(gw_ip_addr_str).expect("failed to parse gateway ip");
|
|
||||||
|
|
||||||
let config = embassy_net::Config::ipv4_static(StaticConfigV4 {
|
|
||||||
address: Ipv4Cidr::new(gw_ip_addr, 24),
|
|
||||||
gateway: Some(gw_ip_addr),
|
|
||||||
dns_servers: Default::default(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64;
|
|
||||||
|
|
||||||
println!("init secondary stack");
|
|
||||||
// Init network stack
|
|
||||||
let (stack, runner) = embassy_net::new(
|
|
||||||
device,
|
|
||||||
config,
|
|
||||||
mk_static!(StackResources<4>, StackResources::<4>::new()),
|
|
||||||
seed,
|
|
||||||
);
|
|
||||||
let stack = mk_static!(Stack, stack);
|
|
||||||
|
|
||||||
let client_config = Configuration::AccessPoint(AccessPointConfiguration {
|
|
||||||
ssid: ssid.clone(),
|
|
||||||
..Default::default()
|
|
||||||
});
|
|
||||||
|
|
||||||
self.controller
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.set_configuration(&client_config)?;
|
|
||||||
|
|
||||||
println!("start new");
|
|
||||||
self.controller.lock().await.start()?;
|
|
||||||
println!("start net task");
|
|
||||||
spawner.spawn(net_task(runner)).ok();
|
|
||||||
println!("run dhcp");
|
|
||||||
spawner.spawn(run_dhcp(stack.clone(), gw_ip_addr_str)).ok();
|
|
||||||
|
|
||||||
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_str}/");
|
|
||||||
stack
|
|
||||||
.config_v4()
|
|
||||||
.inspect(|c| println!("ipv4 config: {c:?}"));
|
|
||||||
|
|
||||||
Ok(stack.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn wifi(
|
|
||||||
&mut self,
|
|
||||||
network_config: &NetworkConfig,
|
|
||||||
) -> FatResult<Stack<'static>> {
|
|
||||||
esp_wifi::wifi_set_log_verbose();
|
|
||||||
let ssid = network_config.ssid.clone();
|
|
||||||
match &ssid {
|
|
||||||
Some(ssid) => {
|
|
||||||
if ssid.is_empty() {
|
|
||||||
bail!("Wifi ssid was empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
bail!("Wifi ssid was empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let ssid = ssid.unwrap().to_string();
|
|
||||||
info!("attempting to connect wifi {ssid}");
|
|
||||||
let password = match network_config.password {
|
|
||||||
Some(ref password) => password.to_string(),
|
|
||||||
None => "".to_string(),
|
|
||||||
};
|
|
||||||
let max_wait = network_config.max_wait;
|
|
||||||
|
|
||||||
let spawner = Spawner::for_current_executor().await;
|
|
||||||
|
|
||||||
let device = self.interface_sta.take().unwrap();
|
|
||||||
let config = embassy_net::Config::dhcpv4(DhcpConfig::default());
|
|
||||||
|
|
||||||
let seed = (self.rng.random() as u64) << 32 | self.rng.random() as u64;
|
|
||||||
|
|
||||||
// Init network stack
|
|
||||||
let (stack, runner) = embassy_net::new(
|
|
||||||
device,
|
|
||||||
config,
|
|
||||||
mk_static!(StackResources<8>, StackResources::<8>::new()),
|
|
||||||
seed,
|
|
||||||
);
|
|
||||||
let stack = mk_static!(Stack, stack);
|
|
||||||
|
|
||||||
let client_config = Configuration::Client(ClientConfiguration {
|
|
||||||
ssid,
|
|
||||||
bssid: None,
|
|
||||||
auth_method: AuthMethod::WPA2Personal, //FIXME read from config, fill via scan
|
|
||||||
password,
|
|
||||||
channel: None,
|
|
||||||
});
|
|
||||||
self.controller
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.set_configuration(&client_config)?;
|
|
||||||
spawner.spawn(net_task(runner)).ok();
|
|
||||||
self.controller.lock().await.start_async().await?;
|
|
||||||
|
|
||||||
let timeout = {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} + max_wait as u64 * 1000;
|
|
||||||
loop {
|
|
||||||
let state = esp_wifi::wifi::sta_state();
|
|
||||||
match state {
|
|
||||||
WifiState::StaStarted => {
|
|
||||||
self.controller.lock().await.connect()?;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} > timeout
|
|
||||||
{
|
|
||||||
bail!("Timeout waiting for wifi sta ready")
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
let timeout = {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} + max_wait as u64 * 1000;
|
|
||||||
loop {
|
|
||||||
let state = esp_wifi::wifi::sta_state();
|
|
||||||
match state {
|
|
||||||
WifiState::StaConnected => {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
if {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} > timeout
|
|
||||||
{
|
|
||||||
bail!("Timeout waiting for wifi sta connected")
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
let timeout = {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} + max_wait as u64 * 1000;
|
|
||||||
while !stack.is_link_up() {
|
|
||||||
if {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} > timeout
|
|
||||||
{
|
|
||||||
bail!("Timeout waiting for wifi link up")
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
let timeout = {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} + max_wait as u64 * 1000;
|
|
||||||
while !stack.is_config_up() {
|
|
||||||
if {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} > timeout
|
|
||||||
{
|
|
||||||
bail!("Timeout waiting for wifi config up")
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(100)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Connected WIFI, dhcp: {:?}", stack.config_v4());
|
|
||||||
Ok(stack.clone())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deep_sleep(
|
|
||||||
&mut self,
|
|
||||||
duration_in_ms: u64,
|
|
||||||
mut rtc: MutexGuard<CriticalSectionRawMutex, Rtc>,
|
|
||||||
) -> ! {
|
|
||||||
// Configure and enter deep sleep using esp-hal. Also keep prior behavior where
|
|
||||||
// duration_in_ms == 0 triggers an immediate reset.
|
|
||||||
|
|
||||||
// Mark the current OTA image as valid if we reached here while in pending verify.
|
// 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 let Ok(cur) = self.ota.current_ota_state() {
|
||||||
if cur == OtaImageState::PendingVerify {
|
if cur == OtaImageState::PendingVerify {
|
||||||
self.ota
|
info!("Marking OTA image as valid");
|
||||||
.set_current_ota_state(OtaImageState::Valid)
|
if let Err(err) = self.ota.set_current_ota_state(Valid) {
|
||||||
.expect("Could not set image to valid");
|
error!("Could not set image to valid: {:?}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
info!("No OTA image to mark as valid");
|
||||||
|
}
|
||||||
|
|
||||||
if duration_in_ms == 0 {
|
if duration_in_ms == 0 {
|
||||||
software_reset();
|
software_reset();
|
||||||
} else {
|
} else {
|
||||||
///let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
|
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(duration_in_ms));
|
||||||
let timer = TimerWakeupSource::new(core::time::Duration::from_millis(5000));
|
|
||||||
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
|
let mut wake_pins: [(&mut dyn RtcPinWithResistors, WakeupLevel); 1] =
|
||||||
[(&mut self.wake_gpio1, WakeupLevel::Low)];
|
[(&mut self.wake_gpio1, WakeupLevel::Low)];
|
||||||
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
|
let ext1 = esp_hal::rtc_cntl::sleep::Ext1WakeupSource::new(&mut wake_pins);
|
||||||
rtc.sleep_deep(&[&timer, &ext1]);
|
self.rtc.sleep_deep(&[&timer, &ext1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
|
pub(crate) async fn load_config(&mut self) -> FatResult<PlantControllerConfig> {
|
||||||
let cfg = PathBuf::try_from(CONFIG_FILE)?;
|
let cfg = PathBuf::try_from(CONFIG_FILE).unwrap();
|
||||||
let config_exist = self.fs.lock().await.exists(&cfg);
|
|
||||||
if !config_exist {
|
|
||||||
bail!("No config file stored")
|
|
||||||
}
|
|
||||||
let data = self.fs.lock().await.read::<4096>(&cfg)?;
|
let data = self.fs.lock().await.read::<4096>(&cfg)?;
|
||||||
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
let config: PlantControllerConfig = serde_json::from_slice(&data)?;
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
@@ -663,333 +417,39 @@ impl Esp<'_> {
|
|||||||
} else {
|
} else {
|
||||||
RESTART_TO_CONF = 0;
|
RESTART_TO_CONF = 0;
|
||||||
}
|
}
|
||||||
|
LAST_CORROSION_PROTECTION_CHECK_DAY = -1;
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
unsafe {
|
unsafe {
|
||||||
if to_config_mode {
|
if to_config_mode {
|
||||||
RESTART_TO_CONF = 1;
|
RESTART_TO_CONF = 1;
|
||||||
}
|
}
|
||||||
LOG_ACCESS
|
log(
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(
|
|
||||||
LogMessage::RestartToConfig,
|
LogMessage::RestartToConfig,
|
||||||
RESTART_TO_CONF as u32,
|
RESTART_TO_CONF as u32,
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
)
|
);
|
||||||
.await;
|
log(
|
||||||
LOG_ACCESS
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(
|
|
||||||
LogMessage::LowVoltage,
|
LogMessage::LowVoltage,
|
||||||
LOW_VOLTAGE_DETECTED as u32,
|
LOW_VOLTAGE_DETECTED as u32,
|
||||||
0,
|
0,
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
)
|
|
||||||
.await;
|
|
||||||
for i in 0..PLANT_COUNT {
|
|
||||||
log::info!(
|
|
||||||
"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 {
|
// is executed before main, no other code will alter these values during printing
|
||||||
log::info!(
|
#[allow(static_mut_refs)]
|
||||||
"CONSECUTIVE_WATERING_PLANT[{}] = {}",
|
for (i, item) in CONSECUTIVE_WATERING_PLANT.iter().enumerate() {
|
||||||
i,
|
info!("CONSECUTIVE_WATERING_PLANT[{i}] = {item}");
|
||||||
CONSECUTIVE_WATERING_PLANT[i]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn mqtt(
|
|
||||||
&mut self,
|
|
||||||
network_config: &'static NetworkConfig,
|
|
||||||
stack: Stack<'static>,
|
|
||||||
) -> 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!("{}/state", base_topic);
|
|
||||||
let round_trip_topic = format!("{}/internal/roundtrip", base_topic);
|
|
||||||
let stay_alive_topic = format!("{}/stay_alive", base_topic);
|
|
||||||
|
|
||||||
let mut builder: McutieBuilder<'_, String, PublishDisplay<String, &str>, 0> =
|
|
||||||
McutieBuilder::new(stack, "plant ctrl", mqtt_url);
|
|
||||||
if network_config.mqtt_user.is_some() && network_config.mqtt_password.is_some() {
|
|
||||||
builder = builder.with_authentication(
|
|
||||||
network_config.mqtt_user.as_ref().unwrap().as_str(),
|
|
||||||
network_config.mqtt_password.as_ref().unwrap().as_str(),
|
|
||||||
);
|
|
||||||
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);
|
|
||||||
|
|
||||||
let spawner = Spawner::for_current_executor().await;
|
|
||||||
spawner.spawn(mqtt_incoming_task(
|
|
||||||
receiver,
|
|
||||||
round_trip_topic.clone(),
|
|
||||||
stay_alive_topic.clone(),
|
|
||||||
))?;
|
|
||||||
spawner.spawn(mqtt_runner(task))?;
|
|
||||||
|
|
||||||
LOG_ACCESS
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(LogMessage::StayAlive, 0, 0, "", &stay_alive_topic)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
LOG_ACCESS
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(LogMessage::MqttInfo, 0, 0, "", mqtt_url)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mqtt_timeout = 15000;
|
|
||||||
let timeout = {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} + mqtt_timeout as u64 * 1000;
|
|
||||||
while !MQTT_CONNECTED_EVENT_RECEIVED.load(Ordering::Relaxed) {
|
|
||||||
let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
|
|
||||||
if cur > timeout {
|
|
||||||
bail!("Timeout waiting MQTT connect event")
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Topic::General(round_trip_topic.clone())
|
|
||||||
.with_display("online_text")
|
|
||||||
.publish()
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let timeout = {
|
|
||||||
let guard = TIME_ACCESS.get().await.lock().await;
|
|
||||||
guard.current_time_us()
|
|
||||||
} + mqtt_timeout as u64 * 1000;
|
|
||||||
while !MQTT_ROUND_TRIP_RECEIVED.load(Ordering::Relaxed) {
|
|
||||||
let cur = TIME_ACCESS.get().await.lock().await.current_time_us();
|
|
||||||
if cur > timeout {
|
|
||||||
//ensure we do not further try to publish
|
|
||||||
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
|
|
||||||
bail!("Timeout waiting MQTT roundtrip")
|
|
||||||
}
|
|
||||||
Timer::after(Duration::from_millis(100)).await;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn mqtt_inner(&mut self, 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,
|
|
||||||
};
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) async fn mqtt_publish(&mut self, 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 self.mqtt_inner(subtopic, message).await {
|
|
||||||
Ok(()) => {}
|
|
||||||
Err(err) => {
|
|
||||||
info!(
|
|
||||||
"Error during mqtt send on topic {} with message {:#?} error is {:?}",
|
|
||||||
subtopic, message, err
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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_ACCESS
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(LogMessage::MqttStayAliveRec, a, 0, "", "")
|
|
||||||
.await;
|
|
||||||
MQTT_STAY_ALIVE.store(value, Ordering::Relaxed);
|
|
||||||
} else {
|
|
||||||
LOG_ACCESS
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.log(LogMessage::UnknownTopic, 0, 0, "", &*topic)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
MqttMessage::Disconnected => {
|
|
||||||
MQTT_CONNECTED_EVENT_RECEIVED.store(false, Ordering::Relaxed);
|
|
||||||
info!("Mqtt disconnected");
|
|
||||||
}
|
|
||||||
MqttMessage::HomeAssistantOnline => {
|
|
||||||
info!("Home assistant is online");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task(pool_size = 2)]
|
|
||||||
async fn net_task(mut runner: Runner<'static, WifiDevice<'static>>) {
|
|
||||||
runner.run().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[embassy_executor::task]
|
|
||||||
async fn run_dhcp(stack: Stack<'static>, gw_ip_addr: &'static str) {
|
|
||||||
use core::net::{Ipv4Addr, SocketAddrV4};
|
|
||||||
|
|
||||||
use edge_dhcp::{
|
|
||||||
io::{self, DEFAULT_SERVER_PORT},
|
|
||||||
server::{Server, ServerOptions},
|
|
||||||
};
|
|
||||||
use edge_nal::UdpBind;
|
|
||||||
use edge_nal_embassy::{Udp, UdpBuffers};
|
|
||||||
|
|
||||||
let ip = Ipv4Addr::from_str(gw_ip_addr).expect("dhcp task failed to parse gw ip");
|
|
||||||
|
|
||||||
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 = unbound_socket
|
|
||||||
.bind(SocketAddr::V4(SocketAddrV4::new(
|
|
||||||
Ipv4Addr::UNSPECIFIED,
|
|
||||||
DEFAULT_SERVER_PORT,
|
|
||||||
)))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
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| log::warn!("DHCP server error: {e:?}"));
|
|
||||||
Timer::after(Duration::from_millis(500)).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ use crate::fat_error::{FatError, FatResult};
|
|||||||
use crate::hal::esp::Esp;
|
use crate::hal::esp::Esp;
|
||||||
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
|
use crate::hal::rtc::{BackupHeader, RTCModuleInteraction};
|
||||||
use crate::hal::water::TankSensor;
|
use crate::hal::water::TankSensor;
|
||||||
use crate::hal::{BoardInteraction, FreePeripherals, Sensor, TIME_ACCESS};
|
use crate::hal::{BoardInteraction, FreePeripherals, Sensor};
|
||||||
use crate::{
|
use crate::{
|
||||||
bail,
|
bail,
|
||||||
config::PlantControllerConfig,
|
config::PlantControllerConfig,
|
||||||
hal::battery::{BatteryInteraction, NoBatteryMonitor},
|
hal::battery::{BatteryInteraction, NoBatteryMonitor},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, FixedOffset, Utc};
|
||||||
use esp_hal::gpio::{Level, Output, OutputConfig};
|
use esp_hal::gpio::{Level, Output, OutputConfig};
|
||||||
use measurements::{Current, Voltage};
|
use measurements::{Current, Voltage};
|
||||||
|
|
||||||
@@ -90,13 +90,22 @@ impl<'a> BoardInteraction<'a> for Initial<'a> {
|
|||||||
&mut self.rtc
|
&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> {
|
async fn set_charge_indicator(&mut self, _charging: bool) -> Result<(), FatError> {
|
||||||
bail!("Please configure board revision")
|
bail!("Please configure board revision")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn deep_sleep(&mut self, duration_in_ms: u64) -> ! {
|
async fn deep_sleep_ms(&mut self, duration_in_ms: u64) -> ! {
|
||||||
let rtc = TIME_ACCESS.get().await.lock().await;
|
self.esp.deep_sleep_ms(duration_in_ms);
|
||||||
self.esp.deep_sleep(duration_in_ms, rtc);
|
|
||||||
}
|
}
|
||||||
fn is_day(&self) -> bool {
|
fn is_day(&self) -> bool {
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
use crate::hal::shared_flash::MutexFlashStorage;
|
||||||
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
use embedded_storage::nor_flash::{check_erase, NorFlash, ReadNorFlash};
|
||||||
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
use esp_bootloader_esp_idf::partitions::FlashRegion;
|
||||||
use esp_storage::FlashStorage;
|
|
||||||
use littlefs2::consts::U4096 as lfsCache;
|
use littlefs2::consts::U4096 as lfsCache;
|
||||||
use littlefs2::consts::U512 as lfsLookahead;
|
use littlefs2::consts::U512 as lfsLookahead;
|
||||||
use littlefs2::driver::Storage as lfs2Storage;
|
use littlefs2::driver::Storage as lfs2Storage;
|
||||||
@@ -9,7 +9,7 @@ use littlefs2::io::Result as lfs2Result;
|
|||||||
use log::error;
|
use log::error;
|
||||||
|
|
||||||
pub struct LittleFs2Filesystem {
|
pub struct LittleFs2Filesystem {
|
||||||
pub(crate) storage: &'static mut FlashRegion<'static, FlashStorage>,
|
pub(crate) storage: &'static mut FlashRegion<'static, MutexFlashStorage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl lfs2Storage for LittleFs2Filesystem {
|
impl lfs2Storage for LittleFs2Filesystem {
|
||||||
@@ -24,7 +24,7 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
|
fn read(&mut self, off: usize, buf: &mut [u8]) -> lfs2Result<usize> {
|
||||||
let read_size: usize = Self::READ_SIZE;
|
let read_size: usize = Self::READ_SIZE;
|
||||||
if off % read_size != 0 {
|
if off % read_size != 0 {
|
||||||
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {} read_size: {}", off, read_size);
|
error!("Littlefs2Filesystem read error: offset not aligned to read size offset: {off} read_size: {read_size}");
|
||||||
return Err(lfs2Error::IO);
|
return Err(lfs2Error::IO);
|
||||||
}
|
}
|
||||||
if buf.len() % read_size != 0 {
|
if buf.len() % read_size != 0 {
|
||||||
@@ -34,7 +34,7 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
match self.storage.read(off as u32, buf) {
|
match self.storage.read(off as u32, buf) {
|
||||||
Ok(..) => Ok(buf.len()),
|
Ok(..) => Ok(buf.len()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Littlefs2Filesystem read error: {:?}", err);
|
error!("Littlefs2Filesystem read error: {err:?}");
|
||||||
Err(lfs2Error::IO)
|
Err(lfs2Error::IO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
|
fn write(&mut self, off: usize, data: &[u8]) -> lfs2Result<usize> {
|
||||||
let write_size: usize = Self::WRITE_SIZE;
|
let write_size: usize = Self::WRITE_SIZE;
|
||||||
if off % write_size != 0 {
|
if off % write_size != 0 {
|
||||||
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {} write_size: {}", off, write_size);
|
error!("Littlefs2Filesystem write error: offset not aligned to write size offset: {off} write_size: {write_size}");
|
||||||
return Err(lfs2Error::IO);
|
return Err(lfs2Error::IO);
|
||||||
}
|
}
|
||||||
if data.len() % write_size != 0 {
|
if data.len() % write_size != 0 {
|
||||||
@@ -53,7 +53,7 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
match self.storage.write(off as u32, data) {
|
match self.storage.write(off as u32, data) {
|
||||||
Ok(..) => Ok(data.len()),
|
Ok(..) => Ok(data.len()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Littlefs2Filesystem write error: {:?}", err);
|
error!("Littlefs2Filesystem write error: {err:?}");
|
||||||
Err(lfs2Error::IO)
|
Err(lfs2Error::IO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,26 +62,26 @@ impl lfs2Storage for LittleFs2Filesystem {
|
|||||||
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
|
fn erase(&mut self, off: usize, len: usize) -> lfs2Result<usize> {
|
||||||
let block_size: usize = Self::BLOCK_SIZE;
|
let block_size: usize = Self::BLOCK_SIZE;
|
||||||
if off % block_size != 0 {
|
if off % block_size != 0 {
|
||||||
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {} block_size: {}", off, block_size);
|
error!("Littlefs2Filesystem erase error: offset not aligned to block size offset: {off} block_size: {block_size}");
|
||||||
return lfs2Result::Err(lfs2Error::IO);
|
return Err(lfs2Error::IO);
|
||||||
}
|
}
|
||||||
if len % block_size != 0 {
|
if len % block_size != 0 {
|
||||||
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {} block_size: {}", len, block_size);
|
error!("Littlefs2Filesystem erase error: length not aligned to block size length: {len} block_size: {block_size}");
|
||||||
return lfs2Result::Err(lfs2Error::IO);
|
return Err(lfs2Error::IO);
|
||||||
}
|
}
|
||||||
|
|
||||||
match check_erase(self.storage, off as u32, (off + len) as u32) {
|
match check_erase(self.storage, off as u32, (off + len) as u32) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Littlefs2Filesystem check erase error: {:?}", err);
|
error!("Littlefs2Filesystem check erase error: {err:?}");
|
||||||
return lfs2Result::Err(lfs2Error::IO);
|
return Err(lfs2Error::IO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match self.storage.erase(off as u32, (off + len) as u32) {
|
match self.storage.erase(off as u32, (off + len) as u32) {
|
||||||
Ok(..) => lfs2Result::Ok(len),
|
Ok(..) => Ok(len),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Littlefs2Filesystem erase error: {:?}", err);
|
error!("Littlefs2Filesystem erase error: {err:?}");
|
||||||
lfs2Result::Err(lfs2Error::IO)
|
Err(lfs2Error::IO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user