From e876652ffea4740f08006a54e4942ba0f0a548c4 Mon Sep 17 00:00:00 2001 From: Chris Veigl Date: Tue, 18 Aug 2026 18:18:44 +0200 Subject: [PATCH 1/6] added key / keycombo selection --- src/main.cpp | 50 +++++++++++++------------- ui/config.html | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 26 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 9db1418..dfc65a4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,20 +30,14 @@ bool sleepMode = false; // define ASCII-key action for each button #define NUM_BUTTONS 4 uint8_t button_map[NUM_BUTTONS] = {BUTTON1, BUTTON2, BUTTON3, BUTTON4}; -//default key map (index in key_codes) -uint8_t key_map[NUM_BUTTONS] = {0, 1, 2, 3}; -//overwrite the key_map (if set to >= 0), (index in key_codes) -int8_t key_code_map[NUM_BUTTONS] = {-1}; +// HID keycode for each button (USB HID Usage Tables: A=0x04, Space=0x2C, Enter=0x28, ...) +uint8_t button_keycodes[NUM_BUTTONS] = {HID_KEY_SPACE, HID_KEY_ENTER, HID_KEY_1, HID_KEY_2}; +// HID modifier byte per button (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08; OR for combinations) +uint8_t button_modifiers[NUM_BUTTONS] = {0, 0, 0, 0}; uint8_t buttonStates = 0; -//this firmware supports following keycodes in the settings (printHelp(), parseCommands() -> enums of keys) -//Space, Enter, 1, 2, Tab, F1, F2, F13, F14 -#define SELECTABLE_KEYS 9 -uint8_t key_codes[SELECTABLE_KEYS] = {HID_KEY_SPACE, HID_KEY_ENTER, HID_KEY_1, HID_KEY_2, HID_KEY_TAB, HID_KEY_F1, HID_KEY_F2, HID_KEY_F13, HID_KEY_F14}; - -// Multi-key state (up to 6 simultaneous keys + modifiers) +// Multi-key state (up to 6 simultaneous keys) static uint8_t active_keys[6] = {0}; -static uint8_t modifiers = 0; // e.g. KEYBOARD_MODIFIER_LEFT_SHIFT BLEDis bledis; BLEHidAdafruit blehid; @@ -288,8 +282,8 @@ void loop() #ifdef OUTPUT_ACTIVE if(i == 0) handleOutput(true,false); #endif - addActiveKey(key_codes[key_map[i]]); - blehid.keyboardReport(modifiers, active_keys); + addActiveKey(button_keycodes[i]); + { uint8_t mods = 0; for (uint8_t j = 0; j < NUM_BUTTONS; j++) if (buttonStates & (1 << j)) mods |= button_modifiers[j]; blehid.keyboardReport(mods, active_keys); } // if (ENABLE_ACTIVITY_LED ) dToggle(LED_R); if (ENABLE_DEBUG_OUTPUT) Serial.println("Button pressed"); } else if ( !pressed && (buttonStates & (1 << i)) ) { @@ -299,8 +293,8 @@ void loop() #ifdef OUTPUT_ACTIVE if(i == 0) handleOutput(false,true); #endif - removeActiveKey(key_codes[key_map[i]]); - blehid.keyboardReport(modifiers, active_keys); + removeActiveKey(button_keycodes[i]); + { uint8_t mods = 0; for (uint8_t j = 0; j < NUM_BUTTONS; j++) if (buttonStates & (1 << j)) mods |= button_modifiers[j]; blehid.keyboardReport(mods, active_keys); } //if (ENABLE_ACTIVITY_LED ) dToggle(LED_R); if (ENABLE_DEBUG_OUTPUT) Serial.println("Button released"); } @@ -363,11 +357,9 @@ bool storeSettings() { snprintf(buffer,MAX_PARAM_LEN,"i:%d\n",sleep_timeout_ms); file.write(buffer); - //save keycodes - //Note: currently only 2 buttons are saved, although it might be possible to - // configure all buttons, but in the UI only 2 are visible (for better overview) + //save keycodes (1-indexed to match serial command chars '1' and '2') for(int i = 0; i<2; i++) { - snprintf(buffer,MAX_PARAM_LEN,"%d:%d\n",i,key_map[i]); + snprintf(buffer,MAX_PARAM_LEN,"%d:%d:%d\n", i+1, button_keycodes[i], button_modifiers[i]); file.write(buffer); } @@ -439,13 +431,19 @@ void parseCommand(char *buf) { Serial.print("New: "); Serial.println(sleep_timeout_ms); break; - //handle button<->key code assignment + //handle button<->key code assignment (format: N::) case '1': case '2': - newValue = buf[0] - '1'; //get button index - Serial.print("Prev: "); Serial.println(key_map[newValue]); - key_map[newValue] = String(buf+2).toInt(); - Serial.print("New: "); Serial.println(key_map[newValue]); + { + uint8_t btnIdx = buf[0] - '1'; + char *sep = strchr(buf+2, ':'); + uint8_t newKeycode = (uint8_t)String(buf+2).toInt(); + uint8_t newModifier = sep ? (uint8_t)String(sep+1).toInt() : 0; + Serial.print("Prev: "); Serial.print(button_keycodes[btnIdx]); Serial.print(":"); Serial.println(button_modifiers[btnIdx]); + button_keycodes[btnIdx] = newKeycode; + button_modifiers[btnIdx] = newModifier; + Serial.print("New: "); Serial.print(button_keycodes[btnIdx]); Serial.print(":"); Serial.println(button_modifiers[btnIdx]); + } break; #ifdef OUTPUT_ACTIVE @@ -512,8 +510,8 @@ void printHelp() { Serial.print("i::Inactivity time [ms]:30000-600000:"); Serial.println(sleep_timeout_ms); Serial.print("d::Connected device::"); Serial.println(central_name); Serial.println("r::Reset paired devices"); - Serial.print("1::Key 1:Space,Enter,1,2,Tab,F1,F2,F13,F14:"); Serial.println(key_map[0]); - Serial.print("2::Key 2:Space,Enter,1,2,Tab,F1,F2,F13,F14:"); Serial.println(key_map[1]); + Serial.print("1::Key 1:"); Serial.print(button_keycodes[0]); Serial.print(":"); Serial.println(button_modifiers[0]); + Serial.print("2::Key 2:"); Serial.print(button_keycodes[1]); Serial.print(":"); Serial.println(button_modifiers[1]); #ifdef OUTPUT_ACTIVE Serial.println("c::Trigger the output"); diff --git a/ui/config.html b/ui/config.html index 93aa955..da1dcfe 100644 --- a/ui/config.html +++ b/ui/config.html @@ -250,6 +250,37 @@

