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::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<CriticalSectionRawMutex, KeyMessage, 16> = Channel::new();
pub static OUTPUT_STRING_CHANNEL: Channel<CriticalSectionRawMutex, String, 16> = Channel::new();
pub struct KeyMessage {
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);
info!("Loading XKB keymap...");
let instant_start = Instant::now();
let keymap = xkb::Keymap::new_from_string(
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();
.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<Output = ()> {
);
info!("Loading XKB compose map...");
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,
COMPOSE_MAP_STRING,
COMPOSE_MAP_LOCALE,
xkb::compose::FORMAT_TEXT_V1,
xkb::compose::COMPILE_NO_FLAGS,
)
.unwrap();
.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<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 previous_state = KeyboardReport::default();
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.
let mut pressed_keys_to_strings = BTreeMap::<Keysym, Option<String>>::new();
async move {
loop {
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 {
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);
let keysym = state.key_get_one_sym(keycode_xkb);
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 {
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 result: Option<(Keysym, Keysym, Option<String>)> = match compose_state
.feed(sym)
let result: Option<(Keysym, Keysym, Option<String>)> = match compose_state.feed(sym)
{
FeedResult::Ignored => {
let string = state.key_get_utf8(keycode_xkb);
@ -238,9 +296,88 @@ 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,
}
fn into_xkb_keycode(rmk_keycode: u8) -> xkb::Keycode {
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,
}
}
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
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 {

View file

@ -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::<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;
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 { }
}

View file

@ -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