From 3b79b73b072cf8b3cd1fb3d56eb373f56a9b9405 Mon Sep 17 00:00:00 2001
From: Andrew Ruthven <andrew@etc.gen.nz>
Date: Sun, 1 Dec 2024 14:42:11 +1300
Subject: [PATCH] New upstream version 7.5

---
 Makefile                 |  91 ++++
 README.txt               | 450 +++++++++++++++++++
 extract_keydefs.sh       |  31 ++
 keymap.default           | 108 +++++
 sapphire.c               | 905 +++++++++++++++++++++++++++++++++++++++
 sapphire.h               | 109 +++++
 sapphire_keymap.sh.part1 |  20 +
 sapphire_keymap.sh.part3 | 103 +++++
 sapphire_startup.sh      |  45 ++
 9 files changed, 1862 insertions(+)
 create mode 100644 Makefile
 create mode 100644 README.txt
 create mode 100755 extract_keydefs.sh
 create mode 100644 keymap.default
 create mode 100644 sapphire.c
 create mode 100644 sapphire.h
 create mode 100644 sapphire_keymap.sh.part1
 create mode 100644 sapphire_keymap.sh.part3
 create mode 100755 sapphire_startup.sh

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 <mlord@pobox.com>, 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 <mlord@pobox.com>, 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 <linux/version.h>
+#include <linux/device.h>
+#include <linux/hid.h>
+#include <linux/module.h>
+#include <linux/timer.h>
+#include <linux/list.h>
+#include <linux/proc_fs.h>
+#include <linux/uaccess.h>
+#include <linux/jiffies.h>
+#include <linux/pm_runtime.h>
+#include <linux/usb.h>
+#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", &macro,
+			&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 <mlord@pobox.com>, 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 <mlord@pobox.com>, 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
-- 
2.30.2