feat: wifi 连接,网络校时,MQTT。

This commit is contained in:
Ivan Li 2022-08-07 11:57:05 +08:00
parent 12f3509304
commit 282e1a01a2
21 changed files with 625 additions and 741 deletions

View File

@ -29,7 +29,6 @@ build-std = ["std", "panic_abort"]
[env]
# 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)
#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" }
# 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
View 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" ]

View 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
View File

@ -0,0 +1 @@
target

34
.gitpod.Dockerfile vendored Normal file
View 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
View 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

View File

@ -1,9 +1,9 @@
[package]
authors = ["Ivan Li <ivanli2048@gmail.com>"]
edition = "2018"
name = "ups-esp32c3-rust"
version = "0.2.0"
authors = ["Ivan Li <ivanli2048@gmail.com>"]
edition = "2021"
resolver = "2"
version = "0.1.0"
[profile.release]
opt-level = "s"
@ -17,18 +17,17 @@ pio = ["esp-idf-sys/pio"]
[dependencies]
anyhow = "1"
embedded-graphics = "0.7.1"
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"
env_logger = "0.9.0"
esp-idf-hal = "0.38.0"
esp-idf-svc = "0.42.1"
esp-idf-sys = { version = "0.31.6", features = ["binstart"] }
log = "0.4.17"
retry = "1.3.1"
ssd1306 = "0.7.0"
sntpc = "0.3.1"
[build-dependencies]
anyhow = "1.0.57"
embuild = "0.29.1"
embuild = "0.29"
anyhow = "1"
[patch.crates-io]
esp-idf-hal = { git = "https://github.com/esp-rs/esp-idf-hal", branch = "master" }

116
docs/README.md Normal file
View 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
View 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
View 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
View 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

View File

@ -6,5 +6,5 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=7000
#CONFIG_FREERTOS_HZ=1000
# Workaround for https://github.com/espressif/esp-idf/issues/7631
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=n
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=n
#CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=needs

View File

@ -1,78 +0,0 @@
use esp_idf_hal::gpio::OutputPin;
use esp_idf_hal::ledc;
use esp_idf_hal::ledc::{config::TimerConfig, Channel, Timer};
use esp_idf_hal::prelude::*;
use esp_idf_sys::EspError;
use std::thread;
use std::time::Duration;
type LedcChannel<P, T, C> = Channel<C, T, Timer<T>, P>;
pub struct Beep<P: OutputPin, T: ledc::HwTimer, C: ledc::HwChannel> {
beat: u8,
ringtone: ringtone::Type,
channel: LedcChannel<P, T, C>,
duty: u32,
}
impl<P: OutputPin, T: ledc::HwTimer, C: ledc::HwChannel> Beep<P, T, C> {
pub fn new(pin: P, timer: T, channel: C) -> Result<Self, EspError> {
let channel = Self::init_channel(pin, timer, channel)?;
let max_duty = channel.get_max_duty();
return Ok(Beep {
channel,
beat: 0,
duty: max_duty * 3 / 4,
ringtone: ringtone::SILENCE,
});
}
fn init_channel(pin: P, timer: T, channel: C) -> Result<LedcChannel<P, T, C>, EspError> {
let config = TimerConfig::default().frequency(2.kHz().into());
let timer = Timer::new(timer, &config)?;
let channel: Channel<C, T, Timer<T>, P> = Channel::new(channel, timer, pin)?;
return Ok(channel);
}
pub fn play(&mut self, rx: &mut std::sync::mpsc::Receiver<ringtone::Type>) {
loop {
let curr_ringtone = rx.try_recv().unwrap_or_else(|_| self.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 {
self.channel.set_duty(0).expect("Failed to set duty");
}
thread::sleep(Duration::from_millis(100));
self.beat += 1;
if self.beat == 16 {
self.beat = 0;
}
}
}
}
pub mod ringtone {
pub type Type = [bool; 16];
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 = [
true, true, false, false, true, true, false, false, true, true, false, false, true, true,
false, false,
];
pub const SILENCE: Type = [false; 16];
pub const SHUTDOWN: Type = [
true, false, true, true, true, true, false, true, true, true, true, true, false, false,
false, false,
];
}

View File

@ -1,47 +0,0 @@
use embedded_hal::digital::blocking::OutputPin;
use std::thread;
use std::time::Duration;
pub struct Blink<T>
where
T: OutputPin,
{
state: bool,
pin: T,
stopped: bool,
}
impl<T> Blink<T>
where
T: OutputPin,
{
pub fn new(pin: T) -> Blink<T> {
return Blink {
state: false,
pin,
stopped: false,
};
}
pub fn toggle(&mut self) -> Result<(), T::Error> {
self.state = !self.state;
if self.state {
self.pin.set_high()
} else {
self.pin.set_low()
}
}
pub fn play(&mut self) {
loop {
if self.stopped {
break;
}
self.toggle().unwrap();
thread::sleep(Duration::from_millis(50));
self.toggle().unwrap();
thread::sleep(Duration::from_millis(950));
}
}
}

View File

@ -1,181 +0,0 @@
use anyhow::{Ok, Result, anyhow};
use retry;
use std::{
sync::{
mpsc,
Arc, Mutex,
},
thread,
time::{self, SystemTime},
};
use embedded_hal::digital::blocking::OutputPin;
use log::*;
pub enum DcOutStatus {
On,
Off,
TurningOn(mpsc::Receiver<bool>),
TurningOff(mpsc::Receiver<bool>),
}
impl DcOutStatus {
pub fn is_on(&self) -> bool {
match self {
DcOutStatus::On => true,
_ => false,
}
}
pub fn is_off(&self) -> bool {
match self {
DcOutStatus::Off => true,
_ => false,
}
}
}
pub struct DcOutController<P>
where
P: OutputPin + Send,
{
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,
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), || {
pin.set_low()
})
.map_err(|_| anyhow::anyhow!("Failed to lock pin"))?;
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,
}
}
}

