mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-11-01 10:20:50 +08:00
feat(hid): 支持控制台鼠标抖动
在控制台鼠键配置菜单增加鼠标抖动开关和间隔设置,默认关闭,间隔支持 10–3600 秒。 设备端按键鼠空闲时间调度,支持配置持久化、热更新、真实输入打断和断连后重新计时。 兼容绝对及相对鼠标模式,按键或按钮按住期间暂停抖动。 验证:608 项 Rust 单测通过;暂存快照前端构建和浏览器交互测试通过;已完成 ARMv7 OpenWrt 部署验证。 Refs #316
This commit is contained in:
500
src/hid/jiggler.rs
Normal file
500
src/hid/jiggler.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
//! Device-side mouse jiggler. Real input invalidates every remaining movement.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::config::{HidBackend, HidConfig, MouseJigglerConfig};
|
||||
|
||||
use super::{CanonicalKey, KeyEventType, KeyboardEvent, MouseEvent, MouseEventType};
|
||||
|
||||
const STEP_DELAY: Duration = Duration::from_millis(100);
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Relative,
|
||||
Absolute,
|
||||
}
|
||||
|
||||
pub(super) struct JigglerState {
|
||||
config: MouseJigglerConfig,
|
||||
relative: bool,
|
||||
absolute: bool,
|
||||
mode: Mode,
|
||||
ready: bool,
|
||||
activity: Instant,
|
||||
keys: HashSet<CanonicalKey>,
|
||||
modifiers: u8,
|
||||
buttons: u8,
|
||||
consumer: u16,
|
||||
last_absolute: Option<(i32, i32)>,
|
||||
return_move: Option<(MouseEvent, Instant)>,
|
||||
generation: u64,
|
||||
}
|
||||
|
||||
impl Default for JigglerState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: MouseJigglerConfig::default(),
|
||||
relative: false,
|
||||
absolute: false,
|
||||
mode: Mode::Relative,
|
||||
ready: false,
|
||||
activity: Instant::now(),
|
||||
keys: HashSet::new(),
|
||||
modifiers: 0,
|
||||
buttons: 0,
|
||||
consumer: 0,
|
||||
last_absolute: None,
|
||||
return_move: None,
|
||||
generation: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl JigglerState {
|
||||
pub fn configure(&mut self, hid: &HidConfig, now: Instant) {
|
||||
let (relative, absolute) = match hid.backend {
|
||||
HidBackend::None => (false, false),
|
||||
HidBackend::Bluetooth => (true, false),
|
||||
HidBackend::Otg => {
|
||||
let functions = hid.effective_otg_functions();
|
||||
(functions.mouse_relative, functions.mouse_absolute)
|
||||
}
|
||||
_ => (true, true),
|
||||
};
|
||||
let mode = if absolute && (hid.mouse_absolute || !relative) {
|
||||
Mode::Absolute
|
||||
} else {
|
||||
Mode::Relative
|
||||
};
|
||||
if self.config != hid.jiggler
|
||||
|| (self.relative, self.absolute, self.mode) != (relative, absolute, mode)
|
||||
{
|
||||
self.config = hid.jiggler.clone();
|
||||
self.relative = relative;
|
||||
self.absolute = absolute;
|
||||
self.mode = mode;
|
||||
self.activity(now);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activity(&mut self, now: Instant) {
|
||||
self.activity = now;
|
||||
self.return_move = None;
|
||||
self.generation = self.generation.wrapping_add(1);
|
||||
}
|
||||
|
||||
pub fn keyboard(&mut self, event: &KeyboardEvent, now: Instant) {
|
||||
self.activity(now);
|
||||
match event.event_type {
|
||||
KeyEventType::Down => {
|
||||
self.keys.insert(event.key);
|
||||
}
|
||||
KeyEventType::Up => {
|
||||
self.keys.remove(&event.key);
|
||||
}
|
||||
}
|
||||
self.modifiers = event.modifiers.to_hid_byte();
|
||||
}
|
||||
|
||||
pub fn mouse(&mut self, event: &MouseEvent, now: Instant) {
|
||||
self.activity(now);
|
||||
match event.event_type {
|
||||
MouseEventType::Move if self.relative => self.mode = Mode::Relative,
|
||||
MouseEventType::MoveAbs if self.absolute => {
|
||||
self.mode = Mode::Absolute;
|
||||
self.last_absolute = Some((event.x.clamp(0, 32767), event.y.clamp(0, 32767)));
|
||||
}
|
||||
MouseEventType::Down => {
|
||||
if let Some(button) = event.button {
|
||||
self.buttons |= button.to_hid_bit();
|
||||
}
|
||||
}
|
||||
MouseEventType::Up => {
|
||||
if let Some(button) = event.button {
|
||||
self.buttons &= !button.to_hid_bit();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consumer(&mut self, usage: u16, now: Instant) {
|
||||
self.activity(now);
|
||||
self.consumer = usage;
|
||||
}
|
||||
|
||||
pub fn reset(&mut self, now: Instant) {
|
||||
self.activity(now);
|
||||
self.keys.clear();
|
||||
self.modifiers = 0;
|
||||
self.buttons = 0;
|
||||
self.consumer = 0;
|
||||
}
|
||||
|
||||
pub fn suspend(&mut self) {
|
||||
self.ready = false;
|
||||
self.last_absolute = None;
|
||||
self.reset(Instant::now());
|
||||
}
|
||||
|
||||
pub fn delay(&self) -> Option<Duration> {
|
||||
(self.config.enabled && (self.relative || self.absolute)).then_some(
|
||||
if self.return_move.is_some() {
|
||||
STEP_DELAY
|
||||
} else {
|
||||
Duration::from_secs(1)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn valid(&self, generation: u64) -> bool {
|
||||
self.config.enabled && self.ready && self.generation == generation
|
||||
}
|
||||
|
||||
pub fn tick(
|
||||
&mut self,
|
||||
ready: bool,
|
||||
resolution: (u32, u32),
|
||||
now: Instant,
|
||||
) -> Option<(MouseEvent, u64)> {
|
||||
if ready != self.ready {
|
||||
self.ready = ready;
|
||||
if ready {
|
||||
// Input may already have arrived since reconnecting. Preserve
|
||||
// held keys/buttons when starting the new idle period.
|
||||
self.activity(now);
|
||||
} else {
|
||||
self.reset(now);
|
||||
self.last_absolute = None;
|
||||
}
|
||||
}
|
||||
if !self.ready
|
||||
|| !self.config.enabled
|
||||
|| (!self.relative && !self.absolute)
|
||||
|| !self.keys.is_empty()
|
||||
|| self.modifiers != 0
|
||||
|| self.buttons != 0
|
||||
|| self.consumer != 0
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if let Some((_, deadline)) = &self.return_move {
|
||||
if now < *deadline {
|
||||
return None;
|
||||
}
|
||||
let (event, _) = self.return_move.take().unwrap();
|
||||
self.activity = now;
|
||||
return Some((event, self.generation));
|
||||
}
|
||||
if now.duration_since(self.activity) < Duration::from_secs(self.config.interval_secs.into())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let (event, back) = if self.mode == Mode::Absolute
|
||||
&& self.absolute
|
||||
&& (self.last_absolute.is_some() || !self.relative)
|
||||
{
|
||||
// An absolute-only device has no way to query the host cursor position.
|
||||
let (x, y) = self.last_absolute.unwrap_or((16384, 16384));
|
||||
let offset = |position: i32, extent: u32| {
|
||||
let delta = (65534 / extent.max(1)).clamp(1, 512) as i32;
|
||||
if position + delta <= 32767 {
|
||||
position + delta
|
||||
} else {
|
||||
position - delta
|
||||
}
|
||||
};
|
||||
(
|
||||
MouseEvent::move_abs(offset(x, resolution.0), offset(y, resolution.1)),
|
||||
MouseEvent::move_abs(x, y),
|
||||
)
|
||||
} else {
|
||||
(MouseEvent::move_rel(2, 0), MouseEvent::move_rel(-2, 0))
|
||||
};
|
||||
self.return_move = Some((back, now + STEP_DELAY));
|
||||
Some((event, self.generation))
|
||||
}
|
||||
|
||||
pub fn failed(&mut self, generation: u64, now: Instant) {
|
||||
if self.generation == generation {
|
||||
self.activity(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config::OtgHidProfile;
|
||||
use crate::hid::{KeyboardModifiers, MouseButton};
|
||||
|
||||
fn configured(now: Instant) -> (JigglerState, HidConfig) {
|
||||
let mut config = HidConfig::default();
|
||||
config.backend = HidBackend::Otg;
|
||||
config.jiggler.enabled = true;
|
||||
config.jiggler.interval_secs = 10;
|
||||
let mut state = JigglerState::default();
|
||||
state.configure(&config, now);
|
||||
assert!(state.tick(true, (1920, 1080), now).is_none());
|
||||
(state, config)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_waits_then_moves_out_and_back_without_counting_synthetic_input() {
|
||||
let now = Instant::now();
|
||||
let (mut state, _) = configured(now);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(9))
|
||||
.is_none());
|
||||
let due = now + Duration::from_secs(10);
|
||||
let (out, generation) = state.tick(true, (1920, 1080), due).unwrap();
|
||||
assert_eq!((out.event_type, out.x, out.y), (MouseEventType::Move, 2, 0));
|
||||
assert!(state.valid(generation));
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), due + STEP_DELAY / 2)
|
||||
.is_none());
|
||||
let (back, _) = state.tick(true, (1920, 1080), due + STEP_DELAY).unwrap();
|
||||
assert_eq!((back.x, back.y), (-2, 0));
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), due + Duration::from_secs(10))
|
||||
.is_none());
|
||||
assert!(state
|
||||
.tick(
|
||||
true,
|
||||
(1920, 1080),
|
||||
due + STEP_DELAY + Duration::from_secs(10)
|
||||
)
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_real_input_cancels_return_and_invalidates_dispatched_movement() {
|
||||
let now = Instant::now();
|
||||
let (mut state, _) = configured(now);
|
||||
state.mouse(&MouseEvent::move_abs(32767, 0), now);
|
||||
let due = now + Duration::from_secs(10);
|
||||
let (out, generation) = state.tick(true, (1920, 1080), due).unwrap();
|
||||
assert!(out.x < 32767 && out.y > 0);
|
||||
let input = due + Duration::from_millis(50);
|
||||
state.mouse(&MouseEvent::move_abs(12000, 24000), input);
|
||||
assert!(!state.valid(generation));
|
||||
assert!(state.tick(true, (1920, 1080), due + STEP_DELAY).is_none());
|
||||
let (next, _) = state
|
||||
.tick(true, (1920, 1080), input + Duration::from_secs(10))
|
||||
.unwrap();
|
||||
assert!(next.x > 12000 && next.x < 12100 && next.y > 24000 && next.y < 24100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_pauses_for_held_keys_modifiers_buttons_and_consumer_controls() {
|
||||
let now = Instant::now();
|
||||
let (mut state, _) = configured(now);
|
||||
let down = KeyboardEvent::key_down(CanonicalKey::KeyA, KeyboardModifiers::default());
|
||||
state.keyboard(&down, now);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(100))
|
||||
.is_none());
|
||||
state.keyboard(
|
||||
&KeyboardEvent::key_up(
|
||||
CanonicalKey::KeyA,
|
||||
KeyboardModifiers {
|
||||
left_ctrl: true,
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
now,
|
||||
);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(100))
|
||||
.is_none());
|
||||
state.reset(now);
|
||||
state.mouse(&MouseEvent::button_down(MouseButton::Left), now);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(100))
|
||||
.is_none());
|
||||
state.mouse(&MouseEvent::button_up(MouseButton::Left), now);
|
||||
state.consumer(0xe9, now);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(100))
|
||||
.is_none());
|
||||
state.consumer(0, now);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(10))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_config_changes_disconnects_and_shutdown_cancel_and_restart_the_wait() {
|
||||
let now = Instant::now();
|
||||
let (mut state, mut config) = configured(now);
|
||||
let due = now + Duration::from_secs(10);
|
||||
let (_, generation) = state.tick(true, (1920, 1080), due).unwrap();
|
||||
config.jiggler.enabled = false;
|
||||
state.configure(&config, due);
|
||||
assert!(!state.valid(generation));
|
||||
assert!(state.delay().is_none());
|
||||
config.jiggler.enabled = true;
|
||||
config.jiggler.interval_secs = 60;
|
||||
state.configure(&config, due);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), due + Duration::from_secs(59))
|
||||
.is_none());
|
||||
assert!(state
|
||||
.tick(false, (1920, 1080), due + Duration::from_secs(60))
|
||||
.is_none());
|
||||
let reconnect = due + Duration::from_secs(100);
|
||||
assert!(state.tick(true, (1920, 1080), reconnect).is_none());
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), reconnect + Duration::from_secs(60))
|
||||
.is_some());
|
||||
state.suspend();
|
||||
assert!(!state.valid(state.generation));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_handles_bluetooth_relative_only_absolute_only_and_keyboard_only() {
|
||||
let now = Instant::now();
|
||||
let (mut state, mut config) = configured(now);
|
||||
config.backend = HidBackend::Bluetooth;
|
||||
state.configure(&config, now);
|
||||
assert_eq!(
|
||||
state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(10))
|
||||
.unwrap()
|
||||
.0
|
||||
.event_type,
|
||||
MouseEventType::Move
|
||||
);
|
||||
config.backend = HidBackend::Otg;
|
||||
config.otg_profile = OtgHidProfile::Custom;
|
||||
config.otg_functions.mouse_relative = false;
|
||||
state.configure(&config, now);
|
||||
assert_eq!(
|
||||
state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(10))
|
||||
.unwrap()
|
||||
.0
|
||||
.event_type,
|
||||
MouseEventType::MoveAbs
|
||||
);
|
||||
config.otg_profile = OtgHidProfile::LegacyKeyboard;
|
||||
state.configure(&config, now);
|
||||
assert!(state.delay().is_none());
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(100))
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_failed_moves_wait_a_full_interval_before_retrying() {
|
||||
let now = Instant::now();
|
||||
let (mut state, _) = configured(now);
|
||||
let due = now + Duration::from_secs(10);
|
||||
let (_, generation) = state.tick(true, (1920, 1080), due).unwrap();
|
||||
state.failed(generation, due);
|
||||
assert!(state.tick(true, (1920, 1080), due + STEP_DELAY).is_none());
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), due + Duration::from_secs(10))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jiggler_reconnect_does_not_forget_input_received_before_the_first_tick() {
|
||||
let now = Instant::now();
|
||||
let (mut state, _) = configured(now);
|
||||
state.suspend();
|
||||
state.keyboard(
|
||||
&KeyboardEvent::key_down(CanonicalKey::KeyA, KeyboardModifiers::default()),
|
||||
now,
|
||||
);
|
||||
assert!(state.tick(true, (1920, 1080), now).is_none());
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(100))
|
||||
.is_none());
|
||||
state.keyboard(
|
||||
&KeyboardEvent::key_up(CanonicalKey::KeyA, KeyboardModifiers::default()),
|
||||
now,
|
||||
);
|
||||
assert!(state
|
||||
.tick(true, (1920, 1080), now + Duration::from_secs(10))
|
||||
.is_some());
|
||||
}
|
||||
|
||||
struct RecordingBackend {
|
||||
mouse: tokio::sync::mpsc::UnboundedSender<MouseEvent>,
|
||||
runtime: tokio::sync::watch::Sender<()>,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl crate::hid::HidBackend for RecordingBackend {
|
||||
async fn init(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn send_keyboard(&self, _: KeyboardEvent) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn send_mouse(&self, event: MouseEvent) -> crate::error::Result<()> {
|
||||
self.mouse.send(event).unwrap();
|
||||
Ok(())
|
||||
}
|
||||
async fn reset(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
async fn shutdown(&self) -> crate::error::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn runtime_snapshot(&self) -> crate::hid::HidBackendRuntimeSnapshot {
|
||||
crate::hid::HidBackendRuntimeSnapshot {
|
||||
initialized: true,
|
||||
online: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
fn subscribe_runtime(&self) -> tokio::sync::watch::Receiver<()> {
|
||||
self.runtime.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn jiggler_worker_runs_without_clients_and_confirmed_input_cancels_return() {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
let backend = std::sync::Arc::new(RecordingBackend {
|
||||
mouse: tx,
|
||||
runtime: tokio::sync::watch::channel(()).0,
|
||||
});
|
||||
let controller = crate::hid::test_controller(backend).await;
|
||||
let (_, config) = configured(Instant::now());
|
||||
controller.configure_jiggler(&config);
|
||||
{
|
||||
let mut state = controller.jiggler.lock();
|
||||
state.ready = true;
|
||||
state.activity(Instant::now() - Duration::from_secs(9));
|
||||
}
|
||||
// Bluetooth publishes status periodically, even with no user input.
|
||||
let changed = controller.jiggler_changed.clone();
|
||||
let notifications = tokio::spawn(async move {
|
||||
loop {
|
||||
changed.notify_one();
|
||||
tokio::time::sleep(Duration::from_millis(20)).await;
|
||||
}
|
||||
});
|
||||
let out = tokio::time::timeout(Duration::from_secs(3), rx.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
notifications.abort();
|
||||
assert_eq!(out.x, 2);
|
||||
controller
|
||||
.send_keyboard_confirmed(KeyboardEvent::key_down(
|
||||
CanonicalKey::KeyA,
|
||||
KeyboardModifiers::default(),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(tokio::time::timeout(Duration::from_millis(300), rx.recv())
|
||||
.await
|
||||
.is_err());
|
||||
controller.shutdown().await.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ mod ch9329_proto;
|
||||
pub mod consumer;
|
||||
pub mod datachannel;
|
||||
mod factory;
|
||||
mod jiggler;
|
||||
pub mod keyboard;
|
||||
mod macos_drag;
|
||||
#[cfg(unix)]
|
||||
@@ -141,6 +142,8 @@ pub struct HidController {
|
||||
backend_available: Arc<AtomicBool>,
|
||||
reset_requested: Arc<AtomicBool>,
|
||||
screen_resolution: parking_lot::RwLock<(u32, u32)>,
|
||||
jiggler: Arc<parking_lot::Mutex<jiggler::JigglerState>>,
|
||||
jiggler_changed: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
|
||||
impl HidController {
|
||||
@@ -164,6 +167,8 @@ impl HidController {
|
||||
backend_available: Arc::new(AtomicBool::new(false)),
|
||||
reset_requested: Arc::new(AtomicBool::new(false)),
|
||||
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
|
||||
jiggler: Arc::new(parking_lot::Mutex::new(jiggler::JigglerState::default())),
|
||||
jiggler_changed: Arc::new(tokio::sync::Notify::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +192,8 @@ impl HidController {
|
||||
backend_available: Arc::new(AtomicBool::new(false)),
|
||||
reset_requested: Arc::new(AtomicBool::new(false)),
|
||||
screen_resolution: parking_lot::RwLock::new((1920, 1080)),
|
||||
jiggler: Arc::new(parking_lot::Mutex::new(jiggler::JigglerState::default())),
|
||||
jiggler_changed: Arc::new(tokio::sync::Notify::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,6 +242,9 @@ impl HidController {
|
||||
|
||||
pub async fn shutdown(&self) -> Result<()> {
|
||||
info!("Shutting down HID controller");
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
self.jiggler.lock().suspend();
|
||||
self.jiggler_changed.notify_one();
|
||||
self.stop_runtime_worker().await;
|
||||
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
@@ -265,6 +275,7 @@ impl HidController {
|
||||
|
||||
info!("Preparing OTG HID backend for gadget rebuild");
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
self.jiggler.lock().suspend();
|
||||
self.stop_runtime_worker().await;
|
||||
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
@@ -403,6 +414,14 @@ impl HidController {
|
||||
self.runtime_state.read().await.clone()
|
||||
}
|
||||
|
||||
/// Jiggler preferences are independent of USB descriptors and device reloads.
|
||||
pub fn configure_jiggler(&self, config: &crate::config::HidConfig) {
|
||||
self.jiggler
|
||||
.lock()
|
||||
.configure(config, std::time::Instant::now());
|
||||
self.jiggler_changed.notify_one();
|
||||
}
|
||||
|
||||
async fn ch9329_backend(&self) -> Result<Arc<dyn HidBackend>> {
|
||||
if !matches!(
|
||||
*self.backend_type.read().await,
|
||||
@@ -436,6 +455,7 @@ impl HidController {
|
||||
pub async fn reload(&self, new_backend_type: HidBackendType) -> Result<()> {
|
||||
info!("Reloading HID backend: {:?}", new_backend_type);
|
||||
self.backend_available.store(false, Ordering::Release);
|
||||
self.jiggler.lock().suspend();
|
||||
self.stop_runtime_worker().await;
|
||||
|
||||
if let Some(backend) = self.backend.write().await.take() {
|
||||
@@ -544,13 +564,53 @@ impl HidController {
|
||||
let pending_move = self.pending_move.clone();
|
||||
let pending_move_flag = self.pending_move_flag.clone();
|
||||
let reset_requested = self.reset_requested.clone();
|
||||
let jiggler = self.jiggler.clone();
|
||||
let jiggler_changed = self.jiggler_changed.clone();
|
||||
let backend_available = self.backend_available.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let mut rx = rx;
|
||||
let mut next_poll = tokio::time::Instant::now();
|
||||
loop {
|
||||
let event = match rx.recv().await {
|
||||
Some(ev) => ev,
|
||||
None => break,
|
||||
let delay = jiggler.lock().delay();
|
||||
let event = tokio::select! {
|
||||
biased;
|
||||
event = rx.recv() => match event {
|
||||
Some(event) => event,
|
||||
None => break,
|
||||
},
|
||||
_ = jiggler_changed.notified() => continue,
|
||||
_ = tokio::time::sleep_until(next_poll), if delay.is_some() => {
|
||||
// Synthetic motion is sent by this same worker, outside the
|
||||
// coalescing queue, so opposite movements cannot cancel out.
|
||||
let active_backend = backend.read().await.clone();
|
||||
let snapshot = active_backend.as_ref().map(|backend| backend.runtime_snapshot());
|
||||
let ready = backend_available.load(Ordering::Acquire)
|
||||
&& snapshot.as_ref().is_some_and(|state| state.initialized && state.online);
|
||||
let resolution = snapshot.and_then(|state| state.screen_resolution).unwrap_or((1920, 1080));
|
||||
let movement = jiggler.lock().tick(ready, resolution, std::time::Instant::now());
|
||||
if let (Some((event, generation)), Some(active_backend)) = (movement, active_backend) {
|
||||
let state = jiggler.clone();
|
||||
let available = backend_available.clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
// Check again after dispatch to the blocking pool:
|
||||
// real input, shutdown or a config change may win.
|
||||
if !available.load(Ordering::Acquire) || !state.lock().valid(generation) {
|
||||
return Ok(());
|
||||
}
|
||||
futures::executor::block_on(active_backend.send_mouse(event))
|
||||
}).await;
|
||||
if !matches!(result, Ok(Ok(()))) {
|
||||
jiggler.lock().failed(generation, std::time::Instant::now());
|
||||
warn!("Mouse jiggler movement failed: {:?}", result);
|
||||
}
|
||||
}
|
||||
// Keep an absolute deadline: periodic backend status
|
||||
// notifications must not restart the timer indefinitely.
|
||||
next_poll = tokio::time::Instant::now()
|
||||
+ jiggler.lock().delay().unwrap_or(Duration::from_secs(1));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if reset_requested.swap(false, Ordering::AcqRel) {
|
||||
@@ -559,15 +619,16 @@ impl HidController {
|
||||
while rx.try_recv().is_ok() {}
|
||||
*pending_move.lock() = None;
|
||||
pending_move_flag.store(false, Ordering::Release);
|
||||
process_hid_event(QueuedHidEvent::Reset, &backend).await;
|
||||
process_hid_event(QueuedHidEvent::Reset, &backend, &jiggler).await;
|
||||
continue;
|
||||
}
|
||||
process_hid_event(event, &backend).await;
|
||||
process_hid_event(event, &backend, &jiggler).await;
|
||||
|
||||
if pending_move_flag.swap(false, Ordering::AcqRel) {
|
||||
let move_event = { pending_move.lock().take() };
|
||||
if let Some(move_event) = move_event {
|
||||
process_hid_event(QueuedHidEvent::Mouse(move_event), &backend).await;
|
||||
process_hid_event(QueuedHidEvent::Mouse(move_event), &backend, &jiggler)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -589,6 +650,8 @@ impl HidController {
|
||||
let events = self.events.clone();
|
||||
let backend_available = self.backend_available.clone();
|
||||
let backend_type = self.backend_type.clone();
|
||||
let jiggler = self.jiggler.clone();
|
||||
let jiggler_changed = self.jiggler_changed.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
loop {
|
||||
@@ -604,6 +667,11 @@ impl HidController {
|
||||
Some(backend.as_ref()),
|
||||
)
|
||||
.await;
|
||||
let state = runtime_state.read().await;
|
||||
if !state.initialized || !state.online {
|
||||
jiggler.lock().suspend();
|
||||
}
|
||||
jiggler_changed.notify_one();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -617,6 +685,7 @@ impl HidController {
|
||||
}
|
||||
|
||||
fn enqueue_mouse_move(&self, event: MouseEvent) -> Result<()> {
|
||||
self.jiggler.lock().activity(std::time::Instant::now());
|
||||
match self.hid_tx.try_send(QueuedHidEvent::Mouse(event.clone())) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
@@ -631,6 +700,8 @@ impl HidController {
|
||||
}
|
||||
|
||||
async fn enqueue_event(&self, event: QueuedHidEvent) -> Result<()> {
|
||||
self.jiggler.lock().activity(std::time::Instant::now());
|
||||
self.jiggler_changed.notify_one();
|
||||
match self.hid_tx.try_send(event) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(mpsc::error::TrySendError::Full(ev)) => {
|
||||
@@ -690,11 +761,23 @@ async fn apply_backend_runtime_state(
|
||||
async fn process_hid_event(
|
||||
event: QueuedHidEvent,
|
||||
backend: &Arc<RwLock<Option<Arc<dyn HidBackend>>>>,
|
||||
jiggler: &Arc<parking_lot::Mutex<jiggler::JigglerState>>,
|
||||
) {
|
||||
let (event, reply) = match event {
|
||||
QueuedHidEvent::Confirmed(event, reply) => (*event, Some(reply)),
|
||||
event => (event, None),
|
||||
};
|
||||
{
|
||||
let mut state = jiggler.lock();
|
||||
let now = std::time::Instant::now();
|
||||
match &event {
|
||||
QueuedHidEvent::Keyboard(event) => state.keyboard(event, now),
|
||||
QueuedHidEvent::Mouse(event) => state.mouse(event, now),
|
||||
QueuedHidEvent::Consumer(event) => state.consumer(event.usage, now),
|
||||
QueuedHidEvent::Reset => state.reset(now),
|
||||
QueuedHidEvent::Confirmed(..) => unreachable!("nested confirmed HID event"),
|
||||
}
|
||||
}
|
||||
let backend = backend.read().await.clone();
|
||||
let result = match backend {
|
||||
Some(backend) => tokio::task::spawn_blocking(move || {
|
||||
|
||||
Reference in New Issue
Block a user