Skip to content

Commit 8a5b6fc

Browse files
authored
Merge pull request #4 from rust-community-pl/2-feat-mqtt-example
feat: esp32 mqtt quiz example project
2 parents 1b04742 + 4aea89f commit 8a5b6fc

File tree

17 files changed

+849
-0
lines changed

17 files changed

+849
-0
lines changed

.github/workflows/rust_ci.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ on:
1010
env:
1111
CARGO_TERM_COLOR: always
1212
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13+
WIFI_SSID: "test"
14+
WIFI_PASSWORD: "test"
15+
MQTT_BROKER_URL: "mqtts://example.com"
16+
MQTT_USER: "test"
17+
MQTT_PASSWORD: "test"
1318

1419
jobs:
1520
rust-checks:
@@ -20,6 +25,7 @@ jobs:
2025
matrix:
2126
dir:
2227
- spi_display_example
28+
- mqtt_example
2329
action:
2430
- command: build
2531
args: --release
@@ -41,4 +47,5 @@ jobs:
4147
- name: Run command
4248
run: |
4349
cd ${{ matrix.dir }}
50+
4451
cargo ${{ matrix.action.command }} ${{ matrix.action.args }}

mqtt_example/.cargo/config.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[build]
2+
target = "xtensa-esp32-espidf"
3+
4+
[target.xtensa-esp32-espidf]
5+
linker = "ldproxy"
6+
runner = "espflash flash --monitor"
7+
rustflags = ["--cfg", "espidf_time64"]
8+
9+
[unstable]
10+
build-std = ["std", "panic_abort"]
11+
12+
[env]
13+
MCU = "esp32"
14+
# Note: this variable is not used by the pio builder (`cargo build --features pio`)
15+
ESP_IDF_VERSION = "v5.3.2"

mqtt_example/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/.vscode
2+
/.embuild
3+
/target
4+
/Cargo.lock

mqtt_example/.idea/.gitignore

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mqtt_example/Cargo.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "esp32-mqtt"
3+
version = "0.1.0"
4+
authors = ["Jagoda Estera Ślązak <[email protected]>"]
5+
edition = "2021"
6+
resolver = "2"
7+
rust-version = "1.77"
8+
9+
[[bin]]
10+
name = "esp32-mqtt"
11+
harness = false # do not use the built in cargo test harness -> resolve rust-analyzer errors
12+
13+
[profile.release]
14+
opt-level = "s"
15+
16+
[profile.dev]
17+
debug = true # Symbols are nice and they don't increase the size on Flash
18+
opt-level = "z"
19+
20+
[features]
21+
default = []
22+
23+
experimental = ["esp-idf-svc/experimental"]
24+
25+
[dependencies]
26+
esp-idf-svc = { version = "0.51", features = ["alloc", "critical-section", "embassy-time-driver", "embassy-sync"] }
27+
embedded-graphics = "0.8.1"
28+
embedded-text = "0.7.2"
29+
mipidsi = "0.9.0"
30+
embedded-hal = "1.0.0"
31+
embedded-svc = "0.28.1"
32+
anyhow = "1.0.95"
33+
log = "0.4.25"
34+
35+
[build-dependencies]
36+
embuild = "0.33"

mqtt_example/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Esp32 quiz device

mqtt_example/build.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fn main() {
2+
embuild::espidf::sysenv::output();
3+
}

mqtt_example/rust-toolchain.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[toolchain]
2+
channel = "esp"

mqtt_example/sdkconfig.defaults

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Rust often needs a bit of an extra main task stack size compared to C (the default is 3K)
2+
CONFIG_ESP_MAIN_TASK_STACK_SIZE=8000
3+
4+
# Use this to set FreeRTOS kernel tick frequency to 1000 Hz (100 Hz by default).
5+
# This allows to use 1 ms granularity for thread sleeps (10 ms by default).
6+
#CONFIG_FREERTOS_HZ=1000
7+
8+
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
9+
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_FULL=y
10+
CONFIG_ESP_TLS_PSK_VERIFICATION=y