View File

@ -1,89 +1,48 @@
#![feature(is_some_with)]
use std::{env, thread::sleep, time::{Duration, UNIX_EPOCH}};
use anyhow::{bail, Ok, Result};
use embedded_svc::mqtt::client::{Publish, QoS};
use esp_idf_sys as _;
use log::{error, info};
use std::{thread, time::Duration, sync::mpsc, env};
use log::*;
use crate::wifi::WiFi;
mod beep;
mod blink;
mod dc_out_controller;
mod manager;
mod screen;
mod message_queue;
mod wifi;
mod time;
use crate::{message_queue::MessageQueue, wifi::Internet, time::Time};
fn main() {
env::set_var("DEFMT_LOG", "trace");
env::set_var("RUST_BACKTRACE", "1");
env::set_var("RUST_LOG", "trace");
env_logger::init();
// 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.
esp_idf_sys::link_patches();
println!("Hello, world!");
let peripherals = esp_idf_hal::peripherals::Peripherals::take().unwrap();
let wifi = Internet::new().unwrap();
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 mut mq = MessageQueue::new();
mq.init().unwrap();
let adc1 = peripherals.adc1;
let adapter_pin = peripherals.pins.gpio1;
let battery_pin = peripherals.pins.gpio2;
let (tx, mut rx) = mpsc::channel();
info!("Starting");
thread::spawn(move || {
let mut blink =
blink::Blink::new(blink_pin.into_output().expect("Failed to set GPIO5 as output"));
blink.play();
});
thread::spawn(move || {
thread::sleep(Duration::from_millis(5000));
});
thread::spawn(move || {
let mut beep = beep::Beep::new(
beep_pin.into_output().expect("Failed to set GPIO6 as output"),
ledc_timer0,
ledc_channel0,
)
.expect("Failed to create beep");
beep.play(&mut rx);
});
let display = screen::Screen::new(i2c0, sda_pin, scl_pin).expect("Failed to create screen");
let dc_out_ctl = dc_out_controller::DcOutController::new(
dc_out_ctl_pin
.into_output()
.expect("Failed to set GPIO3 as output"),
);
let mut manager = manager::Manager::new(
dc_out_ctl,
display,
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");
let mut time = Time::new();
time.sync().unwrap();
loop {
match manager.handling_once() {
Ok(_) => {}
Err(err) => {
error!("Exec manager tick task failed: {}", err);
}
sleep(Duration::from_millis(1000));
if let Some(ref mut mq_client) = mq.client {
let timestamps = time.get_time().as_millis();
info!("timestamps {}", timestamps);
mq_client.publish(
"ups-0.2/heartbeat",
QoS::AtMostOnce,
false,
timestamps.to_string().as_bytes(),
).map_err(|err| {
warn!("publish heartbeat failed {}", err)
}).unwrap();
}
thread::sleep(Duration::from_millis(100));
}
}

View File

@ -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
}

73
src/message_queue.rs Normal file
View File

@ -0,0 +1,73 @@
use std::thread;
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 client: Option<EspMqttClient<ConnState<MessageImpl, EspError>>>,
}
impl MessageQueue {
pub fn new() -> MessageQueue {
return MessageQueue { client: None };
}
pub fn init(&mut self) -> Result<()> {
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.8: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\"");
self.client = Some(client);
anyhow::Ok(())
}
pub fn publish(&mut self, bytes: &[u8]) -> Result<u32> {
self.client
.as_mut()
.unwrap()
.publish("", QoS::AtMostOnce, false, bytes)
.map_err(|err| {
anyhow::anyhow!("publish message to queue was failed!. {}", err)
})
}
}

