Linux Console Graphics with, [DRM], [GBM], [EGL], [GLES], [logind], [libinput], etc. in CL
Continuing from Part 1, the next step is input.
Just commented code this time...
;; osicat wants pointers, but some of the ioctls we use want ints, so ;; add a wrapper (defun ioctl/p (fd req arg) (nix:ioctl fd req (cffi:make-pointer arg))) ;; some ioctl constants i couldn't find in osicat (defconstant +KDGKBMODE+ #x4B44) (defconstant +KDSKBMODE+ #x4B45) (defconstant +K-OFF+ #x04) (defconstant +KDSKBMUTE+ #x4B51) (defconstant +KDSETMODE+ #x4B3A) (defconstant +KD-TEXT+ #x00) (defconstant +KD-GRAPHICS+ #x01) ;; minimal udev bindings to use libinput udev backend (cffi:define-foreign-library libudev (:unix (:or "libudev.so.1" "libudev.so")) (t (:default "libudev"))) (cffi:use-foreign-library libudev) (cffi:defcfun ("udev_new" udev-new) :pointer) (cffi:defcfun ("udev_ref" udev-ref) :pointer (udev :pointer)) (cffi:defcfun ("udev_unref" udev-unref) :pointer (udev :pointer)) ;; store some closure to let us open/close devices from outside ;; lexical scope of the WITH-DBUS* macro (defvar *take-device*) (defvar *release-device*) ;; callbacks that let us handle opening devices for libinput (cffi:defcallback open-restricted :int ((path :string) (flags :int) (user-data :pointer)) (declare (ignorable flags user-data)) (format t "Called open-restricted path = ~s = ~s~%" path (path-device path)) (let ((fd (funcall *take-device* path))) (format t "File descriptor ~A~%" fd) (when (< fd 0) (warn "Failed to open ~A" path)) fd)) (cffi:defcallback close-restricted :void ((fd :int) (user-data :pointer)) (declare (ignorable user-data)) (funcall *release-device* fd)) ;; allocate struct used to pass our callbacks to libinput (defun make-libinput-interface () (let ((interface (cffi:foreign-alloc '(:struct libinput::libinput-interface)))) (cffi:with-foreign-slots ((libinput::open-restricted libinput::close-restricted) interface (:struct libinput::libinput-interface)) (setf libinput::open-restricted (cffi:callback open-restricted)) (setf libinput::close-restricted (cffi:callback close-restricted))) interface)) ;; variable to tell it to exit event loop (defparameter *stop* t) ;; we need major/minor for an open FD to use in close callback (defun fd-device (fd) ;; return device major/minor for a given fd (let ((rdev (nix:stat-rdev (nix:fstat fd)))) (list (ldb (byte 8 8) rdev) (ldb (byte 8 0) rdev)))) (with-dbus* (l lc lp s sc sp) ;; we need to be in control of the session (sc "TakeControl" nil) ;; set our callbacks to open/close files using logind (setf *take-device* (lambda (path) (apply #'sc "TakeDevice" (path-device path)))) (setf *release-device* (lambda (fd) (format t "release device ~s = ~s~%" fd (fd-device fd)) ;; apparently we should close the fd before ;; releasing it? (let ((dev (fd-device fd))) (nix:close fd) (apply #'sc "ReleaseDevice" dev)))) (let* ((tty (sp "TTY")) (tty-path (format nil "/dev/~a" tty)) (seat (car (sp "Seat"))) ;; o-noctty since we don't care about the state of the ;; terminal itself. TakeControl gives us permissions to open ;; the tty, so normal nix:open works for this. (tty-fd (nix:open tty-path (logior nix:o-rdwr nix:o-noctty))) ;; state we will initialize below (old-keyboard-mode nil) (udev nil) (context nil) (libinput-fd nil) (interface)) ;; next we try to mute the tty, or otherwise set mode to off. ;; Handler-case is needed since osicat negative return to errors, ;; and here failure of the first ioctl is OK (handler-case (ioctl/p tty-fd +kdskbmute+ 1) (nix:posix-error () (cffi:with-foreign-object (old-mode :int) (nix:ioctl tty-fd +kdgkbmode+ old-mode) (setf old-keyboard-mode (cffi:mem-ref old-mode :int))))) ;; set terminal to graphics mode to avoid it trying to draw any ;; text or cursor over our graphics (ioctl/p tty-fd +kdsetmode+ +kd-graphics+) ;; we will use the udev backend of libinput, so we don't need to ;; manually find devices and track hotplug ;; first get a udev device (setf udev (udev-new)) ;; create a libinput context, passing it our custom open/close functions (setf context (libinput:udev-create-context (setf interface (make-libinput-interface)) (cffi:null-pointer) udev)) ;; optional: set libinput to send verbose log info to ;; console. Getting log output from CL would require callbacks ;; with support for variadic arguments, which isn't supported by ;; cffi or libffi, so would need a C wrapper lib or ABI specific ;; hacks. (libinput:log-set-priority context :debug) ;; tell libinput to use devices from our seat (will find all ;; usable devices and call our callbacks to open them) (libinput:udev-assign-seat context seat) ;; grab the FD from libinput so we can poll it for events (setf libinput-fd (libinput:get-fd context)) (format t "got fd ~s from libinput~%" libinput-fd) ;; run initial event dispatch. Does some internal state ;; processing and adds `device-notify` events to libinput's ;; internal event queue (libinput:dispatch context) ;; now we are ready to do some input: ;; again, we should be using select/poll/epoll/etc to watch ;; LIBINPUT-FD for events, but will just busy wait. In particular, ;; while events can sit in the queue for a bit without any ;; problem, LIBINPUT:DISPATCH needs to be called immediately after ;; FD is readable for its internal state processing to work ;; correctly. (setf *stop* nil) (format t "waiting for input:~%") (loop do (libinput:dispatch context) ;; loop over all available events (loop for ev = (libinput:get-event context) until (or *stop* ;; get-event returns null pointer if no events ;; available, at which point we go back to outer ;; loop and call dispatch again (cffi:null-pointer-p ev)) do (unwind-protect ;; get some info common to all events (let* ((et (libinput:event-get-type ev)) (dev (libinput:event-get-device ev))) ;; handle some of the events I will see on my device (case et (#.libinput:device-added ;; we get one of these for every input device ;; connected to the seat when we created ;; context, and any that are added later (format t "added device ~s ~s (~s)~%" (libinput:device-get-name dev) (libinput:device-get-output-name dev) (libinput:device-get-sysname dev)) ;; print out what ssort events the new device ;; can generate (loop with caps = '(0 :keyboard 1 :pointer 2 :touch 3 :tablet-tool 4 :tablet-pad 5 :gesture 6 :switch) for i below 8 when (plusp (libinput:device-has-capability dev i)) do (format t " has capability ~s~%" (getf caps i)))) (#.libinput:device-removed (format t "removed device ~s ~s (~s)~%" (libinput:device-get-name dev) (libinput:device-get-output-name dev) (libinput:device-get-sysname dev))) ;; I don't have a keyboard or mouse on this ;; system, so just handling touch events see ;; libinput docs for full list of events ((#.libinput:touch-down #.libinput:touch-motion) ;; for most events, we need to get ;; device-specific event data from generic ;; event to get details (let ((ev2 (libinput:event-get-touch-event ev))) (format t "~%touch ~s @ ~5,3f~%" (if (= et libinput:touch-down) "down" "motion") ;; most input events have timestamps ;; in usec, not guaranteed to always ;; be increasing though (* (libinput:event-touch-get-time-usec ev2) 1.0d-6)) (format t "~& ~s, ~s @ ~5,3f,~5,3f || ~5,3f,~5,3f~%" (libinput:event-touch-get-slot ev2) (libinput:event-touch-get-seat-slot ev2) ;; this is supposedly in mm, but i get ;; device coords from 0 to 4096 from my ;; touchscreen (libinput:event-touch-get-x ev2) (libinput:event-touch-get-y ev2) ;; to get screen coordinates, we need to ;; pass screen size to these, 800x480 in ;; my case (libinput:event-touch-get-x-tranformed ev2 800) (libinput:event-touch-get-y-transformed ev2 480)))) ((#.libinput:touch-up ;; touch-frame event is used to group ;; multiple events when there are multiple ;; touches at once. #.libinput:touch-frame) (let ((ev2 (libinput:event-get-touch-event ev))) (format t "~%touch ~a @ ~5,3f~%" (if (eql et libinput:touch-up) "up" "frame") ;; most input events have timestamps ;; in usec, not guaranteed to always ;; be increasing though (* (libinput:event-touch-get-time-usec ev2) 1.0d-6)))) (otherwise ;; there are also keyboard, pointer, switch, ;; tablet and gesture (for things like ;; multitouch pinch/rotate etc) which are ;; similar. (format t "got event type ~s from device ~s~%" et dev)))) ;; clean up the event when we are done with it (libinput:event-destroy ev))) until *stop*) ;; finally, clean everything up ;; reset TTY state (if old-keyboard-mode (ioctl/p tty-fd +kdskbmode+ old-keyboard-mode) (ioctl/p tty-fd +kdskbmute+ 0)) (ioctl/p tty-fd +kdsetmode+ +kd-text+) ;; and close fd (nix:close tty-fd) ;;(apply #'sc "ReleaseDevice" (fd-device tty-fd)) ;; close libinput context (will call our callbacks to close devices) (libinput:unref context) ;; close udev device (udev-unref udev) ;; delete foreign mem for callback struct (cffi:foreign-free interface) ;; stop keeping bus/etc vars live with closures in global variables (setf *take-device* nil) (setf *release-device* nil) (sc "ReleaseControl")))
gives output like
Called open-restricted path = "/dev/input/event0" = (13 64) File descriptor 15 Called open-restricted path = "/dev/input/event2" = (13 66) File descriptor 16 Called open-restricted path = "/dev/input/event3" = (13 67) File descriptor 17 Called open-restricted path = "/dev/input/event1" = (13 65) File descriptor 18 got fd 12 from libinput waiting for input: added device "gpio_keys" NIL ("event0") has capability :KEYBOARD added device "WaveShare WS170120" NIL ("event2") has capability :TOUCH added device "RC for dw_hdmi" NIL ("event3") has capability :KEYBOARD added device "sunxi-ir" NIL ("event1") has capability :KEYBOARD touch "down" @ 160006.352 0, 0 @ 1658.000,1725.000 || 323.828,202.148 touch frame @ 160006.352 touch "motion" @ 160006.362 0, 0 @ 1626.000,1755.000 || 317.578,205.664 touch frame @ 160006.362 touch "motion" @ 160006.372 0, 0 @ 1623.000,1775.000 || 316.992,208.008 touch frame @ 160006.372 <more motion events skipped> touch "motion" @ 160006.420 0, 0 @ 1613.000,1810.000 || 315.039,212.109 touch frame @ 160006.420 touch up @ 160006.424 touch frame @ 160006.424 release device 15 = (13 64) release device 16 = (13 66) release device 17 = (13 67) release device 18 = (13 65)