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)