- Notifications
You must be signed in to change notification settings - Fork6
Pimoroni Keybow based, WiFi-enabled Macro Pad (a.k.a. poor-man's Elgato Stream Deck)
License
mrusme/kiwi
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Kiwi –KeyboardInterface forWirelessInteraction – is aNerves-based firmware forPimoroni Keybow that transforms the deviceinto a powerful wireless (WiFi) controller for Philips Hue, IFTTT, OBS andeverything else that has an HTTP API!
With Kiwi, you can turn your Pimoroni Keybow into a poor-man's Elgato StreamDeck!
- 2.4 GHz WiFi
- PimoroniKeybow
- microSD card
- Download the latest firmware (
kiwi.tar.gz
) from thereleases page and unpackit it:tar -xzf kiwi.tar.gz
- Insert the microSD card into the card reader of your computer
- Raw-copy the firmware to your microSD card, e.g. using
dd
:sudo dd bs=1m if=./kiwi.fw of=/dev/SDCARD_DEVICE
(whereSDCARD_DEVICE
isyour microSD card, e.g./dev/rdisk3
) - If your computer did not automatically mount a partition (
BOOT-A
) from themicroSD card after the copy has finished, eject the card and plug it back in - Open the folder of your mounted partition (
BOOT-A
) and create a file namedkiwi.txt
(small caps, no spaces) withthe following content. - Adjust the SSID, the PSK (and the KEY_MGMT) values according to your WiFiconfiguration
- If you want to controlOBS from Kiwi, fill
NERVES_NETWORK_OBS_SOCKET
with the URL to the OBS web-socket, otherwiseremove the whole line - Save the file and unmount the partition
- Insert the microSD card into your Keybow and connect it to a power source
- Check your WiFi access point's web-interface to find out the IP address thatwas assigned to your Keybow
As soon as you found the IP you can start configuring the device via its API.
The Kiwi API is accessible viahttp://10.10.10.10:8080/
(where10.10.10.10
is the IP address of the Keybow on your WiFi). Check out the fullOpenAPI 3specification!
Note: All example API calls in this documentation are being performed using
curl
, as it's available for the majority ofplatforms.
The Kiwi API provides a/settings
endpoint for configuring each individualkey.
Retrieve a list of all currently configured settings (e.g. for backing up yourcurrent configuration).
curl"http://10.10.10.10:8080/settings" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{}'
Bulk upsert endpoint, allows upserting a list of settings (e.g. for recovery ofa backup). This endpoint can be used for importing a backed-up configuration.
curl -X"POST""http://10.10.10.10:8080/settings" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "settings": [ { "id": "key_1_in_row_1", "object": { "keydown": { "http": [ ... ] } } }, { "id": "animation_main", "object": { "frames": [ ... ] } } ] }'
Retrieve a list of all currently configured keys.
curl"http://10.10.10.10:8080/settings/keys" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{}'
Retrieve the current configuration for a specific key.
curl"http://10.10.10.10:8080/settings/keys/key_1_in_row_1" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{}'
:key
can be one of the following values:
key_1_in_row_1
key_2_in_row_1
key_3_in_row_1
key_1_in_row_2
key_2_in_row_2
key_3_in_row_2
key_1_in_row_3
key_2_in_row_3
key_3_in_row_3
key_1_in_row_4
key_2_in_row_4
key_3_in_row_4
The key names relate to the position of the key on the Keybow when the deviceis in front of you with theKeybow
label in the top right corner and thepimoroni.com/keybow
url on the bottom right:
Keybow╔════════════════╦════════════════╦════════════════╗║ ║ ║ ║║ ║ ║ ║║ key_1_in_row_1 ║ key_2_in_row_1 ║ key_3_in_row_1 ║║ ║ ║ ║║ ║ ║ ║╠════════════════╬════════════════╬════════════════╣║ ║ ║ ║║ ║ ║ ║║ key_1_in_row_2 ║ key_2_in_row_2 ║ key_3_in_row_2 ║║ ║ ║ ║║ ║ ║ ║╠════════════════╬════════════════╬════════════════╣║ ║ ║ ║║ ║ ║ ║║ key_1_in_row_3 ║ key_2_in_row_3 ║ key_3_in_row_3 ║║ ║ ║ ║║ ║ ║ ║╠════════════════╬════════════════╬════════════════╣║ ║ ║ ║║ ║ ║ ║║ key_1_in_row_4 ║ key_2_in_row_4 ║ key_3_in_row_4 ║║ ║ ║ ║║ ║ ║ ║╚════════════════╩════════════════╩════════════════╝ pimoroni.com/keybow
HTTP actions allow you to run arbitrary HTTP requests (GET
,PUT
,POST
,DELETE
) in order to control basically everything that provides a more or lesssane HTTP API, like IoT devices or web services.
Let's have a look at an example:
In order to configure the first key of the first row to perform a HTTP post toIFTTT's Maker Webhooks(clickDocumentation on that site) when it's pressed (down), run thefollowing command:
curl -X POST"http://10.10.10.10:8080/settings/keys/key_1_in_row_1" \ -H"Content-Type: application/json; charset=utf-8" \ -d$'{ "object": { "keydown": { "http": [ { "body": "{}", "method": "post", "headers": { "content-type": "application/json" }, "url": "https://maker.ifttt.com/trigger/key_1_in_row_1/with/key/your-ifttt-key-here" } ] } } }'
As soon as the curl command returns with HTTP status code200 OK
the key wasset up and its configuration stored to the Keybow's internal storage (which isthe microSD card, of course). You can now press the key (the first one on thetop left) to run the HTTP call.
However, you might have noticed, that thehttp
property is not simply anobject but rather an array containing objects. This allows you to definemultiple HTTP actions to run with the press of a single button. In order to doso, simply add another HTTP request object to thehttp
array:
curl -X POST"http://10.10.10.10:8080/settings/keys/key_1_in_row_1" \ -H"Content-Type: application/json; charset=utf-8" \ -d$'{ "object": { "keydown": { "http": [ { "body": "{}", "method": "post", "headers": { "content-type": "application/json" }, "url": "https://maker.ifttt.com/trigger/turn_lights_off/with/key/your-ifttt-key-here" }, { "body": "{}", "method": "post", "headers": { "content-type": "application/json" }, "url": "https://maker.ifttt.com/trigger/power_on_television/with/key/your-ifttt-key-here" } ] } } }'
The HTTP requests defined within thehttp
array are being runone afteranother (in the order defined within the array, from top to bottom) and notin parallel. This means that the second request waits for the first one tocomplete until it executes. Also keep in mind that, as of right now, subsequentrequests don't care about their prior request's return status and will run nomatter what.
Now that we've learned how HTTP requests work and that we can have multiplerequests running one after another, we can dive deeper into how HTTP requestscan be scripted.
Let's assume, you would like one button to trigger an API that turns alightbulb on or off. The API accepts a boolean value as the lightbulb's state,withtrue
being on,false
being off. Now, in order to program a singlebutton to trigger the lightbulb onand off with each keypress, you need tohave a dynamic value within your request's body, that changes, depending on thecurrent state of the lightbulb. This state could be retrieved inside a separateHTTPGET
request that runs prior to the one updating the lightbulb's status.
Kiwi allows you to do just this. Let's have a look on how such ahttp
definition could look like for a Philips Hue connected lightbulb:
curl -X"POST""http://10.10.10.10:8080/settings/keys/key_1_in_row_1" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "object": { "keydown": { "http": [ { "body": "{}", "method": "get", "headers": { "content-type": "application/json" }, "url": "http://YOUR-HUE-BRIDGE-IP-HERE/api/YOUR-GENERATED-USERNAME-HERE/lights/1" }, { "body": "{\\"on\\": <<{Map.get(previous_http_response, :body) |> Jason.decode!() |> Map.get(\\"state\\") |> Map.get(\\"on\\") |> Kernel.not}>>}", "method": "put", "headers": { "content-type": "application/json" }, "url": "http://YOUR-HUE-BRIDGE-IP-HERE/api/YOUR-GENERATED-USERNAME-HERE/lights/1/state" } ] } } }'
As you can see, we're doing two requests here. First, we run aGET
requestthat retrieves the current status of the Hue lightbulb. In the second requestthen, we use the the scripted HTTP action feature within the request the body.In order to make the script identifiable to Kiwi, it needs to be surrounded by<<{}>>
. As script language we use Elixir.
Every request that uses scripting retrieves a variable namedprevious_http_response
which either contains the HTTP response of thepreviously executed HTTP request ornil
.previous_http_response
is of type%Mojito.Response{}
. In this example, weextract thebody
from the%Mojito.Response{}
(which is a JSON string) anddecode it usingJason.decode!
. Afterwards weMap.get
thestate
map fromthe decoded body map and thenMap.get
itson
property – which is a booleanvalue. Ifon
istrue
, it means that our light is currently turned on.If it'sfalse
, it means that it's currently off. Last but not least, we pipethe boolean value toKernel.not
, which inverts the boolean state. The theinverted state is the return of this script, will be converted to a JSONrepresentation and used as a value in the very place our<<{ ... }>>
is.
The result of all this: If the current state of the lightbulb isfalse
, it'sbeing inverted totrue
and set as value for theon
property inside ourrequest's body. The request will then execute with a JSON body that says{"on": true}
, so that the Hue turns the lightbulb on. When we press the keyanother time, theGET
request will retrievetrue
inside ofresponse.body.state.on
and the upcomingPUT
request will fetch this value,invert it and send{"on": false}
, so that the light turns back off.
Scripted HTTP actions allow you to do many fancy things with little knowledgeof HTTP requests and Elixir. However keep in mind that the scripts you writeare being executed within the same environment in which Kiwi runs and havepretty much the same permissions (access keys, access LEDs, access your WiFi).Hence, always make sure to validate data that you retrieve from endpoints youhave no control of!
Of course you can also do crazy things with the integrated LEDs your Keybow has.In order to configure a fancy key-press light-animation, run the followingcommand:
curl -X POST"http://10.10.10.10:8080/settings/keys/key_1_in_row_1" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "object": { "keydown": { "led": { "frames": [ { "sleep": 50, "keys": { "key_1_in_row_1": { "brightness": 227, "red": 55, "blue": 0, "green": 0 } } }, { "sleep": 50, "keys": { "key_1_in_row_1": { "brightness": 227, "red": 155, "blue": 0, "green": 0 } } }, { "sleep": 50, "keys": { "key_1_in_row_1": { "brightness": 227, "red": 255, "blue": 0, "green": 0 } } }, { "sleep": 50, "keys": { "key_1_in_row_1": { "brightness": 227, "red": 155, "blue": 0, "green": 0 } } }, { "sleep": 50, "keys": { "key_1_in_row_1": { "brightness": 227, "red": 55, "blue": 0, "green": 0 } } }, { "sleep": 0, "keys": { "key_1_in_row_1": { "brightness": 227, "red": 0, "blue": 0, "green": 0 } } } ] } } } }'
When you now press the key, it should light up in red and quickly fade off.As you can see,frames
is an array of animation frames containing theduration between each frame (sleep
) and thekeys
that should be targeted.You can even specify multiple keys by adding them to thekeys
object.
Actions can be combined by adding all desired action to the JSON, e.g.:
curl -X"POST""http://10.10.10.10:8080/settings/keys/key_2_in_row_1" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "object": { "keydown": { "led": { ... }, "http": [{ ... }] } } }'
Retrieve the current configuration for a specific animation.
curl"http://10.10.10.10:8080/settings/animations/animation_main" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{}'
This endpoint allows setting constantly playing LED animations on the keyboard.:animation
defines the animation you'd like to define. By default, Kiwi loadsthe animationanimation_main
.
You can configureanimation_main
like this:
curl -X"POST""http://10.10.10.10:8080/settings/animations/animation_main" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "object": { "frames": [ { "sleep": 500, "keys": { "key_1_in_row_1": { "red": 255, "green": 0, "blue": 0 } } }, ... ... ... ... ... ] } }'
Alternatively you can useimage2kiwi togenerate an animation from a JPG, PNG, GIF, etc. Please refer to itsdocumentation on how image generation works. Here's the curl example of how toset a generated animation:
curl -X"POST""http://10.0.0.219:8080/settings/animations/animation_main" \ -H'Content-Type: application/json; charset=utf-8' \ -d"{\"object\": {\"frames\":$(python3 ./image2kiwi.py ./rainbow.gif) }}"
Notice: image2kiwi scales the image to max 3x4px. If you pass an image that's300x300px, its aspect will be kept and it will be scaled to 3x3px. Hence, thebottom three keys won't light up.
Find the IP address of your Philips Hue Bridge on your network and create adedicated user for Kiwi:
curl -X"POST""http://YOUR-HUE-BRIDGE-IP-HERE/api" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "devicetype": "kiwi#kiwi" }'
This request will return an auto-generated username. With this you can thencheck all your connected lights:
curl"http://YOUR-HUE-BRIDGE-IP-HERE/api/YOUR-GENERATED-USERNAME-HERE/lights" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{}'
After you've identified the light you'd like to turn on/off, configure two keys:
curl -X"POST""http://10.10.10.10:8080/settings/keys/key_1_in_row_1" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "object": { "keydown": { "http": [{ "body": "{\\"on\\": true}", "method": "put", "headers": { "content-type": "application/json" }, "url": "http://YOUR-HUE-BRIDGE-IP-HERE/api/YOUR-GENERATED-USERNAME-HERE/lights/1/state" }] } } }'
curl -X"POST""http://10.10.10.10:8080/settings/keys/key_2_in_row_1" \ -H'Content-Type: application/json; charset=utf-8' \ -d$'{ "object": { "keydown": { "http": { "body": "{\\"on\\": false}", "method": "put", "headers": { "content-type": "application/json" }, "url": "http://YOUR-HUE-BRIDGE-IP-HERE/api/YOUR-GENERATED-USERNAME-HERE/lights/1/state" } } } }'
That's it!
Want to turn your Philips Hue lights on and off with a single button? Checkthe advancedscripted HTTP actionstopic!
- WiFi
- Pimoroni Keybow
- An empty microSD card
- Elixir installed on your computer
Make sure to have Erlang and Elixir installed on your computer!
$ git clone https://github.com/mrusme/kiwi.git$cd kiwi$ make dependencies
Note: For development it makes sense that you enable the WiFi configurationblock withinconfig/config.exs
, so that you can pass the WiFi config viaenvironment variables during the build.
Insert microSD card into the microSD card reader of your computer and find itsblock device, e.g. /dev/disk3 (on a Mac), unmount it and use its raw version:
$ diskutil umountDisk /dev/disk3$ NERVES_NETWORK_KEY_MGMT=WPA-PSK NERVES_NETWORK_SSID=yourWifiNetworkName NERVES_NETWORK_PSK=yourWiFiPassword SD_CARD=/dev/rdisk3 make sdcard
Eject the microSD card, insert it into your Keybow's Raspberry Pi Zero andconnect it to a power source.
As soon as the device has booted it should be connected to the WiFi. If itisn't you probably screwed upNERVES_NETWORK_KEY_MGMT
,NERVES_NETWORK_SSID
and/orNERVES_NETWORK_PSK
and need to re-run the installation with correctvalues.
Sorry.
Check your WiFi access point's web-interface to find out the IP address thatwas assigned to your Keybow. As soon as you found the IP you can startconfiguring the device via its API.
Here's some useful information if you might want to start contributing to thisproject yourself and want to save yourself from having to browse theofficial (undocumented!) Keybow's firmware code.
Keybow╔════╦════╦════╗║ 20 ║ 16 ║ 26 ║╠════╬════╬════╣║ 6 ║ 12 ║ 13 ║╠════╬════╬════╣║ 22 ║ 24 ║ 5 ║╠════╬════╬════╣║ 17 ║ 27 ║ 23 ║╚════╩════╩════╝
- 4 bytes padding at the beginning
- 4 bytes per LED:
brightness, blue, green, red
- 4 bytes padding at the end
[0,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,227,0,0,0,255,255,255,255]
@Gadgetoid provided very goodinsight on this topic here.Quote:
This magic number is the LED frame marker, which is indicated by 3 high bits set to 1: 0b11100000 plus 5 bits of global (applies to R, G and B LEds in a package) brightness: 0b00011111 (giving an 0-31 brightness range). Since we don't want to drive at maximum brightness, but still want colours to have the full 255255255 range, we use global brightness to handle the dimming across the LEDs.
Quote from thelinked wordpress.com site:
...
Each update consists of a start frame of 32 zeroes, 32 bits for every LED andan end frame of 32 ones. I am not sure what the “End Frame” is good for,since its encoding is indistinguishable from a LED set to maximum brightnessand will simply be forwarded to the next LED. In my experiments, omitting theend frame did not have any impact.
One interesting addition is the “global” field for each LED, which allows tocontrol the brightness of the LED in 32 steps from 0 to 31. When tryingdifferent parameters, I was quite surprised to observe that the LEDs did notshow any visible pulse-width-modulation (PWM) flicker at all when the globalbrightness was set to 31. This is quite different from the WS2812, whichshows visible PWM flicker when moving the LEDs.
Interestingly, the APA102 started to flicker once I set the global brightnessto 30 or below.
...
Thanks to Tim who apparently runs that blog!
In most cases, simply unplugging and plugging the Keybow back in will fix everyissue that might occur during runtime.
You can connect to your Kiwi instance through SSH:
ssh root@10.10.10.10
There you'll end up inside a IEx in which you can start investigating yourissue. Keep in mind that there will be no Logger output unless you executeRingLogger.next
or attach to the RingLogger usingRingLogger.attach
.
About
Pimoroni Keybow based, WiFi-enabled Macro Pad (a.k.a. poor-man's Elgato Stream Deck)