minidsp-lcd-monitor: clear entire line when displaying preset/input master
authorClinton Ebadi <clinton@unknownlamer.org>
Sat, 19 Feb 2022 20:57:59 +0000 (15:57 -0500)
committerClinton Ebadi <clinton@unknownlamer.org>
Sat, 19 Feb 2022 20:57:59 +0000 (15:57 -0500)
Otherwise garbage may be left behind (for the input name at least,
preset is only ever a single digit so not strictly needed).

led-controller.scm
mame-catver-filter/catver.lisp [new file with mode: 0644]
minidsp-lcd-monitor/minidsp-lcd-monitor.py [new file with mode: 0755]

index a417fe2..fa75278 100644 (file)
@@ -22,6 +22,7 @@
             (rnrs io ports)
             (srfi srfi-1)
             (srfi srfi-4)
+            (srfi srfi-26)
              (ice-9 format)
 
 
@@ -32,7 +33,8 @@
 
 ;;; stty is less of a pain, just be evil and system
 (system "stty -F /dev/ttyACM0 cs8 115200 ignbrk -brkint -icrnl -imaxbel -opost -onlcr -isig -icanon -iexten -echo -echoe -echok -echoctl -echoke noflsh -ixon -crtscts")
-(sleep 3) ; wait for arduino to finish booting
+(set-current-error-port (open-file "/tmp/led.log" "w0"))
+(sleep 1) ; wait for arduino to finish booting
 
 (set! *random-state* (random-state-from-platform))
 
@@ -46,6 +48,7 @@
 (define serial-lock (make-mutex))
 
 (define pi 3.141592653589793)