View File

@ -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(())
}
}

105
src/time.rs Normal file
View File

@ -0,0 +1,105 @@
use anyhow::bail;
use embedded_svc::sys_time::SystemTime;
use esp_idf_svc::sntp::{self, EspSntp};
use esp_idf_svc::systime::EspSystemTime;
use log::{warn, info};
use sntpc::{Error, NtpContext, NtpResult, NtpTimestampGenerator, NtpUdpSocket, Result};
use std::net::{SocketAddr, ToSocketAddrs, UdpSocket};
use std::time::{Duration};
pub struct Time {
result: Option<NtpResult>,
sntp: Option<Box<EspSntp>>,
}
impl Time {
pub fn new () -> Time {
return Time {
result: None,
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()
}
// pub fn sync(&mut self) -> anyhow::Result<()> {
// let mut error = None;
// for _ in 0..10 {
// let socket = UdpSocket::bind("0.0.0.0:0").expect("Unable to crate UDP socket");
// socket
// .set_read_timeout(Some(Duration::from_secs(2)))
// .expect("Unable to set UDP socket read timeout");
// let sock_wrapper = UdpSocketWrapper(socket);
// let ntp_context = NtpContext::new(StdTimestampGen::default());
// let result = sntpc::get_time("pool.ntp.org:123", sock_wrapper, ntp_context);
// match result {
// Ok(res_time) => {
// println!("Got time: {}.{}", res_time.sec(), res_time.sec_fraction());
// self.result = Some(res_time);
// return anyhow::Ok(())
// }
// Err(err) => {
// error = Some(err);
// warn!("fetch dateTime from ntp failed: {:?}", err);
// },
// }
// }
// bail!("fetch dateTime from ntp failed: {:?}", error)
// }
}
#[derive(Copy, Clone, Default)]
struct StdTimestampGen {
duration: Duration,
}
impl NtpTimestampGenerator for StdTimestampGen {
fn init(&mut self) {
self.duration = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.unwrap();
}
fn timestamp_sec(&self) -> u64 {
self.duration.as_secs()
}
fn timestamp_subsec_micros(&self) -> u32 {
self.duration.subsec_micros()
}
}
#[derive(Debug)]
struct UdpSocketWrapper(UdpSocket);
impl NtpUdpSocket for UdpSocketWrapper {
fn send_to<T: ToSocketAddrs>(&self, buf: &[u8], addr: T) -> Result<usize> {
match self.0.send_to(buf, addr) {
Ok(usize) => Ok(usize),
Err(_) => Err(Error::Network),
}
}
fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, SocketAddr)> {
match self.0.recv_from(buf) {
Ok((size, addr)) => Ok((size, addr)),
Err(_) => Err(Error::Network),
}
}
}

