Mercurial > code > home > repos > homeauto
view service/shuttlepro/shuttlepro.py @ 1749:4b29ce991e59
cloudfree plug sends mqtt metrics, which we export to victoriametrics
author | drewp@bigasterisk.com |
---|---|
date | Sun, 28 Apr 2024 16:01:38 -0700 |
parents | 57ae7dc0f417 |
children |
line wrap: on
line source
#!/usr/bin/env python # # Copyright 2005 Free Software Foundation, Inc. # # This file is part of GNU Radio # # GNU Radio 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, or (at your option) # any later version. # # GNU Radio 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 GNU Radio; see the file COPYING. If not, write to # the Free Software Foundation, Inc., 51 Franklin Street, # Boston, MA 02110-1301, USA. # """ Handler for Griffin PowerMate, Contour ShuttlePro & ShuttleXpress USB knobs modified by drewp@bigasterisk.com """ import os, time, logging import sys import struct import exceptions import threading logging.basicConfig(level=logging.INFO) log = logging.getLogger() def hexint(mask): """ Convert unsigned masks into signed ints. This allows us to use hex constants like 0xf0f0f0f2 when talking to our hardware and not get screwed by them getting treated as python longs. """ if mask >= 2**31: return int(mask-2**32) return mask imported_ok = True try: import select import fcntl except ImportError: imported_ok = False # First a little bit of background: # # The Griffin PowerMate has # * a single knob which rotates # * a single button (pressing the knob) # # The Contour ShuttleXpress (aka SpaceShuttle) has # * "Jog Wheel" -- the knob (rotary encoder) on the inside # * "Shuttle Ring" -- the spring loaded rubber covered ring # * 5 buttons # # The Contour ShuttlePro has # * "Jog Wheel" -- the knob (rotary encoder) on the inside # * "Shuttle Ring" -- the spring loaded rubber covered ring # * 13 buttons # # The Contour ShuttlePro V2 has # *"Jog Wheel" -- the knob (rotary encoder) on the inside # * "Shuttle Ring" -- the spring loaded rubber covered ring # * 15 buttons # We remap all the buttons on the devices so that they start at zero. # For the ShuttleXpress the buttons are 0 to 4 (left to right) # For the ShuttlePro, we number the buttons immediately above # the ring 0 to 4 (left to right) so that they match our numbering # on the ShuttleXpress. The top row is 5, 6, 7, 8. The first row below # the ring is 9, 10, and the bottom row is 11, 12. # For the ShuttlePro V2, buttons 13 & 14 are to the # left and right of the wheel respectively. # We generate 3 kinds of events: # # button press/release (button_number, press/release) # knob rotation (relative_clicks) # typically -1, +1 # shuttle position (absolute_position) # -7,-6,...,0,...,6,7 # ---------------------------------------------------------------- # Our ID's for the devices: # Not to be confused with anything related to magic hardware numbers. ID_POWERMATE = 'powermate' ID_SHUTTLE_XPRESS = 'shuttle xpress' ID_SHUTTLE_PRO = 'shuttle pro' ID_SHUTTLE_PRO_V2 = 'shuttle pro v2' # ------------------------------------------------------------------------ # format of messages that we read from /dev/input/event* # See /usr/include/linux/input.h for more info # #struct input_event { # struct timeval time; = {long seconds, long microseconds} # unsigned short type; # unsigned short code; # unsigned int value; #}; input_event_struct = "@llHHi" input_event_size = struct.calcsize(input_event_struct) # ------------------------------------------------------------------------ # input_event types # ------------------------------------------------------------------------ IET_SYN = 0x00 # aka RESET IET_KEY = 0x01 # key or button press/release IET_REL = 0x02 # relative movement (knob rotation) IET_ABS = 0x03 # absolute position (graphics pad, etc) IET_MSC = 0x04 IET_LED = 0x11 IET_SND = 0x12 IET_REP = 0x14 IET_FF = 0x15 IET_PWR = 0x16 IET_FF_STATUS = 0x17 IET_MAX = 0x1f # ------------------------------------------------------------------------ # input_event codes (there are a zillion of them, we only define a few) # ------------------------------------------------------------------------ # these are valid for IET_KEY IEC_BTN_0 = 0x100 IEC_BTN_1 = 0x101 IEC_BTN_2 = 0x102 IEC_BTN_3 = 0x103 IEC_BTN_4 = 0x104 IEC_BTN_5 = 0x105 IEC_BTN_6 = 0x106 IEC_BTN_7 = 0x107 IEC_BTN_8 = 0x108 IEC_BTN_9 = 0x109 IEC_BTN_10 = 0x10a IEC_BTN_11 = 0x10b IEC_BTN_12 = 0x10c IEC_BTN_13 = 0x10d IEC_BTN_14 = 0x10e IEC_BTN_15 = 0x10f # these are valid for IET_REL (Relative axes) IEC_REL_X = 0x00 IEC_REL_Y = 0x01 IEC_REL_Z = 0x02 IEC_REL_HWHEEL = 0x06 IEC_REL_DIAL = 0x07 # rotating the knob IEC_REL_WHEEL = 0x08 # moving the shuttle ring IEC_REL_MISC = 0x09 IEC_REL_MAX = 0x0f # ------------------------------------------------------------------------ class powermate(object): """ Interface to Griffin PowerMate and Contour Shuttles """ def __init__(self, filename=None, on_event=lambda ev: None): self.on_event = on_event self.handle = -1 if not imported_ok: raise exceptions.RuntimeError, 'powermate not supported on this platform' if filename: if not self._open_device(filename): raise exceptions.RuntimeError, 'Unable to find powermate' else: ok = False for d in range(0, 16): if self._open_device("/dev/input/event%d" % d): ok = True break if not ok: raise exceptions.RuntimeError, 'Unable to find powermate' def __del__(self): self.keep_running = False if self.handle >= 0: os.close(self.handle) self.handle = -1 def _open_device(self, filename): try: self.handle = os.open(filename, os.O_RDWR) if self.handle < 0: print "can't open file" return False # read event device name name = fcntl.ioctl(self.handle, hexint(0x80ff4506), chr(0) * 256) name = name.replace(chr(0), '') print "%s name is %s" % (filename, name) # do we see anything we recognize? if name == 'Griffin PowerMate' or name == 'Griffin SoundKnob': self.id = ID_POWERMATE self.mapper = _powermate_remapper() elif name == 'CAVS SpaceShuttle A/V' or name == 'Contour Design ShuttleXpress': self.id = ID_SHUTTLE_XPRESS self.mapper = _contour_remapper() elif name == 'Contour Design ShuttlePRO': self.id = ID_SHUTTLE_PRO self.mapper = _contour_remapper() elif name == 'Contour Design ShuttlePRO v2': self.id = ID_SHUTTLE_PRO_V2 self.mapper = _contour_remapper() else: os.close(self.handle) self.handle = -1 return False # get exclusive control of the device, using ioctl EVIOCGRAB # there may be an issue with this on non x86 platforms and if # the _IOW,_IOC,... macros in <asm/ioctl.h> are changed fcntl.ioctl(self.handle,hexint(0x40044590), 1) return True except exceptions.OSError: return False def set_event_receiver(self, obj): self.event_receiver = obj def set_led_state(self, static_brightness, pulse_speed=0, pulse_table=0, pulse_on_sleep=0, pulse_on_wake=0): """ What do these magic values mean... """ if self.id != ID_POWERMATE: return False static_brightness &= 0xff; if pulse_speed < 0: pulse_speed = 0 if pulse_speed > 510: pulse_speed = 510 if pulse_table < 0: pulse_table = 0 if pulse_table > 2: pulse_table = 2 pulse_on_sleep = not not pulse_on_sleep # not not = convert to 0/1 pulse_on_wake = not not pulse_on_wake magic = (static_brightness | (pulse_speed << 8) | (pulse_table << 17) | (pulse_on_sleep << 19) | (pulse_on_wake << 20)) data = struct.pack(input_event_struct, 0, 0, 0x04, 0x01, magic) os.write(self.handle, data) return True def read_next(self): s = os.read (self.handle, input_event_size) if not s: return raw_input_event = struct.unpack(input_event_struct,s) sec, usec, type, code, val = self.mapper(raw_input_event) if type == IET_SYN: # ignore pass elif type == IET_MSC: # ignore (seems to be PowerMate reporting led brightness) pass elif type == IET_REL and code == IEC_REL_DIAL: self.on_event({"dial":val}) elif type == IET_REL and code == IEC_REL_WHEEL: self.on_event({"shuttle":val}) elif type == IET_KEY: self.on_event({"key":{"button":code - IEC_BTN_0, "press":val}}) else: print "powermate: unrecognized event: type = 0x%x code = 0x%x val = %d" % (type, code, val) class _powermate_remapper(object): def __init__(self): pass def __call__(self, event): """ Notice how nice and simple this is... """ return event class _contour_remapper(object): def __init__(self): self.prev = None def __call__(self, event): """ ...and how screwed up this is """ sec, usec, type, code, val = event if type == IET_REL and code == IEC_REL_WHEEL: # === Shuttle ring === # First off, this really ought to be IET_ABS, not IET_REL! # They never generate a zero value so you can't # tell when the shuttle ring is back in the center. # We kludge around this by calling both -1 and 1 zero. if val == -1 or val == 1: return (sec, usec, type, code, 0) return event if type == IET_REL and code == IEC_REL_DIAL: # === Jog knob (rotary encoder) === # Dim wits got it wrong again! This one should return a # a relative value, e.g., -1, +1. Instead they return # a total that runs modulo 256 (almost!). For some # reason they count like this 253, 254, 255, 1, 2, 3 if self.prev is None: # first time call self.prev = val return (sec, usec, IET_SYN, 0, 0) # will be ignored above diff = val - self.prev if diff == 0: # sometimes it just sends stuff... return (sec, usec, IET_SYN, 0, 0) # will be ignored above if abs(diff) > 100: # crossed into the twilight zone if self.prev > val: # we've wrapped going forward self.prev = val return (sec, usec, type, code, +1) else: # we've wrapped going backward self.prev = val return (sec, usec, type, code, -1) self.prev = val return (sec, usec, type, code, diff) if type == IET_KEY: # remap keys so that all 3 gadgets have buttons 0 to 4 in common return (sec, usec, type, (IEC_BTN_5, IEC_BTN_6, IEC_BTN_7, IEC_BTN_8, IEC_BTN_0, IEC_BTN_1, IEC_BTN_2, IEC_BTN_3, IEC_BTN_4, IEC_BTN_9, IEC_BTN_10, IEC_BTN_11, IEC_BTN_12, IEC_BTN_13, IEC_BTN_14)[code - IEC_BTN_0], val) return event if __name__ == '__main__': def ev(what): print 'ev', what p = powermate("/dev/input/by-id/usb-Contour_Design_ShuttlePRO-event-if00", ev) while True: p.read_next()