redesigned dir structure for rust 2018 style guide. made (untested) rtc funtion

This commit is contained in:
Philipp_EndevourOS 2025-07-28 17:25:39 +02:00
parent 4dda9548d3
commit 49027fed99
23 changed files with 168 additions and 390 deletions

1
Cargo.lock generated
View File

@ -285,6 +285,7 @@ dependencies = [
"cfg-if", "cfg-if",
"chrono", "chrono",
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
"embedded-hal-async",
"maybe-async-cfg", "maybe-async-cfg",
"paste", "paste",
] ]

View File

@ -5,7 +5,7 @@ edition = "2024"
[[bin]] [[bin]]
name = "fw-anwesenheit" name = "fw-anwesenheit"
path = "./src/bin/main.rs" path = "./src/main.rs"
test = false test = false
doctest = false doctest = false
bench = false bench = false
@ -60,7 +60,7 @@ edge-nal-embassy = { version = "0.6.0", features = ["log"] }
picoserve = { version = "0.16.0", features = ["embassy", "log"] } picoserve = { version = "0.16.0", features = ["embassy", "log"] }
embassy-sync = { version = "0.7.0", features = ["log"] } embassy-sync = { version = "0.7.0", features = ["log"] }
ds3231 = "0.3.0" ds3231 = { version = "0.3.0", features = ["async"] }
ws2812-spi = "0.5.1" ws2812-spi = "0.5.1"

View File

@ -1,31 +0,0 @@
extern crate alloc;
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::string::String;
pub struct Name {
pub first: String,
pub last: String,
}
pub struct IDMapping {
id_map: BTreeMap<TallyID, Name>,
}
impl IDMapping {
pub fn new() -> Self {
IDMapping {
id_map: BTreeMap::new(),
}
}
pub fn map(&self, id: &TallyID) -> Option<&Name> {
self.id_map.get(id)
}
pub fn add_mapping(&mut self, id: TallyID, name: Name) {
self.id_map.insert(id, name);
}
}

View File

@ -1,71 +0,0 @@
extern crate alloc;
use super::Date;
use super::IDMapping;
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
pub struct AttendanceDay {
date: Date,
ids: Vec<TallyID>,
}
impl AttendanceDay {
fn new(date: Date) -> Self {
Self {
date,
ids: Vec::new(),
}
}
// Add an ID to the day.
// Returns false if ID was already present
fn add_id(&mut self, id: TallyID) -> bool {
if self.ids.contains(&id) {
return false;
}
self.ids.push(id);
true
}
}
pub struct IDStore {
days: BTreeMap<Date, AttendanceDay>,
mapping: IDMapping,
}
impl IDStore {
pub fn new() -> Self {
IDStore {
days: BTreeMap::new(),
mapping: IDMapping::new(),
}
}
pub fn new_from_storage() -> Self {
// TODO: implement
todo!()
}
/// Add a new id for the current day
/// Returns false if ID is already present at the current day.
pub fn add_id(&mut self, id: TallyID) -> bool {
self.get_current_day().add_id(id)
}
/// Get the `AttendanceDay` of the current day
/// Creates a new if not exists
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
let current_day: Date = 1;
if self.days.contains_key(&current_day) {
return self.days.get_mut(&current_day).unwrap();
}
self.days
.insert(current_day, AttendanceDay::new(current_day));
self.days.get_mut(&current_day.clone()).unwrap()
}
}

View File

@ -1,8 +0,0 @@
mod id_mapping;
mod id_store;
pub use id_mapping::{IDMapping, Name};
pub use id_store::IDStore;
pub type TallyID = [u8; 8];
pub type Date = u64;

2
src/drivers.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod nfc_reader;
pub mod rtc;

29
src/drivers/nfc_reader.rs Normal file
View File

@ -0,0 +1,29 @@
use embassy_time::{Duration, Timer};
use esp_hal::{uart::Uart, Async};
use log::{ debug, info };
use crate::TallyPublisher;
#[embassy_executor::task]
pub async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: TallyPublisher) {
let mut uart_buffer = [0u8; 64];
loop {
debug!("Looking for NFC...");
match uart_device.read_async(&mut uart_buffer).await {
Ok(n) => {
let mut hex_str = heapless::String::<64>::new();
for byte in &uart_buffer[..n] {
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok();
}
info!("Read {n} bytes from UART: {hex_str}");
chan.publish([1, 0, 2, 5, 0, 8, 12, 15]).await;
}
Err(e) => {
log::error!("Error reading from UART: {e}");
}
}
Timer::after(Duration::from_millis(200)).await;
}
}