View File

@ -1,81 +1,77 @@
use std::{sync::Arc, time::Duration};
use anyhow::{
bail,
Result, Ok,
};
use anyhow::{bail, Ok, Result};
use embedded_svc::{ipv4, wifi::*};
use esp_idf_svc::ping::EspPing;
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 embedded_svc::ping::Ping;
pub struct WiFi {
pub struct Internet {
wifi: Box<EspWifi>,
auto_connect: bool,
connected: bool,
}
impl WiFi {
impl Internet {
pub fn new() -> Result<Self> {
let netif_stack = Arc::new(EspNetifStack::new()?);
let sys_loop_stack = Arc::new(EspSysLoopStack::new()?);
let default_nvs = Arc::new(EspDefaultNvs::new()?);
let wifi = Self::wifi(
netif_stack.clone(),
sys_loop_stack.clone(),
default_nvs.clone(),
)?;
let wifi = Box::new(EspWifi::new(netif_stack, sys_loop_stack, default_nvs)?);
Ok(Self { wifi })
let mut instance = Self { wifi, auto_connect: true, connected: false };
instance.connect_ap()?;
Ok(instance)
}
fn wifi(
netif_stack: Arc<EspNetifStack>,
sys_loop_stack: Arc<EspSysLoopStack>,
default_nvs: Arc<EspDefaultNvs>,
) -> Result<Box<EspWifi>> {
fn connect_ap(
&mut self,
) -> Result<()> {
const SSID: &str = "Ivan Li";
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");
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 {
info!(
"Found configured access point {} on channel {}",
SSID, ours.channel
);
Some(ours.channel)
} else {
info!(
"Configured access point {} not found during scanning, will go with unknown channel",
SSID
);
None
};
// let channel = if let Some(ours) = ours {
// info!(
// "Found configured access point {} on channel {}",
// SSID, ours.channel
// );
// Some(ours.channel)
// } else {
// info!(
// "Configured access point {} not found during scanning, will go with unknown channel",
// SSID
// );
// None
// };
wifi.set_configuration(&Configuration::Mixed(
wifi.set_configuration(&Configuration::Client(
ClientConfiguration {
ssid: SSID.into(),
password: PASSWORD.into(),
channel,
..Default::default()
},
AccessPointConfiguration {
ssid: "aptest".into(),
channel: channel.unwrap_or(1),
channel: None,
..Default::default()
},
// AccessPointConfiguration {
// ssid: "aptest".into(),
// channel: channel.unwrap_or(1),
// ..Default::default()
// },
))?;
info!("Wifi configuration set, about to get status");
@ -85,11 +81,13 @@ impl WiFi {
let status = wifi.get_status();
info!("we have the wifi status");
if let Status(
ClientStatus::Started(ClientConnectionStatus::Connected(ClientIpStatus::Done(
ip_settings,
))),
ApStatus::Started(ApIpStatus::Done),
ApStatus::Stopped
) = status
{
info!("Wifi connected");
@ -99,22 +97,22 @@ impl WiFi {
bail!("Unexpected Wifi status: {:?}", status);
}
Ok(wifi)
Ok(())
}
fn ping(ip_settings: &ipv4::ClientSettings) -> Result<()> {
info!("About to do some pings for {:?}", ip_settings);
let ping_summary =
EspPing::default().ping(ip_settings.subnet.gateway, &Default::default())?;
if ping_summary.transmitted != ping_summary.received {
bail!(
"Pinging gateway {} resulted in timeouts",
ip_settings.subnet.gateway
);
pub fn ping(ip_settings: &ipv4::ClientSettings) -> Result<()> {
info!("About to do some pings for {:?}", ip_settings);
let ping_summary =
EspPing::default().ping(ip_settings.subnet.gateway, &Default::default())?;
if ping_summary.transmitted != ping_summary.received {
bail!(
"Pinging gateway {} resulted in timeouts",
ip_settings.subnet.gateway
);
}
info!("Pinging done");
Ok(())
}
info!("Pinging done");
Ok(())
}
}