mqtt_example/src/battery.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use crate::event::DeviceEvent;
2+
use esp_idf_svc::hal::adc::attenuation::DB_11;
3+
use esp_idf_svc::hal::adc::oneshot::config::{AdcChannelConfig, Calibration};
4+
use esp_idf_svc::hal::adc::oneshot::{AdcChannelDriver, AdcDriver};
5+
use esp_idf_svc::hal::adc::Resolution;
6+
use esp_idf_svc::hal::gpio::ADCPin;
7+
use esp_idf_svc::hal::peripheral::Peripheral;
8+
use log::info;
9+
use std::sync::mpsc;
10+
use std::thread;
11+
use std::thread::{Scope, ScopedJoinHandle};
12+
use std::time::Duration;
13+
14+
mod capacity_curve {
15+
use log::info;
16+
17+
/// Naive const equivalent of `f.powi(n)`.
18+
/// Performance is not a problem, since this is evaluated only at compile time.
19+
/// Thanks to RFC 5314, basic floating point arithmetic is now stable in const
20+
const fn pow(f: f32, n: i32) -> f32 {
21+
let mut out: f32 = 1.0;
22+
let mut i = 0;
23+
while i < n {
24+
out *= f;
25+
i += 1;
26+
}
27+
out
28+
}
29+
30+
/// Maps battery voltage (in mV) to battery capacity (in %).
31+
/// Calculated from battery datasheet using https://mycurvefit.com.
32+
const fn to_capacity(voltage: u16) -> u8 {
33+
const A: f32 = -0.22;
34+
// `B` is rounded from ~24.62 in order to use
35+
// deterministic version of `f32::powi`
36+
// (which is much easier to implement than `f32::powf`)
37+
const B: i32 = 25; // ~24.62;
38+
const C: f32 = 3662.97;
39+
const D: f32 = 104.42;
40+
41+
let battery_level = (D + (A - D) / (1.0 + pow(voltage as f32 / C, B))) as i8;
42+
// `i8::clamp` as of March 2025 is not const
43+
if battery_level > 100 {
44+
return 100;
45+
} else if battery_level < 0 {
46+
return 0;
47+
}
48+
battery_level as u8
49+
}
50+
51+
/// `LOOKUP_RESOLUTION`mV per step
52+
const LOOKUP_RESOLUTION: u16 = 10;
53+
const LOOKUP_SIZE: usize = 121;
54+
const LOOKUP_START: u16 = 3000;
55+
const LOOKUP_END: u16 = LOOKUP_START + (LOOKUP_SIZE as u16 - 1) * LOOKUP_RESOLUTION;
56+
/// Maps voltage to capacity %
57+
/// Where `0` corresponds to `LOOKUP_START`mV
58+
/// and `LOOKUP_SIZE-1` corresponds to `LOOKUP_END`mV
59+
const CAPACITY_LOOKUP: [u8; LOOKUP_SIZE] = {
60+
let mut lookup: [u8; LOOKUP_SIZE] = [0; LOOKUP_SIZE];
61+
// as of March 2025, for loops are not allowed in const context
62+
let mut i = 0;
63+
while i < LOOKUP_SIZE {
64+
lookup[i] = to_capacity(LOOKUP_START + i as u16 * LOOKUP_RESOLUTION);
65+
i += 1;
66+
}
67+
lookup
68+
};
69+
70+
/// Battery is connected to ADC pin through a voltage divider:
71+
/// `V_ADC = R7/(R6+R7) * V_BAT`
72+
/// `ADC_MULTIPLIER = (R6+R7)/R7`
73+
const ADC_MULTIPLIER: u8 = 2;
74+
#[inline]
75+
fn get_battery_voltage(adc_reading: u16) -> u16 {
76+
adc_reading * ADC_MULTIPLIER as u16
77+
}
78+
79+
/// If battery is charging, the ADC reading will show very high readings, above 4.2V.
80+
/// This would cause the battery level to always display 100%, when charging.
81+
/// There is no way to change this behaviour without hardware modifications.
82+
/// We can still naively check for unusually high voltage and assume that battery is charging.
83+
const CHARGING_THRESHOLD: u16 = 4300;
84+
85+
/// Maps `adc_reading` to battery capacity (in %).
86+
/// Returns `None`, if battery is charging.
87+
#[inline]
88+
pub fn get_battery_level(adc_reading: u16) -> Option<u8> {
89+
let voltage = get_battery_voltage(adc_reading);
90+
info!("[Battery reader] Voltage: {:.2}V", voltage as f32 / 1000.0);
91+
if voltage > CHARGING_THRESHOLD {
92+
return None;
93+
}
94+
let lookup_index =
95+
((voltage.clamp(LOOKUP_START, LOOKUP_END) - LOOKUP_START) / LOOKUP_RESOLUTION) as usize;
96+
Some(CAPACITY_LOOKUP[lookup_index])
97+
}
98+
}
99+
100+
pub fn spawn_reader_thread<'scope, T>(
101+
scope: &'scope Scope<'scope, '_>,
102+
adc: impl Peripheral<P = T::Adc> + 'scope + Send,
103+
battery_pin: impl Peripheral<P = T> + 'scope + Send,
104+
sender: mpsc::Sender<DeviceEvent>,
105+
) -> Result<ScopedJoinHandle<'scope, ()>, std::io::Error>
106+
where
107+
T: ADCPin,
108+
{
109+
thread::Builder::new()
110+
.stack_size(8192)
111+
.spawn_scoped(scope, move || {
112+
info!("[Battery reader] Starting...");
113+
let adc_driver = AdcDriver::new(adc).unwrap();
114+
let mut bat_adc_channel = AdcChannelDriver::new(
115+
&adc_driver,
116+
battery_pin,
117+
&AdcChannelConfig {
118+
attenuation: DB_11,
119+
calibration: Calibration::Line,
120+
resolution: Resolution::Resolution12Bit,
121+
},
122+
)
123+
.unwrap();
124+
loop {
125+
if let Ok(reading) = adc_driver.read(&mut bat_adc_channel) {
126+
let battery_level = capacity_curve::get_battery_level(reading);
127+
sender
128+
.send(DeviceEvent::BatteryLevel {
129+
data: battery_level,
130+
})
131+
.ok();
132+
match battery_level {
133+
Some(_) => {
134+
thread::sleep(Duration::from_secs(60));
135+
}
136+
None => {
137+
thread::sleep(Duration::from_secs(2));
138+
}
139+
}
140+
}
141+
}
142+
})
143+
}

0 commit comments

Comments
 (0)