Naive implementation of sending strings to the host

This commit is contained in:
Jakub Hlusička 2026-02-09 02:14:06 +01:00
parent 1e2d43a628
commit 3b364c64c2
4 changed files with 342 additions and 122 deletions

View file

@ -2,20 +2,26 @@ use core::alloc::Layout;
use core::fmt::Debug; use core::fmt::Debug;
use core::slice; use core::slice;
use alloc::alloc::Allocator;
use alloc::boxed::Box; use alloc::boxed::Box;
use alloc::collections::btree_map::BTreeMap; use alloc::collections::btree_map::{BTreeMap, Entry};
use alloc::string::String; use alloc::string::String;
use alloc::sync::Arc;
use alloc::vec::Vec;
use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex; use embassy_sync::blocking_mutex::raw::CriticalSectionRawMutex;
use embassy_sync::channel::Channel; use embassy_sync::channel::Channel;
use embassy_time::Instant; use embassy_time::Instant;
use esp_alloc::MemoryCapability; use esp_alloc::{EspHeap, MemoryCapability};
use itertools::Itertools;
use log::{debug, info, warn}; use log::{debug, info, warn};
use rmk::descriptor::KeyboardReport; use rmk::descriptor::KeyboardReport;
use rmk::futures::FutureExt;
use rmk::hid::Report; use rmk::hid::Report;
use rmk::types::action::{Action, KeyAction}; use rmk::types::action::{Action, KeyAction};
use rmk::{a, k, layer}; use rmk::{a, join_all, k, layer};
use slint::platform::Key; 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::matrix::{MATRIX_COLS, MATRIX_ROWS};
use crate::util::DurationExt; use crate::util::DurationExt;
@ -25,6 +31,7 @@ use crate::{KEYBOARD_REPORT_PROXY, PSRAM_ALLOCATOR};
pub const NUM_LAYER: usize = 1; pub const NUM_LAYER: usize = 1;
pub static KEY_MESSAGE_CHANNEL: Channel<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new(); pub static KEY_MESSAGE_CHANNEL: Channel<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new();
pub static OUTPUT_STRING_CHANNEL: Channel<CriticalSectionRawMutex, String, 16> = Channel::new();
pub struct KeyMessage { pub struct KeyMessage {
pub keysym: Keysym, pub keysym: Keysym,
@ -83,13 +90,16 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
info!("Loading XKB keymap..."); info!("Loading XKB keymap...");
let instant_start = Instant::now(); let instant_start = Instant::now();
let keymap = xkb::Keymap::new_from_string( let keymap = Arc::new_in(
xkb::Keymap::new_from_string(
&context, &context,
keymap_string_buffer, keymap_string_buffer,
xkb::KEYMAP_FORMAT_TEXT_V1, xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS, xkb::KEYMAP_COMPILE_NO_FLAGS,
) )
.unwrap(); .unwrap(),
&PSRAM_ALLOCATOR,
);
let duration = Instant::now().duration_since(instant_start); let duration = Instant::now().duration_since(instant_start);
info!( info!(
"XKB keymap loaded successfully! Took {} seconds.", "XKB keymap loaded successfully! Took {} seconds.",
@ -97,19 +107,69 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
); );
info!("Loading XKB compose map..."); info!("Loading XKB compose map...");
let instant_start = Instant::now(); let instant_start = Instant::now();
let compose_table = xkb::compose::Table::new_from_buffer( let compose_table = Arc::new_in(
xkb::compose::Table::new_from_buffer(
&context, &context,
COMPOSE_MAP_STRING, COMPOSE_MAP_STRING,
COMPOSE_MAP_LOCALE, COMPOSE_MAP_LOCALE,
xkb::compose::FORMAT_TEXT_V1, xkb::compose::FORMAT_TEXT_V1,
xkb::compose::COMPILE_NO_FLAGS, xkb::compose::COMPILE_NO_FLAGS,
) )
.unwrap(); .unwrap(),
&PSRAM_ALLOCATOR,
);
let duration = Instant::now().duration_since(instant_start); let duration = Instant::now().duration_since(instant_start);
info!( info!(
"XKB compose map loaded successfully! Took {} seconds.", "XKB compose map loaded successfully! Took {} seconds.",
duration.display_as_secs() 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<A: Allocator>(
keymap: Arc<xkb::Keymap, A>,
compose_table: Arc<xkb::compose::Table, A>,
) {
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<A: Allocator>(
keymap: Arc<xkb::Keymap, A>,
compose_table: Arc<xkb::compose::Table, A>,
) {
let mut state = xkb::State::new(&keymap); let mut state = xkb::State::new(&keymap);
let mut previous_state = KeyboardReport::default(); let mut previous_state = KeyboardReport::default();
let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS); let mut compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS);
@ -117,7 +177,6 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
// This is a map from the basic keysyms (not a composed ones) to the string that should be produced. // 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::<Keysym, Option<String>>::new(); let mut pressed_keys_to_strings = BTreeMap::<Keysym, Option<String>>::new();
async move {
loop { loop {
let report = KEYBOARD_REPORT_PROXY.receive().await; let report = KEYBOARD_REPORT_PROXY.receive().await;
@ -168,7 +227,7 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
for keycode in released_keys { for keycode in released_keys {
debug!("Release: 0x{:02x} ({})", keycode, keycode); debug!("Release: 0x{:02x} ({})", keycode, keycode);
let keycode_xkb = into_xkb_keycode(keycode); let keycode_xkb = hid_to_xkb_keycode(keycode);
state.update_key(keycode_xkb, KeyDirection::Up); state.update_key(keycode_xkb, KeyDirection::Up);
let keysym = state.key_get_one_sym(keycode_xkb); let keysym = state.key_get_one_sym(keycode_xkb);
let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| { let string = pressed_keys_to_strings.remove(&keysym).unwrap_or_else(|| {
@ -186,11 +245,10 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
for keycode in pressed_keys { for keycode in pressed_keys {
debug!("Pressed: 0x{:02x} ({})", keycode, keycode); debug!("Pressed: 0x{:02x} ({})", keycode, keycode);
let keycode_xkb = into_xkb_keycode(keycode); let keycode_xkb = hid_to_xkb_keycode(keycode);
let sym = state.key_get_one_sym(keycode_xkb); let sym = state.key_get_one_sym(keycode_xkb);
let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state.feed(sym)
.feed(sym)
{ {
FeedResult::Ignored => { FeedResult::Ignored => {
let string = state.key_get_utf8(keycode_xkb); let string = state.key_get_utf8(keycode_xkb);
@ -237,10 +295,89 @@ pub fn create_hid_report_interceptor() -> impl Future<Output = ()> {
} }
} }
} }
}
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<KeysymEntry, &'static EspHeap>;
type KeysymMap = BTreeMap<Keysym, KeysymEntries, &'static EspHeap>;
/// 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,
} }
} }
fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode { pub fn string_to_hid_keycodes(
keymap: &xkb::Keymap,
compose_table: &xkb::compose::Table,
string: &str,
) -> Result<Vec<u8, &'static EspHeap>, 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 // 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; const UNK: u8 = 240;
#[rustfmt::skip] #[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 // TODO: The combination of these two operations should be precomputed
// in a const expr into a single look-up table. // 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 { pub trait TryFromKeysym: Sized {

View file

@ -10,12 +10,14 @@ use alloc::{
rc::Rc, rc::Rc,
string::{String, ToString}, string::{String, ToString},
vec, vec,
vec::Vec,
}; };
use embassy_time::Instant; use embassy_time::Instant;
use hex::FromHexError; use hex::FromHexError;
use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer}; use i_slint_core::model::{ModelChangeListener, ModelChangeListenerContainer};
use log::{error, info, warn}; use log::{error, info, warn};
use password_hash::Key; use password_hash::Key;
use rmk::futures::TryFutureExt;
use slint::{ use slint::{
Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem, Model, ModelExt, ModelNotify, ModelRc, ModelTracker, SharedString, StandardListViewItem,
VecModel, VecModel,
@ -30,10 +32,11 @@ use crate::FRAME_DURATION_MIN;
use crate::{ use crate::{
PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER, PSRAM_ALLOCATOR, SIGNAL_LCD_SUBMIT, SIGNAL_UI_RENDER,
db::{ db::{
AcidDatabase, DbKey, DbPathSpectreUserSites, DbPathSpectreUsers, PartitionAcid, AcidDatabase, DbKey, DbPathSpectreUserSite, DbPathSpectreUserSites, DbPathSpectreUsers,
ReadTransactionExt, PartitionAcid, ReadTransactionExt,
}, },
ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY}, ffi::{alloc::__spre_free, crypto::ACTIVE_ENCRYPTED_USER_KEY},
keymap::OUTPUT_STRING_CHANNEL,
ui::{ ui::{
backend::SlintBackend, backend::SlintBackend,
messages::{ 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({ main.on_users_edit_user({
let state = state.clone(); let state = state.clone();
move |username, new| { move |username, new| {
@ -754,6 +764,32 @@ impl AppViewTrait for StateUserSites {
spectre_derive_site_password(&user_sites.user_key, &site_name_c); spectre_derive_site_password(&user_sites.user_key, &site_name_c);
warn!("Site password: {site_password:?}"); 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::<u8, _>::new_in(&PSRAM_ALLOCATOR))
.unwrap();
write.write(&key, &site_bytes).await.unwrap();
write.commit().await.unwrap();
}
})
.unwrap();
} }
_ => (), _ => (),
} }

View file

@ -8,6 +8,7 @@ export component LoginView inherits VerticalLayout {
in property <[string]> usernames <=> combo_box_username.model; in property <[string]> usernames <=> combo_box_username.model;
callback users_clicked(); callback users_clicked();
callback pw_accepted(int, string, string); callback pw_accepted(int, string, string);
callback test_string_accepted <=> line_edit_test.accepted;
Rectangle { } Rectangle { }
HorizontalLayout { HorizontalLayout {
@ -39,5 +40,9 @@ export component LoginView inherits VerticalLayout {
} }
} }
line_edit_test := LineEdit {
placeholder-text: "Text to send to host.";
}
Rectangle { } Rectangle { }
} }

View file

@ -40,6 +40,7 @@ export component AppWindow inherits Window {
// Login View // Login View
in property <[string]> login_usernames <=> login_view.usernames; in property <[string]> login_usernames <=> login_view.usernames;
callback login_pw_accepted <=> login_view.pw_accepted; callback login_pw_accepted <=> login_view.pw_accepted;
callback login_test_string_accepted <=> login_view.test_string_accepted;
// Users View // Users View
callback users_edit_user <=> users_view.edit_user; callback users_edit_user <=> users_view.edit_user;
// User Edit View // User Edit View