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

View file

@ -80,6 +80,10 @@ password-hash = { path = "../password-hash", default-features = false }
hmac = "0.12.1"
data-encoding-macro = "0.1.19"
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
embedded-cli = { version = "0.2.1", default-features = false, features = ["help", "macros"] }

View file

@ -126,11 +126,11 @@ impl AcidDatabase {
#[cfg(feature = "format-db")]
{
warn!("Formatting EKV database...");
log::warn!("Formatting EKV database...");
db.format()
.await
.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 {
@ -146,7 +146,7 @@ type DbPathSegment<'a> = Cow<'a, str>;
type DbPathBuf<'a> = Vec<DbPathSegment<'a>>;
type DbPath<'a> = [DbPathSegment<'a>];
struct DbKey {
pub struct DbKey {
/// Segments separated by `0x00`, with the whole thing suffixed with `[0x00, 0xFF]`.
bytes: Vec<u8>,
}
@ -160,12 +160,12 @@ impl Deref for 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]);
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.
// 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]`.
@ -188,20 +188,17 @@ impl DbKey {
bytes.push(0x00);
}
if let Some(last_byte) = bytes.last_mut() {
bytes.push(0xFF);
} else {
panic!("An empty path is not a valid path.");
}
assert!(!bytes.is_empty(), "An empty path is not a valid path.");
bytes.push(0xFF);
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[..])
}
fn segments(&self) -> impl Iterator<Item = DbPathSegment<'_>> {
pub fn segments(&self) -> impl Iterator<Item = DbPathSegment<'_>> {
struct SegmentIterator<'a> {
rest: &'a [u8],
}

View file

@ -5,7 +5,15 @@ use core::{
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 embassy_embedded_hal::{adapter::BlockingAsync, flash::partition::Partition};
use embassy_sync::blocking_mutex::raw::{CriticalSectionRawMutex, RawMutex};
@ -16,14 +24,17 @@ use esp_storage::FlashStorage;
use itertools::Itertools;
use log::{info, warn};
use rmk::futures::TryFutureExt;
use serde::{Deserialize, Serialize};
use slint::SharedString;
use spectre_api_sys::{SpectreAlgorithm, SpectreCounter, SpectreKeyPurpose, SpectreUserKey};
use spectre_api_sys::{
SpectreAlgorithm, SpectreCounter, SpectreKeyPurpose, SpectreResultType, SpectreUserKey,
};
#[cfg(feature = "limit-fps")]
use crate::FRAME_DURATION_MIN;
use crate::{
SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
db::{AcidDatabase, PartitionAcid},
db::{AcidDatabase, DbKey, DbPathSpectreUsers, PartitionAcid},
ui::backend::SlintBackend,
util::DurationExt,
};
@ -33,10 +44,131 @@ pub mod window_adapter;
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]
pub async fn run_renderer_task(backend: SlintBackend, flash_part_acid: PartitionAcid) {
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:
// * Store a config as a versioned postcard-serialized struct