v2: Rust rewrite with a YAML grid layout, evdev gestures and a preflighting installer #1
+265
-44
@@ -24,13 +24,120 @@ pub struct Swipe {
|
||||
pub fingers: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// One contact's journey.
|
||||
///
|
||||
/// The start position is per axis and `Option`, NOT a `(0, 0)` sentinel. X and Y
|
||||
/// arrive as SEPARATE events, so a contact's opening frame is `POSITION_X` then
|
||||
/// `POSITION_Y`: capturing "the start" on the first of those records a Y of
|
||||
/// zero, and every later comparison then measures from the top edge of the panel
|
||||
/// rather than from the finger. That made `dy` enormous and positive, so every
|
||||
/// swipe -- horizontal ones included -- classified as up-to-down.
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct Slot {
|
||||
start: (i32, i32),
|
||||
start_x: Option<i32>,
|
||||
start_y: Option<i32>,
|
||||
last: (i32, i32),
|
||||
active: bool,
|
||||
}
|
||||
|
||||
impl Slot {
|
||||
/// An axis that never reported a position contributes no displacement.
|
||||
fn track(&self) -> Track {
|
||||
(
|
||||
(
|
||||
self.start_x.unwrap_or(self.last.0),
|
||||
self.start_y.unwrap_or(self.last.1),
|
||||
),
|
||||
self.last,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The parts of a multitouch protocol-B stream we care about. Kept separate from
|
||||
/// evdev's own types so the slot bookkeeping is testable without a device --
|
||||
/// which is precisely the code the above bug lived in, untested.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Touched {
|
||||
/// `ABS_MT_SLOT`: subsequent events apply to this slot.
|
||||
Slot(i32),
|
||||
/// `ABS_MT_TRACKING_ID`: a new contact when >= 0, a lift when -1.
|
||||
TrackingId(i32),
|
||||
X(i32),
|
||||
Y(i32),
|
||||
}
|
||||
|
||||
/// Slot bookkeeping for multitouch protocol B.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct SlotTracker {
|
||||
slots: HashMap<i32, Slot>,
|
||||
current: i32,
|
||||
peak: usize,
|
||||
}
|
||||
|
||||
impl SlotTracker {
|
||||
pub fn feed(&mut self, ev: Touched) {
|
||||
match ev {
|
||||
Touched::Slot(n) => self.current = n,
|
||||
Touched::TrackingId(id) => {
|
||||
if id < 0 {
|
||||
if let Some(s) = self.slots.get_mut(&self.current) {
|
||||
s.active = false;
|
||||
}
|
||||
} else {
|
||||
self.slots.insert(
|
||||
self.current,
|
||||
Slot {
|
||||
active: true,
|
||||
..Slot::default()
|
||||
},
|
||||
);
|
||||
self.peak = self.peak.max(self.active());
|
||||
}
|
||||
}
|
||||
Touched::X(x) => {
|
||||
let s = self.slot();
|
||||
s.last.0 = x;
|
||||
s.start_x.get_or_insert(x);
|
||||
}
|
||||
Touched::Y(y) => {
|
||||
let s = self.slot();
|
||||
s.last.1 = y;
|
||||
s.start_y.get_or_insert(y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn slot(&mut self) -> &mut Slot {
|
||||
self.slots.entry(self.current).or_insert(Slot {
|
||||
active: true,
|
||||
..Slot::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn active(&self) -> usize {
|
||||
self.slots.values().filter(|s| s.active).count()
|
||||
}
|
||||
|
||||
/// A gesture is over when every contact seen has lifted.
|
||||
pub fn complete(&self) -> bool {
|
||||
!self.slots.is_empty() && self.active() == 0
|
||||
}
|
||||
|
||||
pub fn tracks(&self) -> Vec<Track> {
|
||||
self.slots.values().map(Slot::track).collect()
|
||||
}
|
||||
|
||||
/// Peak simultaneous contacts, which is what `touch.fingers` matches.
|
||||
pub fn peak(&self) -> usize {
|
||||
self.peak
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.slots.clear();
|
||||
self.peak = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Touchpanel {
|
||||
device: Device,
|
||||
cfg: Touch,
|
||||
@@ -129,53 +236,36 @@ impl Touchpanel {
|
||||
/// Blocking gesture loop. Calls `on_event` for every completed gesture,
|
||||
/// including rejected ones, and stops when it returns `false`.
|
||||
pub fn run(&mut self, mut on_event: impl FnMut(Event) -> bool) -> Result<()> {
|
||||
let mut slots: HashMap<i32, Slot> = HashMap::new();
|
||||
let mut current: i32 = 0;
|
||||
let mut peak: usize = 0;
|
||||
let mut tracker = SlotTracker::default();
|
||||
|
||||
loop {
|
||||
for ev in self.device.fetch_events().context("reading touch events")? {
|
||||
match ev.kind() {
|
||||
let touched = match ev.kind() {
|
||||
InputEventKind::AbsAxis(AbsoluteAxisType::ABS_MT_SLOT) => {
|
||||
current = ev.value();
|
||||
Touched::Slot(ev.value())
|
||||
}
|
||||
InputEventKind::AbsAxis(AbsoluteAxisType::ABS_MT_TRACKING_ID) => {
|
||||
if ev.value() < 0 {
|
||||
if let Some(s) = slots.get_mut(¤t) {
|
||||
s.active = false;
|
||||
}
|
||||
} else {
|
||||
slots.insert(
|
||||
current,
|
||||
Slot {
|
||||
start: (0, 0),
|
||||
last: (0, 0),
|
||||
active: true,
|
||||
},
|
||||
);
|
||||
peak = peak.max(slots.values().filter(|s| s.active).count());
|
||||
}
|
||||
Touched::TrackingId(ev.value())
|
||||
}
|
||||
InputEventKind::AbsAxis(AbsoluteAxisType::ABS_MT_POSITION_X) => {
|
||||
update(&mut slots, current, |p| p.0 = ev.value());
|
||||
Touched::X(ev.value())
|
||||
}
|
||||
InputEventKind::AbsAxis(AbsoluteAxisType::ABS_MT_POSITION_Y) => {
|
||||
update(&mut slots, current, |p| p.1 = ev.value());
|
||||
Touched::Y(ev.value())
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
_ => continue,
|
||||
};
|
||||
tracker.feed(touched);
|
||||
}
|
||||
|
||||
// The gesture ends when the last contact lifts.
|
||||
if !slots.is_empty() && slots.values().all(|s| !s.active) {
|
||||
let tracks: Vec<Track> = slots.values().map(|s| (s.start, s.last)).collect();
|
||||
if let Some(ev) = classify(&self.cfg, &tracks, peak) {
|
||||
if tracker.complete() {
|
||||
if let Some(ev) = classify(&self.cfg, &tracker.tracks(), tracker.peak()) {
|
||||
if !on_event(ev) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
slots.clear();
|
||||
peak = 0;
|
||||
tracker.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -255,19 +345,6 @@ pub fn classify(cfg: &Touch, tracks: &[Track], peak: usize) -> Option<Event> {
|
||||
}))
|
||||
}
|
||||
|
||||
fn update(slots: &mut HashMap<i32, Slot>, current: i32, f: impl Fn(&mut (i32, i32))) {
|
||||
let slot = slots.entry(current).or_insert(Slot {
|
||||
start: (0, 0),
|
||||
last: (0, 0),
|
||||
active: true,
|
||||
});
|
||||
f(&mut slot.last);
|
||||
// The first position report after a contact begins is also its origin.
|
||||
if slot.start == (0, 0) {
|
||||
slot.start = slot.last;
|
||||
}
|
||||
}
|
||||
|
||||
/// Candidate touchscreens, for `doctor --list` and the installer.
|
||||
pub fn list_touchscreens() -> Vec<(String, String)> {
|
||||
let mut out = Vec::new();
|
||||
@@ -422,6 +499,150 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// -- slot tracking: real protocol-B event streams -----------------------
|
||||
//
|
||||
// These exist because the classifier tests above build Track tuples by hand
|
||||
// and so never exercised the decoding. The bug that shipped to the rack
|
||||
// display lived exactly here: every swipe came out as up-to-down, and the
|
||||
// display got stuck on the bottom row because "down" from there is a no-op.
|
||||
|
||||
/// One contact moving from `from` to `to`, reported the way the kernel does:
|
||||
/// tracking id, then X and Y as separate events, then a lift.
|
||||
fn contact(t: &mut SlotTracker, slot: i32, from: (i32, i32), to: (i32, i32), steps: i32) {
|
||||
t.feed(Touched::Slot(slot));
|
||||
t.feed(Touched::TrackingId(slot + 1));
|
||||
for i in 0..=steps {
|
||||
let x = from.0 + (to.0 - from.0) * i / steps;
|
||||
let y = from.1 + (to.1 - from.1) * i / steps;
|
||||
t.feed(Touched::Slot(slot));
|
||||
t.feed(Touched::X(x));
|
||||
t.feed(Touched::Y(y));
|
||||
}
|
||||
}
|
||||
|
||||
fn lift(t: &mut SlotTracker, slot: i32) {
|
||||
t.feed(Touched::Slot(slot));
|
||||
t.feed(Touched::TrackingId(-1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_horizontal_swipe_low_on_the_panel_is_not_read_as_downward() {
|
||||
// THE REGRESSION. A right-to-left swipe at y=360 on a 720-tall panel.
|
||||
// With a (0,0) start sentinel, Y was captured as 0 and dy became +360,
|
||||
// dwarfing dx and classifying this as UD.
|
||||
let mut t = SlotTracker::default();
|
||||
contact(&mut t, 0, (900, 360), (700, 362), 10);
|
||||
lift(&mut t, 0);
|
||||
assert!(t.complete());
|
||||
|
||||
match classify(&touch(80, 30, vec![1]), &t.tracks(), t.peak()) {
|
||||
Some(Event::Swipe(s)) => {
|
||||
assert_eq!(
|
||||
s.direction,
|
||||
Direction::RL,
|
||||
"a left swipe must not read as down"
|
||||
);
|
||||
assert_eq!(s.fingers, 1);
|
||||
}
|
||||
other => panic!("expected an RL swipe, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_is_captured_per_axis() {
|
||||
// X arrives before Y, so a start captured on the first event alone would
|
||||
// record y=0 and report the finger travelling the height of the panel.
|
||||
let mut t = SlotTracker::default();
|
||||
t.feed(Touched::Slot(0));
|
||||
t.feed(Touched::TrackingId(1));
|
||||
t.feed(Touched::X(900));
|
||||
t.feed(Touched::Y(360));
|
||||
t.feed(Touched::X(700));
|
||||
t.feed(Touched::Y(360));
|
||||
lift(&mut t, 0);
|
||||
assert_eq!(t.tracks(), vec![((900, 360), (700, 360))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_four_directions_survive_decoding() {
|
||||
let cfg = touch(80, 30, vec![1]);
|
||||
// Centre of a 1280x720 panel, 200px each way.
|
||||
for (to, want) in [
|
||||
((440, 360), Direction::RL),
|
||||
((840, 360), Direction::LR),
|
||||
((640, 160), Direction::DU),
|
||||
((640, 560), Direction::UD),
|
||||
] {
|
||||
let mut t = SlotTracker::default();
|
||||
contact(&mut t, 0, (640, 360), to, 10);
|
||||
lift(&mut t, 0);
|
||||
match classify(&cfg, &t.tracks(), t.peak()) {
|
||||
Some(Event::Swipe(s)) => assert_eq!(s.direction, want, "moving to {to:?}"),
|
||||
other => panic!("moving to {to:?}: {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ghost_contacts_decode_as_one_swipe_of_the_right_length() {
|
||||
// The ILITEK panel reports one finger as two or three contacts.
|
||||
let mut t = SlotTracker::default();
|
||||
contact(&mut t, 0, (900, 300), (700, 300), 8);
|
||||
contact(&mut t, 1, (903, 305), (703, 305), 8);
|
||||
contact(&mut t, 2, (897, 295), (697, 295), 8);
|
||||
lift(&mut t, 0);
|
||||
lift(&mut t, 1);
|
||||
lift(&mut t, 2);
|
||||
|
||||
assert_eq!(t.peak(), 3, "peak contacts drive the touch.fingers match");
|
||||
match classify(&touch(150, 30, vec![1, 2, 3]), &t.tracks(), t.peak()) {
|
||||
Some(Event::Swipe(s)) => {
|
||||
assert_eq!(s.direction, Direction::RL);
|
||||
assert_eq!(s.fingers, 3);
|
||||
}
|
||||
other => panic!("expected one RL swipe: {other:?}"),
|
||||
}
|
||||
// 200px of travel, not 600.
|
||||
match classify(&touch(250, 30, vec![1, 2, 3]), &t.tracks(), t.peak()) {
|
||||
Some(Event::Discarded(_, Rejected::TooShort { travel, .. })) => {
|
||||
assert!((travel - 200.0).abs() < 2.0, "travel was {travel}");
|
||||
}
|
||||
other => panic!("expected 200px of travel: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gesture_is_only_complete_once_every_contact_lifts() {
|
||||
let mut t = SlotTracker::default();
|
||||
assert!(!t.complete(), "nothing has been touched yet");
|
||||
contact(&mut t, 0, (900, 300), (800, 300), 4);
|
||||
assert!(!t.complete(), "still down");
|
||||
contact(&mut t, 1, (500, 300), (400, 300), 4);
|
||||
lift(&mut t, 0);
|
||||
assert!(!t.complete(), "one contact is still down");
|
||||
lift(&mut t, 1);
|
||||
assert!(t.complete());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_clears_the_previous_gesture() {
|
||||
let mut t = SlotTracker::default();
|
||||
contact(&mut t, 0, (900, 300), (700, 300), 4);
|
||||
lift(&mut t, 0);
|
||||
t.reset();
|
||||
assert!(!t.complete());
|
||||
assert_eq!(t.peak(), 0);
|
||||
assert!(t.tracks().is_empty());
|
||||
|
||||
// A second swipe must measure from its own origin, not the first one's.
|
||||
contact(&mut t, 0, (300, 300), (500, 300), 4);
|
||||
lift(&mut t, 0);
|
||||
match classify(&touch(80, 30, vec![1]), &t.tracks(), t.peak()) {
|
||||
Some(Event::Swipe(s)) => assert_eq!(s.direction, Direction::LR),
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_gesture_with_no_contacts_is_not_a_gesture() {
|
||||
assert!(classify(&touch(80, 30, vec![1]), &[], 0).is_none());
|
||||
|
||||
Reference in New Issue
Block a user