Disconnected

btn.addEventListener("click", () => sendValue(p.cmd, el.selectedIndex)); break; + case "keycombo": { + el = document.createElement("input"); + el.type = "text"; + el.readOnly = true; + el.placeholder = "Click here, then press a key combination"; + el.style.cssText = "cursor:pointer;min-width:240px;"; + let kc = parseInt(p.extra) || 0; + let km = parseInt(p.current) || 0; + if (kc) el.value = formatKeyCombo(kc, km); + el.addEventListener("focus", () => { el.style.outline = "2px solid #0078d4"; }); + el.addEventListener("blur", () => { el.style.outline = ""; }); + el.addEventListener("keydown", (e) => { + e.preventDefault(); + if (["Control","Alt","Shift","Meta"].includes(e.key)) return; + const hid = browserCodeToHID(e.code); + if (hid === undefined) { el.value = "(unsupported: " + e.code + ")"; return; } + let mod = 0; + if (e.ctrlKey) mod |= 0x01; + if (e.shiftKey) mod |= 0x02; + if (e.altKey) mod |= 0x04; + if (e.metaKey) mod |= 0x08; + kc = hid; km = mod; + el.value = formatKeyCombo(hid, mod); + }); + btn.addEventListener("click", () => { + sendLine(p.cmd + ":" + kc + ":" + km); + statusEl.textContent += "Sent key combo: " + el.value + "\n"; + }); + break; + } + case "string": el = document.createElement("input"); el.type = "text"; @@ -289,6 +320,70 @@

Disconnected

