Add power on/off, keyboard modifier, more key mappings
[clinton/arduino-kodi-ir-keyboard.git] / src / IR_USB_Keyboard / IR_USB_Keyboard.ino
1 /*
2 The MIT License (MIT)
3
4 Copyright (c) 2016 Markus Zehnder
5
6 Permission is hereby granted, free of charge, to any person obtaining a copy
7 of this software and associated documentation files (the "Software"), to deal
8 in the Software without restriction, including without limitation the rights
9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 copies of the Software, and to permit persons to whom the Software is
11 furnished to do so, subject to the following conditions:
12
13 The above copyright notice and this permission notice shall be included in all
14 copies or substantial portions of the Software.
15
16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 SOFTWARE.
23 */
24
25 /* IR to USB keyboard optimized for KODI openELEC running on Asus Chromebox.
26 * IR device: Logitech Harmony 880 with remote profile: 'Panasonic TV TX-43CXW754'
27 *
28 * Tested with:
29 * - Arduino Leonardo & SparkFun Pro Micro, ATmega32u4 (5V, 16MHz)
30 * - Arduino IDE 1.6.7
31 * - HID Project 2.4.3
32 * - IRLremote 1.7.4
33 *
34 * ISSUES:
35 * - IRLremote doesn't work with Logitech Plex Remote profile. Now (ab)using Panasonic TV TX-43CXW754 device template.
36 * - Watch dog reset doesn't work, according to Google this is most likely an Arduino bootloader bug.
37 * - IRLremote doesn't work with MattairTech MT-DB-U4 1.6.9-mt1
38 */
39 // --------GLOBAL FLAGS ---------------
40 // enable debugging output over usb
41 #define DEBUG_SKETCH
42 //#define WATCHDOG
43
44 // --------INCLUDES ---------------
45
46 #ifdef WATCHDOG
47 #include <avr/wdt.h>
48 #endif
49 #include <HID-Project.h>
50 #include <IRLremote.h>
51 #include "Debug.h"
52 // either include Panasonic or Sony definitions
53 #include "Panasonic.h"
54 //#include "Sony.h"
55
56 // --------CONSTANTS ---------------
57
58 // pin assignments
59 #define RECV_PIN 7
60 #define POWER_SENSE_PIN 8
61 #define POWER_BTN_PIN 9
62
63 // min 500ms hold time seems to be required (< 500ms was too short for power on)
64 #define POWER_BTN_HOLD_TIME 700
65
66 struct CodeMap {
67 uint32_t irCommand;
68 uint8_t modifier;
69 KeyboardKeycode keyCode;
70 };
71
72 // Key press time must be slightly higher then the repeat IR code duration.
73 // My Logitech resends the first repeat after 40ms and afterwards every 110ms
74 const int KEY_PRESS_TIME = 150;
75
76 const int LOOP_DELAY = KEY_PRESS_TIME / 3;
77
78 // IR code to key mapping
79 const uint8_t KEY_CTRL = 1;
80 const uint8_t KEY_ALT = 2;
81 const uint8_t KEY_SHIFT = 4;
82 const uint8_t KEY_GUI = 8;
83
84 // Kodi keyboard controls: http://kodi.wiki/view/Keyboard_controls
85 const CodeMap irToKeyMap[] = {
86 {REMOTE_LEFT , 0, KEY_LEFT_ARROW},
87 {REMOTE_RIGHT , 0, KEY_RIGHT_ARROW},
88 {REMOTE_UP , 0, KEY_UP_ARROW},
89 {REMOTE_DOWN , 0, KEY_DOWN_ARROW},
90 {REMOTE_OK , 0, KEY_RETURN},
91 {REMOTE_ENTER , 0, KEY_TAB}, // Fullscreen playback
92 {REMOTE_MENU , 0, KEY_C},
93 {REMOTE_CLEAR , 0, KEY_BACKSPACE},
94 {REMOTE_EXIT , 0, KEY_ESC},
95 {REMOTE_GUIDE , 0, KEY_E},
96 {REMOTE_INFO , 0, KEY_I},
97 {REMOTE_STOP , 0, KEY_X},
98 {REMOTE_PLAY , 0, KEY_P},
99 {REMOTE_PAUSE , 0, KEY_SPACE},
100 {REMOTE_REC , 0, KEY_B},
101 {REMOTE_REW , 0, KEY_R},
102 {REMOTE_FWD , 0, KEY_F},
103 {REMOTE_PREV , 0, KEY_QUOTE}, // FIXME doesn't seem to work with non-us keyboard layout
104 {REMOTE_SKIP , 0, KEY_PERIOD},
105 {REMOTE_REPLAY , 0, KEY_COMMA},
106 {REMOTE_SUBTITLE, 0, KEY_T}, // toggle subtitles
107 {REMOTE_BLUE , 0, KEY_O}, // Codec Info
108 {REMOTE_RED , 0, KEY_W}, // Marked as watched / unwatched
109 {REMOTE_GREEN , 0, KEY_S}, // shutdown / suspend / reboot menu
110 {REMOTE_YELLOW , 0, KEY_DELETE},
111 {REMOTE_1 , 0, KEY_1},
112 {REMOTE_2 , 0, KEY_2},
113 {REMOTE_3 , 0, KEY_3},
114 {REMOTE_4 , 0, KEY_4},
115 {REMOTE_5 , 0, KEY_5},
116 {REMOTE_6 , 0, KEY_6},
117 {REMOTE_7 , 0, KEY_7},
118 {REMOTE_8 , 0, KEY_8},
119 {REMOTE_9 , 0, KEY_9},
120 {REMOTE_0 , 0, KEY_0},
121 {REMOTE_CH_UP , 0, KEY_PAGE_UP}, // PgUp / Skip to next queued video or next chapter if no videos are queued. / Increase Rating
122 {REMOTE_CH_DOWN , 0, KEY_PAGE_DOWN}, // PgDown / Skip to previous queued video or previous chapter if no videos are queued. / Decrease Rating
123 {REMOTE_ASPECT , 0, KEY_Z}, // Zoom/aspect ratio
124 {REMOTE_MUTE , 0, KEY_VOLUME_MUTE},
125 {REMOTE_VOL_UP , 0, KEY_VOLUME_UP},
126 {REMOTE_VOL_DOWN, 0, KEY_VOLUME_DOWN},
127 {REMOTE_F1 , 0, KEY_A}, // Audio delay control
128 {REMOTE_F2 , 0, KEY_D}, // Move item down (Playlist editor & Favorites window)
129 {REMOTE_F3 , 0, KEY_U}, // Move item up (Playlist editor & Favorites window)
130 {REMOTE_F4 , 0, KEY_Q}, // Queue
131 {REMOTE_F5 , 0, KEY_V}, // Teletext / Visualisation settings
132 {REMOTE_F6 , 0, KEY_Y}, // Switch/choose player
133 {REMOTE_F7 , 0, KEY_HOME}, // Jump to the top of the menu
134 {REMOTE_REC , 0, KEY_PRINTSCREEN}, // Screenshot
135
136 {REMOTE_ARROW_DOWN, KEY_CTRL, KEY_DOWN_ARROW}, // Move subtitles down
137 {REMOTE_ARROW_UP , KEY_CTRL, KEY_UP_ARROW}, // Move subtitles up
138 {REMOTE_F8 , KEY_CTRL, KEY_D}, // boot: ChromeOS TODO test...
139 //{REMOTE_F9 , KEY_CTRL, KEY_W}, // boot: openELEC TODO test...
140 };
141
142 const int IR_KEY_MAP_SIZE = sizeof(irToKeyMap) / sizeof(CodeMap);
143
144 //------------ VARIABLES ---------------------
145
146 unsigned long lastIRValue = 0; // previously received IR code value
147 unsigned long timeKeyDown = 0; // time of key press initiation
148
149 // temporary variables to save latest IR input
150 uint8_t IRProtocol = 0;
151 uint16_t IRAddress = 0;
152 uint32_t IRCommand = 0;
153
154 //========================================
155
156 void setup() {
157 // open debug console
158 #ifdef DEBUG_SKETCH
159 delay(3000);
160 Serial.begin(115200); // afaik the baudrate is ignored on a 32u4
161 #endif
162 DEBUG_PRINTLN("Chromebox_REMOTE_USB_Keyboard");
163
164 pinMode(POWER_SENSE_PIN, INPUT);
165 pinMode(POWER_BTN_PIN, OUTPUT);
166
167 // initialize control over the keyboard:
168 BootKeyboard.begin();
169
170 // Start the receiver
171 attachInterrupt(digitalPinToInterrupt(RECV_PIN), IRLinterrupt<irType>, CHANGE);
172
173 #ifdef WATCHDOG
174 // enable watch dog
175 wdt_enable(WDTO_1S);
176 #endif
177 }
178
179 //========================================
180
181 void loop() {
182 #ifdef WATCHDOG
183 //Test if watchdog interrupt enabled
184 // http://forum.arduino.cc/index.php?topic=295345.msg2628807#msg2628807
185 if (WDTCSR & (1<<WDIE)) {
186 //Prolong watchdog timer
187 wdt_reset();
188 }
189 //No interrupt enabled - Test if watchdog reset enabled
190 else if (WDTCSR & (1<<WDE)) {
191 //Bootloader about to reset - do not prolong watchdog
192 }
193 //It has ben disabled - Enable and prolong
194 else {
195 //Watchdog disabled - Enable again
196 wdt_enable(WDTO_1S);
197 }
198 #endif
199
200 // temporary disable interrupts and print newest input
201 // TODO read up on interrup handling
202 uint8_t oldSREG = SREG;
203 cli();
204
205 if (IRProtocol) {
206 DEBUG_PRINT(IRProtocol);DEBUG_PRINT("/0x");DEBUG_PRINT(IRAddress, HEX);DEBUG_PRINT("/0x");DEBUG_PRINTLN(IRCommand, HEX);
207
208 if (IRProtocol == irType) {
209 // check if it's a NEC repeat code
210 if (IRCommand == 0xFFFF) {
211 IRCommand = lastIRValue;
212 } else {
213 lastIRValue = IRCommand;
214 }
215
216 if (IRCommand != lastIRValue) {
217 // immediately release last key if a different IR value is received. We don't want multiple keys pressed at the same time.
218 releaseKeys();
219 }
220
221 switch (IRCommand) {
222 // special commands
223 case REMOTE_POWER_TOGGLE :
224 DEBUG_PRINTLN("Power toggle");
225 pushPowerButton();
226 break;
227 case REMOTE_POWER_OFF :
228 DEBUG_PRINT("Power off: ");
229 if (digitalRead(POWER_SENSE_PIN) == HIGH) {
230 pushPowerButton();
231 } else {
232 DEBUG_PRINTLN("ignored, device already off");
233 }
234 break;
235 case REMOTE_POWER_ON :
236 DEBUG_PRINT("Power on: ");
237 if (digitalRead(POWER_SENSE_PIN) == LOW) {
238 pushPowerButton();
239 } else {
240 DEBUG_PRINTLN("ignored, device already on");
241 }
242 break;
243 // keyboard commands
244 default :
245 for (int i = 0; i < IR_KEY_MAP_SIZE; i++) {
246 if (irToKeyMap[i].irCommand == IRCommand) {
247 // only press key if not yet pressed
248 if (timeKeyDown == 0) {
249 KeyboardKeycode keyCode = irToKeyMap[i].keyCode;
250 DEBUG_PRINT(millis()); DEBUG_PRINT(" Press key: 0x"); DEBUG_PRINT(keyCode, HEX);
251
252 if (irToKeyMap[i].modifier & KEY_CTRL) {
253 BootKeyboard.press(KEY_LEFT_CTRL);
254 DEBUG_PRINT(" + CTRL ");
255 }
256 if (irToKeyMap[i].modifier & KEY_ALT) {
257 BootKeyboard.press(KEY_LEFT_ALT);
258 DEBUG_PRINT(" + ALT ");
259 }
260 if (irToKeyMap[i].modifier & KEY_SHIFT) {
261 BootKeyboard.press(KEY_LEFT_SHIFT);
262 DEBUG_PRINT(" + SHIFT ");
263 }
264 if (irToKeyMap[i].modifier & KEY_GUI) {
265 BootKeyboard.press(KEY_LEFT_GUI);
266 DEBUG_PRINT(" + GUI ");
267 }
268 DEBUG_PRINTLN();
269 BootKeyboard.press(keyCode);
270 }
271 timeKeyDown = millis();
272 break;
273 }
274 }
275 }
276 }
277
278 } else {
279 // check if it's time to release a previously pressed key
280 if (timeKeyDown > 0 && (millis() - timeKeyDown >= KEY_PRESS_TIME)) {
281 releaseKeys();
282 }
283 }
284
285 IRProtocol = 0;
286 SREG = oldSREG;
287
288 sei();
289
290 delay(LOOP_DELAY);
291 }
292
293 //========================================
294
295 void releaseKeys() {
296 timeKeyDown = 0;
297 BootKeyboard.releaseAll();
298 DEBUG_PRINT(millis()); DEBUG_PRINTLN(" Release keys");
299 }
300
301 //========================================
302
303 void pushPowerButton() {
304 digitalWrite(POWER_BTN_PIN, HIGH);
305 delay(POWER_BTN_HOLD_TIME);
306 digitalWrite(POWER_BTN_PIN, LOW);
307 DEBUG_PRINTLN("power button triggered");
308 }
309
310 //========================================
311
312 // IR decoding callback: update the values to the newest valid input
313 void IREvent(uint8_t protocol, uint16_t address, uint32_t command) {
314 // called when directly received a valid IR signal.
315 // do not use Serial inside, it can crash your program!
316
317 // dont update value if not yet processed
318 if (!IRProtocol) {
319 IRProtocol = protocol;
320 IRAddress = address;
321 IRCommand = command;
322 }
323 }