1
src/drivers/rtc.rs Normal file
View File

@ -0,0 +1 @@

View File

@ -55,8 +55,7 @@ impl<B: Buzzer, L: StatusLed> Feedback<B, L> {
let (buzzer_result, _) = join!(buzzer_handle, led_handle); let (buzzer_result, _) = join!(buzzer_handle, led_handle);
buzzer_result.unwrap_or_else(|err| { buzzer_result.unwrap_or_else(|err| { error!("Failed to buzz: {err}");
error!("Failed to buzz: {err}");
}); });
let _ = self.led_to_status(); let _ = self.led_to_status();

View File

@ -1,26 +0,0 @@
use anyhow::Result;
use std::time::Duration;
mod gpio_buzzer;
mod hotspot;
mod mock;
mod spi_led;
pub use gpio_buzzer::GPIOBuzzer;
pub use mock::{MockBuzzer, MockHotspot, MockLed};
pub use spi_led::SpiLed;
pub trait StatusLed {
fn turn_off(&mut self) -> Result<()>;
fn turn_on(&mut self, color: rgb::RGB8) -> Result<()>;
}
pub trait Buzzer {
fn modulated_tone(
&mut self,
frequency_hz: f64,
duration: Duration,
) -> impl Future<Output = Result<()>> + std::marker::Send;
}

View File

3
src/init.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod hardware;
pub mod network;
pub mod wifi;

View File

@ -1,6 +1,13 @@
use core::slice::RChunks;
use ds3231::{DS3231, Alarm1Config};
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_net::Stack; use embassy_net::{driver, Stack};
use esp_hal::peripherals::{self, GPIO0, GPIO1, GPIO3, GPIO4, GPIO5, GPIO6, GPIO7, GPIO22, GPIO23, I2C0, UART1}; use embassy_sync::mutex::Mutex;
use esp_hal::config;
use esp_hal::gpio::{Input, Pull};
use esp_hal::i2c::master::Config;
use esp_hal::peripherals::{self, GPIO0, GPIO1, GPIO3, GPIO4, GPIO5, GPIO6, GPIO7, GPIO21, GPIO22, GPIO23, I2C0, UART1};
use esp_hal::time::Rate; use esp_hal::time::Rate;
use esp_hal::{ use esp_hal::{
Async, Async,
@ -13,8 +20,10 @@ use esp_hal::{
use esp_println::logger::init_logger; use esp_println::logger::init_logger;
use log::error; use log::error;
mod network; use crate::init::wifi;
mod wifi; use crate::init::network;
const RTC_ADDRESS: u8 = 0x57;
#[panic_handler] #[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! { fn panic(_: &core::panic::PanicInfo) -> ! {
@ -23,7 +32,7 @@ fn panic(_: &core::panic::PanicInfo) -> ! {
esp_bootloader_esp_idf::esp_app_desc!(); esp_bootloader_esp_idf::esp_app_desc!();
pub async fn hardware_init(spawner: &mut Spawner) -> (Uart<'static, Async>, Stack<'static>) { pub async fn hardware_init(spawner: &mut Spawner) -> (Uart<'static, Async>, Stack<'static>, I2c<'static, Async>, GPIO21<'static>) {
let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max()); let config = esp_hal::Config::default().with_cpu_clock(CpuClock::max());
let peripherals = esp_hal::init(config); let peripherals = esp_hal::init(config);
@ -48,9 +57,12 @@ pub async fn hardware_init(spawner: &mut Spawner) -> (Uart<'static, Async>, Stac
let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23); let i2c_device = setup_i2c(peripherals.I2C0, peripherals.GPIO22, peripherals.GPIO23);
//RTC Interrupt pin
let sqw_pin = peripherals.GPIO21;
//TODO change to get I2C device back / maybe init for each protocol //TODO change to get I2C device back / maybe init for each protocol
(uart_device, stack) (uart_device, stack, i2c_device, sqw_pin)
} }
// Initialize the level shifter for the NFC reader and LED (output-enable (OE) input is low, all outputs are placed in the high-impedance (Hi-Z) state) // Initialize the level shifter for the NFC reader and LED (output-enable (OE) input is low, all outputs are placed in the high-impedance (Hi-Z) state)
@ -80,17 +92,40 @@ fn setup_i2c(
sda: GPIO22<'static>, sda: GPIO22<'static>,
scl: GPIO23<'static>, scl: GPIO23<'static>,
) -> I2c<'static, Async> { ) -> I2c<'static, Async> {
let config = esp_hal::i2c::master::Config::default().with_frequency(Rate::from_khz(400));
let i2c_device = I2c::new(i2c0, config); let config = Config::default().with_frequency(Rate::from_khz(400));
match i2c_device { let i2c = match I2c::new(i2c0, config) {
Ok(block) => block.with_sda(sda).with_scl(scl).into_async(), Ok(i2c) => i2c.with_sda(sda).with_scl(scl).into_async(),
Err(e) => { Err(e) => {
error!("Failed to initialize I2C: {e}"); error!("Failed to initialize I2C: {:?}", e);
panic!(); panic!();
} }
} };
i2c
} }
pub async fn rtc_init_iterrupt(sqw_pin: GPIO21<'static>) -> Input<'static> {
let config = esp_hal::gpio::InputConfig::default().with_pull(Pull::Up);
let mut sqw_interrupt = Input::new(sqw_pin, config);
sqw_interrupt
}
pub async fn rtc_config(i2c: I2c<'static, Async>) -> DS3231<I2c<'static, Async>> {
let mut rtc: DS3231<I2c<'static, Async>> = DS3231::new(i2c, RTC_ADDRESS);
let daily_alarm = Alarm1Config::AtTime {
hours: 9,
minutes: 30,
seconds: 0,
is_pm: None, // 24-hour mode
};
if let Err(e) = rtc.set_alarm1(&daily_alarm).await {
error!("Failed to configure RTC: {:?}", e);
panic!();
}
rtc
}
fn setup_spi_led() { fn setup_spi_led() {
} }

View File

@ -6,15 +6,14 @@
use embassy_executor::Spawner; use embassy_executor::Spawner;
use embassy_net::Stack; use embassy_net::Stack;
use embassy_sync::{ use embassy_sync::{
blocking_mutex::raw::NoopRawMutex, blocking_mutex::raw::{CriticalSectionRawMutex, NoopRawMutex}, mutex::Mutex, pubsub::{
pubsub::{
PubSubChannel, Publisher, PubSubChannel, Publisher,
WaitResult::{Lagged, Message}, WaitResult::{Lagged, Message},
}, }
}; };
use embassy_time::{Duration, Timer}; use embassy_time::{Duration, Timer};
use esp_alloc::psram_allocator; use esp_alloc::psram_allocator;
use esp_hal::Async; use esp_hal::{i2c, peripherals, Async};
use esp_hal::uart::Uart; use esp_hal::uart::Uart;
use log::{debug, info}; use log::{debug, info};
use static_cell::make_static; use static_cell::make_static;
@ -22,17 +21,18 @@ use static_cell::make_static;
use crate::{store::TallyID, webserver::start_webserver}; use crate::{store::TallyID, webserver::start_webserver};
mod init; mod init;
mod drivers;
mod store; mod store;
mod webserver; mod webserver;
type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>; type TallyChannel = PubSubChannel<NoopRawMutex, TallyID, 8, 2, 1>;
type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>; type TallyPublisher = Publisher<'static, NoopRawMutex, TallyID, 8, 2, 1>;
static mut UTC_TIME: u64 = 0; static UTC_TIME: Mutex<CriticalSectionRawMutex, u64> = Mutex::new(0);
#[esp_hal_embassy::main] #[esp_hal_embassy::main]
async fn main(mut spawner: Spawner) { async fn main(mut spawner: Spawner) {
let (uart_device, stack) = init::hardware_init(&mut spawner).await; let (uart_device, stack, i2c, sqw_pin) = init::hardware::hardware_init(&mut spawner).await;
wait_for_stack_up(stack).await; wait_for_stack_up(stack).await;
@ -42,7 +42,8 @@ async fn main(mut spawner: Spawner) {
let publisher = chan.publisher().unwrap(); let publisher = chan.publisher().unwrap();
spawner.must_spawn(rfid_reader_task(uart_device, publisher)); spawner.must_spawn(drivers::nfc_reader::rfid_reader_task(uart_device, publisher));
spawner.must_spawn(rtc_task(i2c, sqw_pin));
let mut sub = chan.subscriber().unwrap(); let mut sub = chan.subscriber().unwrap();
loop { loop {
@ -68,24 +69,22 @@ async fn wait_for_stack_up(stack: Stack<'static>) {
} }
#[embassy_executor::task] #[embassy_executor::task]
async fn rfid_reader_task(mut uart_device: Uart<'static, Async>, chan: TallyPublisher) { async fn rtc_task(
let mut uart_buffer = [0u8; 64]; mut i2c: i2c::master::I2c<'static, Async>,
sqw_pin: peripherals::GPIO21<'static>,
) {
let mut rtc_interrupt = init::hardware::rtc_init_iterrupt(sqw_pin).await;
let mut rtc = init::hardware::rtc_config(i2c).await;
loop { loop {
debug!("Looking for NFC..."); rtc_interrupt.wait_for_falling_edge().await;
match uart_device.read_async(&mut uart_buffer).await { debug!("RTC interrupt triggered");
Ok(n) => { if let Ok(datetime) = rtc.datetime().await {
let mut hex_str = heapless::String::<64>::new(); let mut utc_time = UTC_TIME.lock().await;
for byte in &uart_buffer[..n] { *utc_time = datetime.and_utc().timestamp() as u64;
core::fmt::Write::write_fmt(&mut hex_str, format_args!("{:02X} ", byte)).ok(); info!("RTC updated UTC_TIME: {}", *utc_time);
} else {
info!("Failed to read RTC datetime");
} }
info!("Read {n} bytes from UART: {hex_str}");
chan.publish([1, 0, 2, 5, 0, 8, 12, 15]).await;
}
Err(e) => {
log::error!("Error reading from UART: {e}");
}
}
Timer::after(Duration::from_millis(200)).await;
} }
} }

View File

@ -1,22 +1,22 @@
use crate::tally_id::TallyID; extern crate alloc;
use serde::{Deserialize, Serialize};
use std::collections::HashMap; use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::string::String;
#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct Name { pub struct Name {
pub first: String, pub first: String,
pub last: String, pub last: String,
} }
#[derive(Deserialize, Serialize, Clone)]
pub struct IDMapping { pub struct IDMapping {
id_map: HashMap<TallyID, Name>, id_map: BTreeMap<TallyID, Name>,
} }
impl IDMapping { impl IDMapping {
pub fn new() -> Self { pub fn new() -> Self {
IDMapping { IDMapping {
id_map: HashMap::new(), id_map: BTreeMap::new(),
} }
} }
@ -29,48 +29,3 @@ impl IDMapping {
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn basic() {
let mut map = IDMapping::new();
let id1 = TallyID("A2Fb44".to_owned());
let name1 = Name {
first: "Max".to_owned(),
last: "Mustermann".to_owned(),
};
map.add_mapping(id1.clone(), name1.clone());
let res = map.map(&id1);
assert_eq!(res, Some(&name1));
}
#[test]
fn multiple() {
let mut map = IDMapping::new();
let id1 = TallyID("A2Fb44".to_owned());
let name1 = Name {
first: "Max".to_owned(),
last: "Mustermann".to_owned(),
};
let id2 = TallyID("7D3DF5B5".to_owned());
let name2 = Name {
first: "First".to_owned(),
last: "Last".to_owned(),
};
map.add_mapping(id1.clone(), name1.clone());
map.add_mapping(id2.clone(), name2.clone());
let res = map.map(&id1);
assert_eq!(res, Some(&name1));
let res = map.map(&id2);
assert_eq!(res, Some(&name2));
}
}

View File

@ -1,123 +1,20 @@
use anyhow::{Result, anyhow}; extern crate alloc;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use tokio::fs;
use crate::{store::IDMapping, tally_id::TallyID}; use super::Date;
use super::IDMapping;
use super::TallyID;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
/// Represents a single day that IDs can attend
#[derive(Deserialize, Serialize)]
pub struct AttendanceDay { pub struct AttendanceDay {
date: String, date: Date,
ids: Vec<TallyID>, ids: Vec<TallyID>,
} }
/// Stores all the days
#[derive(Deserialize, Serialize)]
pub struct IDStore {
days: HashMap<String, AttendanceDay>,
pub mapping: IDMapping,
}
impl IDStore {
pub fn new() -> Self {
IDStore {
days: HashMap::new(),
mapping: IDMapping::new(),
}
}
/// Creats a new `IDStore` from a json file
pub async fn new_from_json(filepath: &str) -> Result<Self> {
let read_string = fs::read_to_string(filepath).await?;
Ok(serde_json::from_str(&read_string)?)
}
/// Add a new id for the current day
/// Returns false if ID is already present at the current day.
pub fn add_id(&mut self, id: TallyID) -> bool {
self.get_current_day().add_id(id)
}
/// Get the `AttendanceDay` of the current day
/// Creates a new if not exists
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
let current_day = get_day_str();
if self.days.contains_key(&current_day) {
return self.days.get_mut(&current_day).unwrap();
}
self.days.insert(
current_day.clone(),
AttendanceDay::new(&current_day.clone()),
);
self.days.get_mut(&current_day.clone()).unwrap()
}
/// Writes the store to a json file
pub async fn export_json(&self, filepath: &str) -> Result<()> {
fs::write(filepath, serde_json::to_string(&self)?).await?;
Ok(())
}
/// Export the store to a csv file.
/// With days in the rows and IDs in the collum.
pub fn export_csv(&self) -> Result<String> {
let mut csv = String::new();
let seperator = ";";
let mut user_ids: HashSet<TallyID> = HashSet::new();
for day in self.days.values() {
for id in day.ids.iter() {
user_ids.insert(id.clone());
}
}
let mut user_ids: Vec<TallyID> = user_ids.into_iter().collect();
user_ids.sort();
let mut days: Vec<String> = self.days.keys().cloned().collect();
days.sort();
let header = days.join(seperator);
csv.push_str(&format!(
"ID{seperator}Nachname{seperator}Vorname{seperator}{header}\n"
));
for user_id in user_ids.iter() {
let id = &user_id.0.to_string();
let name = self.mapping.map(user_id);
let firstname = name.map(|e| e.first.clone()).unwrap_or("".to_owned());
let lastname = name.map(|e| e.last.clone()).unwrap_or("".to_owned());
csv.push_str(&format!("{id}{seperator}{lastname}{seperator}{firstname}"));
for day in days.iter() {
let was_there: bool = self
.days
.get(day)
.ok_or(anyhow!("Failed to access day"))?
.ids
.contains(user_id);
if was_there {
csv.push_str(&format!("{seperator}x"));
} else {
csv.push_str(seperator);
}
}
csv.push('\n');
}
Ok(csv)
}
}
impl AttendanceDay { impl AttendanceDay {
fn new(day: &str) -> Self { fn new(date: Date) -> Self {
Self { Self {
date: day.to_owned(), date,
ids: Vec::new(), ids: Vec::new(),
} }
} }
@ -133,7 +30,42 @@ impl AttendanceDay {
} }
} }
fn get_day_str() -> String { pub struct IDStore {
let now = chrono::offset::Local::now(); days: BTreeMap<Date, AttendanceDay>,
now.format("%Y-%m-%d").to_string() mapping: IDMapping,
}
impl IDStore {
pub fn new() -> Self {
IDStore {
days: BTreeMap::new(),
mapping: IDMapping::new(),
}
}
pub fn new_from_storage() -> Self {
// TODO: implement
todo!()
}
/// Add a new id for the current day
/// Returns false if ID is already present at the current day.
pub fn add_id(&mut self, id: TallyID) -> bool {
self.get_current_day().add_id(id)
}
/// Get the `AttendanceDay` of the current day
/// Creates a new if not exists
pub fn get_current_day(&mut self) -> &mut AttendanceDay {
let current_day: Date = 1;
if self.days.contains_key(&current_day) {
return self.days.get_mut(&current_day).unwrap();
}
self.days
.insert(current_day, AttendanceDay::new(current_day));
self.days.get_mut(&current_day.clone()).unwrap()
}
} }

View File

@ -1,5 +1,8 @@
mod id_store;
mod id_mapping; mod id_mapping;
mod id_store;
pub use id_mapping::{IDMapping, Name};
pub use id_store::IDStore; pub use id_store::IDStore;
pub use id_mapping::{IDMapping,Name};
pub type TallyID = [u8; 8];
pub type Date = u64;

View File

@ -1,45 +0,0 @@
use std::{
cmp::Ordering,
fmt::Display,
hash::{Hash, Hasher},
};
use serde::{Deserialize, Serialize};
/// Represents the ID that is stored on the Tally
/// Is case-insensitive.
/// While any string can be a ID, most IDs are going to be a hex string.
#[derive(Deserialize, Serialize, Clone)]
pub struct TallyID(pub String);
impl PartialEq for TallyID {
fn eq(&self, other: &Self) -> bool {
self.0.eq_ignore_ascii_case(&other.0)
}
}
impl Hash for TallyID {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.to_uppercase().hash(state);
}
}
impl Eq for TallyID {}
impl Ord for TallyID {
fn cmp(&self, other: &Self) -> Ordering {
self.0.to_uppercase().cmp(&other.0.to_uppercase())
}
}
impl PartialOrd for TallyID {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Display for TallyID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.to_uppercase())
}
}