Compare commits
21 Commits
12f3509304
...
16d1e1962f
Author | SHA1 | Date | |
---|---|---|---|
16d1e1962f | |||
daf6effe59 | |||
09ae17fa31 | |||
2aaa85af71 | |||
711306f2c1 | |||
9b40b5dfdd | |||
2f52f142b8 | |||
281ea38d04 | |||
d1cda50629 | |||
98751ffc9c | |||
7d7048b671 | |||
13a1b99cb5 | |||
eb96ce4afb | |||
25e2770d01 | |||
2a8e7db4b3 | |||
84eb469b8b | |||
3b2497cb7f | |||
3c8fdd124b | |||
d20344fe5e | |||
5058005031 | |||
282e1a01a2 |
@ -29,7 +29,6 @@ build-std = ["std", "panic_abort"]
|
|||||||
[env]
|
[env]
|
||||||
# Note: these variables are not used when using pio builder
|
# Note: these variables are not used when using pio builder
|
||||||
# Enables the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF stable (v4.4)
|
# Enables the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF stable (v4.4)
|
||||||
#ESP_IDF_VERSION = { value = "branch:release/v4.4" }
|
|
||||||
# Enables the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF master (mainline)
|
|
||||||
ESP_IDF_VERSION = { value = "branch:release/v4.4" }
|
ESP_IDF_VERSION = { value = "branch:release/v4.4" }
|
||||||
|
# Enables the esp-idf-sys "native" build feature (`cargo build --features native`) to build against ESP-IDF master (mainline)
|
||||||
|
#ESP_IDF_VERSION = { value = "master" }
|
||||||
|
45
.devcontainer/Dockerfile
Normal file
45
.devcontainer/Dockerfile
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
ARG VARIANT=bullseye
|
||||||
|
FROM debian:${VARIANT}
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
ENV LC_ALL=C.UTF-8
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
|
||||||
|
# Arguments
|
||||||
|
ARG CONTAINER_USER=esp
|
||||||
|
ARG CONTAINER_GROUP=esp
|
||||||
|
ARG TOOLCHAIN_VERSION=1.62.0.0
|
||||||
|
ARG ESP_IDF_VERSION=release/v4.4
|
||||||
|
ARG ESP_BOARD=esp32
|
||||||
|
ARG INSTALL_RUST_TOOLCHAIN=install-rust-toolchain.sh
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y git curl gcc clang ninja-build libudev-dev unzip xz-utils\
|
||||||
|
python3 python3-pip python3-venv libusb-1.0-0 libssl-dev pkg-config libtinfo5 libpython2.7 \
|
||||||
|
&& apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
|
||||||
|
|
||||||
|
# Set users
|
||||||
|
RUN adduser --disabled-password --gecos "" ${CONTAINER_USER}
|
||||||
|
USER ${CONTAINER_USER}
|
||||||
|
WORKDIR /home/${CONTAINER_USER}
|
||||||
|
|
||||||
|
# Install Rust toolchain, extra crates and esp-idf
|
||||||
|
ENV PATH=${PATH}:/home/${CONTAINER_USER}/.cargo/bin:/home/${CONTAINER_USER}/opt/bin
|
||||||
|
|
||||||
|
ADD --chown=${CONTAINER_USER}:${CONTAINER_GROUP} \
|
||||||
|
https://github.com/esp-rs/rust-build/releases/download/v${TOOLCHAIN_VERSION}/${INSTALL_RUST_TOOLCHAIN} \
|
||||||
|
/home/${CONTAINER_USER}/${INSTALL_RUST_TOOLCHAIN}
|
||||||
|
|
||||||
|
RUN chmod a+x ${INSTALL_RUST_TOOLCHAIN} \
|
||||||
|
&& ./${INSTALL_RUST_TOOLCHAIN} \
|
||||||
|
--extra-crates "ldproxy cargo-espflash wokwi-server web-flash" \
|
||||||
|
--export-file /home/${CONTAINER_USER}/export-esp.sh \
|
||||||
|
--esp-idf-version "${ESP_IDF_VERSION}" \
|
||||||
|
--minified-esp-idf "YES" \
|
||||||
|
--build-target "${ESP_BOARD}" \
|
||||||
|
&& rustup component add clippy rustfmt
|
||||||
|
|
||||||
|
# Activate ESP environment
|
||||||
|
RUN echo "source /home/${CONTAINER_USER}/export-esp.sh" >> ~/.bashrc
|
||||||
|
|
||||||
|
CMD [ "/bin/bash" ]
|
45
.devcontainer/devcontainer.json
Normal file
45
.devcontainer/devcontainer.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "esp32_c3_rust_wifi_demo",
|
||||||
|
// Select between image and build propieties to pull or build the image.
|
||||||
|
// "image": "docker.io/espressif/idf-rust:esp32c3_v4.4_1.61.0.0",
|
||||||
|
"build": {
|
||||||
|
"dockerfile": "Dockerfile",
|
||||||
|
"args": {
|
||||||
|
"CONTAINER_USER": "esp",
|
||||||
|
"CONTAINER_GROUP": "esp",
|
||||||
|
"ESP_IDF_VERSION":"release/v4.4",
|
||||||
|
"ESP_BOARD": "esp32c3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"editor.formatOnPaste": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.formatOnSaveMode": "modifications",
|
||||||
|
"editor.formatOnType": true,
|
||||||
|
"lldb.executable": "/usr/bin/lldb",
|
||||||
|
"files.watcherExclude": {
|
||||||
|
"**/target/**": true
|
||||||
|
},
|
||||||
|
"rust-analyzer.checkOnSave.command": "clippy",
|
||||||
|
"rust-analyzer.checkOnSave.allTargets": false,
|
||||||
|
"[rust]": {
|
||||||
|
"editor.defaultFormatter": "rust-lang.rust-analyzer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extensions": [
|
||||||
|
"rust-lang.rust-analyzer",
|
||||||
|
"tamasfe.even-better-toml",
|
||||||
|
"serayuzgur.crates",
|
||||||
|
"mutantdino.resourcemonitor",
|
||||||
|
"yzhang.markdown-all-in-one",
|
||||||
|
"webfreak.debug",
|
||||||
|
"actboy168.tasks"
|
||||||
|
],
|
||||||
|
"forwardPorts": [
|
||||||
|
9012,
|
||||||
|
9333,
|
||||||
|
8000
|
||||||
|
],
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/home/esp/esp32_c3_rust_wifi_demo,type=bind,consistency=cached",
|
||||||
|
"workspaceFolder": "/home/esp/esp32_c3_rust_wifi_demo"
|
||||||
|
}
|
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
|||||||
|
target
|
34
.gitpod.Dockerfile
vendored
Normal file
34
.gitpod.Dockerfile
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Note: gitpod/workspace-base image references older version of CMake, it's necessary to install newer one
|
||||||
|
FROM gitpod/workspace-base
|
||||||
|
ENV LC_ALL=C.UTF-8
|
||||||
|
ENV LANG=C.UTF-8
|
||||||
|
|
||||||
|
# ARGS
|
||||||
|
ARG CONTAINER_USER=gitpod
|
||||||
|
ARG CONTAINER_GROUP=gitpod
|
||||||
|
ARG TOOLCHAIN_VERSION=1.62.0.0
|
||||||
|
ARG ESP_IDF_VERSION="release/v4.4"
|
||||||
|
ARG ESP_BOARD=esp32c3
|
||||||
|
ARG INSTALL_RUST_TOOLCHAIN=install-rust-toolchain.sh
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN sudo install-packages git curl gcc ninja-build libudev-dev libpython2.7 \
|
||||||
|
python3 python3-pip python3-venv libusb-1.0-0 libssl-dev pkg-config libtinfo5 clang
|
||||||
|
# Set User
|
||||||
|
USER ${CONTAINER_USER}
|
||||||
|
WORKDIR /home/${CONTAINER_USER}
|
||||||
|
|
||||||
|
# Install Rust toolchain, extra crates and esp-idf
|
||||||
|
ENV PATH=${PATH}:/home/${CONTAINER_USER}/.cargo/bin:/home/${CONTAINER_USER}/opt/bin
|
||||||
|
ADD --chown=${CONTAINER_USER}:${CONTAINER_GROUP} \
|
||||||
|
https://github.com/esp-rs/rust-build/releases/download/v${TOOLCHAIN_VERSION}/${INSTALL_RUST_TOOLCHAIN} \
|
||||||
|
/home/${CONTAINER_USER}/${INSTALL_RUST_TOOLCHAIN}
|
||||||
|
RUN chmod a+x ${INSTALL_RUST_TOOLCHAIN} \
|
||||||
|
&& ./${INSTALL_RUST_TOOLCHAIN} \
|
||||||
|
--extra-crates "ldproxy cargo-espflash wokwi-server web-flash" \
|
||||||
|
--export-file /home/${CONTAINER_USER}/export-esp.sh \
|
||||||
|
--esp-idf-version "${ESP_IDF_VERSION}" \
|
||||||
|
--minified-esp-idf "YES" \
|
||||||
|
--build-target "${ESP_BOARD}" \
|
||||||
|
&& rustup component add clippy rustfmt
|
||||||
|
|
23
.gitpod.yml
Normal file
23
.gitpod.yml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
image:
|
||||||
|
file: .gitpod.Dockerfile
|
||||||
|
tasks:
|
||||||
|
- name: Setup environment variables for Rust and ESP-IDF
|
||||||
|
command: |
|
||||||
|
source /home/gitpod/export-esp.sh
|
||||||
|
vscode:
|
||||||
|
extensions:
|
||||||
|
- matklad.rust-analyzer
|
||||||
|
- tamasfe.even-better-toml
|
||||||
|
- anwar.resourcemonitor
|
||||||
|
- yzhang.markdown-all-in-one
|
||||||
|
- webfreak.debug
|
||||||
|
- actboy168.tasks
|
||||||
|
- serayuzgur.crates
|
||||||
|
ports:
|
||||||
|
- port: 9012
|
||||||
|
visibility: public
|
||||||
|
- port: 9333
|
||||||
|
visibility: public
|
||||||
|
- port: 8000
|
||||||
|
visibility: public
|
||||||
|
onOpen: open-browser
|
22
Cargo.toml
22
Cargo.toml
@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Ivan Li <ivanli2048@gmail.com>"]
|
|
||||||
edition = "2018"
|
|
||||||
name = "ups-esp32c3-rust"
|
name = "ups-esp32c3-rust"
|
||||||
|
version = "0.2.0"
|
||||||
|
authors = ["Ivan Li <ivanli2048@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
version = "0.1.0"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "s"
|
opt-level = "s"
|
||||||
@ -17,18 +17,20 @@ pio = ["esp-idf-sys/pio"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
embedded-graphics = "0.7.1"
|
embedded-hal = "0.2.7"
|
||||||
embedded-hal = "1.0.0-alpha.8"
|
|
||||||
embedded-hal-0-2 = {package = "embedded-hal", version = "0.2.7", features = ["unproven"]}
|
|
||||||
embedded-svc = "0.22.0"
|
embedded-svc = "0.22.0"
|
||||||
env_logger = "0.9.0"
|
env_logger = "0.9.0"
|
||||||
esp-idf-hal = "0.38.0"
|
esp-idf-hal = "0.38.0"
|
||||||
esp-idf-svc = "0.42.1"
|
esp-idf-svc = "0.42.1"
|
||||||
esp-idf-sys = { version = "0.31.6", features = ["binstart"] }
|
esp-idf-sys = { version = "0.31.6", features = ["binstart"] }
|
||||||
log = "0.4.17"
|
log = "0.4.17"
|
||||||
retry = "1.3.1"
|
serde = {version="1.0.144", features = ["derive"]}
|
||||||
ssd1306 = "0.7.0"
|
serde_json = "1.0.83"
|
||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.57"
|
embuild = "0.29"
|
||||||
embuild = "0.29.1"
|
anyhow = "1"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal", branch = "master" }
|
||||||
|
21
README.md
Normal file
21
README.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# UPS ESP32-C3 Rust
|
||||||
|
|
||||||
|
一个使用 Rust 语言开发的 UPS 程序,适用于 乐鑫×安信可的 ESP32-C3-32S 模块。
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] 输入电压、输出电压、电池电压检测;
|
||||||
|
- [x] Wi-Fi 联网;
|
||||||
|
- [x] 以 NTP 方式校准时间;
|
||||||
|
- [x] 以 MQTT 方式上报状态;
|
||||||
|
- [x] 提供 UPS 电源输出的控制信号;
|
||||||
|
- [ ] 提供 UPS 内置电池充电电路电源输入的控制信号;
|
||||||
|
|
||||||
|
## GPIO 定义
|
||||||
|
|
||||||
|
- `GPIO 1`:UPS 输入电压检测,使用 `ADC 1`;
|
||||||
|
- `GPIO 2`:电池电芯电压检测,使用 `ADC 1`;
|
||||||
|
- `GPIO 3`:UPS 输出电压检测,使用 `ADC 1`;
|
||||||
|
- `GPIO 4`:蜂鸣器模拟信号输出,使用 `CHANNEL 0`, `TIMER 0`;
|
||||||
|
- `GPIO 5`:工作状态指示灯信号输出;
|
||||||
|
- `GPIO 6`:UPS 输出控制信号,适用于 P-MOS 开关;
|
116
docs/README.md
Normal file
116
docs/README.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# esp32_c3_rust_wifi_demo
|
||||||
|
|
||||||
|
## Dev Containers
|
||||||
|
This repository offers Dev Containers supports for:
|
||||||
|
- [Gitpod](https://gitpod.io/)
|
||||||
|
- ["Open in Gitpod" button](https://www.gitpod.io/docs/getting-started#open-in-gitpod-button)
|
||||||
|
- [VS Code Dev Containers](https://code.visualstudio.com/docs/remote/containers#_quick-start-open-an-existing-folder-in-a-container)
|
||||||
|
- [GitHub Codespaces](https://docs.github.com/en/codespaces/developing-in-codespaces/creating-a-codespace)
|
||||||
|
> **Note**
|
||||||
|
>
|
||||||
|
> In order to use Gitpod the project needs to be published in a GitLab, GitHub,
|
||||||
|
> or Bitbucket repository.
|
||||||
|
>
|
||||||
|
> In [order to use GitHub Codespaces](https://github.com/features/codespaces#faq)
|
||||||
|
> the project needs to be published in a GitHub repository and the user needs
|
||||||
|
> to be part of the Codespaces beta or have the project under an organization.
|
||||||
|
|
||||||
|
If using VS Code or GitHub Codespaces, you can pull the image instead of building it
|
||||||
|
from the Dockerfile by selecting the `image` property instead of `build` in
|
||||||
|
`.devcontainer/devcontainer.json`. Further customization of the Dev Container can
|
||||||
|
be achived, see [.devcontainer.json reference](https://code.visualstudio.com/docs/remote/devcontainerjson-reference).
|
||||||
|
|
||||||
|
When using Dev Containers, some tooling to facilitate building, flashing and
|
||||||
|
simulating in Wokwi is also added.
|
||||||
|
### Build
|
||||||
|
- Terminal approach:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/build.sh [debug | release]
|
||||||
|
```
|
||||||
|
> If no argument is passed, `release` will be used as default
|
||||||
|
|
||||||
|
|
||||||
|
- UI approach:
|
||||||
|
|
||||||
|
The default build task is already set to build the project, and it can be used
|
||||||
|
in VS Code and Gitpod:
|
||||||
|
- From the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (`Ctrl-Shift-P` or `Cmd-Shift-P`) run the `Tasks: Run Build Task` command.
|
||||||
|
- `Terminal`-> `Run Build Task` in the menu.
|
||||||
|
- With `Ctrl-Shift-B` or `Cmd-Shift-B`.
|
||||||
|
- From the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (`Ctrl-Shift-P` or `Cmd-Shift-P`) run the `Tasks: Run Task` command and
|
||||||
|
select `Build`.
|
||||||
|
- From UI: Press `Build` on the left side of the Status Bar.
|
||||||
|
|
||||||
|
### Flash
|
||||||
|
|
||||||
|
> **Note**
|
||||||
|
>
|
||||||
|
> When using GitHub Codespaces, we need to make the ports
|
||||||
|
> public, [see instructions](https://docs.github.com/en/codespaces/developing-in-codespaces/forwarding-ports-in-your-codespace#sharing-a-port).
|
||||||
|
|
||||||
|
- Terminal approach:
|
||||||
|
- Using `flash.sh` script:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/flash.sh [debug | release]
|
||||||
|
```
|
||||||
|
> If no argument is passed, `release` will be used as default
|
||||||
|
|
||||||
|
- UI approach:
|
||||||
|
- From the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (`Ctrl-Shift-P` or `Cmd-Shift-P`) run the `Tasks: Run Task` command and
|
||||||
|
select `Build & Flash`.
|
||||||
|
- From UI: Press `Build & Flash` on the left side of the Status Bar.
|
||||||
|
- Any alternative flashing method from host machine.
|
||||||
|
|
||||||
|
|
||||||
|
### Wokwi Simulation
|
||||||
|
When using a custom Wokwi project, please change the `WOKWI_PROJECT_ID` in
|
||||||
|
`run-wokwi.sh`. If no project id is specified, a DevKit for esp32c3 will be
|
||||||
|
used.
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> ESP32-S3 is not available in Wokwi
|
||||||
|
|
||||||
|
- Terminal approach:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/run-wokwi.sh [debug | release]
|
||||||
|
```
|
||||||
|
> If no argument is passed, `release` will be used as default
|
||||||
|
|
||||||
|
- UI approach:
|
||||||
|
|
||||||
|
The default test task is already set to build the project, and it can be used
|
||||||
|
in VS Code and Gitpod:
|
||||||
|
- From the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (`Ctrl-Shift-P` or `Cmd-Shift-P`) run the `Tasks: Run Test Task` command
|
||||||
|
- With `Ctrl-Shift-,` or `Cmd-Shift-,`
|
||||||
|
> **Note**
|
||||||
|
>
|
||||||
|
> This Shortcut is not available in Gitpod by default.
|
||||||
|
- From the [Command Palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) (`Ctrl-Shift-P` or `Cmd-Shift-P`) run the `Tasks: Run Task` command and
|
||||||
|
select `Build & Run Wokwi`.
|
||||||
|
- From UI: Press `Build & Run Wokwi` on the left side of the Status Bar.
|
||||||
|
|
||||||
|
> **Warning**
|
||||||
|
>
|
||||||
|
> The simulation will pause if the browser tab is in the background.This may
|
||||||
|
> affect the execution, specially when debuging.
|
||||||
|
|
||||||
|
#### Debuging with Wokwi
|
||||||
|
|
||||||
|
Wokwi offers debugging with GDB.
|
||||||
|
|
||||||
|
- Terminal approach:
|
||||||
|
```
|
||||||
|
$HOME/.espressif/tools/riscv32-esp-elf/esp-2021r2-patch3-8.4.0/riscv32-esp-elf/bin/riscv32-esp-elf-gdb target/riscv32imc-esp-espidf/debug/esp32_c3_rust_wifi_demo -ex "target remote localhost:9333"
|
||||||
|
```
|
||||||
|
|
||||||
|
> [Wokwi Blog: List of common GDB commands for debugging.](https://blog.wokwi.com/gdb-avr-arduino-cheatsheet/?utm_source=urish&utm_medium=blog)
|
||||||
|
- UI approach:
|
||||||
|
1. Run the Wokwi Simulation in `debug` profile
|
||||||
|
2. Go to `Run and Debug` section of the IDE (`Ctrl-Shift-D or Cmd-Shift-D`)
|
||||||
|
3. Start Debugging by pressing the Play Button or pressing `F5`
|
||||||
|
4. Choose the proper user:
|
||||||
|
- `esp` when using VS Code or GitHub Codespaces
|
||||||
|
- `gitpod` when using Gitpod
|
24
scripts/build.sh
Executable file
24
scripts/build.sh
Executable file
@ -0,0 +1,24 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Gitpod and VsCode Codespaces tasks do not source the user environment
|
||||||
|
if [ "${USER}" == "gitpod" ]; then
|
||||||
|
which idf.py >/dev/null || {
|
||||||
|
source ~/export-esp.sh > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
elif [ "${CODESPACE_NAME}" != "" ]; then
|
||||||
|
which idf.py >/dev/null || {
|
||||||
|
source ~/export-esp.sh > /dev/null 2>&1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
""|"release")
|
||||||
|
cargo build --release
|
||||||
|
;;
|
||||||
|
"debug")
|
||||||
|
cargo build
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Wrong argument. Only \"debug\"/\"release\" arguments are supported"
|
||||||
|
exit 1;;
|
||||||
|
esac
|
22
scripts/flash.sh
Executable file
22
scripts/flash.sh
Executable file
@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BUILD_MODE=""
|
||||||
|
case "$1" in
|
||||||
|
""|"release")
|
||||||
|
bash scripts/build.sh
|
||||||
|
BUILD_MODE="release"
|
||||||
|
;;
|
||||||
|
"debug")
|
||||||
|
bash scripts/build.sh debug
|
||||||
|
BUILD_MODE="debug"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Wrong argument. Only \"debug\"/\"release\" arguments are supported"
|
||||||
|
exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export ESP_ARCH=riscv32imc-esp-espidf
|
||||||
|
|
||||||
|
web-flash --chip esp32c3 target/${ESP_ARCH}/${BUILD_MODE}/esp32-c3-rust-wifi-demo
|
36
scripts/run-wokwi.sh
Executable file
36
scripts/run-wokwi.sh
Executable file
@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
BUILD_MODE=""
|
||||||
|
case "$1" in
|
||||||
|
""|"release")
|
||||||
|
bash scripts/build.sh
|
||||||
|
BUILD_MODE="release"
|
||||||
|
;;
|
||||||
|
"debug")
|
||||||
|
bash scripts/build.sh debug
|
||||||
|
BUILD_MODE="debug"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Wrong argument. Only \"debug\"/\"release\" arguments are supported"
|
||||||
|
exit 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${USER}" == "gitpod" ];then
|
||||||
|
gp_url=$(gp url 9012)
|
||||||
|
echo "gp_url=${gp_url}"
|
||||||
|
export WOKWI_HOST=${gp_url:8}
|
||||||
|
elif [ "${CODESPACE_NAME}" != "" ];then
|
||||||
|
export WOKWI_HOST=${CODESPACE_NAME}-9012.githubpreview.dev
|
||||||
|
fi
|
||||||
|
|
||||||
|
export ESP_ARCH=riscv32imc-esp-espidf
|
||||||
|
|
||||||
|
# TODO: Update with your Wokwi Project
|
||||||
|
export WOKWI_PROJECT_ID=""
|
||||||
|
if [ "${WOKWI_PROJECT_ID}" == "" ]; then
|
||||||
|
wokwi-server --chip esp32c3 target/${ESP_ARCH}/${BUILD_MODE}/esp32-c3-rust-wifi-demo
|
||||||
|
else
|
||||||
|
wokwi-server --chip esp32c3 --id ${WOKWI_PROJECT_ID} target/${ESP_ARCH}/${BUILD_MODE}/esp32-c3-rust-wifi-demo
|
||||||
|
fi
|
@ -6,5 +6,5 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000
|
|||||||
#CONFIG_FREERTOS_HZ=1000
|
#CONFIG_FREERTOS_HZ=1000
|
||||||
|
|
||||||
# Workaround for https://github.com/espressif/esp-idf/issues/7631
|
# Workaround for https://github.com/espressif/esp-idf/issues/7631
|
||||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
|
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
|
||||||
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n
|
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=needs
|
||||||
|
144
src/beep.rs
144
src/beep.rs
@ -1,78 +1,124 @@
|
|||||||
use esp_idf_hal::gpio::OutputPin;
|
use embedded_svc::timer::{PeriodicTimer, TimerService};
|
||||||
use esp_idf_hal::ledc;
|
use esp_idf_hal::gpio::{Gpio4, Output};
|
||||||
use esp_idf_hal::ledc::{config::TimerConfig, Channel, Timer};
|
use esp_idf_hal::ledc::{config::TimerConfig, Channel, Timer};
|
||||||
|
use esp_idf_hal::ledc::{CHANNEL0, TIMER0};
|
||||||
use esp_idf_hal::prelude::*;
|
use esp_idf_hal::prelude::*;
|
||||||
use esp_idf_sys::EspError;
|
use esp_idf_svc::timer::{EspTimer, EspTimerService};
|
||||||
use std::thread;
|
use esp_idf_sys::{ledc_mode_t_LEDC_LOW_SPEED_MODE, ledc_set_freq, EspError};
|
||||||
|
use log::{info, warn};
|
||||||
|
use std::sync::{Arc, Mutex, MutexGuard};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
type LedcChannel<P, T, C> = Channel<C, T, Timer<T>, P>;
|
type LedcChannel<P, T, C> = Channel<C, T, Timer<T>, P>;
|
||||||
|
|
||||||
pub struct Beep<P: OutputPin, T: ledc::HwTimer, C: ledc::HwChannel> {
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
struct BeepState {
|
||||||
beat: u8,
|
beat: u8,
|
||||||
ringtone: ringtone::Type,
|
ringtone: ringtone::Type,
|
||||||
channel: LedcChannel<P, T, C>,
|
|
||||||
duty: u32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P: OutputPin, T: ledc::HwTimer, C: ledc::HwChannel> Beep<P, T, C> {
|
impl BeepState {
|
||||||
pub fn new(pin: P, timer: T, channel: C) -> Result<Self, EspError> {
|
pub fn new() -> Self {
|
||||||
let channel = Self::init_channel(pin, timer, channel)?;
|
Self {
|
||||||
|
|
||||||
let max_duty = channel.get_max_duty();
|
|
||||||
return Ok(Beep {
|
|
||||||
channel,
|
|
||||||
beat: 0,
|
beat: 0,
|
||||||
duty: max_duty * 3 / 4,
|
|
||||||
ringtone: ringtone::SILENCE,
|
ringtone: ringtone::SILENCE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_ringtone(ringtone: ringtone::Type) -> Self {
|
||||||
|
Self { beat: 0, ringtone }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type BeepChannel = Channel<CHANNEL0, TIMER0, Timer<TIMER0>, Gpio4<Output>>;
|
||||||
|
|
||||||
|
pub struct Beep {
|
||||||
|
watch_timer: Option<EspTimer>,
|
||||||
|
state: BeepState,
|
||||||
|
channel: Arc<Mutex<BeepChannel>>,
|
||||||
|
}
|
||||||
|
impl Beep {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let pin = unsafe { Gpio4::<Output>::new() }
|
||||||
|
.into_output()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Failed to set GPIO 4 as ledc output. {}", err))?;
|
||||||
|
let hw_timer = unsafe { TIMER0::new() };
|
||||||
|
let hw_channel = unsafe { CHANNEL0::new() };
|
||||||
|
let channel = Self::init_channel(pin, hw_timer, hw_channel, 1000.Hz().into())
|
||||||
|
.map_err(|err| anyhow::anyhow!("Failed to initialize channel. {}", err))?;
|
||||||
|
|
||||||
|
return anyhow::Ok(Beep {
|
||||||
|
watch_timer: None,
|
||||||
|
state: BeepState::new(),
|
||||||
|
channel: Arc::new(Mutex::new(channel)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
fn init_channel(
|
||||||
fn init_channel(pin: P, timer: T, channel: C) -> Result<LedcChannel<P, T, C>, EspError> {
|
pin: Gpio4<Output>,
|
||||||
let config = TimerConfig::default().frequency(2.kHz().into());
|
timer: TIMER0,
|
||||||
|
channel: CHANNEL0,
|
||||||
|
frequency: Hertz,
|
||||||
|
) -> Result<LedcChannel<Gpio4<Output>, TIMER0, CHANNEL0>, EspError> {
|
||||||
|
let config = TimerConfig::default().frequency(frequency);
|
||||||
let timer = Timer::new(timer, &config)?;
|
let timer = Timer::new(timer, &config)?;
|
||||||
let channel: Channel<C, T, Timer<T>, P> = Channel::new(channel, timer, pin)?;
|
let channel = Channel::new(channel, timer, pin)?;
|
||||||
return Ok(channel);
|
return Ok(channel);
|
||||||
}
|
}
|
||||||
|
pub fn play(&mut self, ringtone: ringtone::Type) -> anyhow::Result<()> {
|
||||||
pub fn play(&mut self, rx: &mut std::sync::mpsc::Receiver<ringtone::Type>) {
|
if self.state.ringtone != ringtone {
|
||||||
loop {
|
self.state = BeepState::from_ringtone(ringtone);
|
||||||
let curr_ringtone = rx.try_recv().unwrap_or_else(|_| self.ringtone);
|
info!("change: {:?}", ringtone);
|
||||||
if !curr_ringtone.eq(&mut self.ringtone) {
|
|
||||||
self.beat = 0;
|
|
||||||
self.ringtone = curr_ringtone;
|
|
||||||
}
|
|
||||||
|
|
||||||
let curr = curr_ringtone[self.beat as usize];
|
|
||||||
if curr {
|
|
||||||
self.channel.set_duty(self.duty).expect("Failed to set duty");
|
|
||||||
} else {
|
} else {
|
||||||
self.channel.set_duty(0).expect("Failed to set duty");
|
return Ok(());
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(100));
|
let state = Arc::new(Mutex::new(self.state));
|
||||||
|
let channel = self.channel.to_owned();
|
||||||
|
let mut timer = EspTimerService::new()?
|
||||||
|
.timer(move || match state.lock().as_mut() {
|
||||||
|
Ok(state) => {
|
||||||
|
if let Err(err) = Self::play_once(state, channel.lock().unwrap()) {
|
||||||
|
warn!("{}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to lock state. {}", err);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|err| anyhow::anyhow!("Init Timer Failed. {}", err))?;
|
||||||
|
timer.every(Duration::from_millis(250))?;
|
||||||
|
|
||||||
self.beat += 1;
|
self.watch_timer = Some(timer);
|
||||||
if self.beat == 16 {
|
return anyhow::Ok(());
|
||||||
self.beat = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn play_once(
|
||||||
|
state: &mut BeepState,
|
||||||
|
mut channel: MutexGuard<BeepChannel>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if state.ringtone.len() <= state.beat as usize {
|
||||||
|
state.beat = 0;
|
||||||
}
|
}
|
||||||
|
let curr = state.ringtone[state.beat as usize];
|
||||||
|
if curr == 0 {
|
||||||
|
channel.set_duty(0).expect("Failed to set duty");
|
||||||
|
} else {
|
||||||
|
unsafe { ledc_set_freq(ledc_mode_t_LEDC_LOW_SPEED_MODE, 0, curr); }
|
||||||
|
channel.set_duty(60).expect("Failed to set duty");
|
||||||
|
}
|
||||||
|
|
||||||
|
state.beat += 1;
|
||||||
|
if state.beat == 16 {
|
||||||
|
state.beat = 0;
|
||||||
|
}
|
||||||
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub mod ringtone {
|
pub mod ringtone {
|
||||||
|
pub type Type = [u32; 16];
|
||||||
pub type Type = [bool; 16];
|
pub const ADAPTER_DOWN: Type = [2300, 2000, 2250, 1950, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||||
|
|
||||||
pub const ADAPTER_DOWN: Type = [
|
|
||||||
true, true, true, true, false, false, false, false, false, false, false, false, false,
|
|
||||||
false, false, false,
|
|
||||||
];
|
|
||||||
pub const BATTERY_LOW: Type = [
|
pub const BATTERY_LOW: Type = [
|
||||||
true, true, false, false, true, true, false, false, true, true, false, false, true, true,
|
2000, 1950, 0, 0, 1980, 1900, 0, 0, 2000, 1900, 0, 0, 1980, 1900, 0, 0,
|
||||||
false, false,
|
|
||||||
];
|
];
|
||||||
pub const SILENCE: Type = [false; 16];
|
pub const SILENCE: Type = [0; 16];
|
||||||
pub const SHUTDOWN: Type = [
|
pub const SHUTDOWN: Type = [
|
||||||
true, false, true, true, true, true, false, true, true, true, true, true, false, false,
|
3450, 3500, 0, 3500, 3050, 3000, 0, 3000, 3050, 1000, 1000, 1000, 0, 0, 0, 0,
|
||||||
false, false,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use embedded_hal::digital::blocking::OutputPin;
|
use embedded_hal::digital::v2::OutputPin;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@ -32,16 +32,16 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play(&mut self) {
|
pub fn play(&mut self) where <T as embedded_hal::digital::v2::OutputPin>::Error: std::fmt::Debug {
|
||||||
loop {
|
loop {
|
||||||
if self.stopped {
|
if self.stopped {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
self.toggle().unwrap();
|
self.toggle().unwrap();
|
||||||
thread::sleep(Duration::from_millis(50));
|
thread::sleep(Duration::from_millis(10));
|
||||||
|
|
||||||
self.toggle().unwrap();
|
self.toggle().unwrap();
|
||||||
thread::sleep(Duration::from_millis(950));
|
thread::sleep(Duration::from_millis(990));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
187
src/charge_controller.rs
Normal file
187
src/charge_controller.rs
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
use std::{
|
||||||
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use embedded_hal::digital::v2::{OutputPin, PinState};
|
||||||
|
use embedded_svc::event_bus::{EventBus, Postbox};
|
||||||
|
use esp_idf_hal::gpio::{Gpio7, Output};
|
||||||
|
use esp_idf_svc::eventloop::{
|
||||||
|
Background, EspBackgroundEventLoop, EspEventFetchData, EspEventLoop, EspEventPostData,
|
||||||
|
EspSubscription, EspTypedEventDeserializer, EspTypedEventSerializer, EspTypedEventSource, User,
|
||||||
|
};
|
||||||
|
use esp_idf_sys::c_types;
|
||||||
|
use log::warn;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
time::Time,
|
||||||
|
voltage_detection::{VoltageDetectionWorker, VOLTAGE_EVENTLOOP},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub static mut CHARGE_STATE_EVENT_LOOP: Option<
|
||||||
|
EspEventLoop<esp_idf_svc::eventloop::User<Background>>,
|
||||||
|
> = None;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
pub enum ChargeStatus {
|
||||||
|
Charging,
|
||||||
|
Charged,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ChargeControllerState {
|
||||||
|
pub status: ChargeStatus,
|
||||||
|
pub pin_state: PinState,
|
||||||
|
pub charge_deadline_at: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChargeControllerState {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
status: ChargeStatus::Charging,
|
||||||
|
pin_state: PinState::Low,
|
||||||
|
charge_deadline_at: Duration::ZERO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> String {
|
||||||
|
let status = match self.status {
|
||||||
|
ChargeStatus::Charging => "Charging",
|
||||||
|
ChargeStatus::Charged => "Charged",
|
||||||
|
};
|
||||||
|
let pin_state = match self.pin_state {
|
||||||
|
PinState::Low => "Low",
|
||||||
|
PinState::High => "High",
|
||||||
|
};
|
||||||
|
let now = Time::new().get_time();
|
||||||
|
let charging_count_down = if now > self.charge_deadline_at {
|
||||||
|
-1i64
|
||||||
|
} else {
|
||||||
|
(self.charge_deadline_at - now).as_secs() as i64
|
||||||
|
};
|
||||||
|
json!({ "status": status, "pin_state": pin_state, "charging_count_down": charging_count_down}).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventSource for ChargeControllerState {
|
||||||
|
fn source() -> *const c_types::c_char {
|
||||||
|
b"Charge\0".as_ptr() as *const _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventSerializer<ChargeControllerState> for ChargeControllerState {
|
||||||
|
fn serialize<R>(
|
||||||
|
event: &ChargeControllerState,
|
||||||
|
f: impl for<'a> FnOnce(&'a EspEventPostData) -> R,
|
||||||
|
) -> R {
|
||||||
|
f(&unsafe { EspEventPostData::new(Self::source(), Self::event_id(), event) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventDeserializer<ChargeControllerState> for ChargeControllerState {
|
||||||
|
fn deserialize<R>(
|
||||||
|
data: &EspEventFetchData,
|
||||||
|
f: &mut impl for<'a> FnMut(&'a ChargeControllerState) -> R,
|
||||||
|
) -> R {
|
||||||
|
f(unsafe { data.as_payload() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChargeCtlPin = Gpio7<Output>;
|
||||||
|
|
||||||
|
pub struct ChargeController {
|
||||||
|
pub state: ChargeControllerState,
|
||||||
|
voltage_subscription: Option<EspSubscription<User<Background>>>,
|
||||||
|
pin: Arc<Mutex<ChargeCtlPin>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChargeController {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let pin = unsafe { Gpio7::<Output>::new() }
|
||||||
|
.into_output()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Make Gpio6 Into output Failed. {}", err))?;
|
||||||
|
|
||||||
|
match EspBackgroundEventLoop::new(&Default::default()) {
|
||||||
|
Ok(eventloop) => unsafe { CHARGE_STATE_EVENT_LOOP = Some(eventloop) },
|
||||||
|
Err(err) => anyhow::bail!("Init Event Loop failed. {:?}", err),
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(Self {
|
||||||
|
state: ChargeControllerState::new(),
|
||||||
|
voltage_subscription: None,
|
||||||
|
pin: Arc::new(Mutex::new(pin)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch(&mut self) -> anyhow::Result<()> {
|
||||||
|
let mut state = self.state.to_owned();
|
||||||
|
let pin = self.pin.to_owned();
|
||||||
|
|
||||||
|
if let Some(event_loop) = unsafe { VOLTAGE_EVENTLOOP.as_mut() } {
|
||||||
|
let voltage_subscription = event_loop
|
||||||
|
.subscribe(move |obj: &VoltageDetectionWorker| {
|
||||||
|
match state.status {
|
||||||
|
ChargeStatus::Charging => {
|
||||||
|
if obj.battery_voltage < 12600 {
|
||||||
|
state.status = ChargeStatus::Charging;
|
||||||
|
} else {
|
||||||
|
let now = Time::new().get_time();
|
||||||
|
if state.charge_deadline_at == Duration::ZERO {
|
||||||
|
state.charge_deadline_at = now + Duration::from_secs(600);
|
||||||
|
} else if now > state.charge_deadline_at {
|
||||||
|
state.status = ChargeStatus::Charged;
|
||||||
|
} else {
|
||||||
|
state.status = ChargeStatus::Charging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChargeStatus::Charged => {
|
||||||
|
if obj.battery_voltage < 10500 {
|
||||||
|
state.status = ChargeStatus::Charging;
|
||||||
|
} else {
|
||||||
|
state.status = ChargeStatus::Charged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match pin.lock() {
|
||||||
|
Ok(pin) => {
|
||||||
|
if let Err(err) = Self::output_ctl(&mut state, pin) {
|
||||||
|
warn!("Put Control Pin State Failed. {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(event_loop) = unsafe { CHARGE_STATE_EVENT_LOOP.as_mut() } {
|
||||||
|
if let Err(err) = event_loop.post(&state, None) {
|
||||||
|
warn!("Post DC Out Status Failed. {}", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("CHARGE_STATE_EVENT_LOOP is None");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|err| anyhow::anyhow!("Subscribe Voltage Failed. {}", err))?;
|
||||||
|
self.voltage_subscription = Some(voltage_subscription);
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Voltage Event Loop is None");
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
fn output_ctl(
|
||||||
|
state: &mut ChargeControllerState,
|
||||||
|
mut pin: MutexGuard<ChargeCtlPin>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if ChargeStatus::Charging == state.status {
|
||||||
|
pin.set_high()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Set DC Output Control Pin High Failed. {}", err))?;
|
||||||
|
state.pin_state = PinState::High;
|
||||||
|
} else if ChargeStatus::Charged == state.status {
|
||||||
|
pin.set_low()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Set DC Output Control Pin Low Failed. {}", err))?;
|
||||||
|
state.pin_state = PinState::Low;
|
||||||
|
}
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
}
|
@ -1,181 +1,251 @@
|
|||||||
use anyhow::{Ok, Result, anyhow};
|
|
||||||
use retry;
|
|
||||||
use std::{
|
use std::{
|
||||||
sync::{
|
sync::{Arc, Mutex, MutexGuard},
|
||||||
mpsc,
|
time::Duration,
|
||||||
Arc, Mutex,
|
|
||||||
},
|
|
||||||
thread,
|
|
||||||
time::{self, SystemTime},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use embedded_hal::digital::blocking::OutputPin;
|
use embedded_hal::digital::v2::{OutputPin, PinState};
|
||||||
use log::*;
|
use embedded_svc::event_bus::{EventBus, Postbox};
|
||||||
|
use esp_idf_hal::gpio::{Gpio6, Output};
|
||||||
|
use esp_idf_svc::eventloop::{
|
||||||
|
Background, EspBackgroundEventLoop, EspEventFetchData, EspEventLoop, EspEventPostData,
|
||||||
|
EspSubscription, EspTypedEventDeserializer, EspTypedEventSerializer, EspTypedEventSource, User,
|
||||||
|
};
|
||||||
|
use esp_idf_sys::c_types;
|
||||||
|
use log::warn;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
time::Time,
|
||||||
|
voltage_detection::{VoltageDetectionWorker, VOLTAGE_EVENTLOOP},
|
||||||
|
};
|
||||||
|
|
||||||
|
const WAITING_OFF_DURATION: u64 = 60;
|
||||||
|
const WAITING_ON_DURATION: u64 = 60;
|
||||||
|
|
||||||
|
pub static mut DC_OUT_STATE_EVENT_LOOP: Option<
|
||||||
|
EspEventLoop<esp_idf_svc::eventloop::User<Background>>,
|
||||||
|
> = None;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
pub enum DcOutStatus {
|
pub enum DcOutStatus {
|
||||||
|
WaitingOn(Duration),
|
||||||
On,
|
On,
|
||||||
Off,
|
Off,
|
||||||
TurningOn(mpsc::Receiver<bool>),
|
WaitingOff,
|
||||||
TurningOff(mpsc::Receiver<bool>),
|
TurningOff(Duration),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DcOutStatus {
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub fn is_on(&self) -> bool {
|
pub struct DcOutControllerState {
|
||||||
match self {
|
pub status: DcOutStatus,
|
||||||
DcOutStatus::On => true,
|
pub pin_state: PinState,
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn is_off(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
DcOutStatus::Off => true,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DcOutController<P>
|
impl DcOutControllerState {
|
||||||
where
|
pub fn new() -> Self {
|
||||||
P: OutputPin + Send,
|
Self {
|
||||||
{
|
|
||||||
pin: Arc<Mutex<P>>,
|
|
||||||
shutdown_tx: Option<mpsc::Sender<bool>>,
|
|
||||||
status: DcOutStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<P> DcOutController<P>
|
|
||||||
where
|
|
||||||
P: OutputPin + Send,
|
|
||||||
P: 'static,
|
|
||||||
P: std::marker::Sync,
|
|
||||||
{
|
|
||||||
pub fn new(pin: P) -> Self {
|
|
||||||
return Self {
|
|
||||||
status: DcOutStatus::On,
|
status: DcOutStatus::On,
|
||||||
|
pin_state: PinState::Low,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_adapter_down(&mut self) {
|
||||||
|
match self.status {
|
||||||
|
DcOutStatus::On => {
|
||||||
|
self.status = DcOutStatus::WaitingOff;
|
||||||
|
}
|
||||||
|
DcOutStatus::WaitingOn(_) => {
|
||||||
|
self.status = DcOutStatus::Off;
|
||||||
|
}
|
||||||
|
DcOutStatus::TurningOff(target) => {
|
||||||
|
let now = Time::new().get_time();
|
||||||
|
if now > target {
|
||||||
|
self.status = DcOutStatus::Off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn turn_off(&mut self) {
|
||||||
|
let now = Time::new().get_time();
|
||||||
|
match self.status {
|
||||||
|
DcOutStatus::On => {
|
||||||
|
self.status =
|
||||||
|
DcOutStatus::TurningOff(now + Duration::from_secs(WAITING_OFF_DURATION));
|
||||||
|
}
|
||||||
|
DcOutStatus::WaitingOff => {
|
||||||
|
self.status =
|
||||||
|
DcOutStatus::TurningOff(now + Duration::from_secs(WAITING_OFF_DURATION));
|
||||||
|
}
|
||||||
|
DcOutStatus::WaitingOn(_) => {
|
||||||
|
self.status = DcOutStatus::Off;
|
||||||
|
}
|
||||||
|
DcOutStatus::TurningOff(target) => {
|
||||||
|
if target < now {
|
||||||
|
self.status = DcOutStatus::Off;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn turn_on(&mut self) {
|
||||||
|
let now = Time::new().get_time();
|
||||||
|
match self.status {
|
||||||
|
DcOutStatus::WaitingOff => {
|
||||||
|
self.status = DcOutStatus::On;
|
||||||
|
}
|
||||||
|
DcOutStatus::Off => {
|
||||||
|
self.status =
|
||||||
|
DcOutStatus::WaitingOn(now + Duration::from_secs(WAITING_ON_DURATION));
|
||||||
|
}
|
||||||
|
DcOutStatus::WaitingOn(target) => {
|
||||||
|
if target <= now {
|
||||||
|
self.status = DcOutStatus::On;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DcOutStatus::TurningOff(target) => {
|
||||||
|
if target <= now {
|
||||||
|
self.status = DcOutStatus::On;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_json(&self) -> String {
|
||||||
|
let status = match self.status {
|
||||||
|
DcOutStatus::WaitingOn(_) => "WaitingOn",
|
||||||
|
DcOutStatus::On => "On",
|
||||||
|
DcOutStatus::Off => "Off",
|
||||||
|
DcOutStatus::WaitingOff => "WaitingOff",
|
||||||
|
DcOutStatus::TurningOff(_) => "TurningOff",
|
||||||
|
};
|
||||||
|
let pin_state = match self.pin_state {
|
||||||
|
PinState::Low => "Low",
|
||||||
|
PinState::High => "High",
|
||||||
|
};
|
||||||
|
let now = Time::new().get_time();
|
||||||
|
let target = match self.status {
|
||||||
|
DcOutStatus::WaitingOn(target) => target,
|
||||||
|
DcOutStatus::TurningOff(target) => target,
|
||||||
|
_ => Duration::ZERO,
|
||||||
|
};
|
||||||
|
let seconds = if now < target {
|
||||||
|
target - now
|
||||||
|
} else {
|
||||||
|
Duration::ZERO
|
||||||
|
}
|
||||||
|
.as_secs();
|
||||||
|
json!({ "status": status, "pin_state": pin_state, "seconds": seconds }).to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventSource for DcOutControllerState {
|
||||||
|
fn source() -> *const c_types::c_char {
|
||||||
|
b"DcOut\0".as_ptr() as *const _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventSerializer<DcOutControllerState> for DcOutControllerState {
|
||||||
|
fn serialize<R>(
|
||||||
|
event: &DcOutControllerState,
|
||||||
|
f: impl for<'a> FnOnce(&'a EspEventPostData) -> R,
|
||||||
|
) -> R {
|
||||||
|
f(&unsafe { EspEventPostData::new(Self::source(), Self::event_id(), event) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventDeserializer<DcOutControllerState> for DcOutControllerState {
|
||||||
|
fn deserialize<R>(
|
||||||
|
data: &EspEventFetchData,
|
||||||
|
f: &mut impl for<'a> FnMut(&'a DcOutControllerState) -> R,
|
||||||
|
) -> R {
|
||||||
|
f(unsafe { data.as_payload() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type DcOutPin = Gpio6<Output>;
|
||||||
|
|
||||||
|
pub struct DcOutController {
|
||||||
|
pub state: DcOutControllerState,
|
||||||
|
voltage_subscription: Option<EspSubscription<User<Background>>>,
|
||||||
|
pin: Arc<Mutex<DcOutPin>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DcOutController {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let pin = unsafe { Gpio6::<Output>::new() }
|
||||||
|
.into_output()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Make Gpio6 Into output Failed. {}", err))?;
|
||||||
|
|
||||||
|
match EspBackgroundEventLoop::new(&Default::default()) {
|
||||||
|
Ok(eventloop) => unsafe { DC_OUT_STATE_EVENT_LOOP = Some(eventloop) },
|
||||||
|
Err(err) => anyhow::bail!("Init Event Loop failed. {:?}", err),
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(Self {
|
||||||
|
state: DcOutControllerState::new(),
|
||||||
|
voltage_subscription: None,
|
||||||
pin: Arc::new(Mutex::new(pin)),
|
pin: Arc::new(Mutex::new(pin)),
|
||||||
shutdown_tx: None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn open(&mut self) -> Result<()> {
|
|
||||||
if self.status.is_on() {
|
|
||||||
trace!("DC OUT already on, skipping");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let pin = self.pin.clone();
|
|
||||||
let mut pin = retry::retry(retry::delay::Fixed::from_millis(100).take(10), || {
|
|
||||||
pin.lock()
|
|
||||||
})
|
})
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to lock pin"))?;
|
}
|
||||||
|
|
||||||
retry::retry(retry::delay::Fixed::from_millis(100).take(10), || {
|
pub fn watch(&mut self) -> anyhow::Result<()> {
|
||||||
|
let mut state = self.state.to_owned();
|
||||||
|
let pin = self.pin.to_owned();
|
||||||
|
|
||||||
|
if let Some(event_loop) = unsafe { VOLTAGE_EVENTLOOP.as_mut() } {
|
||||||
|
let voltage_subscription = event_loop
|
||||||
|
.subscribe(move |obj: &VoltageDetectionWorker| {
|
||||||
|
if obj.adapter_voltage < 1000 {
|
||||||
|
if obj.battery_voltage < 1000 {
|
||||||
|
state.turn_off();
|
||||||
|
} else {
|
||||||
|
state.handle_adapter_down();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.turn_on();
|
||||||
|
}
|
||||||
|
|
||||||
|
match pin.lock() {
|
||||||
|
Ok(pin) => {
|
||||||
|
if let Err(err) = Self::output_ctl(&mut state, pin) {
|
||||||
|
warn!("Put Control Pin State Failed. {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => todo!(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(event_loop) = unsafe { DC_OUT_STATE_EVENT_LOOP.as_mut() } {
|
||||||
|
if let Err(err) = event_loop.post(&state, None) {
|
||||||
|
warn!("Post DC Out Status Failed. {}", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("DC_OUT_STATE_EVENT_LOOP is None");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|err| anyhow::anyhow!("Subscribe Voltage Failed. {}", err))?;
|
||||||
|
self.voltage_subscription = Some(voltage_subscription);
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Voltage Event Loop is None");
|
||||||
|
}
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
fn output_ctl(
|
||||||
|
state: &mut DcOutControllerState,
|
||||||
|
mut pin: MutexGuard<DcOutPin>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if DcOutStatus::Off == state.status {
|
||||||
|
pin.set_high()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Set DC Output Control Pin High Failed. {}", err))?;
|
||||||
|
state.pin_state = PinState::High;
|
||||||
|
} else if DcOutStatus::On == state.status {
|
||||||
pin.set_low()
|
pin.set_low()
|
||||||
})
|
.map_err(|err| anyhow::anyhow!("Set DC Output Control Pin Low Failed. {}", err))?;
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to lock pin"))?;
|
state.pin_state = PinState::Low;
|
||||||
|
|
||||||
self.status = DcOutStatus::On;
|
|
||||||
info!("DC OUT ON");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shutdown(&mut self) {
|
|
||||||
if self.shutdown_tx.is_some() {
|
|
||||||
trace!("Shutdown task already running, skipping...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Shutdown task started...");
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
self.shutdown_tx = Some(tx);
|
|
||||||
|
|
||||||
let pin = Arc::clone(&self.pin);
|
|
||||||
|
|
||||||
let (status_tx, status_rx) = mpsc::channel();
|
|
||||||
self.status = DcOutStatus::TurningOff(status_rx);
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
// Wait for computer shutdown finished
|
|
||||||
let target_at = SystemTime::now() + time::Duration::from_secs(10);
|
|
||||||
loop {
|
|
||||||
if rx.try_recv().is_ok_and(|state| !*state) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if target_at > SystemTime::now() {
|
|
||||||
thread::sleep(time::Duration::from_millis(1000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Shutdown timeout, force shutdown...");
|
|
||||||
|
|
||||||
let result = || {
|
|
||||||
retry::retry(retry::delay::Fixed::from_millis(1000).take(10), || {
|
|
||||||
return pin
|
|
||||||
.lock()
|
|
||||||
.map_err(|_| anyhow::anyhow!("Can not lock pin"))?
|
|
||||||
.set_high()
|
|
||||||
.map_err(|_| anyhow::anyhow!("Can not shutdown"));
|
|
||||||
}).map_err(|_| anyhow!("Failed to send shutdown status"))?;
|
|
||||||
status_tx.send(true).map_err(|_| anyhow!("Failed to send shutdown status"))?;
|
|
||||||
Ok(())
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = result() {
|
|
||||||
warn!("Failed to shutdown: {}", e);
|
|
||||||
} else {
|
|
||||||
info!("Shutdown finished");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop_shutdown(&mut self) -> Result<()> {
|
|
||||||
if let Some(tx) = self.shutdown_tx.as_mut() {
|
|
||||||
info!("Shutdown task stopped...");
|
|
||||||
if retry::retry(retry::delay::Fixed::from_millis(100).take(5), || {
|
|
||||||
tx.send(false)
|
|
||||||
})
|
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to stop shutdown"))
|
|
||||||
.is_err() {
|
|
||||||
warn!("Message to shutdown task was not sent");
|
|
||||||
} else {
|
|
||||||
info!("Shutdown task stopped");
|
|
||||||
}
|
|
||||||
self.shutdown_tx = None;
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_status(&mut self) -> DcOutStatus {
|
|
||||||
match &self.status {
|
|
||||||
DcOutStatus::TurningOn(rx) => {
|
|
||||||
rx.try_recv().map_or((), |state| {
|
|
||||||
trace!("DcOutStatus::TurningOn({})", state);
|
|
||||||
if state {
|
|
||||||
self.status = DcOutStatus::On;
|
|
||||||
} else {
|
|
||||||
self.status = DcOutStatus::Off;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
DcOutStatus::TurningOff(rx) => {
|
|
||||||
rx.try_recv().map_or((), |state| {
|
|
||||||
trace!("DcOutStatus::TurningOff({})", state);
|
|
||||||
if state {
|
|
||||||
self.status = DcOutStatus::Off;
|
|
||||||
} else {
|
|
||||||
self.status = DcOutStatus::On;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
},
|
|
||||||
_default => {}
|
|
||||||
}
|
|
||||||
match &self.status {
|
|
||||||
DcOutStatus::On => DcOutStatus::On,
|
|
||||||
DcOutStatus::Off => DcOutStatus::Off,
|
|
||||||
_ => DcOutStatus::On,
|
|
||||||
}
|
}
|
||||||
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
215
src/main.rs
215
src/main.rs
@ -1,89 +1,188 @@
|
|||||||
#![feature(is_some_with)]
|
use embedded_svc::event_bus::{EventBus};
|
||||||
use esp_idf_sys as _;
|
use esp_idf_svc::eventloop::{EspBackgroundEventLoop};
|
||||||
use log::{error, info};
|
use esp_idf_sys::{self as _};
|
||||||
use std::{thread, time::Duration, sync::mpsc, env};
|
use log::*;
|
||||||
|
use std::{
|
||||||
use crate::wifi::WiFi;
|
env,
|
||||||
|
thread::{self, sleep},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
mod beep;
|
mod beep;
|
||||||
mod blink;
|
mod blink;
|
||||||
mod dc_out_controller;
|
mod dc_out_controller;
|
||||||
mod manager;
|
mod message_queue;
|
||||||
mod screen;
|
mod time;
|
||||||
|
mod voltage_detection;
|
||||||
mod wifi;
|
mod wifi;
|
||||||
|
mod charge_controller;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
beep::{ringtone, Beep},
|
||||||
|
dc_out_controller::{DcOutController, DcOutControllerState, DC_OUT_STATE_EVENT_LOOP},
|
||||||
|
message_queue::MqDto,
|
||||||
|
voltage_detection::{VoltageDetectionWorker, VOLTAGE_EVENTLOOP}, charge_controller::{CHARGE_STATE_EVENT_LOOP, ChargeControllerState, ChargeController},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
message_queue::MessageQueue, time::Time, voltage_detection::VoltageDetection, wifi::Internet,
|
||||||
|
};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
env::set_var("DEFMT_LOG", "trace");
|
env::set_var("DEFMT_LOG", "trace");
|
||||||
|
env::set_var("RUST_BACKTRACE", "1");
|
||||||
env::set_var("RUST_LOG", "trace");
|
env::set_var("RUST_LOG", "trace");
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
// Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once,
|
// Temporary. Will disappear once ESP-IDF 4.4 is released, but for now it is necessary to call this function once,
|
||||||
// or else some patches to the runtime implemented by esp-idf-sys might not link properly.
|
// or else some patches to the runtime implemented by esp-idf-sys might not link properly.
|
||||||
esp_idf_sys::link_patches();
|
esp_idf_sys::link_patches();
|
||||||
|
|
||||||
|
info!("Hello, world!");
|
||||||
|
|
||||||
let peripherals = esp_idf_hal::peripherals::Peripherals::take().unwrap();
|
let peripherals = esp_idf_hal::peripherals::Peripherals::take().unwrap();
|
||||||
|
|
||||||
let blink_pin = peripherals.pins.gpio5;
|
let blink_pin = peripherals.pins.gpio5;
|
||||||
let beep_pin = peripherals.pins.gpio6;
|
|
||||||
let ledc_timer0 = peripherals.ledc.timer0;
|
|
||||||
let ledc_channel0 = peripherals.ledc.channel0;
|
|
||||||
let dc_out_ctl_pin = peripherals.pins.gpio3;
|
|
||||||
let i2c0 = peripherals.i2c0;
|
|
||||||
let sda_pin = peripherals.pins.gpio4;
|
|
||||||
let scl_pin = peripherals.pins.gpio10;
|
|
||||||
|
|
||||||
let adc1 = peripherals.adc1;
|
match EspBackgroundEventLoop::new(&Default::default()) {
|
||||||
let adapter_pin = peripherals.pins.gpio1;
|
Ok(eventloop) => unsafe { VOLTAGE_EVENTLOOP = Some(eventloop) },
|
||||||
let battery_pin = peripherals.pins.gpio2;
|
Err(err) => error!("Init Event Loop failed. {:?}", err),
|
||||||
|
};
|
||||||
let (tx, mut rx) = mpsc::channel();
|
|
||||||
|
|
||||||
info!("Starting");
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut blink =
|
let mut blink = blink::Blink::new(
|
||||||
blink::Blink::new(blink_pin.into_output().expect("Failed to set GPIO5 as output"));
|
blink_pin
|
||||||
|
.into_output()
|
||||||
|
.expect("Failed to set GPIO5 as output"),
|
||||||
|
);
|
||||||
blink.play();
|
blink.play();
|
||||||
});
|
});
|
||||||
|
|
||||||
thread::spawn(move || {
|
let voltage_detection = VoltageDetection::new();
|
||||||
thread::sleep(Duration::from_millis(5000));
|
|
||||||
|
voltage_detection.unwrap()
|
||||||
|
.watching()
|
||||||
|
.expect("Can not watch voltages.");
|
||||||
|
|
||||||
|
let _wifi = Internet::new().unwrap();
|
||||||
|
|
||||||
|
let mut time = Time::new();
|
||||||
|
time.sync().unwrap();
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
let mut beep = Beep::new().unwrap();
|
||||||
|
|
||||||
|
let mut dc_out_controller =
|
||||||
|
DcOutController::new().expect("Can not get DcOutController instance");
|
||||||
|
dc_out_controller
|
||||||
|
.watch()
|
||||||
|
.expect("Can not watch for dc_out_controller");
|
||||||
|
|
||||||
|
let mut charge_controller = ChargeController::new().expect("Can not get ChargeController instance");
|
||||||
|
charge_controller
|
||||||
|
.watch()
|
||||||
|
.expect("Can not watch for charge_controller");
|
||||||
|
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
|
||||||
|
let mut _mq = MessageQueue::new();
|
||||||
|
|
||||||
|
let _mq_subscription;
|
||||||
|
match _mq.watch() {
|
||||||
|
Err(err) => {
|
||||||
|
error!("Can not watch MessageQueue. {}", err);
|
||||||
|
}
|
||||||
|
Ok(subscription) => _mq_subscription = subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
let _mq_tx_for_voltage = _mq.tx.clone();
|
||||||
|
let _mq_tx_for_dc_out_state = _mq.tx.clone();
|
||||||
|
let _mq_tx_for_charge_state = _mq.tx.clone();
|
||||||
|
|
||||||
|
let _voltage_subscription;
|
||||||
|
if let Some(voltage_event_loop) = unsafe { VOLTAGE_EVENTLOOP.as_mut() } {
|
||||||
|
_voltage_subscription = voltage_event_loop
|
||||||
|
.subscribe(move |message: &VoltageDetectionWorker| {
|
||||||
|
if let Ok(json_str) = serde_json::to_string(&message) {
|
||||||
|
match _mq_tx_for_voltage.lock() {
|
||||||
|
Ok(tx) => {
|
||||||
|
let result = tx.send(MqDto {
|
||||||
|
topic: "voltage".to_string(),
|
||||||
|
message: json_str,
|
||||||
|
});
|
||||||
|
if let Err(err) = result {
|
||||||
|
warn!("send voltage to mq message failed. {}", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => warn!("send voltage to mq message failed. {}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect(" Listening Event Loop Failed");
|
||||||
|
} else {
|
||||||
|
panic!("VOLTAGE_EVENTLOOP is undefined!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let _dc_out_state_subscription;
|
||||||
|
if let Some(dc_state_event_loop) = unsafe { DC_OUT_STATE_EVENT_LOOP.as_mut() } {
|
||||||
|
_dc_out_state_subscription = dc_state_event_loop
|
||||||
|
.subscribe(move |message: &DcOutControllerState| {
|
||||||
|
match message.status {
|
||||||
|
dc_out_controller::DcOutStatus::WaitingOff => {
|
||||||
|
beep.play(ringtone::ADAPTER_DOWN).expect("Can not beep.")
|
||||||
|
}
|
||||||
|
dc_out_controller::DcOutStatus::WaitingOn(_) => {
|
||||||
|
beep.play(ringtone::BATTERY_LOW).expect("Can not beep.")
|
||||||
|
}
|
||||||
|
dc_out_controller::DcOutStatus::TurningOff(_) => {
|
||||||
|
beep.play(ringtone::SHUTDOWN).expect("Can not beep.")
|
||||||
|
}
|
||||||
|
_ => beep.play(ringtone::SILENCE).expect("Can not beep."),
|
||||||
|
}
|
||||||
|
match _mq_tx_for_dc_out_state.lock() {
|
||||||
|
Ok(tx) => {
|
||||||
|
let result = tx.send(MqDto {
|
||||||
|
topic: "dc_out_state".to_string(),
|
||||||
|
message: message.to_json(),
|
||||||
});
|
});
|
||||||
|
|
||||||
thread::spawn(move || {
|
if let Err(err) = result {
|
||||||
let mut beep = beep::Beep::new(
|
warn!("send dc_out_state message failed. {}", err)
|
||||||
beep_pin.into_output().expect("Failed to set GPIO6 as output"),
|
}
|
||||||
ledc_timer0,
|
}
|
||||||
ledc_channel0,
|
Err(err) => warn!("send dc_out_state to mq message failed. {}", err),
|
||||||
)
|
}
|
||||||
.expect("Failed to create beep");
|
})
|
||||||
beep.play(&mut rx);
|
.expect(" Listening Event Loop Failed");
|
||||||
|
} else {
|
||||||
|
panic!("DC_OUT_STATE_EVENT_LOOP is undefined!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let _charge_state_subscription;
|
||||||
|
if let Some(charge_state_event_loop) = unsafe { CHARGE_STATE_EVENT_LOOP.as_mut() } {
|
||||||
|
_charge_state_subscription = charge_state_event_loop
|
||||||
|
.subscribe(move |message: &ChargeControllerState| {
|
||||||
|
match _mq_tx_for_charge_state.lock() {
|
||||||
|
Ok(tx) => {
|
||||||
|
let result = tx.send(MqDto {
|
||||||
|
topic: "charge_state".to_string(),
|
||||||
|
message: message.to_json(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let display = screen::Screen::new(i2c0, sda_pin, scl_pin).expect("Failed to create screen");
|
if let Err(err) = result {
|
||||||
|
warn!("send charge_state message failed. {}", err)
|
||||||
let dc_out_ctl = dc_out_controller::DcOutController::new(
|
}
|
||||||
dc_out_ctl_pin
|
}
|
||||||
.into_output()
|
Err(err) => warn!("send charge_state to mq message failed. {}", err),
|
||||||
.expect("Failed to set GPIO3 as output"),
|
}
|
||||||
);
|
})
|
||||||
let mut manager = manager::Manager::new(
|
.expect(" Listening Event Loop Failed");
|
||||||
dc_out_ctl,
|
} else {
|
||||||
display,
|
panic!("CHARGE_STATE_EVENT_LOOP is undefined!");
|
||||||
adc1,
|
}
|
||||||
adapter_pin.into_analog_atten_11db().expect("Failed to set GPIO1 as analog input"),
|
|
||||||
battery_pin.into_analog_atten_11db().expect("Failed to set GPIO2 as analog input"),
|
|
||||||
tx,
|
|
||||||
).expect("Failed to create manager");
|
|
||||||
|
|
||||||
let wifi = WiFi::new().expect("Failed to connect wifi");
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match manager.handling_once() {
|
sleep(Duration::from_millis(100));
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
error!("Exec manager tick task failed: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(100));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
147
src/manager.rs
147
src/manager.rs
@ -1,147 +0,0 @@
|
|||||||
use anyhow::{anyhow, Result};
|
|
||||||
use embedded_hal::digital::blocking::OutputPin;
|
|
||||||
use embedded_hal_0_2::adc::OneShot;
|
|
||||||
use esp_idf_hal::{
|
|
||||||
adc::{Atten11dB, PoweredAdc, ADC1},
|
|
||||||
gpio::{Gpio1, Gpio2},
|
|
||||||
};
|
|
||||||
use log::*;
|
|
||||||
use std::{
|
|
||||||
sync::mpsc,
|
|
||||||
thread,
|
|
||||||
time::{Duration, SystemTime},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{beep::ringtone, dc_out_controller::DcOutController, screen::Screen};
|
|
||||||
|
|
||||||
type AdapterGpio = Gpio1<Atten11dB<ADC1>>;
|
|
||||||
type BatteryGpio = Gpio2<Atten11dB<ADC1>>;
|
|
||||||
|
|
||||||
pub struct Manager<C>
|
|
||||||
where
|
|
||||||
C: OutputPin + Send,
|
|
||||||
{
|
|
||||||
dc_out_controller: DcOutController<C>,
|
|
||||||
screen: Screen,
|
|
||||||
adc: PoweredAdc<ADC1>,
|
|
||||||
adapter_pin: AdapterGpio,
|
|
||||||
battery_pin: BatteryGpio,
|
|
||||||
tx: mpsc::Sender<ringtone::Type>,
|
|
||||||
adapter_downed_at: Option<SystemTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<C> Manager<C>
|
|
||||||
where
|
|
||||||
C: OutputPin + Send,
|
|
||||||
C: 'static,
|
|
||||||
C: std::marker::Sync,
|
|
||||||
{
|
|
||||||
pub fn new(
|
|
||||||
dc_out_controller: DcOutController<C>,
|
|
||||||
screen: Screen,
|
|
||||||
adc1: ADC1,
|
|
||||||
adapter_pin: AdapterGpio,
|
|
||||||
battery_pin: BatteryGpio,
|
|
||||||
tx: mpsc::Sender<ringtone::Type>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let adc = PoweredAdc::new(
|
|
||||||
adc1,
|
|
||||||
esp_idf_hal::adc::config::Config::new().calibration(true),
|
|
||||||
)
|
|
||||||
.map_err(|err| anyhow!("Can not init Adc: {}", err))?;
|
|
||||||
return Ok(Manager {
|
|
||||||
dc_out_controller,
|
|
||||||
screen,
|
|
||||||
adc,
|
|
||||||
adapter_pin,
|
|
||||||
battery_pin,
|
|
||||||
tx,
|
|
||||||
adapter_downed_at: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_adapter_voltage(&mut self) -> Result<f32> {
|
|
||||||
return Ok(self
|
|
||||||
.adc
|
|
||||||
.read(&mut self.adapter_pin)
|
|
||||||
.map_err(|err| anyhow!("Can not read adapter voltage. {:?}", err))?
|
|
||||||
as f32);
|
|
||||||
}
|
|
||||||
pub fn get_battery_voltage(&mut self) -> Result<f32> {
|
|
||||||
return Ok(self
|
|
||||||
.adc
|
|
||||||
.read(&mut self.battery_pin)
|
|
||||||
.map_err(|err| anyhow!("Can not read battery voltage. {:?}", err))?
|
|
||||||
as f32);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn handling_once(&mut self) -> Result<()> {
|
|
||||||
let mut adapter = 0.0_f32;
|
|
||||||
let mut battery = 0.0_f32;
|
|
||||||
for _ in 0..10 {
|
|
||||||
adapter += self.get_adapter_voltage()?;
|
|
||||||
battery += self.get_battery_voltage()?;
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter /= 10.0_f32;
|
|
||||||
battery /= 10.0_f32;
|
|
||||||
|
|
||||||
if is_adapter_down(adapter) {
|
|
||||||
if self.dc_out_controller.get_status().is_off() {
|
|
||||||
self.tx
|
|
||||||
.send(ringtone::SILENCE)
|
|
||||||
.map_err(|err| anyhow!("Can not send silence to Beep. {:?}", err))?;
|
|
||||||
} else if is_battery_down(battery) {
|
|
||||||
self.tx
|
|
||||||
.send(ringtone::BATTERY_LOW)
|
|
||||||
.expect("Can not send message");
|
|
||||||
} else if is_battery_low(battery) {
|
|
||||||
self.dc_out_controller.shutdown();
|
|
||||||
if self.adapter_downed_at.is_none() {
|
|
||||||
info!("Recording adapter downed at: {:?}", SystemTime::now());
|
|
||||||
self.adapter_downed_at = Some(SystemTime::now());
|
|
||||||
} else if self
|
|
||||||
.adapter_downed_at
|
|
||||||
.is_some_and(|at| at.elapsed().is_ok_and(|dur| *dur > Duration::from_secs(5)))
|
|
||||||
{
|
|
||||||
self.tx
|
|
||||||
.send(ringtone::SHUTDOWN)
|
|
||||||
.map_err(|err| anyhow!("Can not send shutdown to Beep. {:?}", err))?;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.tx
|
|
||||||
.send(ringtone::ADAPTER_DOWN)
|
|
||||||
.map_err(|err| anyhow!("Can not send adapter down to Beep. {:?}", err))?;
|
|
||||||
self.dc_out_controller
|
|
||||||
.stop_shutdown()
|
|
||||||
.map_err(|err| anyhow!("Can not stop shutdown. {:?}", err))?;
|
|
||||||
self.adapter_downed_at = None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.dc_out_controller
|
|
||||||
.stop_shutdown()
|
|
||||||
.expect("Can not stop shutdown");
|
|
||||||
self.tx.send(ringtone::SILENCE).map_err(|err| anyhow!("Can not send silence to Beep. {:?}", err))?;
|
|
||||||
self.adapter_downed_at = None;
|
|
||||||
self.dc_out_controller.open()?;
|
|
||||||
}
|
|
||||||
self.screen
|
|
||||||
.draw_voltage(adapter, battery)
|
|
||||||
.map_err(|err| anyhow!("Can not draw voltage. {:?}", err))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_battery_low(battery: f32) -> bool {
|
|
||||||
battery < 1500.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_battery_down(battery: f32) -> bool {
|
|
||||||
battery < 500.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_adapter_down(adapter: f32) -> bool {
|
|
||||||
adapter < 1000.0
|
|
||||||
}
|
|
113
src/message_queue.rs
Normal file
113
src/message_queue.rs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
use std::{
|
||||||
|
sync::{
|
||||||
|
mpsc::{self, Receiver, Sender},
|
||||||
|
Arc, Mutex,
|
||||||
|
},
|
||||||
|
thread::{self, spawn},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use embedded_svc::mqtt::client::{utils::ConnState, Client, Connection, MessageImpl, Publish, QoS};
|
||||||
|
use esp_idf_svc::mqtt::client::{EspMqttClient, MqttClientConfiguration};
|
||||||
|
use esp_idf_sys::EspError;
|
||||||
|
use log::*;
|
||||||
|
|
||||||
|
pub struct MessageQueue {
|
||||||
|
pub tx: Arc<Mutex<Sender<MqDto>>>,
|
||||||
|
pub rx: Arc<Mutex<Receiver<MqDto>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MessageQueueWatcher {
|
||||||
|
pub client: EspMqttClient<ConnState<MessageImpl, EspError>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageQueueWatcher {
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let conf = MqttClientConfiguration {
|
||||||
|
client_id: Some("rust-esp32-std-demo"),
|
||||||
|
crt_bundle_attach: Some(esp_idf_sys::esp_crt_bundle_attach),
|
||||||
|
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (mut client, mut connection) =
|
||||||
|
EspMqttClient::new_with_conn("mqtt://192.168.31.11:1883", &conf)?;
|
||||||
|
|
||||||
|
info!("MQTT client started");
|
||||||
|
thread::spawn(move || {
|
||||||
|
info!("MQTT Listening for messages");
|
||||||
|
|
||||||
|
while let Some(msg) = connection.next() {
|
||||||
|
match msg {
|
||||||
|
Err(e) => info!("MQTT Message ERROR: {}", e),
|
||||||
|
Ok(msg) => info!("MQTT Message: {:?}", msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("MQTT connection loop exit");
|
||||||
|
});
|
||||||
|
|
||||||
|
client
|
||||||
|
.subscribe("esp32-c3-rust-wifi-demo", QoS::AtMostOnce)
|
||||||
|
.map_err(|err| anyhow::anyhow!("subscribe message from queue failed. {}", err))?;
|
||||||
|
|
||||||
|
info!("Subscribed to all topics (esp32-c3-rust-wifi-demo)");
|
||||||
|
|
||||||
|
client
|
||||||
|
.publish(
|
||||||
|
"esp32-c3-rust-wifi-demo/ping",
|
||||||
|
QoS::AtMostOnce,
|
||||||
|
false,
|
||||||
|
"Hello".as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|err| anyhow::anyhow!("publish message to queue failed. {}", err))?;
|
||||||
|
|
||||||
|
info!("Published a hello message to topic \"esp32-c3-rust-wifi-demo/ping\"");
|
||||||
|
|
||||||
|
anyhow::Ok(Self { client })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn publish(&mut self, topic: &str, bytes: &[u8]) -> Result<u32> {
|
||||||
|
self.client
|
||||||
|
.publish(
|
||||||
|
format!("{}/{}", "ups_0_2", topic).as_str(),
|
||||||
|
QoS::AtMostOnce,
|
||||||
|
false,
|
||||||
|
bytes,
|
||||||
|
)
|
||||||
|
.map_err(|err| anyhow::anyhow!("publish message to queue was failed!. {}", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MessageQueue {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
return MessageQueue {
|
||||||
|
tx: Arc::new(Mutex::new(tx)),
|
||||||
|
rx: Arc::new(Mutex::new(rx)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watch(&mut self) -> anyhow::Result<()> {
|
||||||
|
let mut watcher = MessageQueueWatcher::new()
|
||||||
|
.map_err(|err| anyhow::anyhow!("Create MessageQueueWatcher failed. {}", err))?;
|
||||||
|
let rx = self.rx.to_owned();
|
||||||
|
// let (tx, rx) = mpsc::channel::<MqDto>();
|
||||||
|
spawn(move || loop {
|
||||||
|
if let Ok(dto) = rx.lock().unwrap().recv_timeout(Duration::from_millis(400)) {
|
||||||
|
if let Err(err) = watcher.publish(dto.topic.as_str(), dto.message.as_bytes()) {
|
||||||
|
warn!("Can not publish message to MQTT. {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
thread::sleep(Duration::from_millis(100))
|
||||||
|
});
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct MqDto {
|
||||||
|
pub message: String,
|
||||||
|
pub topic: String,
|
||||||
|
}
|
142
src/screen.rs
142
src/screen.rs
@ -1,142 +0,0 @@
|
|||||||
use anyhow::{anyhow, Result, Ok};
|
|
||||||
use embedded_graphics::{
|
|
||||||
mono_font::{ascii::FONT_10X20, iso_8859_10::FONT_6X10, MonoTextStyle},
|
|
||||||
pixelcolor::Rgb565,
|
|
||||||
prelude::{Dimensions, Drawable, Point, Primitive, RgbColor},
|
|
||||||
primitives::{PrimitiveStyleBuilder, Rectangle},
|
|
||||||
text::Text,
|
|
||||||
};
|
|
||||||
use embedded_hal::{delay::blocking::DelayUs, i2c::blocking::{I2c, Operation}};
|
|
||||||
use esp_idf_hal::{
|
|
||||||
delay,
|
|
||||||
gpio::{self},
|
|
||||||
i2c::{self, Master, I2C0},
|
|
||||||
prelude::*,
|
|
||||||
};
|
|
||||||
use log::warn;
|
|
||||||
use ssd1306::{
|
|
||||||
mode::{BufferedGraphicsMode, DisplayConfig},
|
|
||||||
prelude::I2CInterface,
|
|
||||||
size::DisplaySize128x64,
|
|
||||||
Ssd1306,
|
|
||||||
};
|
|
||||||
|
|
||||||
type Display = Ssd1306<
|
|
||||||
I2CInterface<Master<I2C0, gpio::Gpio4<gpio::Unknown>, gpio::Gpio10<gpio::Unknown>>>,
|
|
||||||
DisplaySize128x64,
|
|
||||||
BufferedGraphicsMode<DisplaySize128x64>,
|
|
||||||
>;
|
|
||||||
|
|
||||||
pub struct Screen {
|
|
||||||
pub display: Option<Display>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Screen {
|
|
||||||
pub fn new(
|
|
||||||
i2c: i2c::I2C0,
|
|
||||||
sda: gpio::Gpio4<gpio::Unknown>,
|
|
||||||
scl: gpio::Gpio10<gpio::Unknown>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let config = <i2c::config::MasterConfig as Default>::default().baudrate(400.kHz().into());
|
|
||||||
let mut i2c = i2c::Master::<i2c::I2C0, _, _>::new(i2c, i2c::MasterPins { sda, scl }, config)?;
|
|
||||||
|
|
||||||
let mut buff = [0u8; 10];
|
|
||||||
if let Err(err) = i2c.transaction(0x3C, &mut [Operation::Read(&mut buff)]) {
|
|
||||||
warn!("Failed to initialize display: {}", err);
|
|
||||||
warn!("Failed to initialize display: {}", err);
|
|
||||||
warn!("Failed to initialize display: {}", err);
|
|
||||||
warn!("Failed to initialize display: {}", err);
|
|
||||||
warn!("Failed to initialize display: {}", err);
|
|
||||||
return Ok(Self { display: None });
|
|
||||||
}
|
|
||||||
|
|
||||||
let di = ssd1306::I2CDisplayInterface::new(i2c);
|
|
||||||
|
|
||||||
|
|
||||||
let mut delay = delay::Ets;
|
|
||||||
delay.delay_ms(10_u32)?;
|
|
||||||
|
|
||||||
let mut display = ssd1306::Ssd1306::new(
|
|
||||||
di,
|
|
||||||
ssd1306::size::DisplaySize128x64,
|
|
||||||
ssd1306::rotation::DisplayRotation::Rotate0,
|
|
||||||
)
|
|
||||||
.into_buffered_graphics_mode();
|
|
||||||
|
|
||||||
display
|
|
||||||
.init()
|
|
||||||
.map_err(|err| anyhow!("Can not init display: {:?}", err))?;
|
|
||||||
|
|
||||||
let mut instance = Screen { display: Some(display) };
|
|
||||||
|
|
||||||
instance.draw_boot()?;
|
|
||||||
|
|
||||||
Ok(instance)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_boot(&mut self) -> Result<()> {
|
|
||||||
if self.display.is_none() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let display = self.display.as_mut().unwrap();
|
|
||||||
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
Rectangle::new(
|
|
||||||
display.bounding_box().top_left,
|
|
||||||
display.bounding_box().size,
|
|
||||||
)
|
|
||||||
.into_styled(
|
|
||||||
PrimitiveStyleBuilder::new()
|
|
||||||
.fill_color(Rgb565::BLUE.into())
|
|
||||||
.stroke_color(Rgb565::YELLOW.into())
|
|
||||||
.stroke_width(1)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("Failed to draw rectangle");
|
|
||||||
|
|
||||||
Text::new(
|
|
||||||
"Ivan's UPS",
|
|
||||||
Point::new(
|
|
||||||
12,
|
|
||||||
(display.bounding_box().size.height - 10) as i32 / 2 + 1,
|
|
||||||
),
|
|
||||||
MonoTextStyle::new(&FONT_10X20, Rgb565::WHITE.into()),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("Failed to draw text");
|
|
||||||
|
|
||||||
display
|
|
||||||
.flush()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_voltage(&mut self, adapter: f32, battery: f32) -> Result<()> {
|
|
||||||
if let Some(display) = self.display.as_mut() {
|
|
||||||
display.clear();
|
|
||||||
|
|
||||||
Text::new(
|
|
||||||
format!("Adp. {:.2} mV", adapter).as_str(),
|
|
||||||
Point::new(12, 24),
|
|
||||||
MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE.into()),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("Failed to draw text");
|
|
||||||
Text::new(
|
|
||||||
format!("Bat. {:.2} mV", battery).as_str(),
|
|
||||||
Point::new(12, 36),
|
|
||||||
MonoTextStyle::new(&FONT_6X10, Rgb565::WHITE.into()),
|
|
||||||
)
|
|
||||||
.draw(display)
|
|
||||||
.expect("Failed to draw text");
|
|
||||||
display
|
|
||||||
.flush()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Display error: {:?}", e))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
33
src/time.rs
Normal file
33
src/time.rs
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
use embedded_svc::sys_time::SystemTime;
|
||||||
|
use esp_idf_svc::sntp::{self, EspSntp};
|
||||||
|
use esp_idf_svc::systime::EspSystemTime;
|
||||||
|
use log::{info};
|
||||||
|
use std::time::{Duration};
|
||||||
|
|
||||||
|
pub struct Time {
|
||||||
|
sntp: Option<Box<EspSntp>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Time {
|
||||||
|
pub fn new () -> Time {
|
||||||
|
return Time {
|
||||||
|
sntp: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(&mut self) -> anyhow::Result<()> {
|
||||||
|
let sntp = sntp::EspSntp::new_default().map_err(|err| {
|
||||||
|
anyhow::anyhow!("ESP SNTP Failed: {:?}", err)
|
||||||
|
})?;
|
||||||
|
self.sntp = Some(Box::new(sntp));
|
||||||
|
return anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_time(&mut self) -> Duration {
|
||||||
|
if let Some(ref mut sntp) = self.sntp {
|
||||||
|
info!("ESP SNTP sync status {:?}", sntp.get_sync_status());
|
||||||
|
}
|
||||||
|
EspSystemTime {}.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
219
src/voltage_detection.rs
Normal file
219
src/voltage_detection.rs
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
use std::{
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
thread::{sleep, spawn},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use embedded_hal::{adc::Channel, prelude::_embedded_hal_adc_OneShot};
|
||||||
|
use embedded_svc::event_bus::Postbox;
|
||||||
|
use esp_idf_hal::{
|
||||||
|
adc::{
|
||||||
|
config::{self},
|
||||||
|
Analog, Atten6dB, PoweredAdc, ADC1,
|
||||||
|
},
|
||||||
|
gpio::{Gpio1, Gpio2, Gpio3, Input},
|
||||||
|
};
|
||||||
|
use esp_idf_svc::eventloop::{
|
||||||
|
Background, EspBackgroundEventLoop, EspEventFetchData, EspEventLoop, EspEventPostData,
|
||||||
|
EspTypedEventDeserializer, EspTypedEventSerializer, EspTypedEventSource,
|
||||||
|
};
|
||||||
|
use esp_idf_sys::c_types;
|
||||||
|
use log::{debug, warn};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::time::Time;
|
||||||
|
|
||||||
|
pub static mut VOLTAGE_EVENTLOOP: Option<EspEventLoop<esp_idf_svc::eventloop::User<Background>>> =
|
||||||
|
None;
|
||||||
|
|
||||||
|
const ADAPTER_OFFSET: f32 = 12002f32 / 900f32;
|
||||||
|
const BATTERY_OFFSET: f32 = 12002f32 / 900f32;
|
||||||
|
const OUTPUT_OFFSET: f32 = 12002f32 / 900f32;
|
||||||
|
|
||||||
|
pub struct VoltageDetection {
|
||||||
|
pub worker: VoltageDetectionWorker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct VoltageDetectionWorker {
|
||||||
|
pub adapter_voltage: u16,
|
||||||
|
pub battery_voltage: u16,
|
||||||
|
pub output_voltage: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VoltageDetection {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
return anyhow::Ok(Self {
|
||||||
|
worker: VoltageDetectionWorker::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn watching(&mut self) -> anyhow::Result<()> {
|
||||||
|
match EspBackgroundEventLoop::new(&Default::default()) {
|
||||||
|
Ok(eventloop) => unsafe { VOLTAGE_EVENTLOOP = Some(eventloop) },
|
||||||
|
Err(err) => anyhow::bail!("Init Event Loop failed. {:?}", err),
|
||||||
|
}
|
||||||
|
let worker = Arc::new(Mutex::new(self.worker));
|
||||||
|
|
||||||
|
spawn(move || {
|
||||||
|
let handler = || -> anyhow::Result<()> {
|
||||||
|
let mut adapter_pin = unsafe { Gpio1::<Input>::new() }
|
||||||
|
.into_analog_atten_6db()
|
||||||
|
.map_err(|err| {
|
||||||
|
anyhow::anyhow!("Failed to set GPIO 1 as analog input. {}", err)
|
||||||
|
})?;
|
||||||
|
let mut battery_pin = unsafe { Gpio2::<Input>::new() }
|
||||||
|
.into_analog_atten_6db()
|
||||||
|
.map_err(|err| {
|
||||||
|
anyhow::anyhow!("Failed to set GPIO 2 as analog input. {}", err)
|
||||||
|
})?;
|
||||||
|
let mut output_pin = unsafe { Gpio3::<Input>::new() }
|
||||||
|
.into_analog_atten_6db()
|
||||||
|
.map_err(|err| {
|
||||||
|
anyhow::anyhow!("Failed to set GPIO 3 as analog input. {}", err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut adc = PoweredAdc::new(unsafe { ADC1::new() }, config::Config::new())?;
|
||||||
|
|
||||||
|
let mut worker = worker.lock().map_err(|err| {
|
||||||
|
anyhow::anyhow!("Lock VoltageDetection Worker Failed. {}", err)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut last_runing_at;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
last_runing_at = Time::new().get_time();
|
||||||
|
match worker.read_once(
|
||||||
|
&mut adc,
|
||||||
|
&mut adapter_pin,
|
||||||
|
&mut battery_pin,
|
||||||
|
&mut output_pin,
|
||||||
|
) {
|
||||||
|
Ok(_) => debug!(
|
||||||
|
"Adapter: {},\tBattery: {},\t Output: {}",
|
||||||
|
worker.adapter_voltage, worker.battery_voltage, worker.output_voltage
|
||||||
|
),
|
||||||
|
Err(err) => warn!("Read Failed. {}", err),
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(eventloop) = unsafe { VOLTAGE_EVENTLOOP.as_mut() } {
|
||||||
|
if let Err(err) = eventloop.post(
|
||||||
|
&mut VoltageDetectionWorker {
|
||||||
|
adapter_voltage: worker.adapter_voltage,
|
||||||
|
battery_voltage: worker.battery_voltage,
|
||||||
|
output_voltage: worker.output_voltage,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
) {
|
||||||
|
warn!("Post Result to Event Loop failed. {}", err);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn!("EVENTLOOP IS NONE");
|
||||||
|
}
|
||||||
|
let mut delta = Time::new().get_time() - last_runing_at;
|
||||||
|
|
||||||
|
if delta >= Duration::from_millis(5000) {
|
||||||
|
delta = Duration::ZERO
|
||||||
|
}
|
||||||
|
|
||||||
|
sleep(Duration::from_millis(5000) - delta);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = handler() {
|
||||||
|
warn!("init failed. {}", err)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VoltageDetectionWorker {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
return Self {
|
||||||
|
adapter_voltage: 0,
|
||||||
|
battery_voltage: 0,
|
||||||
|
output_voltage: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_once(
|
||||||
|
&mut self,
|
||||||
|
adc: &mut PoweredAdc<ADC1>,
|
||||||
|
adapter_pin: &mut Gpio1<Atten6dB<ADC1>>,
|
||||||
|
battery_pin: &mut Gpio2<Atten6dB<ADC1>>,
|
||||||
|
output_pin: &mut Gpio3<Atten6dB<ADC1>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match self.read_pin_once(adc, adapter_pin) {
|
||||||
|
Ok(voltage) => {
|
||||||
|
self.adapter_voltage = ((voltage as f32) * ADAPTER_OFFSET) as u16;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Adapter Voltage read failed: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.read_pin_once(adc, battery_pin) {
|
||||||
|
Ok(voltage) => {
|
||||||
|
self.battery_voltage = ((voltage as f32) * BATTERY_OFFSET) as u16;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Adapter Voltage read failed: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match self.read_pin_once(adc, output_pin) {
|
||||||
|
Ok(voltage) => {
|
||||||
|
self.output_voltage = ((voltage as f32) * OUTPUT_OFFSET) as u16;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Adapter Voltage read failed: {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
pub fn read_pin_once<AN: Analog<ADC1>, PIN: Channel<AN, ID = u8>>(
|
||||||
|
&mut self,
|
||||||
|
adc: &mut PoweredAdc<ADC1>,
|
||||||
|
pin: &mut PIN,
|
||||||
|
) -> anyhow::Result<u16> {
|
||||||
|
let mut avg_voltage: u16 = 0;
|
||||||
|
for _ in 0..10 {
|
||||||
|
let voltage = adc.read(pin);
|
||||||
|
match voltage {
|
||||||
|
Ok(voltage) => {
|
||||||
|
avg_voltage += voltage;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
anyhow::bail!("Adapter Voltage read failed: {:?}", err)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
sleep(Duration::from_millis(100));
|
||||||
|
}
|
||||||
|
anyhow::Ok(avg_voltage / 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventSource for VoltageDetectionWorker {
|
||||||
|
fn source() -> *const c_types::c_char {
|
||||||
|
b"VOLTAGES\0".as_ptr() as *const _
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventSerializer<VoltageDetectionWorker> for VoltageDetectionWorker {
|
||||||
|
fn serialize<R>(
|
||||||
|
event: &VoltageDetectionWorker,
|
||||||
|
f: impl for<'a> FnOnce(&'a EspEventPostData) -> R,
|
||||||
|
) -> R {
|
||||||
|
f(&unsafe { EspEventPostData::new(Self::source(), Self::event_id(), event) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EspTypedEventDeserializer<VoltageDetectionWorker> for VoltageDetectionWorker {
|
||||||
|
fn deserialize<R>(
|
||||||
|
data: &EspEventFetchData,
|
||||||
|
f: &mut impl for<'a> FnMut(&'a VoltageDetectionWorker) -> R,
|
||||||
|
) -> R {
|
||||||
|
f(unsafe { data.as_payload() })
|
||||||
|
}
|
||||||
|
}
|
92
src/wifi.rs
92
src/wifi.rs
@ -1,81 +1,77 @@
|
|||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
use anyhow::{
|
use anyhow::{bail, Ok, Result};
|
||||||
bail,
|
use embedded_svc::{ipv4, wifi::*};
|
||||||
Result, Ok,
|
|
||||||
};
|
|
||||||
use esp_idf_svc::ping::EspPing;
|
use esp_idf_svc::ping::EspPing;
|
||||||
use esp_idf_svc::{
|
use esp_idf_svc::{
|
||||||
netif::EspNetifStack, nvs::EspDefaultNvs, sysloop::EspSysLoopStack, wifi::EspWifi
|
netif::EspNetifStack, nvs::EspDefaultNvs, sysloop::EspSysLoopStack, wifi::EspWifi,
|
||||||
};
|
};
|
||||||
use embedded_svc::{wifi::*, ipv4};
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
|
||||||
use embedded_svc::ping::Ping;
|
use embedded_svc::ping::Ping;
|
||||||
|
|
||||||
pub struct WiFi {
|
pub struct Internet {
|
||||||
wifi: Box<EspWifi>,
|
wifi: Box<EspWifi>,
|
||||||
|
auto_connect: bool,
|
||||||
|
connected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WiFi {
|
impl Internet {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let netif_stack = Arc::new(EspNetifStack::new()?);
|
let netif_stack = Arc::new(EspNetifStack::new()?);
|
||||||
let sys_loop_stack = Arc::new(EspSysLoopStack::new()?);
|
let sys_loop_stack = Arc::new(EspSysLoopStack::new()?);
|
||||||
let default_nvs = Arc::new(EspDefaultNvs::new()?);
|
let default_nvs = Arc::new(EspDefaultNvs::new()?);
|
||||||
|
|
||||||
let wifi = Self::wifi(
|
let wifi = Box::new(EspWifi::new(netif_stack, sys_loop_stack, default_nvs)?);
|
||||||
netif_stack.clone(),
|
|
||||||
sys_loop_stack.clone(),
|
|
||||||
default_nvs.clone(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(Self { wifi })
|
let mut instance = Self { wifi, auto_connect: true, connected: false };
|
||||||
|
instance.connect_ap()?;
|
||||||
|
|
||||||
|
Ok(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wifi(
|
fn connect_ap(
|
||||||
netif_stack: Arc<EspNetifStack>,
|
&mut self,
|
||||||
sys_loop_stack: Arc<EspSysLoopStack>,
|
) -> Result<()> {
|
||||||
default_nvs: Arc<EspDefaultNvs>,
|
const SSID: &str = "Ivan";
|
||||||
) -> Result<Box<EspWifi>> {
|
|
||||||
const SSID: &str = "Ivan Li";
|
|
||||||
const PASSWORD: &str = "ivanli.cc";
|
const PASSWORD: &str = "ivanli.cc";
|
||||||
let mut wifi = Box::new(EspWifi::new(netif_stack, sys_loop_stack, default_nvs)?);
|
|
||||||
|
|
||||||
info!("Wifi created, about to scan");
|
info!("Wifi created, about to scan");
|
||||||
|
|
||||||
let ap_infos = wifi.scan()?;
|
let wifi = self.wifi.as_mut();
|
||||||
|
|
||||||
|
// let ap_infos = wifi.scan()?;
|
||||||
|
|
||||||
info!("Wifi AP Count {}", ap_infos.len());
|
// info!("Wifi AP Count {}", ap_infos.len());
|
||||||
|
|
||||||
let ours = ap_infos.into_iter().find(|a| a.ssid == SSID);
|
// let ours = ap_infos.into_iter().find(|a| a.ssid == SSID);
|
||||||
|
|
||||||
let channel = if let Some(ours) = ours {
|
// let channel = if let Some(ours) = ours {
|
||||||
info!(
|
// info!(
|
||||||
"Found configured access point {} on channel {}",
|
// "Found configured access point {} on channel {}",
|
||||||
SSID, ours.channel
|
// SSID, ours.channel
|
||||||
);
|
// );
|
||||||
Some(ours.channel)
|
// Some(ours.channel)
|
||||||
} else {
|
// } else {
|
||||||
info!(
|
// info!(
|
||||||
"Configured access point {} not found during scanning, will go with unknown channel",
|
// "Configured access point {} not found during scanning, will go with unknown channel",
|
||||||
SSID
|
// SSID
|
||||||
);
|
// );
|
||||||
None
|
// None
|
||||||
};
|
// };
|
||||||
|
|
||||||
wifi.set_configuration(&Configuration::Mixed(
|
wifi.set_configuration(&Configuration::Client(
|
||||||
ClientConfiguration {
|
ClientConfiguration {
|
||||||
ssid: SSID.into(),
|
ssid: SSID.into(),
|
||||||
password: PASSWORD.into(),
|
password: PASSWORD.into(),
|
||||||
channel,
|
channel: None,
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
AccessPointConfiguration {
|
|
||||||
ssid: "aptest".into(),
|
|
||||||
channel: channel.unwrap_or(1),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
|
// AccessPointConfiguration {
|
||||||
|
// ssid: "aptest".into(),
|
||||||
|
// channel: channel.unwrap_or(1),
|
||||||
|
// ..Default::default()
|
||||||
|
// },
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
info!("Wifi configuration set, about to get status");
|
info!("Wifi configuration set, about to get status");
|
||||||
@ -85,11 +81,13 @@ impl WiFi {
|
|||||||
|
|
||||||
let status = wifi.get_status();
|
let status = wifi.get_status();
|
||||||
|
|
||||||
|
info!("we have the wifi status");
|
||||||
|
|
||||||
if let Status(
|
if let Status(
|
||||||
ClientStatus::Started(ClientConnectionStatus::Connected(ClientIpStatus::Done(
|
ClientStatus::Started(ClientConnectionStatus::Connected(ClientIpStatus::Done(
|
||||||
ip_settings,
|
ip_settings,
|
||||||
))),
|
))),
|
||||||
ApStatus::Started(ApIpStatus::Done),
|
ApStatus::Stopped
|
||||||
) = status
|
) = status
|
||||||
{
|
{
|
||||||
info!("Wifi connected");
|
info!("Wifi connected");
|
||||||
@ -99,10 +97,10 @@ impl WiFi {
|
|||||||
bail!("Unexpected Wifi status: {:?}", status);
|
bail!("Unexpected Wifi status: {:?}", status);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(wifi)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ping(ip_settings: &ipv4::ClientSettings) -> Result<()> {
|
pub fn ping(ip_settings: &ipv4::ClientSettings) -> Result<()> {
|
||||||
info!("About to do some pings for {:?}", ip_settings);
|
info!("About to do some pings for {:?}", ip_settings);
|
||||||
let ping_summary =
|
let ping_summary =
|
||||||
EspPing::default().ping(ip_settings.subnet.gateway, &Default::default())?;
|
EspPing::default().ping(ip_settings.subnet.gateway, &Default::default())?;
|
||||||
|
Loading…
Reference in New Issue
Block a user