Storage progress

This commit is contained in:
Jakub Hlusička 2026-01-25 01:45:25 +01:00
parent 9aa5430851
commit a3a95b179b
4 changed files with 161 additions and 16 deletions

12
firmware/Cargo.lock generated
View file

@ -11,6 +11,7 @@ dependencies = [
"bytemuck", "bytemuck",
"cc", "cc",
"cfg-if", "cfg-if",
"chrono",
"const-gen", "const-gen",
"critical-section", "critical-section",
"data-encoding-macro", "data-encoding-macro",
@ -45,10 +46,12 @@ dependencies = [
"panic-rtt-target", "panic-rtt-target",
"password-hash 0.1.0", "password-hash 0.1.0",
"pastey 0.2.1", "pastey 0.2.1",
"postcard",
"printf-compat", "printf-compat",
"rand_core 0.9.5", "rand_core 0.9.5",
"rmk", "rmk",
"rtt-target", "rtt-target",
"serde",
"sha2", "sha2",
"slint", "slint",
"slint-build", "slint-build",
@ -818,6 +821,7 @@ dependencies = [
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link",
] ]
@ -1836,6 +1840,12 @@ dependencies = [
"embedded-hal 1.0.0", "embedded-hal 1.0.0",
] ]
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]] [[package]]
name = "embedded-io" name = "embedded-io"
version = "0.6.1" version = "0.6.1"
@ -5670,6 +5680,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [ dependencies = [
"cobs", "cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"heapless 0.7.17", "heapless 0.7.17",
"postcard-derive", "postcard-derive",
"serde", "serde",

View file

@ -80,6 +80,10 @@ password-hash = { path = "../password-hash", default-features = false }
hmac = "0.12.1" hmac = "0.12.1"
data-encoding-macro = "0.1.19" data-encoding-macro = "0.1.19"
embedded-storage-async = "0.4.1" embedded-storage-async = "0.4.1"
postcard = { version = "1.1", default-features = false, features = ["alloc", "postcard-derive"] } # TODO: defmt
serde = { version = "1.0", default-features = false, features = ["derive"] }
# serde_with = { version = "3.16", default-features = false, features = ["alloc", "macros"] }
chrono = { version = "0.4.43", default-features = false, features = ["alloc", "serde"] } # TODO: defmt
# Crates for serial UART CLI # Crates for serial UART CLI
embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] } embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] }

View file

@ -126,11 +126,11 @@ impl AcidDatabase {
#[cfg(feature = "format-db")] #[cfg(feature = "format-db")]
{ {
warn!("Formatting EKV database..."); log::warn!("Formatting EKV database...");
db.format() db.format()
.await .await
.unwrap_or_else(|error| panic!("Failed to format the EKV database: {error:?}")); .unwrap_or_else(|error| panic!("Failed to format the EKV database: {error:?}"));
warn!("EKV database formatted successfully."); log::warn!("EKV database formatted successfully.");
} }
match db.mount().await { match db.mount().await {
@ -146,7 +146,7 @@ type DbPathSegment<'a> = Cow<'a, str>;
type DbPathBuf<'a> = Vec<DbPathSegment<'a>>; type DbPathBuf<'a> = Vec<DbPathSegment<'a>>;
type DbPath<'a> = [DbPathSegment<'a>]; type DbPath<'a> = [DbPathSegment<'a>];
struct DbKey { pub struct DbKey {
/// Segments separated by `0x00`, with the whole thing suffixed with `[0x00, 0xFF]`. /// Segments separated by `0x00`, with the whole thing suffixed with `[0x00, 0xFF]`.
bytes: Vec<u8>, bytes: Vec<u8>,
} }
@ -160,12 +160,12 @@ impl Deref for DbKey {
} }
impl DbKey { impl DbKey {
fn from_raw(mut key: Vec<u8>) -> Self { pub fn from_raw(mut key: Vec<u8>) -> Self {
key.extend_from_slice(&[0x00, 0xFF]); key.extend_from_slice(&[0x00, 0xFF]);
Self { bytes: key } Self { bytes: key }
} }
fn new<'a>(path: impl IntoIterator<Item = DbPathSegment<'a>>) -> Self { pub fn new<'a>(path: impl IntoIterator<Item = DbPathSegment<'a>>) -> Self {
// Null bytes are not allowed within path segments, and will cause a panic. // Null bytes are not allowed within path segments, and will cause a panic.
// Bytes of `0xFF` cannot appear in valid UTF-8. // Bytes of `0xFF` cannot appear in valid UTF-8.
// The byte vector stored in a `DbKey` is formed by interspersing the segments with `0x00`, and suffixing the key with `[0x00, 0xFF]`. // The byte vector stored in a `DbKey` is formed by interspersing the segments with `0x00`, and suffixing the key with `[0x00, 0xFF]`.
@ -188,20 +188,17 @@ impl DbKey {
bytes.push(0x00); bytes.push(0x00);
} }
if let Some(last_byte) = bytes.last_mut() { assert!(!bytes.is_empty(), "An empty path is not a valid path.");
bytes.push(0xFF); bytes.push(0xFF);
} else {
panic!("An empty path is not a valid path.");
}
DbKey { bytes } DbKey { bytes }
} }
fn range_of_children(&self) -> Range<&[u8]> { pub fn range_of_children(&self) -> Range<&[u8]> {
(&self.bytes[0..(self.bytes.len() - 1)])..(&self.bytes[..]) (&self.bytes[0..(self.bytes.len() - 1)])..(&self.bytes[..])
} }
fn segments(&self) -> impl Iterator<Item = DbPathSegment<'_>> { pub fn segments(&self) -> impl Iterator<Item = DbPathSegment<'_>> {
struct SegmentIterator<'a> { struct SegmentIterator<'a> {
rest: &'a [u8], rest: &'a [u8],
} }

