From: Clinton Ebadi Date: Sat, 19 Feb 2022 20:57:59 +0000 (-0500) Subject: minidsp-lcd-monitor: clear entire line when displaying preset/input X-Git-Url: http://git.hcoop.net/clinton/scratch.git/commitdiff_plain/HEAD?hp=e732331a7722e334a0f6adc36275206216dff56c minidsp-lcd-monitor: clear entire line when displaying preset/input Otherwise garbage may be left behind (for the input name at least, preset is only ever a single digit so not strictly needed). --- diff --git a/led-controller.scm b/led-controller.scm index a417fe2..fa75278 100644 --- a/led-controller.scm +++ b/led-controller.scm @@ -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))) @@ -67,53 +70,42 @@ (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 @@ -129,10 +121,6 @@ (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) @@ -156,7 +144,31 @@ (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 index 0000000..8b80872 --- /dev/null +++ b/mame-catver-filter/catver.lisp @@ -0,0 +1,564 @@ +;;; Simple program to parse catver.ini +;;; (http://www.progettosnaps.net/catver/) and filter the results. +;;; 2019 Clinton Ebadi +;;; 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 index 0000000..1e716d5 --- /dev/null +++ b/minidsp-lcd-monitor/minidsp-lcd-monitor.py @@ -0,0 +1,247 @@ +#!/usr/bin/python3 +# minidsp-lcd-monitor.py --- A simple LCD monitor for the minidsp 2x4HD + +# Copyright (C) 2022 Clinton Ebadi + +# Author: Clinton Ebadi + +# 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 . + +# Hardware needed: +# +# * Adafruit USB+Serial RGB Backlight Character LCD Backpack +# +# +# (hard requirement is a 20x4 character lcd with 5x8 cells) +# * Adafruit 20x4 RGB Character LCD (Negative looks cooler but isn't required) +# +# +# Software dependencies: +# +# * minidsp-rs daemon with HTTP API enabled +# + +# 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())