From: Andrew Ruthven Date: Sun, 1 Dec 2024 01:42:11 +0000 (+1300) Subject: New upstream version 7.5 X-Git-Tag: upstream/7.5^0 X-Git-Url: http://git.etc.gen.nz/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3b79b73b072cf8b3cd1fb3d56eb373f56a9b9405;p=sapphire-remote.git New upstream version 7.5 --- diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..0a441a2 --- /dev/null +++ b/Makefile @@ -0,0 +1,91 @@ +# +# Makefile and (un-)Installer for Sapphire remote control driver, +# by Mark Lord 2012-2019. +# +MODNAME=sapphire +MODSOURCE=$(MODNAME).c +CONFLICTS=hid_topseed +BLACKLIST=/etc/modprobe.d/blacklist-$(CONFLICTS).conf +KEYMAP_SCRIPT=sapphire_keymap.sh +EXTRACT_KEYDEFS=extract_keydefs.sh +INPUT_H=$(shell [ -e /usr/include/linux/input-event-codes.h ] && echo /usr/include/linux/input-event-codes.h || echo /usr/include/linux/input.h) + +## MODPARMS is not used any more, but we have to nuke old copies to prevent issues: +MODPARMS=/etc/modprobe.d/$(MODNAME).conf + +CWD = $(shell pwd -P) +KVER ?= $(shell uname -r) +KDIR = /lib/modules/$(KVER) +obj-m += $(MODNAME).o + +#EXTRA_CFLAGS += -Werror + +default: $(INPUT_H) kmod $(KEYMAP_SCRIPT) + +$(INPUT_H): + @echo "ERROR: $@: not found." + @echo + @echo "Please install the system development libraries first." + @echo "Eg. on Ubuntu/Mint systems: sudo apt-get install build-essential" + @echo + @exit 1 + +$(KDIR)/build: + [ -e /usr/src/linux-$(KVER)/Makefile ] && sudo ln -sf /usr/src/linux-$(KVER) $@ || exit 0 + [ -e /usr/src/$(KVER)/Makefile ] && sudo ln -sf /usr/src/$(KVER) $@ || exit 0 + [ -e $@ ] || exit 1 + +kmod: $(KDIR)/build + $(MAKE) -C $(KDIR)/build M=$(CWD) modules + +$(MODNAME).ko: $(MODSOURCE) $(MODNAME).h + $(MAKE) kmod + +$(KEYMAP_SCRIPT): $(INPUT_H) $(EXTRACT_KEYDEFS) $(KEYMAP_SCRIPT).part1 $(KEYMAP_SCRIPT).part3 $(MODNAME).h $(INPUT_H) + @bash -c "type -all gawk" >/dev/null 2>&1 || (\ + echo ;\ + echo "gawk not found, needed by installer; Aborted." ;\ + echo "For Ubuntu/Debian based systems, try this: sudo apt-get install gawk" ;\ + echo ;\ + exit 2 ;\ + ) + chmod 0755 $(EXTRACT_KEYDEFS) + ./$(EXTRACT_KEYDEFS) $(INPUT_H) $(MODNAME).h | cat $(KEYMAP_SCRIPT).part1 - $(KEYMAP_SCRIPT).part3 > $(KEYMAP_SCRIPT) + chmod 0755 $(KEYMAP_SCRIPT) + +$(BLACKLIST): + @echo "Creating $(BLACKLIST)" + @echo "## $(CONFLICTS) Conflicts with $(MODNAME) driver" > $(BLACKLIST) + @echo "blacklist $(CONFLICTS)" >> $(BLACKLIST) + +modinstall: $(MODNAME).ko $(KEYMAP_SCRIPT) + $(MAKE) -C $(KDIR)/build M=$(CWD) modules_install || exit 0 + @[ -e $(KDIR)/extra/sapphire.ko -o -e $(KDIR)/extra/sapphire.ko.gz -o -e $(KDIR)/extra/sapphire.ko.xz ] || exit 1 + depmod $(KVER) + @if [ "$(KVER)" = "$$(uname -r)" ]; then \ + rmmod $(MODNAME) 2>/dev/null ;\ + rmmod $(CONFLICTS) 2>/dev/null ;\ + ./sapphire_startup.sh ;\ + else \ + exit 0 ;\ + fi + +clean: + $(MAKE) -C $(KDIR)/build M=$(CWD) clean + -rm -f $(KEYMAP_SCRIPT) + +clean_modparms: + @if [ -e $(MODPARAMS) ]; then rm -f $(MODPARMS) ; fi ; exit 0 + +uninstall: clean clean_modparms + -rmmod $(MODNAME) + -rm -f $(BLACKLIST) + -rm -f /usr/local/bin/$(KEYMAP_SCRIPT) + -rm -f $(KDIR)/extra/$(MODNAME).ko + depmod + +install: $(MODNAME).ko clean_modparms modinstall $(BLACKLIST) $(KEYMAP_SCRIPT) + install -m 0755 $(KEYMAP_SCRIPT) /usr/local/bin/ + install -m 0755 sapphire_startup.sh /usr/local/bin/ + +all: install diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..5a9a168 --- /dev/null +++ b/README.txt @@ -0,0 +1,450 @@ +Sapphire hid driver, by Mark Lord , September/2015. +http://rtr.ca/sapphire_remote/ + +This package contains a standalone Linux "hid" driver +for the Sapphire Theatrix I/R remote control. + +To build/install the driver, simply do this: + + sudo make install + +That command will need to be repeated after any +subsequent kernel upgrades. + +This driver competes with hid-topseed for control of the device. +As of 2012, several distros are now building the hid-topseed driver +into the main kernel image, preventing it from being blacklisted +and unloaded at run time. To work around this, there is now a script +included in this package for unbinding the device(s) from hid-topseed. +This script should be run from /etc/rc.local, by manually editing +that file and adding this line: + + /usr/local/bin/sapphire_startup.sh + +This driver comes pre-configured for use with Mythtv, +with the default mappings for most buttons set to +what Mythtv usually expects. + +You can customize the mappings, by copying/editing +the keymap.default file, and then feeding your modified +copy as a parameter to /usr/local/bin/sapphire_keymap.sh +after each system startup. + +Or you can directly edit the default keymap that is +built into the driver source code, but that runs a risk +of conflicts when the driver gets updated in the future. + +Best method: copy/edit the keymap.default file, +and save it as: /etc/sapphire.keymap + +Then ensure you have this line (from above) in /etc/rc.local: + + /usr/local/bin/sapphire_startup.sh ## already discussed above + +Done. + +One can also use /proc/sapphire to inject KEY_ presses into the system +from a script, just as if they had been typed on a keyboard or remote. + + Eg. echo "SENDKEY 5" > /proc/sapphire ## sends KEY_4 (defined as 5 in input.h). + +This feature could be used to control mythfrontend from external scripts, +or to automate keyboard input for any other application as well. + +A special keyword "REPEAT_RATE" is recognized by /proc/sapphire, +and enables modifying the ramping repeat attack rate and maximum repeat rate. +Two values must be provided: the number of times/second that the rate is +adjusted, and the maxiumum number of repeats per second. +The default values can be restored like this: + + Eg. echo "REPEAT_RATE 40 10" > /proc/sapphire + +The same syntax can also be used in a keymap file. + +* * * * * * * * * * + +Below is a handy list of most of the available KEY_* names +for use with definitions in the keymap file. +These were extracted from /usr/include/linux/input.h: + +KEY_0 +KEY_102ND +KEY_1 +KEY_2 +KEY_3 +KEY_4 +KEY_5 +KEY_6 +KEY_7 +KEY_8 +KEY_9 +KEY_A +KEY_AB +KEY_ADDRESSBOOK +KEY_AGAIN +KEY_ALTERASE +KEY_ANGLE +KEY_APOSTROPHE +KEY_ARCHIVE +KEY_AUDIO +KEY_AUX +KEY_B +KEY_BACK +KEY_BACKSLASH +KEY_BACKSPACE +KEY_BASSBOOST +KEY_BATTERY +KEY_BLUE +KEY_BLUETOOTH +KEY_BOOKMARKS +KEY_BREAK +KEY_BRIGHTNESS_CYCLE +KEY_BRIGHTNESSDOWN +KEY_BRIGHTNESSUP +KEY_BRIGHTNESS_ZERO +KEY_BRL_DOT10 +KEY_BRL_DOT1 +KEY_BRL_DOT2 +KEY_BRL_DOT3 +KEY_BRL_DOT4 +KEY_BRL_DOT5 +KEY_BRL_DOT6 +KEY_BRL_DOT7 +KEY_BRL_DOT8 +KEY_BRL_DOT9 +KEY_C +KEY_CALC +KEY_CALENDAR +KEY_CAMERA +KEY_CAMERA_FOCUS +KEY_CANCEL +KEY_CAPSLOCK +KEY_CD +KEY_CHANNEL +KEY_CHANNELDOWN +KEY_CHANNELUP +KEY_CHAT +KEY_CLEAR +KEY_CLOSE +KEY_CLOSECD +KEY_COFFEE +KEY_COMMA +KEY_COMPOSE +KEY_COMPUTER +KEY_CONFIG +KEY_CONNECT +KEY_CONTEXT_MENU +KEY_COPY +KEY_CUT +KEY_CYCLEWINDOWS +KEY_D +KEY_DASHBOARD +KEY_DATABASE +KEY_DEL_EOL +KEY_DEL_EOS +KEY_DELETE +KEY_DELETEFILE +KEY_DEL_LINE +KEY_DIGITS +KEY_DIRECTION +KEY_DIRECTORY +KEY_DISPLAY_OFF +KEY_DISPLAYTOGGLE +KEY_DOCUMENTS +KEY_DOLLAR +KEY_DOT +KEY_DOWN +KEY_DVD +KEY_E +KEY_EDIT +KEY_EDITOR +KEY_EJECTCD +KEY_EJECTCLOSECD +KEY_EMAIL +KEY_END +KEY_ENTER +KEY_EPG +KEY_EQUAL +KEY_ESC +KEY_EURO +KEY_EXIT +KEY_F10 +KEY_F11 +KEY_F12 +KEY_F13 +KEY_F14 +KEY_F15 +KEY_F1 +KEY_F16 +KEY_F17 +KEY_F18 +KEY_F19 +KEY_F20 +KEY_F21 +KEY_F22 +KEY_F23 +KEY_F24 +KEY_F2 +KEY_F +KEY_F3 +KEY_F4 +KEY_F5 +KEY_F6 +KEY_F7 +KEY_F8 +KEY_F9 +KEY_FASTFORWARD +KEY_FAVORITES +KEY_FILE +KEY_FINANCE +KEY_FIND +KEY_FIRST +KEY_FN +KEY_FN_1 +KEY_FN_2 +KEY_FN_B +KEY_FN_D +KEY_FN_E +KEY_FN_ESC +KEY_FN_F +KEY_FN_F10 +KEY_FN_F1 +KEY_FN_F11 +KEY_FN_F12 +KEY_FN_F2 +KEY_FN_F3 +KEY_FN_F4 +KEY_FN_F5 +KEY_FN_F6 +KEY_FN_F7 +KEY_FN_F8 +KEY_FN_F9 +KEY_FN_S +KEY_FORWARD +KEY_FORWARDMAIL +KEY_FRAMEBACK +KEY_FRAMEFORWARD +KEY_FRONT +KEY_G +KEY_GAMES +KEY_GOTO +KEY_GRAPHICSEDITOR +KEY_GRAVE +KEY_GREEN +KEY_H +KEY_HANGEUL +KEY_HANJA +KEY_HELP +KEY_HENKAN +KEY_HIRAGANA +KEY_HOME +KEY_HOMEPAGE +KEY_HP +KEY_I +KEY_INFO +KEY_INSERT +KEY_INS_LINE +KEY_ISO +KEY_J +KEY_K +KEY_KATAKANA +KEY_KATAKANAHIRAGANA +KEY_KBDILLUMDOWN +KEY_KBDILLUMTOGGLE +KEY_KBDILLUMUP +KEY_KEYBOARD +KEY_KP0 +KEY_KP1 +KEY_KP2 +KEY_KP3 +KEY_KP4 +KEY_KP5 +KEY_KP6 +KEY_KP7 +KEY_KP8 +KEY_KP9 +KEY_KPASTERISK +KEY_KPCOMMA +KEY_KPDOT +KEY_KPENTER +KEY_KPEQUAL +KEY_KPJPCOMMA +KEY_KPLEFTPAREN +KEY_KPMINUS +KEY_KPPLUS +KEY_KPPLUSMINUS +KEY_KPRIGHTPAREN +KEY_KPSLASH +KEY_L +KEY_LANGUAGE +KEY_LAST +KEY_LEFT +KEY_LEFTALT +KEY_LEFTBRACE +KEY_LEFTCTRL +KEY_LEFTMETA +KEY_LEFTSHIFT +KEY_LINEFEED +KEY_LIST +KEY_LOGOFF +KEY_M +KEY_MACRO +KEY_MAIL +KEY_MAX +KEY_MEDIA +KEY_MEDIA_REPEAT +KEY_MEMO +KEY_MENU +KEY_MESSENGER +KEY_MHP +KEY_MINUS +KEY_MODE +KEY_MOVE +KEY_MP3 +KEY_MSDOS +KEY_MUHENKAN +KEY_MUTE +KEY_N +KEY_NEW +KEY_NEWS +KEY_NEXT +KEY_NEXTSONG +KEY_NUMERIC_0 +KEY_NUMERIC_1 +KEY_NUMERIC_2 +KEY_NUMERIC_3 +KEY_NUMERIC_4 +KEY_NUMERIC_5 +KEY_NUMERIC_6 +KEY_NUMERIC_7 +KEY_NUMERIC_8 +KEY_NUMERIC_9 +KEY_NUMERIC_POUND +KEY_NUMERIC_STAR +KEY_NUMLOCK +KEY_O +KEY_OK +KEY_OPEN +KEY_OPTION +KEY_P +KEY_PAGEDOWN +KEY_PAGEUP +KEY_PASTE +KEY_PAUSE +KEY_PAUSECD +KEY_PC +KEY_PHONE +KEY_PLAY +KEY_PLAYCD +KEY_PLAYER +KEY_PLAYPAUSE +KEY_POWER +KEY_POWER2 +KEY_PRESENTATION +KEY_PREVIOUS +KEY_PREVIOUSSONG +KEY_PRINT +KEY_PROG1 +KEY_PROG2 +KEY_PROG3 +KEY_PROG4 +KEY_PROGRAM +KEY_PROPS +KEY_PVR +KEY_Q +KEY_QUESTION +KEY_R +KEY_RADIO +KEY_RECORD +KEY_RED +KEY_REDO +KEY_REFRESH +KEY_REPLY +KEY_RESERVED +KEY_RESTART +KEY_REWIND +KEY_RFKILL +KEY_RIGHT +KEY_RIGHTALT +KEY_RIGHTBRACE +KEY_RIGHTCTRL +KEY_RIGHTMETA +KEY_RIGHTSHIFT +KEY_RO +KEY_S +KEY_SAT +KEY_SAT2 +KEY_SAVE +KEY_SCALE +KEY_SCREEN +KEY_SCROLLDOWN +KEY_SCROLLLOCK +KEY_SCROLLUP +KEY_SEARCH +KEY_SELECT +KEY_SEMICOLON +KEY_SEND +KEY_SENDFILE +KEY_SETUP +KEY_SHOP +KEY_SHUFFLE +KEY_SLASH +KEY_SLEEP +KEY_SLOW +KEY_SOUND +KEY_SPACE +KEY_SPELLCHECK +KEY_SPORT +KEY_SPREADSHEET +KEY_STOP +KEY_STOPCD +KEY_SUBTITLE +KEY_SUSPEND +KEY_SWITCHVIDEOMODE +KEY_SYSRQ +KEY_T +KEY_TAB +KEY_TAPE +KEY_TEEN +KEY_TEXT +KEY_TIME +KEY_TITLE +KEY_TUNER +KEY_TV +KEY_TV2 +KEY_TWEN +KEY_U +KEY_UNDO +KEY_UNKNOWN +KEY_UP +KEY_UWB +KEY_V +KEY_VCR +KEY_VCR2 +KEY_VENDOR +KEY_VIDEO +KEY_VIDEO_NEXT +KEY_VIDEOPHONE +KEY_VIDEO_PREV +KEY_VOICEMAIL +KEY_VOLUMEDOWN +KEY_VOLUMEUP +KEY_W +KEY_WAKEUP +KEY_WIMAX +KEY_WLAN +KEY_WORDPROCESSOR +KEY_WPS_BUTTON +KEY_WWW +KEY_X +KEY_XFER +KEY_Y +KEY_YELLOW +KEY_YEN +KEY_Z +KEY_ZENKAKUHANKAKU +KEY_ZOOM +KEY_ZOOMIN +KEY_ZOOMOUT +KEY_ZOOMRESET + diff --git a/extract_keydefs.sh b/extract_keydefs.sh new file mode 100755 index 0000000..865db6f --- /dev/null +++ b/extract_keydefs.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# +# Extract button and key definitions from sapphire.h and input.h: +# + +function parse_sapphire_h(){ + echo + echo "## buttons, macros, and other definitions extracted from $1:" + echo "##" + gawk ' + ($2 == "BEGIN_PARSING"){c=1;next} + (c == 0){next} + /^ [XS]APPHIRE_([A-Z0-9_]+)\>([ ]+)= 0x/{printf "B[%s]=%s\n",$1,$3;next} + /^ KEY_MACRO_[0-7]\>([ ]+)= 0x/{printf "B[%s]=0x%08x\n",$1,strtonum($3)} + /^ KEY_([A-Z0-9_]+)\>([ ]+)= 0x/{printf "K[%s]=0x%08x\n",$1,strtonum($3);next} + /^ ([A-Z0-9_]+)\>([ ]+)= 0x/{printf "M[%s]=0x%08x\n",$1,strtonum($3);next} + /^[}];/{if (c == 1) nextfile} + ' < "$1" +} + +function parse_input_h(){ + echo + echo "## keycodes extracted from $1:" + echo "##" + gawk ' /^#define KEY_([A-Z0-9_]+)\>([ ]+)((0x[0-9a-fA-F]+)|([0-9]+))\>/{ + printf "K[%s]=0x%08x\n",$2,strtonum($3)} + ' < "$1" | grep -v KEY_RESERVED +} + +parse_input_h "$1" && parse_sapphire_h "$2" + diff --git a/keymap.default b/keymap.default new file mode 100644 index 0000000..5c38b8e --- /dev/null +++ b/keymap.default @@ -0,0 +1,108 @@ +# +# Default keymap from sapphire.c v3.0. +# +# Edit this as you please, keeping in mind that spaces are NOT permitted +# within each column. +# +# Eg. this is INCORRECT: KEY_F15 | ALT +# whereas this is good: KEY_F15|ALT +# +# Use the provided sapphire_keymap.sh script to install these +# mappings at runtime in place of the driver's built-in mappings. +# Keep your own copy of this file separate from the driver source, +# so that subsequent changes in the driver mappings won't clobber +# your own definitions here. +# +# As in the example above, "KeyCode" values can be combined with +# any mix of these "modifier" keys: SHIFT, ALT, CTRL, and/or META. +# +# Eg. SAPPHIRE_PLAY KEY_P|CTRL NO_REPEAT ## Mythtv Play/Pause function +# Or: SAPPHIRE_PLAY KEY_P|CTRL|ALT NO_REPEAT ## something else +# +# The "SpecialOptions" field is used to indicate the desired auto-repeat rate +# for each button individually. The choices are: +# +# NO_REPEAT - the button will not auto-repeat when held down. +# SLOW_REPEAT - the button will repeat slowly. +# RAMP_REPEAT - the button will repeat slowly at first, but speed up if held down. +# LONGKEY - no auto-repeat: special behaviour described below. +# +# The special value "LONGKEY" can be used in "SpecialOptions" +# insted of an auto-repeat setting. This causes a button on the remote +# to have two separate functions, depending upon how quickly it is released. +# +# A short-press/release gets the regular "KeyCode" value, but if the button +# is held down for one second or longer, then an alternate value can be send instead. +# Just combine the desired alternate keycode with the LONGKEY tag. +# +# Eg. SAPPHIRE_MUTE KEY_F9 LONGKEY|KEY_F15 ## Mute (tap) or Audiosync (hold) +# +# ButtonName KeyCode SpecialOptions ## Comments +# +SAPPHIRE_UP KEY_UP RAMP_REPEAT ## up (CUSTOM) +SAPPHIRE_DOWN KEY_DOWN RAMP_REPEAT ## down (CUSTOM) +SAPPHIRE_RIGHT KEY_RIGHT SLOW_REPEAT ## right +SAPPHIRE_LEFT KEY_LEFT SLOW_REPEAT ## left +SAPPHIRE_ENTEROK KEY_ENTER LONGKEY|KEY_F15 ## Select (tap) or Audiosync (hold) +SAPPHIRE_BACK KEY_ESC SLOW_REPEAT ## Back +SAPPHIRE_PLAY KEY_P|CTRL NO_REPEAT ## Play/Pause +SAPPHIRE_PAUSE KEY_P NO_REPEAT ## Play/Pause +SAPPHIRE_VOLUP KEY_RIGHTBRACE FAST_REPEAT ## Volume Up +SAPPHIRE_VOLDOWN KEY_LEFTBRACE FAST_REPEAT ## Volume Down +SAPPHIRE_CHUP KEY_PAGEUP MED_REPEAT ## channel up (CUSTOM) +SAPPHIRE_CHDOWN KEY_PAGEDOWN MED_REPEAT ## channel down (CUSTOM) +SAPPHIRE_MUTE KEY_F9 LONGKEY|KEY_F15 ## Mute (tap) or Audiosync (hold) +SAPPHIRE_RECORD KEY_R NO_REPEAT ## Record/Delete (CUSTOM) +SAPPHIRE_FWD KEY_DOT NO_REPEAT ## FFwd +SAPPHIRE_REW KEY_COMMA NO_REPEAT ## Rewind +SAPPHIRE_ANGLE KEY_W NO_REPEAT ## Adjust Fill +SAPPHIRE_SAP KEY_W|CTRL NO_REPEAT ## Toggle Aspect Ratio +SAPPHIRE_DVDMENU KEY_M NO_REPEAT ## Menu +SAPPHIRE_INFOEPG KEY_I NO_REPEAT ## Info +SAPPHIRE_TAB KEY_END SLOW_REPEAT ## Commskip Fwd +SAPPHIRE_BACKTAB KEY_HOME SLOW_REPEAT ## Commskip Rev +SAPPHIRE_RADIO KEY_F7|ALT NO_REPEAT ## Signal Monitor +SAPPHIRE_LASTCH KEY_H NO_REPEAT ## previous chan +SAPPHIRE_LANGUAGE KEY_EQUAL|SHIFT NO_REPEAT ## Next Audio Track +SAPPHIRE_TELETEXTCC KEY_T NO_REPEAT ## Toggle Closed-Caption +SAPPHIRE_SUBTITLE KEY_F17|ALT NO_REPEAT ## Toggle subtitle (CUSTOM) +SAPPHIRE_HOMEHOUSE KEY_F18 NO_REPEAT ## jump to MainMenu (CUSTOM jumppoint) +SAPPHIRE_BLUEVIDEOS KEY_F14 NO_REPEAT ## MythVideo (CUSTOM jumppoint) +SAPPHIRE_LIVETV KEY_F16 NO_REPEAT ## Program Guide (CUSTOM jumppoint) +SAPPHIRE_REDDVDVCD KEY_F13 LONGKEY|KEY_F13|ALT ## PlayDVD/DVDMenu (CUSTOM jumppoints) +SAPPHIRE_YELLOWPICTURES KEY_F14|ALT NO_REPEAT ## Watch Recordings (CUSTOM jumppoint) +SAPPHIRE_1 KEY_1 SLOW_REPEAT ## 1 +SAPPHIRE_2 KEY_2 SLOW_REPEAT ## 2 +SAPPHIRE_3 KEY_3 SLOW_REPEAT ## 3 +SAPPHIRE_4 KEY_4 SLOW_REPEAT ## 4 +SAPPHIRE_5 KEY_5 SLOW_REPEAT ## 5 +SAPPHIRE_6 KEY_6 SLOW_REPEAT ## 6 +SAPPHIRE_7 KEY_7 SLOW_REPEAT ## 7 +SAPPHIRE_8 KEY_8 SLOW_REPEAT ## 8 +SAPPHIRE_9 KEY_9 SLOW_REPEAT ## 9 +SAPPHIRE_0 KEY_0 SLOW_REPEAT ## 0 +SAPPHIRE_STOP KEY_MINUS LONGKEY|KEY_MINUS|SHIFT ## dash/underscore symbols +SAPPHIRE_POWER KEY_F2 NO_REPEAT ## start frontend (CUSTOM desktop script) +SAPPHIRE_CLEAR KEY_MACRO_0 NO_REPEAT ## Adjust audiosync by 90msecs (see below) +SAPPHIRE_GREENMUSIC KEY_A NO_REPEAT ## Adjust time stretch + +# +# Now for the macro definitions: each macro sends a list of up to eight keycodes: +# +KEY_MACRO_0 KEY_F15 KEY_DOWN KEY_LEFT ## Adjust AudioSync by +90msecs +KEY_MACRO_1 +KEY_MACRO_2 +KEY_MACRO_2 +KEY_MACRO_3 +KEY_MACRO_4 +KEY_MACRO_5 +KEY_MACRO_6 +KEY_MACRO_7 + +# +# These control how quickly buttons repeat: +# +RAMP_RATE 80 6 +SLOW_RATE 4 +MED_RATE 6 +FAST_RATE 8 diff --git a/sapphire.c b/sapphire.c new file mode 100644 index 0000000..3e2d7ad --- /dev/null +++ b/sapphire.c @@ -0,0 +1,905 @@ +/* + * sapphire.c + * + * Copyright Mark Lord , 2012-2020. + * http://rtr.ca/sapphire_remote/ + * + * This is a HID driver for the TopSeed Cyberlink receiver + * when paired with the "Sapphire TheatriX" remote control. + * + * This replaces the in-kernel "hid-topseed" driver, + * and is tailored specifically for use with Mythtv + * without need of any external configuration or LIRC etc. + * + * Buttons can have variable repeat rates, distinguish between short + * and long presses, and can send macros instead of just a single keycode. + * + * Wake-from-suspend works by pressing the remote's "Power" button, + * after correctly configuring /proc/acpi/wakeup before suspending. + * + * User-defined keymaps and macros can be (re)configured on the fly + * via a /proc/ interface, using the supplied sapphire_keymap.sh script. + * + * 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; specifically version 2 of the License (GPLv2). + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "sapphire.h" + +#define SAPPHIRE_NAME "sapphire" +#define SAPPHIRE_VERSION "7.5" + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,6,0) +#define PROC_OPS +#endif +/* + * "LONGKEY": Any button can be used to generate a second, different keycode, + * by setting the "repeats" value to "LONGKEY | KEY_XXX" for the second keycode. + * When that button is then tapped (quick press/release), the regular keycode is sent. + * But when the button is pressed/held for one second, the second keycode is sent + * instead of the regular keycode. This allows buttons to have dual functionalty, + * to solve some sticky issues. + * + * Eg. by default below, the MUTE button is set up this way: a short tap of MUTE + * sends the "Mute" command, but holding it down longer will instead activate + * the "Adjust Audio Sync" pop-up. Very, VERY handy, that. + * + * EDIT the "keycode" and "repeats" columns below to customize the keymap to your needs: + */ +#define SAPPHIRE_NUM_BUTTONS 46 /* The Sapphire remote sends 46 unique button codes */ +#define SAPPHIRE_KEYMAP_SIZE (SAPPHIRE_NUM_BUTTONS) +static struct sapphire_map { + unsigned int button; /* raw data from sensor */ + unsigned int keycode; /* keyboard value to pass to mythtv, or "0" for nothing */ + unsigned int repeats; /* auto-repeat setting */ +} sapphire_keymap[SAPPHIRE_KEYMAP_SIZE] = { + /* button keycode repeats mythtv action */ + /* ========== ======== =========== ============= */ + {SAPPHIRE_UP , KEY_UP , RAMP_REPEAT }, /* up (CUSTOM) */ + {SAPPHIRE_DOWN , KEY_DOWN , RAMP_REPEAT }, /* down (CUSTOM) */ + {SAPPHIRE_RIGHT , KEY_RIGHT , SLOW_REPEAT }, /* right */ + {SAPPHIRE_LEFT , KEY_LEFT , SLOW_REPEAT }, /* left */ + {SAPPHIRE_ENTEROK , KEY_ENTER , LONGKEY|KEY_F15 }, /* Select (tap) or Audiosync (hold) */ + {SAPPHIRE_BACK , KEY_ESC , SLOW_REPEAT }, /* Back */ + {SAPPHIRE_PLAY , KEY_P|CTRL , NO_REPEAT }, /* Play/Pause */ + {SAPPHIRE_PAUSE , KEY_P , NO_REPEAT }, /* Play/Pause */ + {SAPPHIRE_VOLUP , KEY_RIGHTBRACE , FAST_REPEAT }, /* Volume Up */ + {SAPPHIRE_VOLDOWN , KEY_LEFTBRACE , FAST_REPEAT }, /* Volume Down */ + {SAPPHIRE_CHUP , KEY_PAGEUP , MED_REPEAT }, /* channel up (CUSTOM) */ + {SAPPHIRE_CHDOWN , KEY_PAGEDOWN , MED_REPEAT }, /* channel down (CUSTOM) */ + {SAPPHIRE_MUTE , KEY_F9 , LONGKEY|KEY_F15 }, /* Mute (tap) or Audiosync (hold) */ + {SAPPHIRE_RECORD , KEY_R , NO_REPEAT }, /* Record/Delete (CUSTOM) */ + {SAPPHIRE_FWD , KEY_DOT , NO_REPEAT }, /* FFwd */ + {SAPPHIRE_REW , KEY_COMMA , NO_REPEAT }, /* Rewind */ + {SAPPHIRE_ANGLE , KEY_W , NO_REPEAT }, /* Adjust Fill */ + {SAPPHIRE_SAP , KEY_W|CTRL , NO_REPEAT }, /* Toggle Aspect Ratio */ + {SAPPHIRE_DVDMENU , KEY_M , LONGKEY|KEY_F13|ALT }, /* Menu/DVDMenu (CUSTOM) */ + {SAPPHIRE_INFOEPG , KEY_I , NO_REPEAT }, /* Info */ + {SAPPHIRE_TAB , KEY_END , SLOW_REPEAT }, /* Commskip Fwd */ + {SAPPHIRE_BACKTAB , KEY_HOME , SLOW_REPEAT }, /* Commskip Rev */ + {SAPPHIRE_RADIO , KEY_F7|ALT , NO_REPEAT }, /* Signal Monitor */ + {SAPPHIRE_LASTCH , KEY_H , NO_REPEAT }, /* previous chan */ + {SAPPHIRE_LANGUAGE , KEY_EQUAL|SHIFT, NO_REPEAT }, /* Next Audio Track */ + {SAPPHIRE_TELETEXTCC , KEY_T , NO_REPEAT }, /* Toggle Closed-Caption */ + {SAPPHIRE_SUBTITLE , KEY_F17|ALT , NO_REPEAT }, /* Toggle subtitle (CUSTOM) */ + {SAPPHIRE_HOMEHOUSE , KEY_F18 , NO_REPEAT }, /* jump to MainMenu (CUSTOM jumppoint) */ + {SAPPHIRE_BLUEVIDEOS , KEY_F14 , NO_REPEAT }, /* MythVideo (CUSTOM jumppoint) */ + {SAPPHIRE_LIVETV , KEY_F16 , NO_REPEAT }, /* Program Guide (CUSTOM jumppoint) */ + {SAPPHIRE_REDDVDVCD , KEY_F13 , LONGKEY|KEY_F13|ALT }, /* PlayDVD/DVDMenu (CUSTOM jumppoints) */ + {SAPPHIRE_YELLOWPICTURES, KEY_F14|ALT , NO_REPEAT }, /* Watch Recordings (CUSTOM jumppoint) */ + {SAPPHIRE_1 , KEY_1 , SLOW_REPEAT }, /* 1 */ + {SAPPHIRE_2 , KEY_2 , SLOW_REPEAT }, /* 2 */ + {SAPPHIRE_3 , KEY_3 , SLOW_REPEAT }, /* 3 */ + {SAPPHIRE_4 , KEY_4 , SLOW_REPEAT }, /* 4 */ + {SAPPHIRE_5 , KEY_5 , SLOW_REPEAT }, /* 5 */ + {SAPPHIRE_6 , KEY_6 , SLOW_REPEAT }, /* 6 */ + {SAPPHIRE_7 , KEY_7 , SLOW_REPEAT }, /* 7 */ + {SAPPHIRE_8 , KEY_8 , SLOW_REPEAT }, /* 8 */ + {SAPPHIRE_9 , KEY_9 , SLOW_REPEAT }, /* 9 */ + {SAPPHIRE_0 , KEY_0 , SLOW_REPEAT }, /* 0 */ + {SAPPHIRE_STOP , KEY_MINUS , LONGKEY|KEY_MINUS|SHIFT }, /* dash/underscore symbols */ + {SAPPHIRE_POWER , KEY_F2 , NO_REPEAT }, /* start frontend (CUSTOM desktop script) */ + {SAPPHIRE_CLEAR , KEY_MACRO_0 , NO_REPEAT }, /* Adjust audiosync by 90msecs */ + {SAPPHIRE_GREENMUSIC , KEY_A , NO_REPEAT }, /* Adjust time stretch */ +}; + +/* + * Up to 8 key "macros" can be defined here. + * These are mapped using the MACRO_[0-7] enums above, + * and each can consist of up to 8 KEY_* values, + * with zeros for any trailing unused values. + * The special KEY_DELAY can be used anywhere inside a macro + * to inject a half-second pause before continuing. + */ +static unsigned int sapphire_macros[8][8] = { + /* KEY_MACRO_0 */ {KEY_F15,KEY_DOWN,KEY_LEFT,0,}, /* adjust audiosync by 90msec */ + /* KEY_MACRO_1 */ {0,}, + /* KEY_MACRO_2 */ {0,}, + /* KEY_MACRO_3 */ {0,}, + /* KEY_MACRO_4 */ {0,}, + /* KEY_MACRO_5 */ {0,}, + /* KEY_MACRO_6 */ {0,}, + /* KEY_MACRO_7 */ {0,} +}; + +#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 14, 0) +#define SAPPHIRE_TIMER_ARG_T unsigned long +static inline void sapphire_timer_setup(struct timer_list *timer_p, void *callback) +{ + init_timer(timer_p); + timer_p->function = callback; + timer_p->data = (long)timer_p; +} +#else +#define sapphire_timer_setup(timer_p, callback) timer_setup(timer_p, callback, 0) +#define SAPPHIRE_TIMER_ARG_T struct timer_list * +#endif + +static struct sapphire_dev { + spinlock_t lock; + struct input_dev *idev; + struct timer_list key_timer; + u64 last_event; + unsigned long delay_end; + unsigned long next_wakeup; + unsigned int ready; + unsigned int event_count; + unsigned int down_key; /* button currently being pressed */ + unsigned int next_repeat; /* repeat interval for down_key */ + unsigned int last_key; + unsigned int dualkey_short; /* dual-function button "short press" value */ + unsigned int dualkey_long; /* dual-function button "long press" value */ + unsigned int *delayed_macro; /* macro to be resumed after a 1/2 second delay */ + unsigned int delayed_next; /* next button within the macro to be resumed */ + unsigned int raw_key; /* a completely untranslated button being pressed */ +} sapphire_dev, *dev = &sapphire_dev; + +enum { + SLOW_RATE = HZ / 4, /* repeat rate for SLOW_REPEAT */ + MED_RATE = HZ / 6, /* repeat rate for MED_REPEAT */ + FAST_RATE = HZ / 8, /* repeat rate for FAST_REPEAT */ + RAMP_MAX = HZ / 10, /* max repeat rate for RAMP_REPEAT */ + RAMP_ADJ = HZ / 40, /* amount to speed up by after each repeat */ + RAMPING = 0x80000000, /* flag to indicate current dev->down_key is using RAMP_REPEAT */ + MODIFIERS = CTRL|SHIFT|ALT|META, /* convenient mask of all modifier keys */ + PRESSED = 1, /* for use with input_event() */ + RELEASED = 0, /* for use with input_event() */ +}; + +static unsigned int slow_rate = SLOW_RATE; +static unsigned int med_rate = MED_RATE; +static unsigned int fast_rate = FAST_RATE; +static unsigned int ramp_adj = RAMP_ADJ; +static unsigned int ramp_max = RAMP_MAX; + +/* + * Translate a [XS]APPHIRE_ "button" into a KEY_ "keycode" or "macro". + */ +static unsigned int sapphire_remap(unsigned int button, unsigned int *repeats) +{ + struct sapphire_map *map; + + for (map = sapphire_keymap; map != (sapphire_keymap + SAPPHIRE_KEYMAP_SIZE); ++map) { + if (map->button == button && map->keycode) { + *repeats = map->repeats; + return map->keycode; + } + } + *repeats = NO_REPEAT; + return 0; +} + +/* + * Remember the last key pressed for later retrieval from /proc/sapphire + */ +static void sapphire_save_key(unsigned int key) +{ + if (key) + dev->last_key = key; + dev->event_count++; + dev->last_event = get_jiffies_64(); +} + +/* + * Send "modifier" (CTRL, SHIFT, ALT, META) press/release events to the Linux input system. + */ +static void sapphire_send_modifiers(unsigned int modifiers, int pressed_released) +{ + if (modifiers & CTRL) + input_event(dev->idev, EV_KEY, KEY_LEFTCTRL, pressed_released); + if (modifiers & SHIFT) + input_event(dev->idev, EV_KEY, KEY_LEFTSHIFT, pressed_released); + if (modifiers & ALT) + input_event(dev->idev, EV_KEY, KEY_LEFTALT, pressed_released); + if (modifiers & META) + input_event(dev->idev, EV_KEY, KEY_LEFTMETA, pressed_released); +} + +/* + * Start/modify the keypress/repeat timer: + */ +static void sapphire_set_timer (unsigned long timeout) +{ + if (timeout) + dev->next_wakeup = jiffies + timeout; + else if (!time_before(jiffies, dev->next_wakeup)) + dev->next_wakeup = jiffies + 2; + mod_timer(&dev->key_timer, dev->next_wakeup); +} + +static void sapphire_send_key(unsigned int key, unsigned int repeat_delay); + +static int sapphire_send_macro (unsigned int *macro, unsigned int next) +{ + unsigned int i; + for (i = next; i <= 7 && macro[i]; ++i) { + unsigned int key = macro[i]; + if (key == KEY_DELAY) { + if (i < 7 && macro[i + 1]) { + dev->delayed_macro = macro; + dev->delayed_next = i + 1; + sapphire_set_timer(HZ / 2); + return 1; + } + break; + } + sapphire_send_key(key, NO_REPEAT); + } + return 0; +} + +/* + * Send a translated key/macro to the Linux input system. + */ +static void sapphire_send_key(unsigned int key, unsigned int repeat_delay) +{ + if (!key) + return; + sapphire_save_key(key); + if ((key & KEY_MACRO_0) == KEY_MACRO_0) { + unsigned int n = key & (KEY_MACRO_0 ^ KEY_MACRO_7); + if (sapphire_send_macro(sapphire_macros[n], 0)) + return; + } else { + unsigned int modifiers = key & MODIFIERS; + if (modifiers) { + key &= ~MODIFIERS; + sapphire_send_modifiers(modifiers, PRESSED); + } + input_event(dev->idev, EV_KEY, key, PRESSED); + input_event(dev->idev, EV_KEY, key, RELEASED); + if (modifiers) + sapphire_send_modifiers(modifiers, RELEASED); + input_sync(dev->idev); + } + if (repeat_delay) + sapphire_set_timer(repeat_delay); +} + +/* + * Timer callback function, used to handle repeats and "LONGKEYs". + */ +static void sapphire_key_timeout(SAPPHIRE_TIMER_ARG_T arg) +{ + unsigned long flags; + + spin_lock_irqsave(&dev->lock, flags); + dev->ready = 1; + /* + * FIXME: not sure if this can ever happen for real. + * + * In theory, we could get blocked on the spin_lock_irqsave() above + * while another part of the driver is busy modifying the timer value + * in response to a new button event. This could lead to us handling + * the new event immediately rather than after the newly set timeout. + * Guard against it by verifying that "next_wakeup" has really arrived. + */ + if (time_before(jiffies, dev->next_wakeup)) { + /* Spinlock race on an old timer value; skip this wakeup. */ + printk(KERN_INFO "%s: workaround activated: jiffies=%lu wakeup=%lu\n", __func__, jiffies, dev->next_wakeup); + sapphire_set_timer(0); /* reset the timer with same next_wakeup value */ + } else if (dev->delayed_macro) { + unsigned int *macro = dev->delayed_macro; + dev->delayed_macro = NULL; + sapphire_send_macro(macro, dev->delayed_next); + } else if (dev->down_key) { + unsigned int next_repeat = dev->next_repeat & ~RAMPING; + if (dev->next_repeat & RAMPING) { + /* Gradually ramp-up the repeat rate for this key */ + if (next_repeat > ramp_max) { + next_repeat -= ramp_adj; + if (next_repeat < ramp_max) + next_repeat = ramp_max; + } + dev->next_repeat = next_repeat | RAMPING; + } + sapphire_send_key(dev->down_key, next_repeat); + } else if (dev->dualkey_short) { + sapphire_send_key(dev->dualkey_long, NO_REPEAT); + dev->dualkey_long = dev->dualkey_short = 0; + } + spin_unlock_irqrestore(&dev->lock, flags); +} + +/* + * Pass along a button press event, setting up the repeat mechanism appropriately. + */ +static void sapphire_handle_key (unsigned int key, unsigned int repeats) +{ + const unsigned int REPEAT_DELAY = (HZ/2) - (HZ/6); + unsigned int next_repeat = 0; + + switch (repeats) { + case RAWKEY: + del_timer(&dev->key_timer); + input_event(dev->idev, EV_KEY, key, PRESSED); + input_sync(dev->idev); + dev->raw_key = key; + return; + case SLOW_REPEAT: + next_repeat = slow_rate; + break; + case MED_REPEAT: + next_repeat = med_rate; + break; + case FAST_REPEAT: + next_repeat = fast_rate; + break; + case RAMP_REPEAT: + next_repeat = (HZ/3) | RAMPING; + break; + default: + if (repeats & LONGKEY) { + /* + * We don't know if it's a short or long press yet. + * Start the key_timer to sort things out. + */ + dev->dualkey_short = key; + dev->dualkey_long = repeats & ~LONGKEY; + sapphire_set_timer(HZ); + dev->delay_end = dev->next_wakeup; + return; + } + } + sapphire_send_key(key, next_repeat ? REPEAT_DELAY : NO_REPEAT); + if (next_repeat ) { + dev->down_key = key; + dev->next_repeat = next_repeat; + } +} + +/* + * Handle a low-level press or release event, + */ +static void sapphire_event(u32 data) +{ + /* Treat each new event as a "release" for any previous "press" of a button */ + if (dev->down_key || dev->dualkey_short) { + dev->down_key = 0; + del_timer(&dev->key_timer); + if (dev->dualkey_short) { + if (time_before(jiffies, dev->delay_end)) + sapphire_send_key(dev->dualkey_short, NO_REPEAT); + else + sapphire_send_key(dev->dualkey_long, NO_REPEAT); + dev->dualkey_long = dev->dualkey_short = 0; + } + } + if (dev->raw_key) { + input_event(dev->idev, EV_KEY, dev->raw_key, RELEASED); + input_sync(dev->idev); + dev->raw_key = 0; + } + /* Key "press" events are > 0x10; nothing required here for "release" events (<= 0x10) */ + if (data > 0x10) { /* key "press" event? */ + unsigned int repeats, key = sapphire_remap(data, &repeats); + if (key) { + sapphire_handle_key(key, repeats); + return; + } + printk(KERN_INFO "%s: unmapped button: 0x%08x\n", __func__, data); + } + sapphire_save_key(0); /* timestamp the event in /proc/, for shutdown timing.. */ +} + +/* + * Debouncing, primarily for when multiple receivers are used with a single remote. + */ +static int sapphire_debounce (u32 data) +{ + static u32 debounce_data; + static u64 debounce_time; + u64 now = get_jiffies_64(); + + if (debounce_time && data == debounce_data) { + u64 elapsed = now - debounce_time; + if (elapsed < (HZ/8)) { + //printk(KERN_INFO "d=%08x ld=%08x t=%llu lt=%llu IGNORE\n", data, debounce_data, now, debounce_time); + return 0; /* discard */ + } + } + //printk(KERN_INFO "k=%08x lk=%08x t=%llu lt=%llu\n", data, debounce_data, now, debounce_time); + debounce_data = data; + debounce_time = now; + return 1; /* keep */ +} + +/* + * This gets called each time the IR receiver "receives" something from a remote control. + * The "size" of "raw_data" bytes received varies depending upon the button pressed, + * but we need only the first four bytes to uniquely distinguish all buttons from each other. + * The exceptions are the "OK" and "Enter" buttons, which send 100% identical data, + * so we cannot distinguish those from each other. All other buttons send unique data. + */ +static int sapphire_raw_event(struct hid_device *hdev, struct hid_report *report, u8 *raw_data, int size) +{ + unsigned long flags; + u32 data = 0; + int i; + + if (!(hdev->claimed & HID_CLAIMED_INPUT)) + return 0; + if (dev->ready) { + if (size > 4) /* Use only the first four bytes or less */ + size = 4; + for (i = size; i > 0; ) + data = (data << 8) | raw_data[--i]; + spin_lock_irqsave(&dev->lock, flags); + if (sapphire_debounce(data)) + sapphire_event(data); + spin_unlock_irqrestore(&dev->lock, flags); + } + return -1; /* event was handled (used to return "1" here) */ +} + +/* + * This hook provides a means for other drivers to feed + * their button press/release events through this driver, + * to take advantage of sapphire's macros, variable repeats, + * long/short press mappings, modifier keys (CTRL,ALT,SHIFT,META), + * and the /proc/sapphire interface. + * + * The incoming data must be pre-translated to sapphire buttons + * by the caller, sending non-zero [XS]APPHIRE_* codes for presses, + * and zero for button release events. + * + * There is a "glue.c" driver available demonstrating this, + * in conjunction with a tiny patch to cx88-input.c. + * This allows cx88 remote controls to feed through sapphire.c + * and behave similarly to the native Sapphire remote-control. + */ +void sapphire_relay (u32 data) +{ + unsigned long flags; + + spin_lock_irqsave(&dev->lock, flags); + sapphire_event(data); + spin_unlock_irqrestore(&dev->lock, flags); +} +EXPORT_SYMBOL(sapphire_relay); + +/* + * Parse and save a single macro definition written via /proc/ + */ +static int sapphire_store_macro(char *buf) +{ + unsigned long flags; + unsigned int vals[8] = {0,}, macro, n; + char excess; + int count; + + count = sscanf(buf, "%x %x %x %x %x %x %x %x %x %c", ¯o, + &vals[0], &vals[1], &vals[2], &vals[3], + &vals[4], &vals[5], &vals[6], &vals[7], &excess); + if (count < 1 || count > 9 || (macro & KEY_MACRO_0) != KEY_MACRO_0) + return -EINVAL; + spin_lock_irqsave(&dev->lock, flags); + n = macro & (KEY_MACRO_0 ^ KEY_MACRO_7); + memcpy(sapphire_macros[n], vals, sizeof(vals)); + spin_unlock_irqrestore(&dev->lock, flags); + return 0; +} + +/* + * Parse and save a single button mapping written via /proc/ + */ +static int sapphire_store_keymap(struct sapphire_map *map, char *buf) +{ + unsigned long flags; + unsigned int vals[3] = {0,}; + char excess; + + if (3 != sscanf(buf, "%x %x %x %c", &vals[0], &vals[1], &vals[2], &excess)) + return -EINVAL; + spin_lock_irqsave(&dev->lock, flags); + map->button = vals[0]; + map->keycode = vals[1]; + map->repeats = vals[2]; + spin_unlock_irqrestore(&dev->lock, flags); + return 0; +} + +/* + * Provide useful info via /proc/ + */ +static int sapphire_oldproc_read(char *buf, char **start, off_t offset, int count, int *eof, void *data) +{ + u64 last_event, now, elapsed; + unsigned long flags; + unsigned int len, last_key = 0, secs = ~0, event_count; + + spin_lock_irqsave(&dev->lock, flags); + event_count = dev->event_count; + last_event = dev->last_event; + last_key = dev->last_key; + spin_unlock_irqrestore(&dev->lock, flags); + + now = get_jiffies_64(); + elapsed = now - last_event; + secs = ((unsigned int)elapsed) / HZ; + len = sprintf(buf, "event_count %u\nlast_key 0x%08x\nelapsed_secs %u\n", event_count, last_key, secs); + *eof = 1; + return len; +} + +static int sapphire_get_HZ_vals (u8 *line, const char *keyword, int *val1, int *val2) +{ + int v1, v2, keyword_len = strlen(keyword); + + if (0 != strncmp(line, keyword, keyword_len) || line[keyword_len] != ' ') + return -EAGAIN; + line += keyword_len + 1; + if ((val2 && 2 == sscanf(line, "%u %u", &v1, &v2)) || (1 == sscanf(line, "%u", &v1))) { + *val1 = HZ / v1; + if (val2) + *val2 = HZ / v2; + return 0; + } + return -EINVAL; +} + +/* + * Handles writes to /proc/ for altering key/macro mappings on the fly, + * and also for injecting keycodes from scripts etc. + */ +static int sapphire_oldproc_write(struct file *file, const char __user *ubuf, unsigned long count, void *data) +{ + static const char SENDKEY[7] = "SENDKEY"; + static const char REPEAT_RATE[] = "REPEAT_RATE"; // Original backward-compatible name for RAMP_RATE + static const char RAMP_RATE[] = "RAMP_RATE"; + static const char SLOW_RATE[] = "SLOW_RATE"; + static const char MED_RATE[] = "MED_RATE"; + static const char FAST_RATE[] = "FAST_RATE"; + unsigned int len = count, val, ret = 0; + u8 *buf = kmalloc(len + 2, GFP_KERNEL), *line, *eol; + struct sapphire_map *map; + + if (!buf) + return -ENOMEM; + if (copy_from_user(buf, ubuf, len)) { + kfree(buf); + return -EFAULT; + } + if (len == 0 || buf[len - 1] != '\n') + buf[len++] = '\n'; + buf[len++] = '\0'; + for (line = buf; line[0]; ) { + int sendkey = 0; + for (eol = line; *eol != '\n'; ++eol); + *eol = '\0'; + if (0 == strncmp(line, SENDKEY, sizeof(SENDKEY))) { + line += sizeof(SENDKEY); + sendkey = 1; + } else { + ret = sapphire_get_HZ_vals(line, REPEAT_RATE, &ramp_adj, &ramp_max); + if (ret == -EAGAIN) + ret = sapphire_get_HZ_vals(line, RAMP_RATE, &ramp_adj, &ramp_max); + if (ret == -EAGAIN) + ret = sapphire_get_HZ_vals(line, SLOW_RATE, &slow_rate, NULL); + if (ret == -EAGAIN) + ret = sapphire_get_HZ_vals(line, MED_RATE, &med_rate, NULL); + if (ret == -EAGAIN) + ret = sapphire_get_HZ_vals(line, FAST_RATE, &fast_rate, NULL); + if (ret != -EAGAIN) { + if (ret == 0) + goto next_line; + break; + } + } + if (1 != sscanf(line, "%x", &val)) { + ret = -EINVAL; + break; + } + if (sendkey) { + unsigned long flags; + spin_lock_irqsave(&dev->lock, flags); + sapphire_send_key(val, NO_REPEAT); + spin_unlock_irqrestore(&dev->lock, flags); + } else { + ret = sapphire_store_macro(line); + if (ret) { + ret = -EINVAL; + for (map = sapphire_keymap; map != (sapphire_keymap + SAPPHIRE_KEYMAP_SIZE); ++map) { + if (!map->button || map->button == val) { + ret = sapphire_store_keymap(map, line); + break; + } + } + if (ret) + break; + } + } +next_line: + line = eol + 1; + } + if (ret && line) + printk(KERN_ERR "%s: parse error: \"%s\"\n", __func__, line); + kfree(buf); + return ret ? ret : count; +} + +#if (LINUX_VERSION_CODE < KERNEL_VERSION(3,10,0)) + +static void sapphire_create_proc_entry (void) +{ + static struct proc_dir_entry *entry; + + entry = create_proc_entry(SAPPHIRE_NAME, S_IFREG|S_IRUGO|S_IWUGO, NULL); + if (entry) { + entry->write_proc = sapphire_oldproc_write; + entry->read_proc = sapphire_oldproc_read; + } +} + +#else /* LINUX_VERSION_CODE */ + +static ssize_t sapphire_newproc_write (struct file *file, const char __user *buffer, size_t count, loff_t *ppos) +{ + return sapphire_oldproc_write(file, buffer, count, PDE_DATA(file_inode(file))); +} + +static ssize_t sapphire_newproc_read (struct file *file, char __user *buffer, size_t count, loff_t *ppos) +{ + unsigned char *kbuf = (void *)__get_free_page(GFP_KERNEL); + ssize_t len; + int eof = 0; + + if (!kbuf) + return -ENOMEM; + len = sapphire_oldproc_read(kbuf, NULL, 0, PAGE_SIZE - 1, &eof, PDE_DATA(file_inode(file))); + len = simple_read_from_buffer(buffer, count, ppos, kbuf, len); + free_page((long)kbuf); + return len; +} + +#ifdef PROC_OPS +static const struct proc_ops sapphire_newproc_fops = { + .proc_read = sapphire_newproc_read, + .proc_write = sapphire_newproc_write, + .proc_lseek = default_llseek, +}; +#else +static const struct file_operations sapphire_newproc_fops = { + .read = sapphire_newproc_read, + .write = sapphire_newproc_write, + .llseek = default_llseek, +}; +#endif + +static void sapphire_create_proc_entry (void) +{ + static struct proc_dir_entry *entry; + entry = proc_create_data(SAPPHIRE_NAME, S_IFREG|S_IRUGO|S_IWUGO, NULL, &sapphire_newproc_fops, dev); +} + +#endif /* LINUX_VERSION_CODE */ + +/* + * Invoked by the kernel each time one of our IR receivers is plugged into a USB port. + * We can/do handle multiple receivers, but there's no point to ever have more than one, + * because the receiver hardware is not keyed to a specific transmitter (remote control). + * + * If buttons are pressed before probe is run, the driver sometimes receives + * a single PRESS event at startup, with no corresponding RELEASE event. + * This is bad. We prevent the stray presses by ignoring all input from the device + * for one second after probe, and then set ready=1 to enable normal processing. + */ +static int sapphire_probe(struct hid_device *hdev, const struct hid_device_id *id) +{ + int ret = hid_parse(hdev); +printk(KERN_INFO "%s: hdev=%p id=%p\n", __func__, hdev, id); + if (ret) + return ret; + ret = hid_hw_start(hdev, HID_CONNECT_DEFAULT); + if (ret) { + printk(KERN_INFO SAPPHIRE_NAME ": hw_start failed, err=%d\n", ret); + } else { + /* Eat spurious data from the device for one second after connect */ + unsigned long flags; + spin_lock_irqsave(&dev->lock, flags); + sapphire_event(0); /* clear current downkey state */ + dev->ready = 0; /* ignore input until the timer fires */ + sapphire_set_timer(HZ); + spin_unlock_irqrestore(&dev->lock, flags); + } + return ret; +} + +/* + * Invoked by the kernel each time one of our IR receivers is unplugged from a USB port. + */ +static void sapphire_remove(struct hid_device *hdev) +{ +printk(KERN_INFO "%s: hdev=%p\n", __func__, hdev); + /* + * We are supposed to do hid_hw_stop() here. + * But some Ubuntu kernels (eg. 3.11.x, 3.2.x) totally kill the device + * when doing this, such that even hid_hw_start() won't revive it. + * Other kernels handle it much better, but picking and choosing + * can be very problematic. So.. + */ + hid_hw_stop(hdev); +} + +#ifdef CONFIG_PM +/* + * The IR receiver becomes non-responsive when subjected to USB autosuspend. + * This is disabled by default at probe() time, but system scripts + * (particularly on suspend/resume) may modify the setting, + * and the result is that the IR receiver could still suffer autosuspend. + * + * So.. when the system does try it, by calling sapphire_suspend() below, + * we reject the attempt (-EBUSY) and also reset the autosuspend setting + * back to "on" again. Unfortunately, this cannot be done directly + * from sapphire_suspend() due to spinlock recursion in the PM code, + * so we schedule a worker to do it from a less-encumbered state. + */ +struct sapphire_pm_work { + struct work_struct work; + struct usb_device *udev; +}; + +static void sapphire_disable_usb_autosuspend(struct work_struct *work) +{ + struct sapphire_pm_work *w = container_of(work, struct sapphire_pm_work, work); + + usb_disable_autosuspend(w->udev); + kfree(w); + printk(KERN_INFO SAPPHIRE_NAME ": disabled USB autosuspend\n"); +} + +static void sapphire_schedule_pm_work(struct hid_device *hdev) +{ + struct sapphire_pm_work *w = kmalloc(sizeof(struct sapphire_pm_work), GFP_ATOMIC); + if (w) { + w->udev = container_of(hdev->dev.parent->parent, struct usb_device, dev); + INIT_WORK(&w->work, sapphire_disable_usb_autosuspend); + schedule_work(&w->work); + } +} + +static int sapphire_suspend(struct hid_device *hdev, pm_message_t message) +{ + if (PMSG_IS_AUTO(message)) { + sapphire_schedule_pm_work(hdev); /* prevent future attempts */ + return -EBUSY; /* prevent _this_ USB autosuspend attempt */ + } + return 0; +} + +static int sapphire_resume(struct hid_device *hdev) +{ + return 0; +} + +#endif + +/* + * Structures used to associate this driver with specific USB device types. + */ +static const struct hid_device_id sapphire_devices[] = { + {HID_USB_DEVICE(0x0766,0x0204)}, + {} +}; +MODULE_DEVICE_TABLE(hid, sapphire_devices); +static struct hid_driver sapphire_driver = { + .name = SAPPHIRE_NAME, + .id_table = sapphire_devices, + .raw_event = sapphire_raw_event, + .probe = sapphire_probe, + .remove = sapphire_remove, +#ifdef CONFIG_PM + .suspend = sapphire_suspend, + .resume = sapphire_resume, +#endif +}; + +/* + * We don't use the input_dev provided by the hid layer, + * because (1) it provides two of them (mouse & keyboard), + * and (2) we need an input_dev even if no receiver is plugged in. + * Here we allocate a single global input_dev for use by the driver. + */ +static int sapphire_create (void) +{ + int keycode, err; + + memset(dev, 0, sizeof(*dev)); /* paranoia */ + spin_lock_init(&dev->lock); + sapphire_timer_setup(&dev->key_timer, sapphire_key_timeout); + + dev->idev = input_allocate_device(); + if (!dev->idev) { + printk(KERN_ERR "%s: input_allocate_device() failed\n", __func__); + return -ENOMEM; + } + dev->idev->name = SAPPHIRE_NAME; + dev->idev->phys = "none"; + dev->idev->evbit[0] = BIT_MASK(EV_KEY); + for (keycode = 1; keycode < KEY_MAX; ++keycode) + __set_bit(keycode, dev->idev->keybit); + err = input_register_device(dev->idev); + if (err) { + printk(KERN_ERR "%s: input_register_device() failed, ret=%d\n", __func__, err); + input_free_device(dev->idev); + dev->idev = NULL; + return err; + } + return 0; +} + +/* + * This is the inverse of sapphire_create_inputdev(). + */ +static void sapphire_destroy (void) +{ + del_timer_sync(&dev->key_timer); + input_unregister_device(dev->idev); + input_free_device(dev->idev); +} + +/* + * Invoked by the kernel when this module is loaded. + */ +static int __init sapphire_init(void) +{ + int ret; + + printk(KERN_INFO "%s: " SAPPHIRE_NAME " remote control driver v%s\n", __func__, SAPPHIRE_VERSION); + ret = sapphire_create(); + if (ret) + return ret; + dev->last_event = get_jiffies_64(); + ret = hid_register_driver(&sapphire_driver); + if (ret) + sapphire_destroy(); + else + sapphire_create_proc_entry(); + return ret; +} + +/* + * Invoked by the kernel when this module is removed/unloaded. + */ +static void __exit sapphire_exit(void) +{ + remove_proc_entry(SAPPHIRE_NAME, NULL); + hid_unregister_driver(&sapphire_driver); + sapphire_destroy(); +} + +module_init(sapphire_init); +module_exit(sapphire_exit); +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Mark Lord"); diff --git a/sapphire.h b/sapphire.h new file mode 100644 index 0000000..cd23f68 --- /dev/null +++ b/sapphire.h @@ -0,0 +1,109 @@ +/* + * sapphire.h + * + * Copyright Mark Lord , 2012-2020. + * http://rtr.ca/sapphire_remote/ + * + * Button definitions shared with sapphire_keymap.sh + * and other drivers which may use sapphire_relay(). + * + * 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; specifically version 2 of the License (GPLv2). + */ + +/* + * These enum's are shared with external keymaps and the sapphire_keymap.sh script: + */ +enum { /* + * BEGIN_PARSING (marker used by script that extracts these for sapphire_keymap.sh) + * + * Raw button values from the Sapphire remote: + * + */ + SAPPHIRE_UP = 0x00520000 , /* UP */ + SAPPHIRE_DOWN = 0x00510000 , /* DOWN */ + SAPPHIRE_RIGHT = 0x004f0000 , /* RIGHT */ + SAPPHIRE_LEFT = 0x00500000 , /* LEFT */ + SAPPHIRE_ENTEROK = 0x00280000 , /* ENTER/OK */ + SAPPHIRE_BACK = 0x00040003 , /* BACK */ + SAPPHIRE_PLAY = 0x40000003 , /* PLAY */ + SAPPHIRE_PAUSE = 0x80000003 , /* PAUSE */ + SAPPHIRE_VOLUP = 0x00002003 , /* VOL+ */ + SAPPHIRE_VOLDOWN = 0x00004003 , /* VOL- */ + SAPPHIRE_CHUP = 0x00010003 , /* CH/PG+ */ + SAPPHIRE_CHDOWN = 0x00020003 , /* CH/PG- */ + SAPPHIRE_MUTE = 0x00001003 , /* MUTE */ + SAPPHIRE_RECORD = 0x00008003 , /* RECORD */ + SAPPHIRE_FWD = 0x08000003 , /* FWD */ + SAPPHIRE_REW = 0x04000003 , /* REW */ + SAPPHIRE_ANGLE = 0x00010004 , /* ANGLE */ + SAPPHIRE_SAP = 0x00100004 , /* SAP */ + SAPPHIRE_DVDMENU = 0x00040004 , /* DVDMENU */ + SAPPHIRE_INFOEPG = 0x02000003 , /* INFO/EPG */ + SAPPHIRE_TAB = 0x00000103 , /* TAB */ + SAPPHIRE_BACKTAB = 0x00000203 , /* BACKTAB */ + SAPPHIRE_RADIO = 0x00000403 , /* RADIO */ + SAPPHIRE_LASTCH = 0x00400004 , /* LASTCH */ + SAPPHIRE_LANGUAGE = 0x00020004 , /* LANGUAGE */ + SAPPHIRE_TELETEXTCC = 0x00200004 , /* TELETEXT/CC */ + SAPPHIRE_SUBTITLE = 0x00080004 , /* SUBTITLE */ + SAPPHIRE_HOMEHOUSE = 0x00000104 , /* HOME (house) */ + SAPPHIRE_BLUEVIDEOS = 0x00002004 , /* BLUE/VIDEOS */ + SAPPHIRE_LIVETV = 0x00000204 , /* LIVETV */ + SAPPHIRE_REDDVDVCD = 0x00008004 , /* RED/DVD/VCD */ + SAPPHIRE_YELLOWPICTURES = 0x00001004 , /* YELLOW/PICTURES*/ + SAPPHIRE_1 = 0x001e0000 , /* 1 */ + SAPPHIRE_2 = 0x001f0000 , /* 2 */ + SAPPHIRE_3 = 0x00200000 , /* 3 */ + SAPPHIRE_4 = 0x00210000 , /* 4 */ + SAPPHIRE_5 = 0x00220000 , /* 5 */ + SAPPHIRE_6 = 0x00230000 , /* 6 */ + SAPPHIRE_7 = 0x00240000 , /* 7 */ + SAPPHIRE_8 = 0x00250000 , /* 8 */ + SAPPHIRE_9 = 0x00260000 , /* 9 */ + SAPPHIRE_0 = 0x00270000 , /* 0 */ + SAPPHIRE_STOP = 0x00100003 , /* STOP */ + SAPPHIRE_POWER = 0x00000202 , /* POWER */ + SAPPHIRE_CLEAR = 0x004c0000 , /* CLEAR */ + SAPPHIRE_GREENMUSIC = 0x00000804 , /* GREEN/MUSIC */ + + /* + * Modifier buttons: "OR" these with "regular" KEY_ values as desired: + */ + CTRL = 0x80000000 , + SHIFT = 0x40000000 , + ALT = 0x20000000 , + META = 0x10000000 , + + /* + * Special "macro" keys: + */ + KEY_DELAY = 0x00fffff7 , + KEY_MACRO_0 = 0x00fffff8 , + KEY_MACRO_1 = 0x00fffff9 , + KEY_MACRO_2 = 0x00fffffa , + KEY_MACRO_3 = 0x00fffffb , + KEY_MACRO_4 = 0x00fffffc , + KEY_MACRO_5 = 0x00fffffd , + KEY_MACRO_6 = 0x00fffffe , + KEY_MACRO_7 = 0x00ffffff , + + /* + * Per-button automatic repeat rates: + */ + NO_REPEAT = 0x00000000 , /* no autorepeat */ + SLOW_REPEAT = 0x00000004 , /* repeats slowly (default is 4/sec) */ + MED_REPEAT = 0x00000002 , /* repeats slowly (default is 6/sec) */ + FAST_REPEAT = 0x00000008 , /* repeats quickly (default is 8/sec) */ + RAMP_REPEAT = 0x00000003 , /* repeat rate increases as button is held, up to 10/sec */ + RAWKEY = 0xffffffff , /* do not send "release" event until button is really released */ + LONGKEY = 0x08000000 , /* no autorepeat; short/long presses send different codes */ +}; + +/* + * This is a hook for external modules to tie into, + * allowing other remote-drivers to use the sapphire + * driver's translations and advanced features. + */ +extern void sapphire_relay (u32 data); diff --git a/sapphire_keymap.sh.part1 b/sapphire_keymap.sh.part1 new file mode 100644 index 0000000..e594104 --- /dev/null +++ b/sapphire_keymap.sh.part1 @@ -0,0 +1,20 @@ +#!/bin/bash +# +# sapphire_keymap.sh by Mark Lord , April/2013. +# http://rtr.ca/sapphire_remote/ +# +# Script to translate a symbolic keymap file for the Sapphire driver +# into hex format and feed the results to /proc/sapphire. +# +# This can be invoked on the fly, as often as desired. +# Eg. to change keymaps to match a specific program, such as mythfrontend or xbmc. +# +# By default, this script reads key mappings from /etc/sapphire.keymap +# but this can be overridden by supplying an alternative path/file as a parameter. +# +# Eg. sapphire_keymap.sh ~/.sapphire_keymap +# + +## Arrays for Buttons, Keycodes, and Modifiers: +##' +declare -A K B M diff --git a/sapphire_keymap.sh.part3 b/sapphire_keymap.sh.part3 new file mode 100644 index 0000000..7385692 --- /dev/null +++ b/sapphire_keymap.sh.part3 @@ -0,0 +1,103 @@ + +PROCFILE=/proc/sapphire +MYNAME="${0##*/}" + +function get_multipart_keycode(){ + local parts="$1" val=0 zero_ok=0 p v + while [ "$parts" != "" ]; do + p="${parts%%|*}" + [ "$p" = "$parts" ] && parts="" || parts="${parts#*|}" + v="${K[$p]}" + if [ "$v" = "" ]; then + v="${M[$p]}" + if [ "$v" = "" ]; then + logger -s "$MYNAME: ERROR0: $p: unrecognized key/modifier" + exit 1 + fi + fi + [ $((v)) -eq 0 ] && zero_ok=1 || val=$((val + v)) + done + [ $val -ne 0 -o $zero_ok -eq 1 ] && printf "0x%08x" $val +} + +function parse_keymap(){ + local line blabel bval klabel kval olabel oval out i + while read line ; do + line="${line%%#*}" + set -- $line + [ "$1" = "" ] && continue + if [ "$1" = "REPEAT_RATE" -a "$3" != "" ]; then + out="$1 $2 $3" + elif [ "$1" = "SLOW_RATE" -a "$2" != "" ]; then + out="$1 $2" + elif [ "$1" = "MED_RATE" -a "$2" != "" ]; then + out="$1 $2" + elif [ "$1" = "FAST_RATE" -a "$2" != "" ]; then + out="$1 $2" + else + blabel="$1" + bval="${B[$blabel]}" + if [ "$bval" = "" ]; then + logger -s "$MYNAME: ERROR1: $line blabel=$blabel" + exit 1 + fi + out= + if [ "${blabel:0:10}" = "KEY_MACRO_" ]; then + out="$bval" + olabel="" + for i in {2..8} ; do + klabel="${!i}" + [ "$klabel" = "" ] && break + olabel="$olabel $klabel" + kval=$(get_multipart_keycode "$klabel") + if [ "$kval" = "" ]; then + logger -s "$MYNAME: ERROR2: $line" + exit 1 + fi + out="$out $kval" + done + klabel="" + else + klabel="$2" + olabel="$3" + if [ "$blabel" = "" -o "$klabel" = "" -o "$olabel" = "" ]; then + logger -s "$MYNAME: ERROR3: $line" + exit 1 + fi + kval=$(get_multipart_keycode "$klabel") + oval=$(get_multipart_keycode "$olabel") + if [ "$bval" = "" -o "$kval" = "" -o "$oval" = "" ]; then + logger -s "$MYNAME: ERROR4: $blabel=$bval $klabel=$kval $olabel=$oval" + exit 1 + fi + out="$bval $kval $oval" + fi + fi + [ $VERBOSE -gt 0 ] && echo "echo \"$out\" >$PROCFILE # $blabel $klabel $olabel" >&2 + echo "$out" >$PROCFILE || exit 2 + out= + done +} + +if [ ! -e $PROCFILE ]; then + echo "$PROCFILE: not found (is driver loaded?)" + exit 1 +fi + +VERBOSE=0 +while [ "$1" = "-v" -o "$1" = "--verbose" ]; do + VERBOSE=$((VERBOSE + 1)) + shift +done + +KEYMAP="$1" +[ "$KEYMAP" = "" ] && KEYMAP=/etc/sapphire.keymap +if [ ! -e "$KEYMAP" ]; then + logger -s "$MYNAME: $KEYMAP: not readable" + exit 1 +fi + +type -all logger &>/dev/null && logcmd="logger -s --" || logcmd=echo +$logcmd "${0##*/}: sending \"$KEYMAP\" to $PROCFILE" +cat "$KEYMAP" | parse_keymap + diff --git a/sapphire_startup.sh b/sapphire_startup.sh new file mode 100755 index 0000000..d8cc599 --- /dev/null +++ b/sapphire_startup.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# +# Some distros seem to be building the hid-topseed driver into +# the core kernel image, rather than leaving as a loadable module. +# +# So, for our sapphire driver to bind to a device, +# we first have to get the hid-topseed driver to unbind from it. +# +TOPSEED=/sys/bus/hid/drivers/topseed +SAPPHIRE=/sys/bus/hid/drivers/sapphire +#rmmod hid-topseed &>/dev/null +modprobe sapphire &>/dev/null + +# Transfer control of devices bound to hid-topseed over to sapphire +if [ -d $SAPPHIRE -a -e $TOPSEED/unbind ]; then + cd $TOPSEED + for dev in [0-9]*[-0-9A-F] ; do + if [ -e "$dev" ]; then + echo "$dev" > unbind + echo "$dev" > $SAPPHIRE/bind + fi + done +fi +rmmod hid-topseed &>/dev/null + +# +# Ubuntu/Mint kernels (and likely others too) don't like it +# when we unload and reload the sapphire driver. +# They disable the USB IR receiver and fail to reenable it. +# The workaround below seems to restore functionality. +# +cd $SAPPHIRE || exit 1 +for dev in [0-9]*[0-9A-F] ; do + if [ -e "$dev" ]; then + if cd -P "$dev/../.." ; then + if [ -e authorized ]; then + echo 0 > authorized + echo 1 > authorized + fi + cd - >/dev/null + fi + fi +done +[ -e /etc/sapphire.keymap -a -x /usr/local/bin/sapphire_keymap.sh ] && /usr/local/bin/sapphire_keymap.sh +exit 0