+(define tau (* 2 pi))
 
 (define (deg->rad degrees)
   (* degrees (/ pi 180)))
       (values (+ r₁ m) (+ g₁ m) (+ b₁ m)))))
 
 (define (set-led-color/primitive! r g b)
-  ;; if put-bytevector is not atomic, may need to have a writer thread
-  (with-mutex serial-lock
-             (put-bytevector serial-out (u8vector r g b))
-             (read-line serial-in 'concat)))
+  (call-with-blocked-asyncs (lambda ()
+                             (with-mutex serial-lock
+                               (format serial-out "~A,~A,~A~c" r g b #\return)
+                               (read-line serial-in 'concat)))))
 
 (define (set-led-hsv! h s v)
   (with-mutex color-lock
              (set! current-color (vector h s v))
              (receive (r g b)
                       (hsv->rgb h s v)
-                      (let ((r' (inexact->exact (truncate (* (* r 1) 255))))
-                            (g' (inexact->exact (truncate (* (* g 1) 255))))
-                            (b' (inexact->exact (truncate (* (* b 1) 255)))))
+                      (let ((r' (inexact->exact (truncate (* (* r 1) pwm-resolution))))
+                            (g' (inexact->exact (truncate (* (* g 1) pwm-resolution))))
+                            (b' (inexact->exact (truncate (* (* b 1) pwm-resolution)))))
                         (set-led-color/primitive! r' g' b')))))
 
 (define zzz (if (and (> (length (command-line)) 1)
                     (number? (string->number (second (command-line)))))
                (string->number (second (command-line)))
-               45000))
+               4500))
 
 (display (command-line))
 
-(let ((zone-out (format #f "hey man i'm getting zzzzzzzzzzooooned out ~A~%" zzz)))
+#;(let ((zone-out (format #f "hey man i'm getting zzzzzzzzzzooooned out ~A~%" zzz)))
   (display zone-out)
   (system (format #f "echo \"~A\" | festival --tts" zone-out)))
 
-(define tick-increment (/ (* 2 pi) (* 360 10)))
-
-(define (random-ticks n) (* tick-increment (random n)))
-
-(define (hsv-fade)
-  (let loop ()
-    (with-mutex color-lock
-               (match current-color
-                      (#(h s v)
-                       (let ((h (euclidean-remainder (+ h tick-increment) (* 2 pi))))
-                         (display (set-led-hsv! h s v) (current-error-port))))))
-    (usleep zzz)
-    (loop))
-  
-#;
-  (let loop ((tick 0))
-    (let ((h (euclidean-remainder tick (* 2 pi))))
-      (display (set-led-hsv! h 1.0 1.0) (current-error-port))
-      (usleep zzz)
-      (loop (+ tick-increment tick)))))
-
+(define (rgb-test)
+  (letrec ((loop (lambda (i fn)
+                  (cond ((= i pwm-resolution) #t)
+                        (else (display (fn i))
+                              (usleep zzz)
+                              (loop (1+ i) fn))))))
+    (while #t
+          (loop 0 (cut set-led-color/primitive! <> 0 0))
+          (loop 0 (cut set-led-color/primitive! 0 <> 0))
+          (loop 0 (cut set-led-color/primitive! 0 0 <>)))))
 
 ;;; Multi-processing is for hep cats
 
     (kill-fx))
   (set! running-fx (make-thread fx)))
 
-(define (reverse-tick!)
-  (set! tick-increment (- tick-increment)))
-
-
 (define (led-command-handler request body)
   (match (split-and-decode-uri-path (uri-path (request-uri request)))
     (("set" "rgb" r g b)
      (values '((content-type . (text/plain)))
               "luser\n"))))
 
-(set-current-error-port (open-file "/tmp/led.log" "w0"))
+
 
 (define (start-led-server)
   (set! server-thread (make-thread (lambda ()  (run-server led-command-handler 'http '(#:port 58080))))))
+
+(define (current-time/usec)
+  (let ((now (gettimeofday)))
+    (+ (* (car now) (expt 10 6)) (cdr now))))
+
+(define (fx-driver fx)
+  (let loop ((t (current-time/usec))
+            (dt 0))
+    ;; We live a moment in the past
+    (receive (h s v)
+       (fx t dt)
+      (display (set-led-hsv! h s v) (current-error-port)))
+    (let ((now (current-time/usec)))
+      (loop now (- now t)))))
+
+(define (hsv-fx t dt)
+  (define tick-increment (/ (deg->rad 1280) (expt 10 6)))
+  (with-mutex color-lock
+             (match current-color
+               (#(h s v)
+                (let ((h (euclidean-remainder (+ h (* tick-increment dt)) tau)))
+                  (values h s v))))))
+
+
diff --git a/mame-catver-filter/catver.lisp b/mame-catver-filter/catver.lisp
new file mode 100644 (file)
index 0000000..8b80872
--- /dev/null
@@ -0,0 +1,564 @@
+;;; Simple program to parse catver.ini
+;;; (http://www.progettosnaps.net/catver/) and filter the results.
+;;; 2019 Clinton Ebadi <clinton@unknownlamer.org>
+;;; CC0 To the extent possible under law, Clinton Ebadi has waived all
+;;; copyright and related or neighboring rights to MAME Category
+;;; Filter.
+
+(quicklisp:quickload '(:parser.ini :cl-ppcre :alexandria :arnesi))
+
+(import '(alexandria:read-file-into-string))
+
+;; fixme: parser.ini doesn't handle DOS line endings so file has to be
+;; converted to unix line endings. manually open the stream instead
+;; after I remember how external formats work again.
+
+;; fixme: since I'm using an ad-hoc parser and put zero effort into this, you have to edit catver.ini and extract just the [Category] section
+(defparameter *catver-source* (pathname "/home/clinton/src/repos/local/scratch/mame-catver-filter/pS_CatVer_230/catver-only.ini"))
+
+;; parser.ini barfed on catver (ate all ram and then died...)
+;; (defparameter *parsed-catver*
+;;   (let ((parser.ini:*value-terminating-whitespace-expression* #\Newline))
+;;     (parser.ini:parse *catver-source* 'list)))
+
+(defparameter *catver-hash* (make-hash-table :test 'equal))
+
+(with-open-file (catver-stream *catver-source* )
+  (arnesi:awhile (read-line catver-stream nil nil)
+    (cl-ppcre:register-groups-bind (game category)
+       ("([^=]+)=([^=]+)" arnesi:it)
+      (format t "KEY ~A VALUE ~A~%" game category)
+      (setf (gethash category *catver-hash*) (cons game (gethash category *catver-hash*))))))
+
+(defun print-cats ()
+  (format t "(")
+  (maphash (lambda (key value)
+            (format t "~S~%" key))
+          *catver-hash*)
+  (format t ")"))
+
+(defun filter-games (categories)
+  (mapcan (lambda (cat) (gethash cat *catver-hash*)) categories))
+
+(defun dump-games (filename categories)
+  (with-open-file (outf filename :direction :output :if-exists :error)
+    (mapc (lambda (x) (format outf "~A~%" x)) (filter-games categories))))
+
+
+;; 2020-11-19: might want to review these for new categories, haven't checked since mame 0.214
+(defparameter *filtered-cats*
+  '("Maze / Shooter Small"
+    "Sports / Football"
+    "Sports / Soccer"
+    "Driving / Truck Guide"
+    "Shooter / Flying Vertical"
+    "Sports / Basketball"
+    "Sports / Baseball"
+    "Driving / 1st Person"
+    "Shooter / Gun"
+    "Sports / Wrestling"
+    "Shooter / Field"
+    "Sports / Tennis"
+    "Shooter / Flying Horizontal"
+    "Puzzle / Match"
+    "Casino / Misc."
+    "Driving / Motorbike"
+    "Maze / Collect"
+    "Fighter / 2.5D"
+    "Sports / Skateboarding"
+    "Shooter / Misc. Horizontal"
+    "Sports / Track & Field"
+    "Sports / Pool"
+    "Computer / Training Board"
+    "Shooter / Gallery"
+    "Shooter / Flying (chase view)"
+    "Platform / Run Jump"
+    "Sports / Volleyball"
+    "Driving / Race 1st Person"
+    "Fighter / Versus"
+    "Platform / Shooter Scrolling"
+    "Maze / Change Surface"
+    "Driving / Race (chase view)"
+    "Shooter / Misc. Vertical"
+    "Puzzle / Sliding"
+    "Shooter / Flying 1st Person"
+    "Driving / Plane"
+    "Sports / Fishing"
+    "Platform / Fighter Scrolling"
+    "Sports / Golf"
+    "Platform / Run, Jump & Scrolling"
+    "Shooter / 1st Person"
+    "Maze / Shooter Large"
+    "Shooter / 3rd Person"
+    "Sports / Bowling"
+    "Shooter / Misc."
+    "Sports / Skiing"
+    "Shooter / Walking"
+    "Sports / Darts"
+    "Sports / Hockey"
+    "Maze / Outline"
+    "Driving / Race Track"
+    "Climbing / Tree - Plant"
+    "Driving / Misc."
+    "Shooter / Flying"
+    "Shooter / Driving (chase view)"
+    "Driving / Boat"
+    "Puzzle / Drop"
+    "Puzzle / Misc."
+    "Sports / Misc."
+    "Platform / Fighter"
+    "Fighter / 2D"
+    "Driving / Race (chase view) Bike"
+    "Ball & Paddle / Breakout"
+    "Sports / Horse Racing"
+    "Sports / Armwrestling"
+    "Sports / Boxing"
+    "Fighter / 3D"
+    "Shooter / Driving"
+    "Shooter / Versus"
+    "Shooter / Driving Vertical"
+    "Fighter / Vertical"
+    "Driving / Race"
+    "Rhythm / Dance"
+    "Maze / Surround"
+    "Misc. / Toy Cars"
+    "Puzzle / Maze"
+    "Platform / Shooter"
+    "Maze / Escape"
+    "Shooter / Driving Diagonal"
+    "Ball & Paddle / Pong"
+    "Maze / Collect & Put"
+    "Rhythm / Instruments"
+    "Fighter / Misc."
+    "Maze / Digging"
+    "Maze / Blocks"
+    "Maze / Cross"
+    "Shooter / Driving Horizontal"
+    "Fighter / Field"
+    "Sports / Bull Fighting"
+    "Shooter / Driving 1st Person"
+    "Maze / Ladders"
+    "Puzzle / Reconstruction"
+    "Maze / Driving"
+    "Maze / Defeat Enemies"
+    "Ball & Paddle / Jump and Touch"
+    "Climbing / Building"
+    "Puzzle / Outline"
+    "Driving / Race Bike"
+    "Shooter / Command"
+    "Sports / Sumo"
+    "Fighter / Compilation"
+    "Rhythm / Misc."
+    "Driving / Demolition Derby"
+    "Maze / Paint"
+    "Sports / Rugby Football"
+    "Fighter / Driving Vertical"
+    "Fighter / Versus Co-op"
+    "Maze / Fighter"
+    "Misc. / Prediction"
+    "Fighter / Asian 3D"
+    "Maze / Move and Sort"
+    "Sports / Hang Gliding"
+    "Maze / Integrate"
+    "Shooter / Flying Diagonal"
+    "Platform / Maze"
+    "Sports / Horseshoes"
+    "Sports / Handball"
+    "Maze / Ball Guide"
+    "Misc. / Reflex"
+    "Driving / Motorbike (Motocross)"
+    "Driving / Guide and Collect"
+    "Sports / Volley - Soccer"
+    "Misc. / Versus"
+    "Tabletop / Othello - Reversi"
+    "Driving / Landing"
+    "Music / Drum Machine"
+    "Maze / Marble Madness"
+    "Shooter / Submarine"
+    "Maze / Misc."
+    "Sports / Dodgeball"
+    "Shooter / Motorbike"
+    "Sports / Ping Pong"
+    "Misc. / Hot-air Balloon"
+    "Driving / Guide and Shoot"
+    "Sports / SkyDiving"
+    "Climbing / Mountain - Wall"
+    "Shooter / Underwater"
+    "Sports / Shuffleboard"
+    "Ball & Paddle / Misc."
+    "Sports / Swimming"
+    "Maze / Run Jump"
+    "Driving / Catch"
+    "Sports / Gun"))
+
+;; for reference
+(defparameter *all-cats*
+  '("Maze / Shooter Small"
+    "System / Device"
+    "Slot Machine / Video Slot"
+    "Sports / Football"
+    "Sports / Soccer"
+    "Game Console / Home Videogame"
+    "Electromechanical / Misc."
+    "Driving / Truck Guide"
+    "Shooter / Flying Vertical"
+    "MultiGame / Compilation"
+    "Sports / Basketball"
+    "Tabletop / Multi-Games"
+    "Sports / Baseball"
+    "Electromechanical / Pinball"
+    "Music / JukeBox"
+    "Driving / 1st Person"
+    "Shooter / Gun"
+    "Computer / Workstation - Server"
+    "Computer / Business - Terminal"
+    "Sports / Wrestling"
+    "System / BIOS"
+    "Puzzle / Match * Mature *"
+    "Shooter / Field"
+    "Casino / Cards"
+    "Misc. / Clock"
+    "Sports / Tennis"
+    "Shooter / Flying Horizontal"
+    "Puzzle / Match"
+    "Tabletop / Mahjong * Mature *"
+    "Casino / Misc."
+    "Driving / Motorbike"
+    "Maze / Collect"
+    "Fighter / 2.5D"
+    "Computer / Single Board"
+    "Utilities / EPROM Programmer"
+    "Sports / Skateboarding"
+    "Handheld / Electronic Game"
+    "Tabletop / Cards"
+    "Shooter / Misc. Horizontal"
+    "Sports / Track & Field"
+    "Sports / Pool"
+    "Misc. / Print Club"
+    "Computer / Training Board"
+    "Shooter / Gallery"
+    "Computer / Home System"
+    "Computer / Portable Digital Teletype"
+    "Quiz / Questions in Japanese"
+    "Shooter / Flying (chase view)"
+    "Platform / Run Jump"
+    "Tabletop / Hanafuda * Mature *"
+    "Computer / Construction Kit"
+    "Slot Machine / Reels"
+    "Board Game / Chess Machine"
+    "Computer / Misc."
+    "Sports / Volleyball"
+    "Driving / Race 1st Person"
+    "Utilities / Modem"
+    "Fighter / Versus"
+    "Casino / Multiplay"
+    "Platform / Shooter Scrolling"
+    "Maze / Change Surface"
+    "Driving / Race (chase view)"
+    "Slot Machine / Video Slot * Mature *"
+    "Shooter / Misc. Vertical"
+    "Coin Pusher / Misc."
+    "Puzzle / Sliding"
+    "Calculator / Pocket Computer"
+    "Computer / Development System"
+    "Tabletop / Mahjong"
+    "Shooter / Flying 1st Person"
+    "Driving / Plane"
+    "Sports / Fishing"
+    "Platform / Fighter Scrolling"
+    "Computer / Microcomputer"
+    "Sports / Golf"
+    "Music / Synthesizer"
+    "Platform / Run, Jump & Scrolling"
+    "Medal Game / Action"
+    "Misc. / Pinball"
+    "Shooter / 1st Person"
+    "Maze / Shooter Large"
+    "Shooter / 3rd Person"
+    "Sports / Bowling"
+    "Shooter / Misc."
+    "Misc. / Toy Robot"
+    "Sports / Skiing"
+    "Misc. / Multiplay"
+    "Shooter / Walking"
+    "Utilities / Test"
+    "Sports / Darts"
+    "Sports / Hockey"
+    "Maze / Outline"
+    "Telephone / Car Phone"
+    "Driving / Race Track"
+    "Climbing / Tree - Plant"
+    "Misc. / Catch"
+    "Whac-A-Mole / Hammer"
+    "Electromechanical / Redemption"
+    "Misc. / Bank-teller Terminal"
+    "Driving / Misc."
+    "Computer / Punched Car"
+    "Shooter / Flying"
+    "Shooter / Driving (chase view)"
+    "Driving / Boat"
+    "Puzzle / Drop"
+    "Puzzle / Misc."
+    "Misc. / Coin Pusher"
+    "Sports / Misc."
+    "Platform / Fighter"
+    "Fighter / 2D"
+    "Tabletop / Misc."
+    "Misc. / Electronic Board Game"
+    "Driving / Race (chase view) Bike"
+    "Ball & Paddle / Breakout"
+    "Sports / Horse Racing"
+    "Sports / Armwrestling"
+    "Misc. / Laser Disk Simulator"
+    "Sports / Boxing"
+    "Computer / Word-processing Machine"
+    "Fighter / 3D"
+    "Shooter / Driving"
+    "Calculator / Astrological Computer"
+    "Misc. / Teletype"
+    "Shooter / Versus"
+    "Shooter / Driving Vertical"
+    "Fighter / Vertical"
+    "Handheld / Pocket Device - Pad - PDA"
+    "Casino / Bingo"
+    "Puzzle / Toss"
+    "Casino / Racing"
+    "Electromechanical / Reels"
+    "Driving / Race"
+    "Board Game / Backgammon"
+    "Rhythm / Dance"
+    "Handheld / Plug n' Play TV Game"
+    "Misc. / Document Processors"
+    "Maze / Surround"
+    "Puzzle / Drop * Mature *"
+    "Board Game / Bridge Machine"
+    "Quiz / Questions in English"
+    "Misc. / Toy Cars"
+    "Multiplay / Mini-Games"
+    "Puzzle / Maze"
+    "Platform / Shooter"
+    "Maze / Escape"
+    "Casino / Lottery"
+    "Quiz / Questions in English * Mature *"
+    "Sports / Pool * Mature *"
+    "Medal Game / Bingo"
+    "Medal Game / Casino"
+    "MultiGame / Mini-Games"
+    "Handheld / Home Videogame Console"
+    "Platform / Run Jump * Mature *"
+    "Fighter / Versus * Mature *"
+    "Shooter / Driving Diagonal"
+    "Ball & Paddle / Breakout * Mature *"
+    "Ball & Paddle / Pong"
+    "Maze / Collect & Put"
+    "Rhythm / Instruments"
+    "Fighter / Misc."
+    "Misc. / Spank * Mature *"
+    "Maze / Digging"
+    "Maze / Blocks"
+    "Misc. / Device Programmer"
+    "Misc. / Cash Counter"
+    "Maze / Cross"
+    "Shooter / Driving Horizontal"
+    "Electromechanical / Bingo"
+    "Fighter / Field"
+    "Computer / Laptop - Notebook - Portable"
+    "Sports / Multiplay"
+    "Misc. / Unknown"
+    "Puzzle / Toss * Mature *"
+    "Sports / Bull Fighting"
+    "Shooter / Driving 1st Person"
+    "Casino / Cards * Mature *"
+    "Maze / Ladders"
+    "Misc. / Electronic Typewriter"
+    "Puzzle / Reconstruction"
+    "Misc. / Redemption"
+    "Maze / Driving"
+    "Handheld / Child Computer"
+    "Maze / Defeat Enemies"
+    "Ball & Paddle / Jump and Touch"
+    "Computer / Cablenet Controller"
+    "Climbing / Building"
+    "Utilities / Update"
+    "Puzzle / Outline"
+    "Computer / Child Computer"
+    "Medical Equipment / Visual Field Screener"
+    "Casino / Unknown"
+    "Board Game / Checker Machine"
+    "Misc. / Fingerprint Reader"
+    "Driving / Race Bike"
+    "Music / Audio Sequencer"
+    "Misc. / Jump and Bounce"
+    "Casino / Roulette"
+    "Shooter / Command"
+    "Sports / Sumo"
+    "Casino / Misc. * Mature *"
+    "Fighter / Compilation"
+    "Rhythm / Misc."
+    "Driving / Demolition Derby"
+    "Whac-A-Mole / Shooter"
+    "Maze / Paint"
+    "Sports / Rugby Football"
+    "Tabletop / Multiplay"
+    "Fighter / Driving Vertical"
+    "Fighter / Versus Co-op"
+    "Board Game / Dame Machine"
+    "Puzzle / Outline * Mature *"
+    "Maze / Fighter"
+    "Misc. / Satellite Receiver"
+    "Tabletop / Othello - Reversi * Mature *"
+    "Misc. / Speech Synthesizer"
+    "Misc. / Prediction"
+    "Electromechanical / Utilities"
+    "Printer / Matrix Printer"
+    "Misc. / EPROM Programmer"
+    "Misc. / Engine Control Unit"
+    "Shooter / Gallery * Mature *"
+    "Fighter / Asian 3D"
+    "Medal Game / Timing"
+    "Sports / Cards"
+    "Game Console / Fitness Game"
+    "Maze / Move and Sort"
+    "Computer Graphic Workstation / Broadcast Television"
+    "Quiz / Questions in Korean"
+    "Medal Game / Adventure"
+    "Misc. / Graphics Display Controller"
+    "Driving / Ambulance Guide"
+    "Misc. / Dartboard"
+    "Misc. / Electronic Game"
+    "Misc. / Wavetables Generator"
+    "Misc. / Car Voice Alert"
+    "Music / Instruments"
+    "Shooter / Flying * Mature *"
+    "Sports / Hang Gliding"
+    "Tabletop / Hanafuda"
+    "Puzzle / Reconstruction * Mature *"
+    "Quiz / Questions in Italian"
+    "Driving / FireTruck Guide"
+    "Utilities / Arcade System"
+    "Maze / Integrate"
+    "Whac-A-Mole / Fighter"
+    "Quiz / Questions in German"
+    "Maze / Collect * Mature *"
+    "Shooter / Flying Diagonal"
+    "Misc. / Pinball * Mature *"
+    "Tabletop / Match * Mature *"
+    "Misc. / Mini-Games"
+    "Medal Game / Horse Racing"
+    "Board Game / Cards"
+    "Tabletop / Renju"
+    "Platform / Maze"
+    "Utilities / Network Processor"
+    "Misc. / Similar Bowling Game"
+    "Whac-A-Mole / Gun"
+    "Tabletop / Go"
+    "Utilities / Redemption Board"
+    "Sports / Horseshoes"
+    "Misc. / Digital MultiMeter (DMM)"
+    "Computer / Pocket PC"
+    "Misc. / Drum Machine"
+    "Sports / Handball"
+    "Quiz / Questions in Japanese * Mature *"
+    "Telephone / ComputerPhone"
+    "Misc. / Dog Sitter"
+    "Telephone / Mobile Phone - Smartphone"
+    "Maze / Ball Guide"
+    "Misc. / Reflex"
+    "Casino / Horse Racing"
+    "Driving / Motorbike (Motocross)"
+    "Misc. / Gambling Board"
+    "Driving / Guide and Collect"
+    "Tabletop / Shougi"
+    "Misc. / Educational Game"
+    "Sports / Volley - Soccer"
+    "Misc. / Versus"
+    "Tabletop / Othello - Reversi"
+    "Fighter / Multiplay"
+    "Misc. / Response Time"
+    "Printer / Handbook"
+    "Misc. / DVD Reader-Writer"
+    "Driving / Landing"
+    "Maze / Escape * Mature *"
+    "Printer / Laser Printer"
+    "Misc. / In Circuit Emulator"
+    "Game Console / Home Videogame Console Expansion"
+    "MultiGame / Gambling"
+    "MultiGame / Gambling Board"
+    "Music / Drum Machine"
+    "Maze / Marble Madness"
+    "Quiz / Questions in Spanish"
+    "MultiGame / Compilation * Mature *"
+    "Printer / 3D Printer"
+    "Shooter / Submarine"
+    "Medical Equipment / ECG Unit"
+    "Computer / Video Production"
+    "Misc. / Time-Access Control TerminalTime and access control terminal"
+    "Maze / Misc."
+    "Printer / Thermal Printer"
+    "Multiplay / Compilation"
+    "Music / Tone Generator"
+    "Sports / Dodgeball"
+    "Calculator / Math Game Learning"
+    "Shooter / Motorbike"
+    "Medal Game / Versus"
+    "Utilities / Test ROM"
+    "Printer / Barcode Printer"
+    "Misc. / Pachinko"
+    "Puzzle / Paint * Mature *"
+    "Puzzle / Misc. * Mature *"
+    "Medal Game / Driving"
+    "Misc. / Virtual Environment"
+    "Sports / Ping Pong"
+    "Medal Game / Compilation"
+    "Misc. / VTR Control"
+    "Misc. / Hot-air Balloon"
+    "Misc. / Shoot Photos"
+    "Puzzle / Sliding * Mature *"
+    "Maze / Blocks * Mature *"
+    "Quiz / Questions in Chinese"
+    "Driving / Guide and Shoot"
+    "Sports / SkyDiving"
+    "Climbing / Mountain - Wall"
+    "Shooter / Outline * Mature *"
+    "Multiplay / Cards"
+    "Quiz / Questions in French"
+    "Puzzle / Cards"
+    "Misc. / Portable Media Player"
+    "Utilities / Weight Scale"
+    "Electromechanical / Change Money"
+    "Misc. / Graphic Tablet"
+    "Shooter / Flying Horizontal * Mature *"
+    "Misc. / Robot Control"
+    "Shooter / Underwater"
+    "Sports / Shuffleboard"
+    "Misc. / Temperature Controller"
+    "Misc. / Laserdisc Simulator"
+    "Casino / Multi-Games"
+    "Ball & Paddle / Misc."
+    "Utilities / Arcade Switcher"
+    "Shooter / Flying Vertical * Mature *"
+    "Calculator / Talking Calculator"
+    "Computer / Educational Game"
+    "Misc. / DVD Player"
+    "Maze / Digging * Mature *"
+    "Whac-A-Mole / Footsteps"
+    "Handheld / E-Book Reading"
+    "Multiplay / Misc. * Mature *"
+    "Sports / Swimming"
+    "Computer / Milling"
+    "Misc. / Order"
+    "Misc. / Dot-Matrix Display"
+    "Medal Game / Cards"
+    "Music / MIDI Player"
+    "Maze / Run Jump"
+    "Utilities / Electronic Digital Thermostat"
+    "Utilities / TV Test Pattern Generator"
+    "Misc. / Credit Card Terminal"
+    "Multiplay / Mini-Games * Mature *"
+    "Music / Player"
+    "Misc. / Laserdisc Player"
+    "Driving / Catch"
+    "Sports / Gun"
+    "Computer / Programming Machine"
+    "Medal Game / Dance"
+    "Handheld / Handpuppet Toy"))
diff --git a/minidsp-lcd-monitor/minidsp-lcd-monitor.py b/minidsp-lcd-monitor/minidsp-lcd-monitor.py
new file mode 100755 (executable)
index 0000000..1e716d5
--- /dev/null
@@ -0,0 +1,247 @@
+#!/usr/bin/python3
+# minidsp-lcd-monitor.py --- A simple LCD monitor for the minidsp 2x4HD
+
+# Copyright (C) 2022 Clinton Ebadi <clinton@unknownlamer.org>
+
+# Author: Clinton Ebadi <clinton@unknownlamer.org>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Hardware needed:
+#
+# * Adafruit USB+Serial RGB Backlight Character LCD Backpack
+#     <https://learn.adafruit.com/usb-plus-serial-backpack/overview>
+#     <https://learn.adafruit.com/usb-plus-serial-backpack/command-reference>
+#     (hard requirement is a 20x4 character lcd with 5x8 cells)
+# * Adafruit 20x4 RGB Character LCD (Negative looks cooler but isn't required)
+#     <https://www.adafruit.com/product/498>
+#
+# Software dependencies:
+#
+# * minidsp-rs daemon with HTTP API enabled
+#    <https://minidsp-rs.pages.dev/daemon/http>
+
+# TODO (probably for someone else, patches welcome):
+# - Configuration file
+# - Support different sizes of LCDs or at least controlling which data
+#   is shown and on which line.
+# - More consistent naming scheme for LCDDriver methods
+# - Remove extra pip from bottom of volume bar? Actual characters
+#   don't use it so might look better that way.
+
+import asyncio
+import json
+import pprint
+import serial
+import sys
+import time
+import websockets
+
+# Configuration
+LCD_ROWS = 4
+LCD_COLS = 20
+LCD_CONTRAST = 255
+LCD_BRIGHTNESS = 200
+
+SERIAL_PORT = '/dev/ttyACM0'
+
+MINIDSP_WS = 'ws://127.0.0.1:5380/devices/0'
+
+# LCD control
+class LCDDriver:
+    def __init__(self, port = SERIAL_PORT, cols = LCD_COLS, rows = LCD_ROWS, contrast = LCD_CONTRAST, brightness = LCD_BRIGHTNESS):
+        self.lcd_rows = rows
+        self.lcd_cols = cols
+        self.lcd_contrast = contrast
+        self.lcd_brightness = brightness
+        self.serio = serial.Serial(port, 9600, timeout=1)
+
+        #self.cmd_set_display_size(20, 4)
+        self.cmd_clear_screen ()
+        self.cmd_set_contrast (contrast)
+        self.cmd_set_brightness(brightness)
+        self.cmd_backlight_enable ()
+
+    # Low level command sending, use cmd_FOO functions instead
+    def send_command(self, codes):
+        self.serio.write(b'\xFE')
+        self.serio.write(bytes(codes))
+
+    # high level interface
+    def write(self, txt):
+        self.serio.write(txt)
+
+    # https://learn.adafruit.com/usb-plus-serial-backpack/command-reference
+    def cmd_backlight_enable(self, state=True):
+        if state:
+            # Adafruit backpack doesn't support timeout
+            self.send_command([0x42, 0])
+        else:
+            self.send_command([0x46])
+
+    def cmd_autoscroll(self, enable=True):
+        if enable:
+            self.send_command(b'\x51')
+        else:
+            self.send_command(b'\x52')
+
+    def cmd_set_brightness(self, brightness):
+        self.send_command([0x98, brightness])
+        self.lcd_brightness = brightness
+
+    def cmd_set_backlight_color(self, r, g, b):
+        self.send_command([0xD0, r, g, b])
+
+    def cmd_set_display_size(self, cols, rows):
+        self.send_command([0xD1, cols, rows])
+
+    def cmd_set_contrast(self, contrast):
+        self.send_command([0x50, contrast])
+        self.lcd_contrast = contrast
+
+    def cmd_clear_screen (self):
+        self.send_command(b'\x58')
+
+    def cmd_cursor_set_position(self, col, row):
+        self.send_command([0x47, col, row])
+
+    def cmd_cursor_home(self):
+        self.send_command(b'\x48')
+
+    def cmd_cursor_forward(self):
+        self.send_command(b'\x4A')
+
+    def cmd_cursor_back(self):
+        self.send_command(b'\x4B')
+
+    def cmd_character_set(self, slot, chars):
+        self.send_command([0x4E, slot] + chars)
+
+class DSPMonitor:
+    # Volume bar characters
+    #
+    # Bars are set every other character column so that they appear even
+    # despite gaps between characters. Using 17 characters and making each
+    # visible bar a 2% increment gives us a max of 102%.
+    custom_bar_1 = [0x0,0x10,0x10,0x10,0x10,0x10,0x10,0x0]
+    custom_bar_2 = [0x0,0x14,0x14,0x14,0x14,0x14,0x14,0x0]
+    custom_bar_3 = [0x0,0x15,0x15,0x15,0x15,0x15,0x15,0x0]
+    custom_dB    = [0x6,0x5,0x5,0x6,0xd,0x15,0x15,0x1e] # dB, kind of...
+
+    minidsp_min_volume_db = -127
+
+    def __init__(self, wsurl = MINIDSP_WS):
+        self.wsurl = wsurl
+        self.dspattrs = { 'preset': None, 'source': None, 'volume': None, 'mute': None }
+        self.lcd = LCDDriver ()
+
+        self.lcd.cmd_character_set(0, self.custom_bar_1)
+        self.lcd.cmd_character_set(1, self.custom_bar_2)
+        self.lcd.cmd_character_set(2, self.custom_bar_3)
+        self.lcd.cmd_character_set(3, self.custom_dB)
+
+        self.lcd.cmd_autoscroll(False)
+        self.lcd.cmd_set_backlight_color(255, 0, 100)
+        self.lcd.cmd_cursor_home()
+        self.lcd.write(b'DSP Monitor Ready')
+
+    async def run(self):
+        self.lcd.cmd_clear_screen()
+        self.lcd.cmd_cursor_home()
+        self.lcd.write(b'DSP Monitor Connecting...')
+        # pings disabled as minidsp-rs does not appear to support them
+        async for websocket in websockets.connect(self.wsurl, ping_interval=None):
+            try:
+                force_redisplay = True
+                self.lcd.cmd_clear_screen()
+                self.lcd.cmd_cursor_home()
+                async for message in websocket:
+                    await self.process_ws_message(message, force_redisplay)
+                    force_redisplay = False
+            except websockets.ConnectionClosed:
+                self.lcd.cmd_clear_screen()
+                self.lcd.cmd_cursor_home()
+                self.lcd.write(b'Connection Lost'.center(20))
+                time.sleep(0.5)
+                continue
+
+    async def process_ws_message(self,message, force_redisplay=False):
+        print (message);
+        parsed = json.loads(message)
+        pprint.pp (parsed['master'])
+        self.update_screen(self.dspattrs | parsed['master'], force=force_redisplay)
+
+    def update_screen(self, attrs=None, force=False):
+        if attrs == None:
+            attrs = self.dspattrs
+        if attrs['source'] != self.dspattrs['source'] or force:
+            self.draw_source(attrs['source'])
+        if attrs['preset'] != self.dspattrs['preset'] or force:
+            self.draw_preset(attrs['preset'])
+        if attrs['volume'] != self.dspattrs['volume'] or attrs['mute'] != self.dspattrs['mute'] or force:
+            pct = int(abs((self.minidsp_min_volume_db - attrs['volume']) / abs(self.minidsp_min_volume_db)) * 100)
+            print('pct {}, min {}, vol {}'.format(pct, self.minidsp_min_volume_db, attrs['volume']))
+            self.draw_volume_bar (pct, attrs['mute'])
+
+        self.dspattrs = attrs
+
+    def draw_source(self, source_name):
+        self.lcd.cmd_cursor_set_position(1,1)
+        self.lcd.write(b'SOURCE: ')
+        self.lcd.write(bytes(source_name.ljust(12), 'ascii'))
+
+    def draw_preset(self, preset_number):
+        self.lcd.cmd_cursor_set_position(1, 2)
+        self.lcd.write(b'PRESET: ')
+        self.lcd.write(bytes(str(preset_number + 1).ljust(12), 'ascii'))
+
+    def draw_volume_bar(self, percentage, muted):
+        full_blocks = int(percentage / 6)
+        partial_ticks = int(percentage % 6 / 2)
+
+        #self.lcd.cmd_cursor_set_position(1, 3)
+        #self.lcd.write(b'VOLUME:')
+
+        #self.lcd.cmd_cursor_set_position(1, 4)
+        if muted:
+            self.lcd.cmd_cursor_set_position(1, 3)
+            self.lcd.write(b'MUTED'.center(20))
+            self.lcd.cmd_cursor_set_position(1, 4)
+            self.lcd.write(b' '.center(20))
+        else:
+            for row in [3, 4]:
+                self.lcd.cmd_cursor_set_position(1, row)
+                for i in range(full_blocks):
+                    self.lcd.write([2])
+
+                if partial_ticks == 2:
+                    self.lcd.write([1])
+                elif partial_ticks == 1:
+                    self.lcd.write([0])
+                else:
+                    self.lcd.write(b' ')
+
+                for i in range(18 - full_blocks - 1):
+                    self.lcd.write(b' ')
+
+        self.lcd.cmd_cursor_set_position(18, 4)
+        if percentage == 0:
+            self.lcd.write (b'min')
+        elif percentage == 100:
+            self.lcd.write (b'max')
+        else:
+            self.lcd.write (bytes(f'{percentage:2d}%', 'ascii'))
+
+dspmon = DSPMonitor()
+asyncio.run(dspmon.run())