View file

@ -5,7 +5,15 @@ use core::{
ops::{Deref, DerefMut, Range}, ops::{Deref, DerefMut, Range},
}; };
use alloc::{borrow::Cow, boxed::Box, ffi::CString, vec::Vec}; use alloc::{
borrow::Cow,
boxed::Box,
ffi::CString,
string::{String, ToString},
vec,
vec::Vec,
};
use chrono::{DateTime, NaiveDateTime};
use ekv::{Database, MountError, flash::PageID}; use ekv::{Database, MountError, flash::PageID};
use embassy_embedded_hal::{adapter::BlockingAsync, flash::partition::Partition}; use embassy_embedded_hal::{adapter::BlockingAsync, flash::partition::Partition};
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, RawMutex}; use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, RawMutex};
@ -16,14 +24,17 @@ use esp_storage::FlashStorage;
use itertools::Itertools; use itertools::Itertools;
use log::{info, warn}; use log::{info, warn};
use rmk::futures::TryFutureExt; use rmk::futures::TryFutureExt;
use serde::{Deserialize, Serialize};
use slint::SharedString; use slint::SharedString;
use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreKeyPurpose, SpectreUserKey}; use spectre_api_sys::{
SpectreAlgorithm, SpectreCounter, SpectreKeyPurpose, SpectreResultType, SpectreUserKey,
};
#[cfg(feature = "limit-fps")] #[cfg(feature = "limit-fps")]
use crate::FRAME_DURATION_MIN; use crate::FRAME_DURATION_MIN;
use crate::{ use crate::{
SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
db::{AcidDatabase, PartitionAcid}, db::{AcidDatabase, DbKey, DbPathSpectreUsers, PartitionAcid},
ui::backend::SlintBackend, ui::backend::SlintBackend,
util::DurationExt, util::DurationExt,
}; };
@ -33,10 +44,131 @@ pub mod window_adapter;
slint::include_modules!(); slint::include_modules!();
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
struct SpectreUsersConfig {
users: Vec<SpectreUserConfig>,
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
struct SpectreUserConfig {
username: String,
}
trait ReprConvert: Copy {
type Repr: Copy;
fn into_repr(self) -> Self::Repr;
fn from_repr(repr: Self::Repr) -> Self;
}
impl ReprConvert for SpectreAlgorithm {
type Repr = u32;
fn from_repr(repr: Self::Repr) -> Self {
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
}
fn into_repr(self) -> Self::Repr {
self as Self::Repr
}
}
impl ReprConvert for SpectreResultType {
type Repr = u32;
fn from_repr(repr: Self::Repr) -> Self {
unsafe { core::mem::transmute::<Self::Repr, Self>(repr) }
}
fn into_repr(self) -> Self::Repr {
self as Self::Repr
}
}
mod with_repr {
use serde::{Deserialize, Deserializer, Serializer};
use super::ReprConvert;
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: ReprConvert<Repr = u32>,
S: Serializer,
{
serializer.serialize_u32(value.into_repr())
}
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: ReprConvert<Repr = u32>,
D: Deserializer<'de>,
{
<u32 as Deserialize>::deserialize(deserializer).map(T::from_repr)
}
}
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
struct SpectreSiteConfig {
#[serde(with = "with_repr")]
algorithm: SpectreAlgorithm,
counter: SpectreCounter::Type,
#[serde(rename = "type")]
#[serde(with = "with_repr")]
result_type: SpectreResultType,
password: Option<String>,
#[serde(with = "with_repr")]
login_type: SpectreResultType,
login_name: Option<String>,
uses: u32,
last_used: NaiveDateTime,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct SpectreSite {
username: String,
site_name: String,
config: SpectreSiteConfig,
}
#[embassy_executor::task] #[embassy_executor::task]
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) { pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
let db = AcidDatabase::mount(flash_part_acid).await; let db = AcidDatabase::mount(flash_part_acid).await;
let write = db.write_transaction().await;
let value = SpectreUsersConfig {
users: vec![SpectreUserConfig {
username: "test".to_string(),
}],
};
{
let mut write = db.write_transaction().await;
write
.write(
&DbKey::new(DbPathSpectreUsers),
&postcard::to_allocvec(&value).unwrap(),
)
.await
.unwrap();
write.commit().await.unwrap();
}
let read_value = {
let read = db.read_transaction().await;
// TODO: https://github.com/embassy-rs/ekv/issues/20
let mut buffer = Vec::new();
let length = read
.read(&DbKey::new(DbPathSpectreUsers), &mut buffer)
.await
.unwrap();
buffer.resize(length, 0);
read.read(&DbKey::new(DbPathSpectreUsers), &mut buffer)
.await
.unwrap();
postcard::from_bytes::<SpectreUsersConfig>(&buffer).unwrap()
};
assert_eq!(value, read_value, "values do not match");
// TODO: // TODO:
// * Store a config as a versioned postcard-serialized struct // * Store a config as a versioned postcard-serialized struct