initial commit
This commit is contained in:
commit
2ef1b13bbe
9 changed files with 311 additions and 0 deletions
16
.cargo/config.toml
Normal file
16
.cargo/config.toml
Normal file
|
@ -0,0 +1,16 @@
|
|||
[build]
|
||||
target = "xtensa-esp32-espidf"
|
||||
|
||||
[target.xtensa-esp32-espidf]
|
||||
linker = "ldproxy"
|
||||
runner = "espflash flash --monitor"
|
||||
rustflags = [ "--cfg", "espidf_time64"]
|
||||
|
||||
[unstable]
|
||||
build-std = ["std", "panic_abort"]
|
||||
|
||||
[env]
|
||||
MCU="esp32"
|
||||
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
|
||||
ESP_IDF_VERSION = "v5.3.3"
|
||||
|
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
/.vscode
|
||||
/.embuild
|
||||
/target
|
||||
/Cargo.lock
|
||||
/espflash_ports.toml
|
33
Cargo.toml
Normal file
33
Cargo.toml
Normal file
|
@ -0,0 +1,33 @@
|
|||
[package]
|
||||
name = "flipflop"
|
||||
version = "0.1.0"
|
||||
authors = ["Brent Schroeter <contact@brentsch.com>"]
|
||||
edition = "2021"
|
||||
resolver = "2"
|
||||
rust-version = "1.77"
|
||||
|
||||
[[bin]]
|
||||
name = "flipflop"
|
||||
harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors
|
||||
|
||||
[profile.release]
|
||||
opt-level = "s"
|
||||
|
||||
[profile.dev]
|
||||
debug = true
|
||||
opt-level = "z"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
experimental = ["esp-idf-svc/experimental"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
esp-idf-svc = "0.51"
|
||||
anyhow = { version = "1.0.99", features = ["backtrace"] }
|
||||
chrono = { version = "0.4.41", default-features = false, features = ["now"] }
|
||||
chrono-tz = "0.10.4"
|
||||
|
||||
[build-dependencies]
|
||||
embuild = "0.33"
|
97
README.md
Normal file
97
README.md
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Flip-Flop
|
||||
|
||||
Firmware for a daily timer switch powered by the
|
||||
[QT Py ESP32 Pico](https://learn.adafruit.com/adafruit-qt-py-esp32-pico/overview).
|
||||
|
||||
This application uses NTP to synchronize the microcontroller clock, allowing
|
||||
the system to recover automatically after losing power, so long as it remains
|
||||
within range of a known WiFi access point.
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration values are hard-coded in `src/config.rs`. Previously,
|
||||
configuration was handled by [toml-cfg](https://crates.io/crates/toml-cfg/),
|
||||
but hard-coding the values accomplishes the same thing with fewer moving parts.
|
||||
|
||||
## Toolchain Installation and Usage
|
||||
|
||||
Refer to the
|
||||
[Rust on ESP book](https://docs.espressif.com/projects/rust/book/introduction.html)
|
||||
for installation of the development toolchain.
|
||||
|
||||
Development was completed on a system running Fedora 42, which has some
|
||||
dependencies which diverge from the official documentation.
|
||||
|
||||
### Detailed Instructions for Fedora 42
|
||||
|
||||
#### Installing espup
|
||||
|
||||
Espressif has documentation for [setting up a development
|
||||
environment](https://docs.espressif.com/projects/rust/book/installation/index.html);
|
||||
however, these do not work out of the box with Fedora 42. In addition to installing
|
||||
Rust--
|
||||
|
||||
```sh
|
||||
sudo dnf install rustup
|
||||
rustup-init
|
||||
```
|
||||
|
||||
--we must install several dependencies before `espup` will compile in the next step (note that the perl packages are case-sensitive):
|
||||
|
||||
```sh
|
||||
sudo dnf install git wget flex bison gperf python3 python3-pip cmake ninja-build ccache libffi-devel openssl-devel dfu-util libusb1 perl-FindBin perl-IPC-Cmd perl-File-Compare
|
||||
pip install virtualenv
|
||||
```
|
||||
|
||||
Now, we can run:
|
||||
|
||||
```sh
|
||||
cargo install espup --locked
|
||||
cargo install espflash
|
||||
cargo install ldproxy # Required for std development
|
||||
espup install
|
||||
```
|
||||
|
||||
This will install the relevant Espressif Rust, LLVM, and GCC tools, as well as generating an activation script in `~/export-esp.sh`.
|
||||
If you want to use this toolchain by default, you may want to add `source ~/export-esp.sh` to your `.profile`.
|
||||
|
||||
#### Connecting and Flashing the MCU
|
||||
|
||||
Once the development board (or serial console) is connected to the computer via USB, the example project may be flashed with:
|
||||
|
||||
```sh
|
||||
cargo run
|
||||
```
|
||||
|
||||
This compiles the project and shells out automatically to `espflash` to flash and establish a serial console connection to the board.
|
||||
In my case, it identified the correct device automatically. However in some cases, the device exposed to the file system may
|
||||
have restrictive permissions applied, which will cause an error to the effect of:
|
||||
|
||||
```
|
||||
[2025-09-06T05:46:02Z INFO ] Serial port: '/dev/<DEVICE NAME>'
|
||||
[2025-09-06T05:46:02Z INFO ] Connecting...
|
||||
Error: × Failed to open serial port /dev/<DEVICE NAME>
|
||||
╰─▶ Error while connecting to device
|
||||
```
|
||||
|
||||
Granting read/write permissions to all users, should be sufficient to resolve the above issue (note that this is required each time the device is physically reconnected):
|
||||
|
||||
```sh
|
||||
chmod a+rw /dev/<DEVICE NAME>
|
||||
```
|
||||
|
||||
The port will be saved to `espflash_ports.toml`. For further information, refer to the [`espflash` documentation](https://github.com/esp-rs/espflash/blob/1daf446a0a553d23309e77c8781679ca25fc007a/espflash/README.md).
|
||||
|
||||
When successful, `espflash` will identify the connected MCU and its specifications.
|
||||
|
||||
```
|
||||
[2025-09-06T05:47:07Z INFO ] Serial port: '/dev/ttyACM0'
|
||||
[2025-09-06T05:47:07Z INFO ] Connecting...
|
||||
[2025-09-06T05:47:08Z INFO ] Using flash stub
|
||||
Chip type: esp32 (revision v3.0)
|
||||
Crystal frequency: 40 MHz
|
||||
Flash size: 8MB
|
||||
Features: WiFi, BT, Dual Core, 240MHz, Embedded Flash, Embedded PSRAM, VRef calibration in efuse, Coding Scheme None
|
||||
```
|
||||
|
||||
It will then spend several seconds flashing, booting, and finally executing the project on the ESP32 system.
|
3
build.rs
Normal file
3
build.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
fn main() {
|
||||
embuild::espidf::sysenv::output();
|
||||
}
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[toolchain]
|
||||
channel = "esp"
|
10
sdkconfig.defaults
Normal file
10
sdkconfig.defaults
Normal file
|
@ -0,0 +1,10 @@
|
|||
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
|
||||
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
|
||||
|
||||
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
|
||||
# This allows to use 1 ms granularity for thread sleeps (10 ms by default).
|
||||
#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
|
5
src/config.rs
Normal file
5
src/config.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
pub(crate) const SNTP_SERVER: &'static str = "time.nist.gov";
|
||||
pub(crate) const T_ON: &'static str = "17:00";
|
||||
pub(crate) const T_OFF: &'static str = "09:00";
|
||||
pub(crate) const WIFI_SSID: &'static str = "Example";
|
||||
pub(crate) const WIFI_PASS: &'static str = "guest";
|
140
src/main.rs
Normal file
140
src/main.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
use std::default::Default;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use chrono::{NaiveTime, Utc};
|
||||
use chrono_tz::US::Pacific;
|
||||
use esp_idf_svc::{
|
||||
eventloop::EspSystemEventLoop,
|
||||
hal::{delay::FreeRtos, gpio::*, modem::Modem, peripheral::Peripheral, prelude::Peripherals},
|
||||
sntp::{EspSntp, SntpConf, SyncMode, SyncStatus},
|
||||
wifi::{AuthMethod, BlockingWifi, ClientConfiguration, Configuration, EspWifi},
|
||||
};
|
||||
use log::info;
|
||||
|
||||
mod config;
|
||||
|
||||
const SNTP_STATUS_POLL_INTVL_MS: u32 = 2000;
|
||||
const CONTROL_LOOP_INTVL_MS: u32 = 60000;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// It is necessary to call this function once. Otherwise some patches to
|
||||
// the runtime implemented by esp-idf-sys might not link properly. Refer
|
||||
// to https://github.com/esp-rs/esp-idf-template/issues/71.
|
||||
esp_idf_svc::sys::link_patches();
|
||||
|
||||
// Bind the log crate to the ESP Logging facilities
|
||||
esp_idf_svc::log::EspLogger::initialize_default();
|
||||
|
||||
let t_on = NaiveTime::parse_from_str(config::T_ON, "%H:%M")?;
|
||||
let t_off = NaiveTime::parse_from_str(config::T_OFF, "%H:%M")?;
|
||||
if t_on == t_off {
|
||||
bail!("t_on and t_off must have distinct values");
|
||||
}
|
||||
|
||||
let peripherals = Peripherals::take()?;
|
||||
|
||||
let mut switch = PinDriver::output(peripherals.pins.gpio7)?;
|
||||
switch.set_low()?;
|
||||
|
||||
let sysloop = EspSystemEventLoop::take()?;
|
||||
|
||||
// Dropping EspWifi shuts down the connection, so this variable must remain
|
||||
// in scope.
|
||||
let _wifi = init_wifi(peripherals.modem, sysloop);
|
||||
|
||||
// SNTP client will continue running in the background until this value is
|
||||
// dropped.
|
||||
let ntp = EspSntp::new(&SntpConf {
|
||||
servers: [config::SNTP_SERVER],
|
||||
sync_mode: SyncMode::Smooth,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
// get_sync_status() returns "Completed" one time and then reverts to
|
||||
// "Reset" on subsequent calls.
|
||||
while ntp.get_sync_status() != SyncStatus::Completed {
|
||||
info!("Waiting for SNTP to sync...");
|
||||
FreeRtos::delay_ms(SNTP_STATUS_POLL_INTVL_MS);
|
||||
}
|
||||
|
||||
// Main control loop:
|
||||
loop {
|
||||
let now = Utc::now().with_timezone(&Pacific);
|
||||
info!("Current time: {}", now);
|
||||
|
||||
let t = now.time();
|
||||
|
||||
let active = match (t_on < t_off, t > t_on, t > t_off) {
|
||||
// Active period falls within single day, and t falls between t_on
|
||||
// and t_off.
|
||||
(true, true, false) => true,
|
||||
// Active period crosses midnight, and t does not fall between
|
||||
// t_off and t_on.
|
||||
(false, true, true) => true,
|
||||
(false, false, false) => true,
|
||||
// All other cases.
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if active {
|
||||
switch.set_high()?;
|
||||
} else {
|
||||
switch.set_low()?;
|
||||
}
|
||||
|
||||
// TODO: enter low power mode ("light sleep" or "deep sleep") instead
|
||||
// of waiting in normal mode.
|
||||
FreeRtos::delay_ms(CONTROL_LOOP_INTVL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
/// Start WiFi module and connect to access point. Returns an error if either
|
||||
/// of WiFi startup or connection fail.
|
||||
fn init_wifi(
|
||||
modem: impl Peripheral<P = Modem> + 'static,
|
||||
sysloop: EspSystemEventLoop,
|
||||
) -> Result<Box<EspWifi<'static>>> {
|
||||
let auth_method = AuthMethod::WPA2Personal;
|
||||
let mut esp_wifi = EspWifi::new(modem, sysloop.clone(), None)?;
|
||||
let mut wifi = BlockingWifi::wrap(&mut esp_wifi, sysloop)?;
|
||||
wifi.set_configuration(&Configuration::Client(ClientConfiguration::default()))?;
|
||||
|
||||
info!("Starting wifi...");
|
||||
wifi.start()?;
|
||||
info!("Scanning...");
|
||||
let ap_infos = wifi.scan()?;
|
||||
let ours = ap_infos.into_iter().find(|a| a.ssid == config::WIFI_SSID);
|
||||
let channel = if let Some(ours) = ours {
|
||||
info!(
|
||||
"Found configured access point {} on channel {}",
|
||||
config::WIFI_SSID, ours.channel
|
||||
);
|
||||
Some(ours.channel)
|
||||
} else {
|
||||
info!(
|
||||
"Configured access point {} not found during scanning, will go with unknown channel",
|
||||
config::WIFI_SSID
|
||||
);
|
||||
None
|
||||
};
|
||||
wifi.set_configuration(&Configuration::Client(ClientConfiguration {
|
||||
ssid: config::WIFI_SSID
|
||||
.try_into()
|
||||
.expect("Could not parse the given SSID into WiFi config"),
|
||||
password: config::WIFI_PASS
|
||||
.try_into()
|
||||
.expect("Could not parse the given password into WiFi config"),
|
||||
channel,
|
||||
auth_method,
|
||||
..Default::default()
|
||||
}))?;
|
||||
|
||||
info!("Connecting wifi...");
|
||||
wifi.connect()?;
|
||||
info!("Waiting for DHCP lease...");
|
||||
wifi.wait_netif_up()?;
|
||||
let ip_info = wifi.wifi().sta_netif().get_ip_info()?;
|
||||
info!("Wifi DHCP info: {:?}", ip_info);
|
||||
|
||||
Ok(Box::new(esp_wifi))
|
||||
}
|
Loading…
Add table
Reference in a new issue