diff --git a/firmware/acid-firmware/src/keymap.rs b/firmware/acid-firmware/src/keymap.rs index 44aca3d..5413158 100644 --- a/firmware/acid-firmware/src/keymap.rs +++ b/firmware/acid-firmware/src/keymap.rs @@ -2,20 +2,26 @@ use core::alloc::Layout; use core::fmt::Debug; use core::slice; +use alloc::alloc::Allocator; use alloc::boxed::Box; -use alloc::collections::btree_map::BTreeMap; +use alloc::collections::btree_map::{BTreeMap, Entry}; use alloc::string::String; +use alloc::sync::Arc; +use alloc::vec::Vec; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::channel::Channel; use embassy_time::Instant; -use esp_alloc::MemoryCapability; +use esp_alloc::{EspHeap, MemoryCapability}; +use itertools::Itertools; use log::{debug, info, warn}; use rmk::descriptor::KeyboardReport; +use rmk::futures::FutureExt; use rmk::hid::Report; use rmk::types::action::{Action, KeyAction}; -use rmk::{a, k, layer}; +use rmk::{a, join_all, k, layer}; use slint::platform::Key; -use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, Status}; +use static_cell::StaticCell; +use xkbcommon::xkb::{self, FeedResult, KeyDirection, Keysym, ModMask, Status}; use crate::matrix::{MATRIX_COLS, MATRIX_ROWS}; use crate::util::DurationExt; @@ -25,6 +31,7 @@ use crate::{KEYBOARD_REPORT_PROXY, PSRAM_ALLOCATOR}; pub const NUM_LAYER: usize = 1; pub static KEY_MESSAGE_CHANNEL: Channel = Channel::new(); +pub static OUTPUT_STRING_CHANNEL: Channel = Channel::new(); pub struct KeyMessage { pub keysym: Keysym, @@ -83,13 +90,16 @@ pub fn create_hid_report_interceptor() -> impl Future { let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); info!("Loading XKB keymap..."); let instant_start = Instant::now(); - let keymap = xkb::Keymap::new_from_string( - &context, - keymap_string_buffer, - xkb::KEYMAP_FORMAT_TEXT_V1, - xkb::KEYMAP_COMPILE_NO_FLAGS, - ) - .unwrap(); + let keymap = Arc::new_in( + xkb::Keymap::new_from_string( + &context, + keymap_string_buffer, + xkb::KEYMAP_FORMAT_TEXT_V1, + xkb::KEYMAP_COMPILE_NO_FLAGS, + ) + .unwrap(), + &PSRAM_ALLOCATOR, + ); let duration = Instant::now().duration_since(instant_start); info!( "XKB keymap loaded successfully! Took {} seconds.", @@ -97,19 +107,69 @@ pub fn create_hid_report_interceptor() -> impl Future { ); info!("Loading XKB compose map..."); let instant_start = Instant::now(); - let compose_table = xkb::compose::Table::new_from_buffer( - &context, - COMPOSE_MAP_STRING, - COMPOSE_MAP_LOCALE, - xkb::compose::FORMAT_TEXT_V1, - xkb::compose::COMPILE_NO_FLAGS, - ) - .unwrap(); + let compose_table = Arc::new_in( + xkb::compose::Table::new_from_buffer( + &context, + COMPOSE_MAP_STRING, + COMPOSE_MAP_LOCALE, + xkb::compose::FORMAT_TEXT_V1, + xkb::compose::COMPILE_NO_FLAGS, + ) + .unwrap(), + &PSRAM_ALLOCATOR, + ); let duration = Instant::now().duration_since(instant_start); info!( "XKB compose map loaded successfully! Took {} seconds.", duration.display_as_secs() ); + + async move { + join_all![ + send_keycodes_to_slint(keymap.clone(), compose_table.clone()), + send_strings_to_host(keymap, compose_table) + ] + .await; + } +} + +async fn send_strings_to_host( + keymap: Arc, + compose_table: Arc, +) { + loop { + let string = OUTPUT_STRING_CHANNEL.receive().await; + let keycodes = string_to_hid_keycodes(&keymap, &compose_table, &string); + + warn!("keycodes for {string:?}: {keycodes:02x?}"); + + if let Ok(keycodes) = keycodes { + for keycode_chunk in keycodes.chunks(6) { + rmk::channel::KEYBOARD_REPORT_RECEIVER + .send(Report::KeyboardReport(KeyboardReport { + modifier: Default::default(), // TODO + reserved: Default::default(), // TODO + leds: Default::default(), + keycodes: { + // Send multiple keycodes at the same time. + let mut keycodes = [0; _]; + keycodes[0..keycode_chunk.len()].copy_from_slice(keycode_chunk); + keycodes + }, + })) + .await; + rmk::channel::KEYBOARD_REPORT_RECEIVER + .send(Report::KeyboardReport(KeyboardReport::default())) + .await; + } + } + } +} + +async fn send_keycodes_to_slint( + keymap: Arc, + compose_table: Arc, +) { let mut state = xkb::State::new(&keymap); let mut previous_state = KeyboardReport::default(); let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS); @@ -117,130 +177,207 @@ pub fn create_hid_report_interceptor() -> impl Future { // This is a map from the basic keysyms (not a composed ones) to the string that should be produced. let mut pressed_keys_to_strings = BTreeMap::>::new(); - async move { - loop { - let report = KEYBOARD_REPORT_PROXY.receive().await; + loop { + let report = KEYBOARD_REPORT_PROXY.receive().await; - if let Report::KeyboardReport(report) = &report { - const KEYCODES_LEN_MODIFIERS: usize = 8; - const KEYCODES_LEN_REGULAR: usize = 6; - const KEYCODES_LEN: usize = KEYCODES_LEN_MODIFIERS + KEYCODES_LEN_REGULAR; + if let Report::KeyboardReport(report) = &report { + const KEYCODES_LEN_MODIFIERS: usize = 8; + const KEYCODES_LEN_REGULAR: usize = 6; + const KEYCODES_LEN: usize = KEYCODES_LEN_MODIFIERS + KEYCODES_LEN_REGULAR; - let mut pressed_keys = rmk::heapless::Vec::::new(); - let mut released_keys = rmk::heapless::Vec::::new(); + let mut pressed_keys = rmk::heapless::Vec::::new(); + let mut released_keys = rmk::heapless::Vec::::new(); - let pressed_mods_bits = !previous_state.modifier & report.modifier; - let released_mods_bits = previous_state.modifier & !report.modifier; + let pressed_mods_bits = !previous_state.modifier & report.modifier; + let released_mods_bits = previous_state.modifier & !report.modifier; - for index in 0..KEYCODES_LEN_MODIFIERS { - const USB_HID_LEFT_CTRL: u8 = 0xE0; - let mod_bit = 1_u8 << index; - let mod_keycode = USB_HID_LEFT_CTRL + index as u8; + for index in 0..KEYCODES_LEN_MODIFIERS { + const USB_HID_LEFT_CTRL: u8 = 0xE0; + let mod_bit = 1_u8 << index; + let mod_keycode = USB_HID_LEFT_CTRL + index as u8; - if pressed_mods_bits & mod_bit != 0 { - pressed_keys.push(mod_keycode).unwrap(); - } - - if released_mods_bits & mod_bit != 0 { - released_keys.push(mod_keycode).unwrap(); - } + if pressed_mods_bits & mod_bit != 0 { + pressed_keys.push(mod_keycode).unwrap(); } - // TODO: This currently depends on pressed keys not changing position in the array. - // Should be made independent of that. - for (&keycode_old, &keycode_new) in - core::iter::zip(&previous_state.keycodes, &report.keycodes) + if released_mods_bits & mod_bit != 0 { + released_keys.push(mod_keycode).unwrap(); + } + } + + // TODO: This currently depends on pressed keys not changing position in the array. + // Should be made independent of that. + for (&keycode_old, &keycode_new) in + core::iter::zip(&previous_state.keycodes, &report.keycodes) + { + if keycode_old == keycode_new { + continue; + } + + if keycode_old != 0 { + released_keys.push(keycode_old).unwrap(); + } + + if keycode_new != 0 { + pressed_keys.push(keycode_new).unwrap(); + } + } + + previous_state = *report; + + for keycode in released_keys { + debug!("Release: 0x{:02x} ({})", keycode, keycode); + let keycode_xkb = hid_to_xkb_keycode(keycode); + state.update_key(keycode_xkb, KeyDirection::Up); + let keysym = state.key_get_one_sym(keycode_xkb); + let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| { + warn!("Could not determine the string of a released key: keysym={keysym:?} pressed_keys_to_strings={pressed_keys_to_strings:?}"); + None + }); + KEY_MESSAGE_CHANNEL + .send(KeyMessage { + keysym, + string, + direction: KeyDirection::Up, + }) + .await; + } + + for keycode in pressed_keys { + debug!("Pressed: 0x{:02x} ({})", keycode, keycode); + let keycode_xkb = hid_to_xkb_keycode(keycode); + let sym = state.key_get_one_sym(keycode_xkb); + + let result: Option<(Keysym, Keysym, Option)> = match compose_state.feed(sym) { - if keycode_old == keycode_new { - continue; + FeedResult::Ignored => { + let string = state.key_get_utf8(keycode_xkb); + Some((sym, sym, Some(string))) } - - if keycode_old != 0 { - released_keys.push(keycode_old).unwrap(); + FeedResult::Accepted => { + let status = compose_state.status(); + debug!("Compose status: {status:?}"); + match status { + Status::Nothing => { + let string = state.key_get_utf8(keycode_xkb); + Some((sym, sym, Some(string))) + } + Status::Composing => None, + Status::Composed => { + let composed_sym = compose_state.keysym().unwrap_or_default(); + let string = compose_state.utf8().unwrap_or_default(); + Some((sym, composed_sym, Some(string))) + } + Status::Cancelled => None, + } } + }; - if keycode_new != 0 { - pressed_keys.push(keycode_new).unwrap(); - } - } + if let Some((basic_keysym, composed_keysym, string)) = result { + // Change `Some("")` into `None`. + let string = string.filter(|string| !string.is_empty()); - previous_state = *report; + info!( + "Basic keysym: {basic_keysym:?}, composed keysym: {composed_keysym:?}, string: {:?}", + string.as_ref() + ); - for keycode in released_keys { - debug!("Release: 0x{:02x} ({})", keycode, keycode); - let keycode_xkb = into_xkb_keycode(keycode); - state.update_key(keycode_xkb, KeyDirection::Up); - let keysym = state.key_get_one_sym(keycode_xkb); - let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| { - warn!("Could not determine the string of a released key: keysym={keysym:?} pressed_keys_to_strings={pressed_keys_to_strings:?}"); - None - }); + pressed_keys_to_strings.insert(basic_keysym, string.clone()); KEY_MESSAGE_CHANNEL .send(KeyMessage { - keysym, + keysym: composed_keysym, string, - direction: KeyDirection::Up, + direction: KeyDirection::Down, }) .await; - } - - for keycode in pressed_keys { - debug!("Pressed: 0x{:02x} ({})", keycode, keycode); - let keycode_xkb = into_xkb_keycode(keycode); - let sym = state.key_get_one_sym(keycode_xkb); - - let result: Option<(Keysym, Keysym, Option)> = match compose_state - .feed(sym) - { - FeedResult::Ignored => { - let string = state.key_get_utf8(keycode_xkb); - Some((sym, sym, Some(string))) - } - FeedResult::Accepted => { - let status = compose_state.status(); - debug!("Compose status: {status:?}"); - match status { - Status::Nothing => { - let string = state.key_get_utf8(keycode_xkb); - Some((sym, sym, Some(string))) - } - Status::Composing => None, - Status::Composed => { - let composed_sym = compose_state.keysym().unwrap_or_default(); - let string = compose_state.utf8().unwrap_or_default(); - Some((sym, composed_sym, Some(string))) - } - Status::Cancelled => None, - } - } - }; - - if let Some((basic_keysym, composed_keysym, string)) = result { - // Change `Some("")` into `None`. - let string = string.filter(|string| !string.is_empty()); - - info!( - "Basic keysym: {basic_keysym:?}, composed keysym: {composed_keysym:?}, string: {:?}", - string.as_ref() - ); - - pressed_keys_to_strings.insert(basic_keysym, string.clone()); - KEY_MESSAGE_CHANNEL - .send(KeyMessage { - keysym: composed_keysym, - string, - direction: KeyDirection::Down, - }) - .await; - state.update_key(keycode_xkb, KeyDirection::Down); - } + state.update_key(keycode_xkb, KeyDirection::Down); } } } } } -fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode { +pub fn keysym_to_xkb_keycode() {} + +/// Based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L389 +/// Fields reordered for `Ord` based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L404 +#[derive(PartialEq, Eq, PartialOrd, Ord)] +struct KeysymEntry { + layout: xkb::LayoutIndex, + level: xkb::LevelIndex, + keycode: xkb::Keycode, + mask: xkb::ModMask, +} + +type KeysymEntries = Vec; +type KeysymMap = BTreeMap; + +/// Based on https://github.com/xkbcommon/libxkbcommon/blob/6c67e3d41d3215ab1edd4406de215c7bf1f20c74/tools/how-to-type.c#L434 +fn lookup_keysym_entries( + entries: &mut KeysymMap, + keysym: xkb::Keysym, + insert: bool, +) -> Option<&mut KeysymEntries> { + match entries.entry(keysym) { + Entry::Occupied(occupied) => Some(occupied.into_mut()), + Entry::Vacant(vacant) if insert => Some(vacant.insert(Vec::new_in(&PSRAM_ALLOCATOR))), + Entry::Vacant(_) => None, + } +} + +pub fn string_to_hid_keycodes( + keymap: &xkb::Keymap, + compose_table: &xkb::compose::Table, + string: &str, +) -> Result, char> { + let mut hid_keycodes = Vec::new_in(&PSRAM_ALLOCATOR); + + for character in string.chars() { + let keysym = xkbcommon::xkb::utf32_to_keysym(character as u32); + let mut found_keycode = None; + + keymap.key_for_each(|keymap, keycode| { + if found_keycode.is_some() { + return; + } + + for layout_index in 0..keymap.num_layouts_for_key(keycode) { + for level_index in 0..keymap.num_levels_for_key(keycode, layout_index) { + let [current_keysym] = + keymap.key_get_syms_by_level(keycode, layout_index, level_index) + else { + // Multi-keysym levels are currently not handled. + continue; + }; + + if current_keysym == &keysym { + let mut masks: [ModMask; 32 /* TODO: Re-evaluate the appropriate size */] = Default::default(); + let masks_len = keymap.key_get_mods_for_level( + keycode, + layout_index, + level_index, + &mut masks, + ); + let masks = &mut masks[0..masks_len]; + warn!("string_to_hid_keycodes: char = {character:?}, sym = {keysym:?}, code = {keycode:?}, layout = {layout_index:?}, level = {level_index:?}, masks = {masks:?}"); + found_keycode = Some(keycode); + } + } + } + }); + + let Some(found_keycode) = found_keycode else { + return Err(character); + }; + let hid_keycode = xkb_to_hid_keycode(found_keycode); + + hid_keycodes.push(hid_keycode); + } + + Ok(hid_keycodes) +} + +const fn hid_to_xkb_keycode_inner(rmk_keycode: u8) -> u8 { // https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/drivers/hid/hid-input.c?id=refs/tags/v6.18#n27 const UNK: u8 = 240; #[rustfmt::skip] @@ -267,7 +404,48 @@ fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode { // TODO: The combination of these two operations should be precomputed // in a const expr into a single look-up table. - xkb::Keycode::new((HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32) + HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE + // xkb::Keycode::new((HID_KEYBOARD[rmk_keycode as usize] + MIN_KEYCODE) as u32) +} + +pub const fn hid_to_xkb_keycode(hid_keycode: u8) -> xkb::Keycode { + const HID_TO_XKB: [u8; 256] = { + let mut hid_to_xkb = [0_u8; _]; + let mut hid: u8 = 0; + + loop { + hid_to_xkb[hid as usize] = hid_to_xkb_keycode_inner(hid); + let overflow; + (hid, overflow) = hid.overflowing_add(1); + if overflow { + break; + } + } + + hid_to_xkb + }; + + xkb::Keycode::new(HID_TO_XKB[hid_keycode as usize] as u32) +} + +pub const fn xkb_to_hid_keycode(xkb_keycode: xkb::Keycode) -> u8 { + const XKB_TO_HID: [u8; 256] = { + let mut xkb_to_hid = [0_u8; _]; + let mut hid: u8 = 0; + + loop { + xkb_to_hid[hid_to_xkb_keycode_inner(hid as u8) as usize] = hid; + let overflow; + (hid, overflow) = hid.overflowing_add(1); + if overflow { + break; + } + } + + xkb_to_hid + }; + + XKB_TO_HID[xkb_keycode.raw() as usize] } pub trait TryFromKeysym: Sized { diff --git a/firmware/acid-firmware/src/ui/mod.rs b/firmware/acid-firmware/src/ui/mod.rs index 9fe7441..7a55a23 100644 --- a/firmware/acid-firmware/src/ui/mod.rs +++ b/firmware/acid-firmware/src/ui/mod.rs @@ -10,12 +10,14 @@ use alloc::{ rc::Rc, string::{String, ToString}, vec, + vec::Vec, }; use embassy_time::Instant; use hex::FromHexError; use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer}; use log::{error, info, warn}; use password_hash::Key; +use rmk::futures::TryFutureExt; use slint::{ Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem, VecModel, @@ -30,10 +32,11 @@ use crate::FRAME_DURATION_MIN; use crate::{ PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, db::{ - AcidDatabase, DbKey, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid, - ReadTransactionExt, + AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers, + PartitionAcid, ReadTransactionExt, }, ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY}, + keymap::OUTPUT_STRING_CHANNEL, ui::{ backend::SlintBackend, messages::{ @@ -189,6 +192,13 @@ impl State { } }); + main.on_login_test_string_accepted(|string| { + slint::spawn_local(async move { + OUTPUT_STRING_CHANNEL.send(string.to_string()).await; + }) + .unwrap(); + }); + main.on_users_edit_user({ let state = state.clone(); move |username, new| { @@ -754,6 +764,32 @@ impl AppViewTrait for StateUserSites { spectre_derive_site_password(&user_sites.user_key, &site_name_c); warn!("Site password: {site_password:?}"); + + for character in site_password.chars() { + let keysym = xkbcommon::xkb::utf32_to_keysym(character as u32); + } + + slint::spawn_local({ + let username = user_sites.username.clone(); + let db = state.db.clone(); + async move { + let mut write = db.write_transaction().await; + let key = DbKey::new(DbPathSpectreUserSite { + user_sites: DbPathSpectreUserSites { + username: Cow::Borrowed(&username), + }, + site: Cow::Borrowed(&site_name), + }); + let site = SpectreSiteConfig::default(); + let site_bytes = + postcard::to_extend(&site, Vec::::new_in(&PSRAM_ALLOCATOR)) + .unwrap(); + + write.write(&key, &site_bytes).await.unwrap(); + write.commit().await.unwrap(); + } + }) + .unwrap(); } _ => (), } diff --git a/firmware/acid-firmware/ui/login-view.slint b/firmware/acid-firmware/ui/login-view.slint index 8747d28..b8b08e5 100644 --- a/firmware/acid-firmware/ui/login-view.slint +++ b/firmware/acid-firmware/ui/login-view.slint @@ -8,6 +8,7 @@ export component LoginView inherits VerticalLayout { in property <[string]> usernames <=> combo_box_username.model; callback users_clicked(); callback pw_accepted(int, string, string); + callback test_string_accepted <=> line_edit_test.accepted; Rectangle { } HorizontalLayout { @@ -39,5 +40,9 @@ export component LoginView inherits VerticalLayout { } } + line_edit_test := LineEdit { + placeholder-text: "Text to send to host."; + } + Rectangle { } } diff --git a/firmware/acid-firmware/ui/main.slint b/firmware/acid-firmware/ui/main.slint index a2e7a08..c8fa9f3 100644 --- a/firmware/acid-firmware/ui/main.slint +++ b/firmware/acid-firmware/ui/main.slint @@ -40,6 +40,7 @@ export component AppWindow inherits Window { // Login View in property <[string]> login_usernames <=> login_view.usernames; callback login_pw_accepted <=> login_view.pw_accepted; + callback login_test_string_accepted <=> login_view.test_string_accepted; // Users View callback users_edit_user <=> users_view.edit_user; // User Edit View