From 84ca87607f89b8391ff2672ee32e5b1a74e3d8c3 Mon Sep 17 00:00:00 2001 From: snoutbug Date: Mon, 20 Nov 2023 16:04:21 +0100 Subject: [PATCH 1/4] Add support for Multi-touch --- multitouch.go | 207 +++++++++++++++++++++++++++++++++++++++++ multitouch_test.go | 225 +++++++++++++++++++++++++++++++++++++++++++++ uinputdefs.go | 6 ++ 3 files changed, 438 insertions(+) create mode 100644 multitouch.go create mode 100644 multitouch_test.go diff --git a/multitouch.go b/multitouch.go new file mode 100644 index 0000000..a612a43 --- /dev/null +++ b/multitouch.go @@ -0,0 +1,207 @@ +package uinput + +import ( + "fmt" + "io" + "os" +) + +// MultiTouch is an input device that uses absolute axis events. +// Unlike the TouchPad, MultiTouch supports the simulation of multiple inputs (contacts) +// allowing for different gestures, for exmaple pinch to zoom. +// Each contact point is assigned a slot, making it necessary to define the maxmimum +// expected amount of contact points . +// Since MultiTouch uses absolute axis events, it is necessary to define the size +// of the rectangle in which the contacs may move upon creation of the device. +type MultiTouch interface { + //Gets all contacts which can then be manipulated + GetContacts() []multiTouchContact + + // FetchSyspath will return the syspath to the device file. + FetchSyspath() (string, error) + + io.Closer +} + +type vMultiTouch struct { + name []byte + deviceFile *os.File + contacts []multiTouchContact +} + +// The contact can be described as a finger contacting the surface of the MultiTouch device. +type multiTouchContact struct { + multitouch *vMultiTouch + slot int32 + tracking_id int32 +} + +// CreateMultiTouch will create a new multitouch device. Note that you will need to define the x and y-axis boundaries +// (min and max) within which the contacs maybe moved around, as well as the maximum amount of contacts allowed. +func CreateMultiTouch(path string, name []byte, minX int32, maxX int32, minY int32, maxY int32, maxContacts int32) (MultiTouch, error) { + err := validateDevicePath(path) + if err != nil { + return nil, err + } + err = validateUinputName(name) + if err != nil { + return nil, err + } + + fd, err := createMultiTouch(path, name, minX, maxX, minY, maxY, maxContacts) + if err != nil { + return nil, err + } + + var multitouch vMultiTouch = vMultiTouch{name: name, deviceFile: fd} + + for i := int32(0); i < maxContacts; i++ { + multitouch.contacts = append(multitouch.contacts, multiTouchContact{slot: i, multitouch: &multitouch}) + } + + return multitouch, nil +} + +func (vMulti vMultiTouch) GetContacts() []multiTouchContact { + return vMulti.contacts +} + +func (vMulti vMultiTouch) FetchSyspath() (string, error) { + return fetchSyspath(vMulti.deviceFile) +} + +func (vMulti vMultiTouch) Close() error { + return closeDevice(vMulti.deviceFile) +} + +func createMultiTouch(path string, name []byte, minX int32, maxX int32, minY int32, maxY int32, maxContacts int32) (fd *os.File, err error) { + deviceFile, err := createDeviceFile(path) + if err != nil { + return nil, fmt.Errorf("could not create absolute axis input device: %v", err) + } + + err = registerDevice(deviceFile, uintptr(evKey)) + if err != nil { + _ = deviceFile.Close() + return nil, fmt.Errorf("failed to register key device: %v", err) + } + + for _, event := range []int{evBtnTouch} { + err = ioctl(deviceFile, uiSetKeyBit, uintptr(event)) + if err != nil { + _ = deviceFile.Close() + return nil, fmt.Errorf("failed to register button event %v: %v", event, err) + } + } + + err = registerDevice(deviceFile, uintptr(evAbs)) + if err != nil { + _ = deviceFile.Close() + return nil, fmt.Errorf("failed to register absolute axis input device: %v", err) + } + + for _, event := range []int{ + absMtSlot, + absMtTrackingId, + absMtPositionX, + absMtPositionY, + } { + err = ioctl(deviceFile, uiSetAbsBit, uintptr(event)) + if err != nil { + _ = deviceFile.Close() + return nil, fmt.Errorf("failed to register absolute axis event %v: %v", event, err) + } + } + + var absMin [absSize]int32 + absMin[absMtPositionX] = minX + absMin[absMtPositionY] = minY + absMin[absMtTrackingId] = 0x00 + absMin[absMtSlot] = 0x00 + + var absMax [absSize]int32 + absMax[absMtPositionX] = maxX + absMax[absMtPositionY] = maxY + absMax[absMtTrackingId] = maxContacts + absMax[absMtSlot] = maxContacts + + return createUsbDevice(deviceFile, + uinputUserDev{ + Name: toUinputName(name), + ID: inputID{ + Bustype: busUsb, + Vendor: 0x0, + Product: 0x0, + Version: 0}, + Absmin: absMin, + Absmax: absMax}) +} + +func (c multiTouchContact) TouchDownAt(x int32, y int32) error { + var events []inputEvent + + events = append(events, inputEvent{ + Type: evAbs, + Code: absMtPositionX, + Value: x, + }) + + if x == 0 && y == 0 { + y-- + } + + events = append(events, inputEvent{ + Type: evAbs, + Code: absMtPositionY, + Value: y, + }) + + events = append(events, inputEvent{ + Type: evAbs, + Code: 0x32, + Value: 0x32, + }) + + c.tracking_id = c.slot + + return c.sendAbsEvent(events) +} + +func (c multiTouchContact) TouchUp() error { + c.tracking_id = -1 + return c.sendAbsEvent(nil) +} + +func (c multiTouchContact) sendAbsEvent(events []inputEvent) error { + var ev []inputEvent + + ev = append(ev, inputEvent{ + Type: evAbs, + Code: absMtSlot, + Value: c.slot, + }) + + ev = append(ev, inputEvent{ + Type: evAbs, + Code: absMtTrackingId, + Value: c.tracking_id, + }) + + if events != nil { + ev = append(ev, events...) + } + + for _, iev := range ev { + buf, err := inputEventToBuffer(iev) + if err != nil { + return fmt.Errorf("writing abs event failed: %v", err) + } + + _, err = c.multitouch.deviceFile.Write(buf) + if err != nil { + return fmt.Errorf("failed to write abs event to device file: %v", err) + } + } + + return syncEvents(c.multitouch.deviceFile) +} diff --git a/multitouch_test.go b/multitouch_test.go new file mode 100644 index 0000000..91fda8b --- /dev/null +++ b/multitouch_test.go @@ -0,0 +1,225 @@ +package uinput + +import ( + "fmt" + "io/ioutil" + "os" + "testing" + "time" +) + +func TestBasicMultiTouchMoves(t *testing.T) { + absDev, err := CreateMultiTouch("/dev/uinput", []byte("Test MultiTouch"), 0, 1024, 0, 768, 3) + if err != nil { + t.Fatalf("Failed to create the virtual multi touch device. Last error was: %s\n", err) + } + defer func(absDev MultiTouch) { + err := absDev.Close() + if err != nil { + t.Fatalf("Failed to close device. Last error was: %s\n", err) + } + }(absDev) + + contacts := absDev.GetContacts() + + if len(contacts) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contacts))) + } + + err = contacts[0].TouchDownAt(0, 0) + if err != nil { + t.Fatalf("Failed to move contact 0 to initial position. Last error was: %s\n", err) + } + + err = contacts[0].TouchDownAt(100, 200) + if err != nil { + t.Fatalf("Failed to move contact 0 to position x:100, y:200. Last error was: %s\n", err) + } +} + +func TestBasicMultiTouchGesture(t *testing.T) { + absDev, err := CreateMultiTouch("/dev/uinput", []byte("Test MultiTouch"), 0, 1024, 0, 768, 3) + if err != nil { + t.Fatalf("Failed to create the virtual multi touch device. Last error was: %s\n", err) + } + defer func(absDev MultiTouch) { + err := absDev.Close() + if err != nil { + t.Fatalf("Failed to close device. Last error was: %s\n", err) + } + }(absDev) + + contacts := absDev.GetContacts() + + if len(contacts) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contacts))) + } + + for i := int32(0); i < 200; i++ { + time.Sleep(3 * time.Millisecond) + for n := int32(0); n < 3; n++ { + y := 255 - i + err := contacts[n].TouchDownAt(0, y) + if err != nil { + t.Fatalf("Failed to move contact %s at [0, %s]", fmt.Sprint(i), fmt.Sprint(y)) + } + } + } +} + +func TestMultiTouchCreationFailsOnEmptyPath(t *testing.T) { + expected := "device path must not be empty" + _, err := CreateMultiTouch("", []byte("TouchDevice"), 0, 1024, 0, 768, 3) + if err.Error() != expected { + t.Fatalf("Expected: %s\nActual: %s", expected, err) + } +} + +func TestMultiTouchCreationFailsOnNonExistentPathName(t *testing.T) { + path := "/some/bogus/path" + _, err := CreateMultiTouch(path, []byte("TouchDevice"), 0, 1024, 0, 768, 3) + if !os.IsNotExist(err) { + t.Fatalf("Expected: os.IsNotExist error\nActual: %s", err) + } +} + +func TestMultiTouchCreationFailsOnWrongPathName(t *testing.T) { + file, err := ioutil.TempFile(os.TempDir(), "uinput-MultiTouch-test-") + if err != nil { + t.Fatalf("Failed to setup test. Unable to create tempfile: %v", err) + } + defer file.Close() + + expected := "failed to register key device: failed to close device: inappropriate ioctl for device" + _, err = CreateMultiTouch(file.Name(), []byte("TouchDevice"), 0, 1024, 0, 768, 3) + if err == nil || !(expected == err.Error()) { + t.Fatalf("Expected: %s\nActual: %s", expected, err) + } +} + +func TestMultiTouchCreationFailsIfNameIsTooLong(t *testing.T) { + name := "adsfdsferqewoirueworiuejdsfjdfa;ljoewrjeworiewuoruew;rj;kdlfjoeai;jfewoaifjef;das" + expected := fmt.Sprintf("device name %s is too long (maximum of %d characters allowed)", name, uinputMaxNameSize) + _, err := CreateMultiTouch("/dev/uinput", []byte(name), 0, 1024, 0, 768, 3) + if err.Error() != expected { + t.Fatalf("Expected: %s\nActual: %s", expected, err) + } +} + +func TestMultiTouchMoveToFailsOnClosedDevice(t *testing.T) { + absDev, err := CreateMultiTouch("/dev/uinput", []byte("Test MultiTouch"), 0, 1024, 0, 768, 3) + if err != nil { + t.Fatalf("Failed to create the virtual touch pad. Last error was: %s\n", err) + } + + _ = absDev.Close() + + contacts := absDev.GetContacts() + + if len(contacts) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contacts))) + } + + err = contacts[0].TouchDownAt(1, 1) + if err != nil { + t.Fatalf("Expected error due to closed device, but no error was returned.") + } +} + +func TestMultipleMultiTouchsWithDifferentSizes(t *testing.T) { + horizontal, err := CreateMultiTouch("/dev/uinput", []byte("horizontal_pad"), 0, 200, 0, 100, 3) + if err != nil { + t.Fatalf("Failed to create the virtual touch pad. Last error was: %s\n", err) + } + defer horizontal.Close() + vertical, err := CreateMultiTouch("/dev/uinput", []byte("vertical_pad"), 0, 100, 0, 200, 3) + if err != nil { + t.Fatalf("Failed to create the virtual touch pad. Last error was: %s\n", err) + } + defer vertical.Close() + + contactsHorizontal := horizontal.GetContacts() + + if len(contactsHorizontal) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contactsHorizontal))) + } + + err = contactsHorizontal[0].TouchDownAt(200, 100) + if err != nil { + t.Fatalf("Unable to move cursor on horizontal pad: %v", err) + } + + contactsVertical := vertical.GetContacts() + + if len(contactsVertical) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contactsVertical))) + } + + err = contactsHorizontal[0].TouchDownAt(100, 200) + if err != nil { + t.Fatalf("Unable to move cursor on horizontal pad: %v", err) + } +} + +func TestMultiTouchPositioningInUpperLeftCorner(t *testing.T) { + dev, err := CreateMultiTouch("/dev/uinput", []byte("MultiTouch"), 0, 200, 0, 100, 3) + if err != nil { + t.Fatalf("Failed to create the virtual touch pad. Last error was: %s\n", err) + } + defer dev.Close() + + contacts := dev.GetContacts() + + if len(contacts) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contacts))) + } + + err = contacts[0].TouchDownAt(0, 0) + + if err != nil { + t.Fatalf("Failed to move cursor to upper left corner: %v", err) + } +} + +func TestMultiTouchSingleTouchEvent(t *testing.T) { + dev, err := CreateMultiTouch("/dev/uinput", []byte("MultiTouch"), 0, 200, 0, 100, 3) + if err != nil { + t.Fatalf("Failed to create the virtual touch pad. Last error was: %s\n", err) + } + defer dev.Close() + + contacts := dev.GetContacts() + + if len(contacts) != 3 { + t.Fatalf("Failed to create contacts, expected 3, got %s", fmt.Sprint(len(contacts))) + } + + err = contacts[0].TouchDownAt(0, 0) + if err != nil { + t.Fatalf("Failed to issue touch down event at [0,0]: %v", err) + } + + err = contacts[0].TouchUp() + if err != nil { + t.Fatalf("Failed to issue touch up event [0,0]: %v", err) + } + +} + +func TestMultiTouchSyspath(t *testing.T) { + dev, err := CreateMultiTouch("/dev/uinput", []byte("MultiTouch"), 0, 1024, 0, 768, 3) + if err != nil { + t.Fatalf("Failed to create the virtual touch pad. Last error was: %s\n", err) + } + + sysPath, err := dev.FetchSyspath() + if err != nil { + t.Fatalf("Failed to fetch syspath. Last error was: %s\n", err) + } + + if sysPath[:32] != "/sys/devices/virtual/input/input" { + t.Fatalf("Expected syspath to start with /sys/devices/virtual/input/input, but got %s", sysPath) + } + + t.Logf("Syspath: %s", sysPath) +} diff --git a/uinputdefs.go b/uinputdefs.go index 2c13632..741212e 100644 --- a/uinputdefs.go +++ b/uinputdefs.go @@ -40,6 +40,12 @@ const ( absHat0X = 0x10 absHat0Y = 0x11 + absMtSlot = 0x2f + absMtTouchMajor = 0x30 + absMtPositionX = 0x35 + absMtPositionY = 0x36 + absMtTrackingId = 0x39 + synReport = 0 evMouseBtnLeft = 0x110 evMouseBtnRight = 0x111 From 2ae95c1f6c752a5703d7ac43473d334b3916a50e Mon Sep 17 00:00:00 2001 From: snoutbug Date: Mon, 20 Nov 2023 16:08:40 +0100 Subject: [PATCH 2/4] Add Comments --- multitouch.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/multitouch.go b/multitouch.go index a612a43..3e54e0b 100644 --- a/multitouch.go +++ b/multitouch.go @@ -137,6 +137,7 @@ func createMultiTouch(path string, name []byte, minX int32, maxX int32, minY int Absmax: absMax}) } +// The contact will be held down at the coordinates specified func (c multiTouchContact) TouchDownAt(x int32, y int32) error { var events []inputEvent @@ -167,6 +168,7 @@ func (c multiTouchContact) TouchDownAt(x int32, y int32) error { return c.sendAbsEvent(events) } +// The contact will be raised off of the surface func (c multiTouchContact) TouchUp() error { c.tracking_id = -1 return c.sendAbsEvent(nil) From 71085ee632e88eebb25d1ca015fddcfaf6625f11 Mon Sep 17 00:00:00 2001 From: snoutbug Date: Mon, 20 Nov 2023 16:14:37 +0100 Subject: [PATCH 3/4] Remove unused inputEvent --- multitouch.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/multitouch.go b/multitouch.go index 3e54e0b..b4c26c6 100644 --- a/multitouch.go +++ b/multitouch.go @@ -157,12 +157,6 @@ func (c multiTouchContact) TouchDownAt(x int32, y int32) error { Value: y, }) - events = append(events, inputEvent{ - Type: evAbs, - Code: 0x32, - Value: 0x32, - }) - c.tracking_id = c.slot return c.sendAbsEvent(events) From 6aab0616d3e5b6b358ebca491154d7e07f5edc36 Mon Sep 17 00:00:00 2001 From: snoutbug Date: Tue, 21 Nov 2023 21:03:05 +0100 Subject: [PATCH 4/4] Fix Error Handling in Test-Function --- multitouch_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multitouch_test.go b/multitouch_test.go index 91fda8b..be6fd6a 100644 --- a/multitouch_test.go +++ b/multitouch_test.go @@ -121,7 +121,7 @@ func TestMultiTouchMoveToFailsOnClosedDevice(t *testing.T) { } err = contacts[0].TouchDownAt(1, 1) - if err != nil { + if err == nil { t.Fatalf("Expected error due to closed device, but no error was returned.") } }