From 59850fd49befc928a4ce338f05608e4b443a9125 Mon Sep 17 00:00:00 2001 From: Harald Albrecht Date: Thu, 3 Jan 2019 20:47:03 +0100 Subject: [PATCH] initial version of multibow --- LICENSE | 19 +++ README.md | 59 +++++++++ mock/keybow.lua | 210 ++++++++++++++++++++++++++++++ mock/mockkeybow.lua | 24 ++++ multibow.jpg | Bin 0 -> 26041 bytes sdcard/keys.lua | 4 + sdcard/layouts/empty.lua | 22 ++++ sdcard/layouts/shift.lua | 38 ++++++ sdcard/layouts/vsc-golang.lua | 74 +++++++++++ sdcard/snippets/morekeys.lua | 108 +++++++++++++++ sdcard/snippets/multibow.lua | 176 +++++++++++++++++++++++++ sdcard/snippets/routehandlers.lua | 63 +++++++++ test.lua | 30 +++++ 13 files changed, 827 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 mock/keybow.lua create mode 100644 mock/mockkeybow.lua create mode 100644 multibow.jpg create mode 100644 sdcard/keys.lua create mode 100644 sdcard/layouts/empty.lua create mode 100644 sdcard/layouts/shift.lua create mode 100644 sdcard/layouts/vsc-golang.lua create mode 100644 sdcard/snippets/morekeys.lua create mode 100644 sdcard/snippets/multibow.lua create mode 100644 sdcard/snippets/routehandlers.lua create mode 100644 test.lua diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..46eeb12 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright 2019 Harald Albrecht + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..180c343 --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Multibow + +GitHub: [github.com/thediveo/multibow](https://github.com/thediveo/multibow) + +Multibow turns a [Pimoroni Keybow](https://shop.pimoroni.com/products/keybow) +into a macro keyboard with multiple layouts, switchable at any time. (Keybow is +a solderless DIY 4x3 mechanical USB keyboard, powered by a Raspberry Pi. And +yes, these days even keyboards run Linux...) + +![Multibow on Keybow](multibow.jpg) + +Layouts included: +- Debug Go programs and packages in VisualStudio Code with its Go extension. +- "Empty" keyboard layout what does nothing, very useful when using cycling between different keyboard layouts to have one non-reacting layout. +- permanent layout for cycling between the other layouts and for changing the keybow LED brightness. + +## Licenses + +Multibow is (c) 2019 Harald Albrecht and is licensed under the MIT license, see +the [LICENSE](LICENSE) file. + +The file `keybow.lua` included from +[pimoroni/keybow-firmware](https://github.com/pimoroni/keybow-firmware) for +testing purposes is licensed under the MIT license, as declared by Pimoroni's +keybow-firmware GitHub repository. + +## Installation + +1. Download the [Pibow +firmware](https://github.com/pimoroni/keybow-firmware/releases) and copy all +files inside its `sdcard/` subdirectory onto an empty, FAT32 formatted microSD +card. Copy only the files **inside** `sdcard/`, but do **not** place them into a +~~`sdcard`~~ directory on your microSD card. + +2. Download all files from the `sdcard/` subdirectory of this repository and +then copy them onto the microSD card. This will overwrite but one file +`key.lua`, all other files are new. + +## Multiple Keyboard Layouts + +To enable one or more multibow keyboard layouts, edit `sdcard/keys.lua` +accordingly to require them. The default configuration is as follows: + +```lua +require "layouts/shift" -- for cycling between layouts. +require "layouts/vsc-golang" -- debugging Go programs in VisualStudio Code. +require "layouts/empty" -- empty, do-nothing layout. +``` + + +## Developing + +For some basic testing, run `lua test.lua` from the base directory of this +repository. It pulls in `keybow`, then mocks some functionality of it, and +finally starts `sdcard/keys.lua` as usual. + +This helps in detecting syntax and logic erros early, avoiding the +rinse-and-repeat cycle with copying to microSD card, starting the Keybow +hardware, and then wondering what went wrong, without any real clue. diff --git a/mock/keybow.lua b/mock/keybow.lua new file mode 100644 index 0000000..6d8fd46 --- /dev/null +++ b/mock/keybow.lua @@ -0,0 +1,210 @@ +keybow = {} + +local KEYCODES = "abcdefghijklmnopqrstuvwxyz1234567890\n\a\b\t -=[]\\#;'`,./" +local SHIFTED_KEYCODES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()\a\a\a\a\a_+{}|~:\"~<>?" + +keybow.LEFT_CTRL = 0 +keybow.LEFT_SHIFT = 1 +keybow.LEFT_ALT = 2 +keybow.LEFT_META = 3 + +keybow.RIGHT_CTRL = 4 +keybow.RIGHT_SHIFT = 5 +keybow.RIGHT_ALT = 6 +keybow.RIGHT_META = 7 + +keybow.ENTER = 0x28 +keybow.ESC = 0x29 +keybow.BACKSPACE = 0x2a +keybow.TAB = 0x2b +keybow.SPACE = 0x2c +keybow.CAPSLOCK = 0x39 + +keybow.LEFT_ARROW = 0x50 +keybow.RIGHT_ARROW = 0x4f +keybow.UP_ARROW = 0x52 +keybow.DOWN_ARROW = 0x51 + +keybow.F1 = 0x3a +keybow.F2 = 0x3b +keybow.F3 = 0x3c +keybow.F4 = 0x3d +keybow.F5 = 0x3e +keybow.F6 = 0x3f +keybow.F7 = 0x40 +keybow.F8 = 0x41 +keybow.F9 = 0x42 +keybow.F10 = 0x43 +keybow.F11 = 0x44 +keybow.F12 = 0x45 + +keybow.KEY_DOWN = true +keybow.KEY_UP = false + +-- Functions exposed from C + +function keybow.set_modifier(key, state) + keybow_set_modifier(key, state) +end + +function keybow.sleep(time) + keybow_sleep(time) +end + +function keybow.usleep(time) + keybow_usleep(time) +end + +function keybow.text(text) + for i = 1, #text do + local c = text:sub(i, i) + keybow.tap_key(c) + end + + keybow.set_modifier(keybow.LEFT_SHIFT, false) +end + +-- Lighting control + +function keybow.set_pixel(x, r, g, b) + keybow_set_pixel(x, r, g, b) +end + +function keybow.auto_lights(state) + keybow_auto_lights(state) +end + +function keybow.clear_lights() + keybow_clear_lights() +end + +function keybow.load_pattern(file) + keybow_load_pattern(file) +end + +-- Meta keys - ctrl, shift, alt and win/apple + +function keybow.tap_left_ctrl() + keybow.set_modifier(keybow.LEFT_CTRL, keybow.KEY_DOWN) + keybow.set_modifier(keybow.LEFT_CTRL, keybow.KEY_UP) +end + +function keybow.tap_right_ctrl() + keybow.set_modifier(keybow.RIGHT_CTRL, keybow.KEY_DOWN) + keybow.set_modifier(keybow.RIGHT_CTRL, keybow.KEY_UP) +end + +function keybow.tap_left_shift() + keybow.set_modifier(keybow.LEFT_SHIFT, keybow.KEY_DOWN) + keybow.set_modifier(keybow.LEFT_SHIFT, keybow.KEY_UP) +end + +function keybow.tap_right_shift() + keybow.set_modifier(keybow.RIGHT_SHIFT, keybow.KEY_DOWN) + keybow.set_modifier(keybow.RIGHT_SHIFT, keybow.KEY_UP) +end + +function keybow.tap_left_alt() + keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_DOWN) + keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_UP) +end + +function keybow.tap_right_alt() + keybow.set_modifier(keybow.RIGHT_ALT, keybow.KEY_DOWN) + keybow.set_modifier(keybow.RIGHT_ALT, keybow.KEY_UP) +end + +function keybow.tap_left_meta() + keybow.set_modifier(keybow.LEFT_META, keybow.KEY_DOWN) + keybow.set_modifier(keybow.LEFT_META, keybow.KEY_UP) +end + +function keybow.tap_right_meta() + keybow.set_modifier(keybow.RIGHT_META, keybow.KEY_DOWN) + keybow.set_modifier(keybow.RIGHT_META, keybow.KEY_UP) +end + +-- Function keys + +function keybow.tap_function_key(index) + index = 57 + index -- Offset to 0x39 (F1 is 0x3a) + keybow.set_key(index, true) + keybow.set_key(index, false) +end + +function keybow.ascii_to_shift(key) + if not (type(key) == "string") then + return false + end + + return SHIFTED_KEYCODES.find(key) ~= nil +end + +function keybow.ascii_to_hid(key) + if not (type(key) == "string") then + return key + end + + key = key:lower() + + code = KEYCODES:find(key) + + if code == nil then return nil end + + return code + 3 +end + +function keybow.set_key(key, pressed) + if type(key) == "string" then + local hid_code = nil + local shifted = SHIFTED_KEYCODES:find(key, 1, true) ~= nil + + if shifted then + hid_code = SHIFTED_KEYCODES:find(key, 1, true) + else + hid_code = KEYCODES:find(key, 1, true) + end + + if not (hid_code == nil) then + hid_code = hid_code + 3 + if shifted then keybow.set_modifier(keybow.LEFT_SHIFT, pressed) end + keybow_set_key(hid_code, pressed) + end + + else -- already a key code + keybow_set_key(key, pressed) + end +end + +function keybow.tap_enter() + keybow.set_key(keybow.ENTER, true) + keybow.set_key(keybow.ENTER, false) +end + +function keybow.tap_space() + keybow.set_key(keybow.SPACE, true) + keybow.set_key(keybow.SPACE, false) +end + +function keybow.tap_shift() + keybow.set_key(keybow.LEFT_SHIFT, true) + keybow.set_key(keybow.LEFT_SHIFT, false) +end + +function keybow.tap_tab() + keybow.set_key(keybow.TAB, true) + keybow.set_key(keybow.TAB, false) +end + +function keybow.tap_key(key) + keybow.set_key(key, true) + keybow.set_key(key, false) +end + +function keybow.press_key(key) + keybow.set_key(key, true) +end + +function keybow.release_key(key) + keybow.set_key(key, false) +end diff --git a/mock/mockkeybow.lua b/mock/mockkeybow.lua new file mode 100644 index 0000000..8a1efa9 --- /dev/null +++ b/mock/mockkeybow.lua @@ -0,0 +1,24 @@ +require "keybow" + +function keybow.set_pixel(pix, r, g, b) + -- print("set_pixel", pix, r, g, b) +end + +function keybow.auto_lights(onoff) + -- print("auto_lights", onoff) +end + +function keybow.clear_lights() + -- print("clear_lights") +end + +function keybow.tap_key(key) + print("tap_key", key) +end + +function keybow.set_modifier(mod, key) + print("set_modifier", mod, key) +end + +require "keys" +setup() diff --git a/multibow.jpg b/multibow.jpg new file mode 100644 index 0000000000000000000000000000000000000000..035b069846005f979910f47068e950d4e7490c0d GIT binary patch literal 26041 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 Wlu??_Q!*X+d 1 then; b = 0.4; end + mb.set_brightness(b) +end + +mb.register_permanent_keymap({ + [11] = {c={r=1, g=1, b=1}, h=shift.cycle}, + [8] = {c={r=0.5, g=0.5, b=0.5}, h=shift.brightness} +}, "shift") diff --git a/sdcard/layouts/vsc-golang.lua b/sdcard/layouts/vsc-golang.lua new file mode 100644 index 0000000..3956618 --- /dev/null +++ b/sdcard/layouts/vsc-golang.lua @@ -0,0 +1,74 @@ +-- VSC Go extension debug Keybow layout + +require "snippets/multibow" + +--[[ +The Keybow layout is as follows when in landscape orientation, with the USB +cable going off "northwards": + + ┋┋ +┌────┐ ┌────┐ ┌────┐ ┌────┐ +│ 11 │ │ 8 │ │ 5 │ │ 2 │ +└────┘ └────┘ └────┘ └────┘ +┌────┐ ┌────┐ ┌────┐ ┌────┐ +│ 10 │ │ 7 │ │ 4 │ │ 1 │ +└────┘ └────┘ └────┘ └────┘ +┌────┐ ┌────┐ ┌────┐ ┌────┐ +│ 9 │ │ 6 │ │ 3 │ │ 0 │ +└────┘ └────┘ └────┘ └────┘ + +]]-- + +RED = { r=1, g=0, b=0 } +YELLOW = { r=1, g=0.8, b=0 } +GREEN = { r=0, g=1, b=0 } +BLUE = { r=0, g=0, b=1 } +BLUECYAN = { r=0, g=0.7, b=1 } +BLUEGRAY = { r=0.7, g=0.7, b=1 } +CYAN = { r=0, g=1, b=1 } + + +-- AND NOW FOR SOMETHING DIFFERENT: THE REAL MEAT -- + +function debug_stop(key) + mb.tap(key, keybow.F5, keybow.LEFT_SHIFT) +end + +function debug_restart(key) + mb.tap(key, keybow.F5, keybow.LEFT_SHIFT, keybow.LEFT_CTRL) +end + +function debug_continue(key) + mb.tap(key, keybow.F5) +end + +function debug_stepover(key) + mb.tap(key, keybow.F10) +end + +function debug_stepinto(key) + mb.tap(key, keybow.F11) +end + +function debug_stepout(key) + mb.tap(key, keybow.F11, keybow.LEFT_SHIFT) +end + +function go_test_package(key) + mb.tap(key, "P", keybow.LEFT_SHIFT, keybow.LEFT_CTRL) + keybow.sleep(250) + keybow.text("go test package") + keybow.tap_enter() +end + + +mb.register_keymap({ + [10] = {c=RED, h=debug_stop}, + [7] = {c=YELLOW, h=debug_restart}, + [1] = {c=CYAN, h=go_test_package}, + + [9] = {c=GREEN, h=debug_continue}, + [6] = {c=BLUECYAN, h=debug_stepinto}, + [3] = {c=BLUE, h=debug_stepover}, + [0] = {c=BLUEGRAY, h=debug_stepout}, +}, 'vsc-golang-debug') diff --git a/sdcard/snippets/morekeys.lua b/sdcard/snippets/morekeys.lua new file mode 100644 index 0000000..a991c2e --- /dev/null +++ b/sdcard/snippets/morekeys.lua @@ -0,0 +1,108 @@ +--[[ +Provide additional keybow USB HID key definitions. + +For more information about USB HID keyboard scan codes, for instance, +see: https://gist.github.com/MightyPork/6da26e382a7ad91b5496ee55fdc73db2 + +Copyright 2019 Harald Albrecht + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]]-- + +require("keybow") + +keybow.SYSRQ = 0x46 +keybow.SCROLLLOCK = 0x47 +keybow.PAUSE = 0x48 +keybow.INSERT = 0x49 +keybow.DELETE = 0x4c +keybow.HOME = 0x4a +keybow.END = 0x4d +keybow.PAGEUP = 0x4b +keybow.PAGEOWN = 0x4e + +keybow.F13 = 0x68 +keybow.F14 = 0x69 +keybow.F15 = 0x6a +keybow.F16 = 0x6b +keybow.F17 = 0x6c +keybow.F18 = 0x6d +keybow.F19 = 0x6e +keybow.F20 = 0x6f +keybow.F21 = 0x70 +keybow.F22 = 0x71 +keybow.F23 = 0x72 +keybow.F24 = 0x73 + +keybow.KPSLASH = 0x54 +keybow.KPASTERISK = 0x55 +keybow.KPMINUS = 0x56 +keybow.KPPLUS = 0x57 +keybow.KPENTER = 0x58 +keybow.KP1 = 0x59 +keybow.KP2 = 0x5a +keybow.KP3 = 0x5b +keybow.KP4 = 0x5c +keybow.KP5 = 0x5d +keybow.KP6 = 0x5e +keybow.KP7 = 0x5f +keybow.KP8 = 0x60 +keybow.KP9 = 0x61 +keybow.KP0 = 0x62 +keybow.KPDOT = 0x63 +keybow.KPEQUAL = 0x67 + +keybow.COMPOSE = 0x65 +keybow.POWER = 0x66 + +keybow.OPEN = 0x74 +keybow.HELP = 0x75 +keybow.PROPS = 0x76 +keybow.FRONT = 0x77 +keybow.STOP = 0x78 +keybow.AGAIN = 0x79 +keybow.UNDO = 0x7a +keybow.CUT = 0x7b +keybow.COPY = 0x7c +keybow.PASTE = 0x7d +keybow.FIND = 0x7e +keybow.MUTE = 0x7f +keybow.VOLUMEUP = 0x80 +keybow.VOLUMEDOWN = 0x81 + +keybow.MEDIA_PLAYPAUSE = 0xe8 +keybow.MEDIA_STOPCD = 0xe9 +keybow.MEDIA_PREVIOUSSONG = 0xea +keybow.MEDIA_NEXTSONG = 0xeb +keybow.MEDIA_EJECTCD = 0xec +keybow.MEDIA_VOLUMEUP = 0xed +keybow.MEDIA_VOLUMEDOWN = 0xee +keybow.MEDIA_MUTE = 0xef +keybow.MEDIA_WWW = 0xf0 +keybow.MEDIA_BACK = 0xf1 +keybow.MEDIA_FORWARD = 0xf2 +keybow.MEDIA_STOP = 0xf3 +keybow.MEDIA_FIND = 0xf4 +keybow.MEDIA_SCROLLUP = 0xf5 +keybow.MEDIA_SCROLLDOWN = 0xf6 +keybow.MEDIA_EDIT = 0xf7 +keybow.MEDIA_SLEEP = 0xf8 +keybow.MEDIA_COFFEE = 0xf9 +keybow.MEDIA_REFRESH = 0xfa +keybow.MEDIA_CALC = 0xfb diff --git a/sdcard/snippets/multibow.lua b/sdcard/snippets/multibow.lua new file mode 100644 index 0000000..9b5e7f6 --- /dev/null +++ b/sdcard/snippets/multibow.lua @@ -0,0 +1,176 @@ +--[[ +Copyright 2019 Harald Albrecht + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]]-- + +require "keybow" + +mb = {} +mb.path = (...):match("^(.-)[^%/]+$") + +require(mb.path .. "morekeys") +require(mb.path .. "routehandlers") + + +mb.brightness = 0.4 +mb.keymaps = {} +mb.permanent_keymaps = {} +mb.current_keymap_set = nil +mb.current_keymap_idx = 0 + + +-- +function mb.tap(keyno, key, ...) + for modifier_argno = 1, select('#', ...) do + local modifier = select(modifier_argno, ...) + if modifier then; keybow.set_modifier(modifier, keybow.KEY_DOWN); end + end + keybow.tap_key(key) + for modifier_argno = 1, select('#', ...) do + local modifier = select(modifier_argno, ...) + if modifier then; keybow.set_modifier(modifier, keybow.KEY_UP); end + end +end + + +-- Registers a keymap by name (and optional) index. The name is simply used +-- for allowing multiple SHIFT (modifier) levels for the same keyboard layout. +-- The indices are then used to differentiate between different SHIFT/modifier +-- levels of the same keyboard layout. +-- +-- Indices start at 0, this is the un-SHIFT-ed keymap for a layout. +-- +-- If this is the first keymap getting registered, then it will also made +-- activated. +function mb.register_keymap(keymap, name, index) + -- register + local kms = mb.keymaps[name] + kms = kms and kms or {} -- if name isn't known yet, create new keymap array for name + local index = index and #kms or 0 -- appends keymap if index is nil + kms[index] = keymap + mb.keymaps[name] = kms + -- ensure that first registered keymap also automatically gets activated + -- (albeit the LEDs will only update later). + if mb.current_keymap_set == nil then + mb.current_keymap_set = kms + mb.current_keymap_idx = index + end +end + + +-- Cycles through the available (non-permanent) keymaps. When switching to +-- the next keymap, we will always activate the index 0 layout. +function mb.cycle_keymaps() + local first_kms + local first_name + local next = false + local next_kms + local next_name + for name, kms in pairs(mb.keymaps) do + if first_kms == nil then + first_kms = kms; first_name = name + end + if kms == mb.current_keymap_set then + next = true + elseif next then + next_kms = kms; next_name = name + next = false + end + end + if next_kms == nil then + next_kms = first_kms; next_name = first_name + end + mb.activate_keymap(next_name, 0) +end + + +-- Activates a specific keymap by name and index. +function mb.activate_keymap(name, index) + print("activate_keymap", name, index) + local kms = mb.keymaps[name] + mb.current_keymap_set = kms + mb.current_keymap_idx = index + keybow.clear_lights() + mb.activate_leds() +end + + +-- Registers a permanent keymap (as opposed to switchable keymaps). As their +-- name suggest, permanent keymaps are permanently active and have priority +-- over any "standard" activated keymap. As they are permanent, there is no +-- "index" sub-layout mechanism, but instead all permanent keymaps are always +-- active. If permanent keymaps define overlapping keys, then the result is +-- undefined. +function mb.register_permanent_keymap(keymap, name) + -- register + mb.permanent_keymaps[name] = keymap + mb.activate_leds() +end + + +-- Sets the Keybow key LEDs maximum brightness, in the range [0.1..1]. +function mb.set_brightness(brightness) + if brightness < 0.1 then; brightness = 0.1; end + if brightness > 1 then; brightness = 1; end + mb.brightness = brightness + mb.activate_leds() +end + + +-- Sets key LED to specific color, taking brightness into consideration. +-- The color is a triple (table) with the elements r, g, and b. Each color +-- component is in the range [0..1]. +function mb.led(keyno, color) + if color ~= nil then + keybow.set_pixel(keyno, + color.r * mb.brightness * 255, color.g * mb.brightness * 255, color.b * mb.brightness * 255) + else + keybow.set_pixel(keyno, 0, 0, 0) + end +end + + +-- Restores Keybow LEDs according to current keymap and the permanent keymaps. +function mb.activate_leds() + -- current keymap + if mb.current_keymap_set ~= nil then + for keyno, keydef in pairs(mb.current_keymap_set[mb.current_keymap_idx]) do + print("LED", keyno) + mb.led(keyno, keydef.c) + end + end + -- permanent keymap(s) + for name, pkm in pairs(mb.permanent_keymaps) do + for keyno, keydef in pairs(pkm) do + print("pLED", keyno) + mb.led(keyno, keydef.c) + end + end +end + + +-- Disables the automatic Keybow lightshow and sets the key LED colors. +function setup() + -- Disables the automatic keybow lightshow and switches all key LEDs off + -- because the LEDs might be in a random state after power on. + keybow.auto_lights(false) + keybow.clear_lights() + mb.activate_leds() +end diff --git a/sdcard/snippets/routehandlers.lua b/sdcard/snippets/routehandlers.lua new file mode 100644 index 0000000..14abcc7 --- /dev/null +++ b/sdcard/snippets/routehandlers.lua @@ -0,0 +1,63 @@ +--[[ +Copyright 2019 Harald Albrecht + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]]-- + +-- This all-key, central key router forwards Keybow key events to +-- their correct handlers, depending on which keyboard layout currently +-- is active. +function mb.route(keyno, pressed) + -- Check key in permanent keymaps + local keydef + for name, pkm in pairs(mb.permanent_keymaps) do + keydef = pkm[keyno] + if keydef ~= nil then; break; end + end + -- Check key in current keymap + if keydef == nil and mb.current_keymap_set ~= nil then + keydef = mb.current_keymap_set[mb.current_keymap_idx][keyno] + end + + if keydef == nil then; return; end + + if pressed then + for led = 0, 11 do + if led ~= keyno then; mb.led(led, {r=0, g=0, b=0}); end + end + print("route to key #", keyno) + keydef.h(keyno) + else + mb.activate_leds() + end +end + +-- Routes all keybow key handling through our central key router +function handle_key_00(pressed); mb.route(0, pressed); end +function handle_key_01(pressed); mb.route(1, pressed); end +function handle_key_02(pressed); mb.route(2, pressed); end +function handle_key_03(pressed); mb.route(3, pressed); end +function handle_key_04(pressed); mb.route(4, pressed); end +function handle_key_05(pressed); mb.route(5, pressed); end +function handle_key_06(pressed); mb.route(6, pressed); end +function handle_key_07(pressed); mb.route(7, pressed); end +function handle_key_08(pressed); mb.route(8, pressed); end +function handle_key_09(pressed); mb.route(9, pressed); end +function handle_key_10(pressed); mb.route(10, pressed); end +function handle_key_11(pressed); mb.route(11, pressed); end diff --git a/test.lua b/test.lua new file mode 100644 index 0000000..bec4718 --- /dev/null +++ b/test.lua @@ -0,0 +1,30 @@ +package.path = "./sdcard/?.lua;./mock/?.lua;" .. package.path + +require "mockkeybow" + +print() +print("**** key 00...") +handle_key_00(true) +handle_key_00(false) +print() +print("**** key 03...") +handle_key_03(true) +handle_key_03(false) +print() + +print("**** key 11...") +handle_key_11(true) +handle_key_11(false) + +print("**** key 00...") +handle_key_00(true) +handle_key_00(false) + +print() +print("**** key 11...") +handle_key_11(true) +handle_key_11(false) + +print("**** key 00...") +handle_key_00(true) +handle_key_00(false)