await sendLine(`${cmd}:${value}`); statusEl.textContent += `Sent ${cmd}:${value}` + "\n"; } + +/* ================= HID KEY HELPERS ================= */ + +const HID_KEY_MAP = { + 'KeyA':{hid:0x04,name:'A'}, 'KeyB':{hid:0x05,name:'B'}, 'KeyC':{hid:0x06,name:'C'}, + 'KeyD':{hid:0x07,name:'D'}, 'KeyE':{hid:0x08,name:'E'}, 'KeyF':{hid:0x09,name:'F'}, + 'KeyG':{hid:0x0A,name:'G'}, 'KeyH':{hid:0x0B,name:'H'}, 'KeyI':{hid:0x0C,name:'I'}, + 'KeyJ':{hid:0x0D,name:'J'}, 'KeyK':{hid:0x0E,name:'K'}, 'KeyL':{hid:0x0F,name:'L'}, + 'KeyM':{hid:0x10,name:'M'}, 'KeyN':{hid:0x11,name:'N'}, 'KeyO':{hid:0x12,name:'O'}, + 'KeyP':{hid:0x13,name:'P'}, 'KeyQ':{hid:0x14,name:'Q'}, 'KeyR':{hid:0x15,name:'R'}, + 'KeyS':{hid:0x16,name:'S'}, 'KeyT':{hid:0x17,name:'T'}, 'KeyU':{hid:0x18,name:'U'}, + 'KeyV':{hid:0x19,name:'V'}, 'KeyW':{hid:0x1A,name:'W'}, 'KeyX':{hid:0x1B,name:'X'}, + 'KeyY':{hid:0x1C,name:'Y'}, 'KeyZ':{hid:0x1D,name:'Z'}, + 'Digit1':{hid:0x1E,name:'1'}, 'Digit2':{hid:0x1F,name:'2'}, 'Digit3':{hid:0x20,name:'3'}, + 'Digit4':{hid:0x21,name:'4'}, 'Digit5':{hid:0x22,name:'5'}, 'Digit6':{hid:0x23,name:'6'}, + 'Digit7':{hid:0x24,name:'7'}, 'Digit8':{hid:0x25,name:'8'}, 'Digit9':{hid:0x26,name:'9'}, + 'Digit0':{hid:0x27,name:'0'}, + 'Enter':{hid:0x28,name:'Enter'}, 'Escape':{hid:0x29,name:'Esc'}, 'Backspace':{hid:0x2A,name:'Backspace'}, + 'Tab':{hid:0x2B,name:'Tab'}, 'Space':{hid:0x2C,name:'Space'}, + 'Minus':{hid:0x2D,name:'-'}, 'Equal':{hid:0x2E,name:'='}, 'BracketLeft':{hid:0x2F,name:'['}, + 'BracketRight':{hid:0x30,name:']'}, 'Backslash':{hid:0x31,name:'\\'}, + 'Semicolon':{hid:0x33,name:';'}, 'Quote':{hid:0x34,name:"'"}, 'Backquote':{hid:0x35,name:'`'}, + 'Comma':{hid:0x36,name:','}, 'Period':{hid:0x37,name:'.'}, 'Slash':{hid:0x38,name:'/'}, + 'CapsLock':{hid:0x39,name:'CapsLock'}, + 'F1':{hid:0x3A,name:'F1'}, 'F2':{hid:0x3B,name:'F2'}, 'F3':{hid:0x3C,name:'F3'}, + 'F4':{hid:0x3D,name:'F4'}, 'F5':{hid:0x3E,name:'F5'}, 'F6':{hid:0x3F,name:'F6'}, + 'F7':{hid:0x40,name:'F7'}, 'F8':{hid:0x41,name:'F8'}, 'F9':{hid:0x42,name:'F9'}, + 'F10':{hid:0x43,name:'F10'}, 'F11':{hid:0x44,name:'F11'}, 'F12':{hid:0x45,name:'F12'}, + 'PrintScreen':{hid:0x46,name:'PrtSc'}, 'ScrollLock':{hid:0x47,name:'ScrLk'}, 'Pause':{hid:0x48,name:'Pause'}, + 'Insert':{hid:0x49,name:'Insert'}, 'Home':{hid:0x4A,name:'Home'}, 'PageUp':{hid:0x4B,name:'PgUp'}, + 'Delete':{hid:0x4C,name:'Delete'}, 'End':{hid:0x4D,name:'End'}, 'PageDown':{hid:0x4E,name:'PgDn'}, + 'ArrowRight':{hid:0x4F,name:'→'}, 'ArrowLeft':{hid:0x50,name:'←'}, + 'ArrowDown':{hid:0x51,name:'↓'}, 'ArrowUp':{hid:0x52,name:'↑'}, + 'NumLock':{hid:0x53,name:'NumLock'}, + 'NumpadDivide':{hid:0x54,name:'Num/'}, 'NumpadMultiply':{hid:0x55,name:'Num*'}, + 'NumpadSubtract':{hid:0x56,name:'Num-'}, 'NumpadAdd':{hid:0x57,name:'Num+'}, + 'NumpadEnter':{hid:0x58,name:'NumEnter'}, + 'Numpad1':{hid:0x59,name:'Num1'}, 'Numpad2':{hid:0x5A,name:'Num2'}, 'Numpad3':{hid:0x5B,name:'Num3'}, + 'Numpad4':{hid:0x5C,name:'Num4'}, 'Numpad5':{hid:0x5D,name:'Num5'}, 'Numpad6':{hid:0x5E,name:'Num6'}, + 'Numpad7':{hid:0x5F,name:'Num7'}, 'Numpad8':{hid:0x60,name:'Num8'}, 'Numpad9':{hid:0x61,name:'Num9'}, + 'Numpad0':{hid:0x62,name:'Num0'}, 'NumpadDecimal':{hid:0x63,name:'Num.'}, + 'F13':{hid:0x68,name:'F13'}, 'F14':{hid:0x69,name:'F14'}, 'F15':{hid:0x6A,name:'F15'}, + 'F16':{hid:0x6B,name:'F16'}, 'F17':{hid:0x6C,name:'F17'}, 'F18':{hid:0x6D,name:'F18'}, + 'F19':{hid:0x6E,name:'F19'}, 'F20':{hid:0x6F,name:'F20'}, 'F21':{hid:0x70,name:'F21'}, + 'F22':{hid:0x71,name:'F22'}, 'F23':{hid:0x72,name:'F23'}, 'F24':{hid:0x73,name:'F24'}, +}; + +const HID_CODE_NAMES = Object.fromEntries(Object.values(HID_KEY_MAP).map(v => [v.hid, v.name])); + +function browserCodeToHID(code) { return HID_KEY_MAP[code]?.hid; } + +function hidKeycodeToName(hid) { + return HID_CODE_NAMES[hid] || ('0x' + hid.toString(16).toUpperCase().padStart(2, '0')); +} + +function formatKeyCombo(keycode, modifier) { + if (!keycode) return "(none)"; + const mods = []; + if (modifier & 0x11) mods.push('Ctrl'); + if (modifier & 0x22) mods.push('Shift'); + if (modifier & 0x44) mods.push('Alt'); + if (modifier & 0x88) mods.push('Win'); + return [...mods, hidKeycodeToName(keycode)].join('+'); +} From 30fd9b24c2b26706a262941710352d58f1635fee Mon Sep 17 00:00:00 2001 From: Chris Veigl Date: Tue, 18 Aug 2026 18:30:23 +0200 Subject: [PATCH 2/6] added mode 4 (relais output disabled) --- src/main.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index dfc65a4..2fdae56 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -60,7 +60,7 @@ void printHelp(); //different modes for the output int mode = 0; // how many modes are used - #define MODE_MAX 3 + #define MODE_MAX 4 //pin for the mode switch button uint8_t pin_mode = PIN_MODE; //output mode: 0: click the output on each action; 1: toggle the output on each action @@ -472,7 +472,7 @@ void parseCommand(char *buf) { case 'm': Serial.print("Prev: "); Serial.println(mode+1); newValue = String(buf+2).toInt(); - if(newValue >= 1 && newValue <= 3) mode = newValue-1; + if(newValue >= 1 && newValue <= 4) mode = newValue-1; Serial.print("New: "); Serial.println(mode+1); break; case 'c': @@ -518,7 +518,7 @@ void printHelp() { Serial.println("o::Output mode:click,toggle"); Serial.print("t::Mode 1 - Tremor Timeout [ms]:300-5000:"); Serial.println(tremor_timeout_ms); Serial.print("p::Mode 3 - Auto-Pause Timeout [s]:2-600:"); Serial.println(pause_timeout_s); - Serial.print("m::Startup Mode:1-3:"); Serial.println(mode+1); + Serial.print("m::Startup Mode:1-4:"); Serial.println(mode+1); #endif Serial.println("?::Print out supported commands and build date"); //examples for more commands (+types) @@ -587,6 +587,10 @@ void handleOutput(bool pressed, bool released) { break; + //output disabled: ignore all button events + case 3: + break; + default: break; } } From 739c44c60d67cd9689498c663d08852967bf9886 Mon Sep 17 00:00:00 2001 From: Chris Veigl Date: Tue, 18 Aug 2026 18:30:37 +0200 Subject: [PATCH 3/6] updated readme --- README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 34d302c..7f1bf08 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,14 @@ Press __connect__ and select the BleenyButton serial interface (shown as somethi The UI will show you the possible options. +### Key combination assignment + +Key 1 and Key 2 support any key or key combination (e.g. `Alt+H`, `Ctrl+Shift+Z`, `F13`). In the web UI, click the **Key 1** or **Key 2** field and press the desired key (with any modifiers held down) — the field auto-detects and displays the combination. Click **Update** to send it to the device, then **Store new settings** to save. + +The serial command format is `1::` where both values are decimal USB HID codes (e.g. `1:11:4` sets Key 1 to `Alt+H`). Modifier bits: Ctrl=0x01, Shift=0x02, Alt=0x04, GUI/Win=0x08. + +> **Note:** Some browser/OS shortcuts (e.g. `Ctrl+W`, `Alt+F4`) are intercepted before reaching the web UI. Use the serial terminal for those combinations. + __Important:__ The settings are not saved automatically, you need to execute __Store new settings on the device__! ### Currently implemented settings @@ -105,8 +113,8 @@ __Important:__ The settings are not saved automatically, you need to execute __S | Inactivity time [ms] | i | Integer, 30000-600000 [ms] (30s - 10min) | Time before the BleenyButton enters the power-down mode. Will be reset by pressing the button or an established BLE connection | | Connected device | d | Info | This information heavily depends on the BLE stack of the host device, it is not always available, even if a device is connected! | | Reset paired devices | r | Action | Reset the BLE pairings | -| Key 1 | 1 | Selection: Space, Enter, 1, 2, Tab, F1, F2, F13, F14 | Select the keyboard key to be send when pressing the main button | -| Key 2 | 2 | Selection: Space, Enter, 1, 2, Tab, F1, F2, F13, F14 | If a second button is connected, this key will be sent. | +| Key 1 | 1 | Key combination (any key + optional modifiers Ctrl/Shift/Alt/GUI) | Select the keyboard key or key combination to send when pressing the main button. Use the web UI to auto-detect by pressing the desired key. | +| Key 2 | 2 | Key combination (any key + optional modifiers Ctrl/Shift/Alt/GUI) | If a second button is connected, this key combination will be sent. | | Print out supported commands and build date | ? | Action | Print out possible commands | The following commands are available when building with output enabled: @@ -117,7 +125,7 @@ The following commands are available when building with output enabled: | Output mode | o | Selection: click, toggle | Either click the output for 0.1s or toggle when the button is pressed (or command "Trigger output" is sent) | | Mode 1 - Tremor timeout | t | Integer, 300-5000 [ms] (0.3-5s) | Defines the delay before another output trigger can happen, used in Mode 1 | | Mode 3 - Auto click delay | p | Integer, 2-600 [s] | After the output is triggered, this time will pass until the output is triggered again. | -| Startup Mode | m | Integer, 1-3 | Select the output mode on startup. The mode can be changed with the second button on the bottom (PIN_MODE) | +| Startup Mode | m | Integer, 1-4 | Select the output mode on startup. The mode can be changed with the second button on the bottom (PIN_MODE) | ### Output mode description @@ -133,6 +141,10 @@ Button presses are directly mapped to the output. Note: each edge (press / relea A button press triggers the output. Afterwards, the button is locked for a given timeout, then another automatic trigger of the output happens. +#### Mode 4 - disabled + +The relay output is fully disabled. Button presses only send the configured BLE key — no relay switching occurs. + # Acknowledgement This work has been accomplished at the UAS Technikum Wien in course of the R&D-project [InDiKo](https://www.technikum-wien.at/en/research-projects/indiko/) (MA23 project 38-09), which is supported by the [City of Vienna](https://www.wien.gv.at/kontakte/ma23/index.html). From fecb9d6a8c157dfaf5f7a61d33e70ca9e273f099 Mon Sep 17 00:00:00 2001 From: Chris Veigl Date: Thu, 20 Aug 2026 01:00:38 +0200 Subject: [PATCH 4/6] add mouse action support --- src/main.cpp | 53 ++++++++++++++++++++++++++----------- ui/config.html | 72 +++++++++++++++++++++++++++++++++++++------------- 2 files changed, 92 insertions(+), 33 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 2fdae56..13b29c9 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,8 @@ uint8_t button_map[NUM_BUTTONS] = {BUTTON1, BUTTON2, BUTTON3, BUTTON4}; uint8_t button_keycodes[NUM_BUTTONS] = {HID_KEY_SPACE, HID_KEY_ENTER, HID_KEY_1, HID_KEY_2}; // HID modifier byte per button (Ctrl=0x01, Shift=0x02, Alt=0x04, GUI=0x08; OR for combinations) uint8_t button_modifiers[NUM_BUTTONS] = {0, 0, 0, 0}; +// Action type per button: 0=key press, 1=left click, 2=right click, 3=scroll up, 4=scroll down +uint8_t button_actions[NUM_BUTTONS] = {0, 0, 0, 0}; uint8_t buttonStates = 0; // Multi-key state (up to 6 simultaneous keys) @@ -52,7 +54,7 @@ void printHelp(); /******* output to 3.5mm jackplug ******/ //use output functions ('c' command) -//#define OUTPUT_ACTIVE +#define OUTPUT_ACTIVE #ifdef OUTPUT_ACTIVE //latching 1 coil relay on P0.02 (D18) & P0.29 (D20) @@ -282,8 +284,16 @@ void loop() #ifdef OUTPUT_ACTIVE if(i == 0) handleOutput(true,false); #endif - addActiveKey(button_keycodes[i]); - { uint8_t mods = 0; for (uint8_t j = 0; j < NUM_BUTTONS; j++) if (buttonStates & (1 << j)) mods |= button_modifiers[j]; blehid.keyboardReport(mods, active_keys); } + switch (button_actions[i]) { + case 0: // key press + addActiveKey(button_keycodes[i]); + { uint8_t mods = 0; for (uint8_t j = 0; j < NUM_BUTTONS; j++) if (buttonStates & (1 << j)) mods |= button_modifiers[j]; blehid.keyboardReport(mods, active_keys); } + break; + case 1: blehid.mouseButtonPress(MOUSE_BUTTON_LEFT); break; + case 2: blehid.mouseButtonPress(MOUSE_BUTTON_RIGHT); break; + case 3: blehid.mouseScroll( 1); break; + case 4: blehid.mouseScroll(-1); break; + } // if (ENABLE_ACTIVITY_LED ) dToggle(LED_R); if (ENABLE_DEBUG_OUTPUT) Serial.println("Button pressed"); } else if ( !pressed && (buttonStates & (1 << i)) ) { @@ -293,8 +303,16 @@ void loop() #ifdef OUTPUT_ACTIVE if(i == 0) handleOutput(false,true); #endif - removeActiveKey(button_keycodes[i]); - { uint8_t mods = 0; for (uint8_t j = 0; j < NUM_BUTTONS; j++) if (buttonStates & (1 << j)) mods |= button_modifiers[j]; blehid.keyboardReport(mods, active_keys); } + switch (button_actions[i]) { + case 0: // key release + removeActiveKey(button_keycodes[i]); + { uint8_t mods = 0; for (uint8_t j = 0; j < NUM_BUTTONS; j++) if (buttonStates & (1 << j)) mods |= button_modifiers[j]; blehid.keyboardReport(mods, active_keys); } + break; + case 1: + case 2: blehid.mouseButtonRelease(); break; + case 3: + case 4: break; // scroll: no action on release + } //if (ENABLE_ACTIVITY_LED ) dToggle(LED_R); if (ENABLE_DEBUG_OUTPUT) Serial.println("Button released"); } @@ -357,9 +375,9 @@ bool storeSettings() { snprintf(buffer,MAX_PARAM_LEN,"i:%d\n",sleep_timeout_ms); file.write(buffer); - //save keycodes (1-indexed to match serial command chars '1' and '2') + //save button actions and keycodes (1-indexed to match serial command chars '1' and '2') for(int i = 0; i<2; i++) { - snprintf(buffer,MAX_PARAM_LEN,"%d:%d:%d\n", i+1, button_keycodes[i], button_modifiers[i]); + snprintf(buffer,MAX_PARAM_LEN,"%d:%d:%d:%d\n", i+1, button_actions[i], button_keycodes[i], button_modifiers[i]); file.write(buffer); } @@ -431,18 +449,23 @@ void parseCommand(char *buf) { Serial.print("New: "); Serial.println(sleep_timeout_ms); break; - //handle button<->key code assignment (format: N::) + //handle button action assignment (format: N:::) + // actions: 0=key press, 1=left click, 2=right click, 3=scroll up, 4=scroll down case '1': case '2': { uint8_t btnIdx = buf[0] - '1'; - char *sep = strchr(buf+2, ':'); - uint8_t newKeycode = (uint8_t)String(buf+2).toInt(); - uint8_t newModifier = sep ? (uint8_t)String(sep+1).toInt() : 0; - Serial.print("Prev: "); Serial.print(button_keycodes[btnIdx]); Serial.print(":"); Serial.println(button_modifiers[btnIdx]); + char *sep1 = strchr(buf+2, ':'); + char *sep2 = sep1 ? strchr(sep1+1, ':') : nullptr; + uint8_t newAction = (uint8_t)String(buf+2).toInt(); + uint8_t newKeycode = sep1 ? (uint8_t)String(sep1+1).toInt() : 0; + uint8_t newModifier = sep2 ? (uint8_t)String(sep2+1).toInt() : 0; + if (newAction > 4) newAction = 0; + Serial.print("Prev: "); Serial.print(button_actions[btnIdx]); Serial.print(":"); Serial.print(button_keycodes[btnIdx]); Serial.print(":"); Serial.println(button_modifiers[btnIdx]); + button_actions[btnIdx] = newAction; button_keycodes[btnIdx] = newKeycode; button_modifiers[btnIdx] = newModifier; - Serial.print("New: "); Serial.print(button_keycodes[btnIdx]); Serial.print(":"); Serial.println(button_modifiers[btnIdx]); + Serial.print("New: "); Serial.print(button_actions[btnIdx]); Serial.print(":"); Serial.print(button_keycodes[btnIdx]); Serial.print(":"); Serial.println(button_modifiers[btnIdx]); } break; @@ -510,8 +533,8 @@ void printHelp() { Serial.print("i::Inactivity time [ms]:30000-600000:"); Serial.println(sleep_timeout_ms); Serial.print("d::Connected device::"); Serial.println(central_name); Serial.println("r::Reset paired devices"); - Serial.print("1::Key 1:"); Serial.print(button_keycodes[0]); Serial.print(":"); Serial.println(button_modifiers[0]); - Serial.print("2::Key 2:"); Serial.print(button_keycodes[1]); Serial.print(":"); Serial.println(button_modifiers[1]); + Serial.print("1::Button 1:"); Serial.print(button_actions[0]); Serial.print(":"); Serial.print(button_keycodes[0]); Serial.print(":"); Serial.println(button_modifiers[0]); + Serial.print("2::Button 2:"); Serial.print(button_actions[1]); Serial.print(":"); Serial.print(button_keycodes[1]); Serial.print(":"); Serial.println(button_modifiers[1]); #ifdef OUTPUT_ACTIVE Serial.println("c::Trigger the output"); diff --git a/ui/config.html b/ui/config.html index da1dcfe..c87ded1 100644 --- a/ui/config.html +++ b/ui/config.html @@ -184,7 +184,8 @@

Disconnected

type: parts[1].replace(/[<>]/g, ""), description: parts[2], extra: parts[3] || null, - current: parts[4] || null + current: parts[4] || null, + current2: parts[5] || null }); }); } @@ -210,7 +211,7 @@

Disconnected

let el = null; - const btn = document.createElement("button"); + let btn = document.createElement("button"); btn.textContent = "Update"; switch (p.type) { @@ -250,34 +251,69 @@

Disconnected

btn.addEventListener("click", () => sendValue(p.cmd, el.selectedIndex)); break; - case "keycombo": { - el = document.createElement("input"); - el.type = "text"; - el.readOnly = true; - el.placeholder = "Click here, then press a key combination"; - el.style.cssText = "cursor:pointer;min-width:240px;"; - let kc = parseInt(p.extra) || 0; - let km = parseInt(p.current) || 0; - if (kc) el.value = formatKeyCombo(kc, km); - el.addEventListener("focus", () => { el.style.outline = "2px solid #0078d4"; }); - el.addEventListener("blur", () => { el.style.outline = ""; }); - el.addEventListener("keydown", (e) => { + case "buttonaction": { + const ACTION_LABELS = ["Key press", "Left click", "Right click", "Scroll up", "Scroll down"]; + let action = parseInt(p.extra) || 0; + let kc = parseInt(p.current) || 0; + let km = parseInt(p.current2) || 0; + + const actionSel = document.createElement("select"); + ACTION_LABELS.forEach((name, idx) => { + const opt = document.createElement("option"); + opt.value = idx; + opt.textContent = name; + actionSel.appendChild(opt); + }); + actionSel.value = action; + actionSel.style.margin = "0 6px"; + + const keyInput = document.createElement("input"); + keyInput.type = "text"; + keyInput.readOnly = true; + keyInput.placeholder = "Click here, then press a key combination"; + keyInput.style.cssText = "cursor:pointer;min-width:240px;"; + if (kc) keyInput.value = formatKeyCombo(kc, km); + + const syncKeyInputState = () => { + const isKey = parseInt(actionSel.value) === 0; + keyInput.disabled = !isKey; + keyInput.style.opacity = isKey ? "1" : "0.4"; + keyInput.style.cursor = isKey ? "pointer" : "default"; + }; + syncKeyInputState(); + + actionSel.addEventListener("change", () => { + action = parseInt(actionSel.value); + syncKeyInputState(); + }); + keyInput.addEventListener("focus", () => { if (!keyInput.disabled) keyInput.style.outline = "2px solid #0078d4"; }); + keyInput.addEventListener("blur", () => { keyInput.style.outline = ""; }); + keyInput.addEventListener("keydown", (e) => { + if (keyInput.disabled) return; e.preventDefault(); if (["Control","Alt","Shift","Meta"].includes(e.key)) return; const hid = browserCodeToHID(e.code); - if (hid === undefined) { el.value = "(unsupported: " + e.code + ")"; return; } + if (hid === undefined) { keyInput.value = "(unsupported: " + e.code + ")"; return; } let mod = 0; if (e.ctrlKey) mod |= 0x01; if (e.shiftKey) mod |= 0x02; if (e.altKey) mod |= 0x04; if (e.metaKey) mod |= 0x08; kc = hid; km = mod; - el.value = formatKeyCombo(hid, mod); + keyInput.value = formatKeyCombo(hid, mod); }); btn.addEventListener("click", () => { - sendLine(p.cmd + ":" + kc + ":" + km); - statusEl.textContent += "Sent key combo: " + el.value + "\n"; + action = parseInt(actionSel.value); + sendLine(p.cmd + ":" + action + ":" + kc + ":" + km); + const label = action === 0 ? "Key: " + keyInput.value : ACTION_LABELS[action]; + statusEl.textContent += "Sent button action: " + label + "\n"; }); + + // Custom ordering: Update btn → action select → key input + div.appendChild(btn); + div.appendChild(actionSel); + div.appendChild(keyInput); + btn = null; el = null; // prevent generic re-append break; } From 6366374656c4a3e215cd016a782e068303c99cb7 Mon Sep 17 00:00:00 2001 From: Chris Veigl Date: Thu, 20 Aug 2026 01:06:35 +0200 Subject: [PATCH 5/6] allow manual input for key combos if desired --- ui/config.html | 77 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/ui/config.html b/ui/config.html index c87ded1..c34381a 100644 --- a/ui/config.html +++ b/ui/config.html @@ -253,8 +253,8 @@

Disconnected

case "buttonaction": { const ACTION_LABELS = ["Key press", "Left click", "Right click", "Scroll up", "Scroll down"]; - let action = parseInt(p.extra) || 0; - let kc = parseInt(p.current) || 0; + let action = parseInt(p.extra) || 0; + let kc = parseInt(p.current) || 0; let km = parseInt(p.current2) || 0; const actionSel = document.createElement("select"); @@ -270,15 +270,34 @@

Disconnected

const keyInput = document.createElement("input"); keyInput.type = "text"; keyInput.readOnly = true; - keyInput.placeholder = "Click here, then press a key combination"; - keyInput.style.cssText = "cursor:pointer;min-width:240px;"; + keyInput.style.cssText = "min-width:240px;"; if (kc) keyInput.value = formatKeyCombo(kc, km); + // Auto-detect checkbox + const autoCheck = document.createElement("input"); + autoCheck.type = "checkbox"; + autoCheck.checked = true; + autoCheck.id = "autodetect_" + p.cmd; + autoCheck.style.marginLeft = "8px"; + const autoLabel = document.createElement("label"); + autoLabel.htmlFor = autoCheck.id; + autoLabel.textContent = " auto detect"; + autoLabel.style.cssText = "font-size:0.85em;cursor:pointer;user-select:none;"; + const syncKeyInputState = () => { - const isKey = parseInt(actionSel.value) === 0; + const isKey = parseInt(actionSel.value) === 0; + const isAuto = autoCheck.checked; keyInput.disabled = !isKey; + if (isKey) { + keyInput.readOnly = isAuto; + keyInput.style.cursor = isAuto ? "pointer" : "text"; + keyInput.placeholder = isAuto + ? "Click here, then press a key combination" + : "Type e.g. Ctrl+Alt+Del"; + } keyInput.style.opacity = isKey ? "1" : "0.4"; - keyInput.style.cursor = isKey ? "pointer" : "default"; + autoCheck.disabled = !isKey; + autoLabel.style.opacity = isKey ? "1" : "0.4"; }; syncKeyInputState(); @@ -286,10 +305,12 @@

Disconnected

action = parseInt(actionSel.value); syncKeyInputState(); }); + autoCheck.addEventListener("change", syncKeyInputState); + keyInput.addEventListener("focus", () => { if (!keyInput.disabled) keyInput.style.outline = "2px solid #0078d4"; }); keyInput.addEventListener("blur", () => { keyInput.style.outline = ""; }); keyInput.addEventListener("keydown", (e) => { - if (keyInput.disabled) return; + if (keyInput.disabled || !keyInput.readOnly) return; // manual mode: let browser handle typing e.preventDefault(); if (["Control","Alt","Shift","Meta"].includes(e.key)) return; const hid = browserCodeToHID(e.code); @@ -302,17 +323,25 @@

Disconnected

kc = hid; km = mod; keyInput.value = formatKeyCombo(hid, mod); }); + btn.addEventListener("click", () => { action = parseInt(actionSel.value); + if (action === 0 && !autoCheck.checked) { + const parsed = parseKeyComboText(keyInput.value); + kc = parsed.keycode; km = parsed.modifier; + if (kc) keyInput.value = formatKeyCombo(kc, km); // normalise display + } sendLine(p.cmd + ":" + action + ":" + kc + ":" + km); const label = action === 0 ? "Key: " + keyInput.value : ACTION_LABELS[action]; statusEl.textContent += "Sent button action: " + label + "\n"; }); - // Custom ordering: Update btn → action select → key input + // Custom ordering: Update btn → action select → key input → checkbox → label div.appendChild(btn); div.appendChild(actionSel); div.appendChild(keyInput); + div.appendChild(autoCheck); + div.appendChild(autoLabel); btn = null; el = null; // prevent generic re-append break; } @@ -420,6 +449,38 @@

Disconnected

if (modifier & 0x88) mods.push('Win'); return [...mods, hidKeycodeToName(keycode)].join('+'); } + +// Reverse name→HID map for manual text parsing +const HID_NAME_TO_CODE = {}; +Object.values(HID_KEY_MAP).forEach(v => { HID_NAME_TO_CODE[v.name.toLowerCase()] = v.hid; }); +const HID_TEXT_ALIASES = { + 'delete':0x4C,'del':0x4C,'escape':0x29,'insert':0x49,'ins':0x49, + 'pageup':0x4B,'pgup':0x4B,'page up':0x4B,'pagedown':0x4E,'pgdn':0x4E,'page down':0x4E, + 'up':0x52,'down':0x51,'left':0x50,'right':0x4F, + 'space':0x2C,'enter':0x28,'return':0x28,'tab':0x2B,'backspace':0x2A,'bs':0x2A, + 'capslock':0x39,'caps':0x39,'numlock':0x53,'scrolllock':0x47,'scrlk':0x47, + 'printscreen':0x46,'prtsc':0x46,'pause':0x48,'break':0x48, +}; + +function nameToHID(name) { + const lower = name.trim().toLowerCase(); + return HID_NAME_TO_CODE[lower] || HID_TEXT_ALIASES[lower] || 0; +} + +// Parse a manually typed combo like "Ctrl+Alt+Del" into {keycode, modifier} +function parseKeyComboText(text) { + let mod = 0, keycode = 0; + text.split('+').forEach(part => { + const upper = part.trim().toUpperCase(); + if (upper === 'CTRL' || upper === 'CONTROL') { mod |= 0x01; return; } + if (upper === 'SHIFT') { mod |= 0x02; return; } + if (upper === 'ALT') { mod |= 0x04; return; } + if (upper === 'WIN' || upper === 'META' || upper === 'GUI' || upper === 'CMD') { mod |= 0x08; return; } + const hid = nameToHID(part); + if (hid) keycode = hid; + }); + return { keycode, modifier: mod }; +} From 8df077def0e366be317d230c10bc41ffd7641ae2 Mon Sep 17 00:00:00 2001 From: Chris Veigl Date: Thu, 20 Aug 2026 01:08:06 +0200 Subject: [PATCH 6/6] default to OUTPUT_ACTIVE disabled --- src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.cpp b/src/main.cpp index 13b29c9..2094e4f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -54,7 +54,7 @@ void printHelp(); /******* output to 3.5mm jackplug ******/ //use output functions ('c' command) -#define OUTPUT_ACTIVE +//#define OUTPUT_ACTIVE #ifdef OUTPUT_ACTIVE //latching 1 coil relay on P0.02 (D18) & P0.29 (D20)