Pimoroni Keybow Upgrade

I’ve had a little 3 key Pimoroni Keybow sat on my desk for a while. It was running the same basic config I had setup when I bought it, namely mapping the three buttons to volume down, mute and volume up respectively.

Pimoroni Keybow Mini

While this was useful, it felt like there where better uses for it.

With more and more time being spent in video meetings having quick shortcuts to mute the mic or toggle the camera on/off sounded like a good idea. But then I wondered if I could find a way to switch the key mapping on the fly.

The key mapping is done by editing a short Lua script. This is stored on the sdcard that the Pi Zero that holds the Keybow boots from. This means the layout is normally fixed. Except the latest version (0.0.4) of the sdcard image on the Pimoroni Github page added support for starting a USB serial link as well as the HID used to send the keyboard events. This is exposed in the Lua environment so I managed to build the following script.

require "keybow"

-- Keybow MINI volume/zoom controls --

function setup()
    keybow.use_mini()
    keybow.auto_lights(false)
    keybow.clear_lights()
    keybow.set_pixel(0, 0, 255, 255)
    keybow.set_pixel(1, 255, 0, 255)
    keybow.set_pixel(2, 0, 255, 255)
end

-- Key mappings --

state = 'zoom'

function handle_minikey_02(pressed)
    if state == 'zoom' then
	if pressed then
            keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_DOWN)
            keybow.tap_key("v")
            keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_UP)
        end
    elseif state == 'media' then
        keybow.set_media_key(keybow.MEDIA_VOL_UP, pressed)
    end
end

function handle_minikey_01(pressed)
    if state == 'zoom' then
	if pressed then
            keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_DOWN)
            keybow.tap_key("a")
            keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_UP)
        end
    elseif state == 'media' then
        keybow.set_media_key(keybow.MEDIA_MUTE, pressed)
    end
end

function handle_minikey_00(pressed)
    if state == 'zoom' then
	if pressed then
             keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_DOWN)
             keybow.tap_key("n")
             keybow.set_modifier(keybow.LEFT_ALT, keybow.KEY_UP)
        end
    elseif state == 'media' then
        keybow.set_media_key(keybow.MEDIA_VOL_DOWN, pressed)
    end
end

local function isempty(s)
  return s == nil or s == ''
end

function tick()
    local line
    line = keybow_serial_read()
    if not isempty(line) then 
        -- keybow_serial_write( line .. "\n" )
        if line == 'zoom' then
            keybow.clear_lights()
            keybow.set_pixel(0, 0, 255, 255)
            keybow.set_pixel(1, 255, 0, 255)
            keybow.set_pixel(2, 0, 255, 255)
            state = 'zoom'
        elseif line == 'media' then
            keybow.clear_lights()
            keybow.set_pixel(0, 255, 0, 255)
            keybow.set_pixel(1, 0, 255, 255)
            keybow.set_pixel(2, 255, 0, 255)
            state = 'media'
        end
    end

end

The serial port gets setup on /dev/ttyACM0 on my laptop so I’m toggling between the 2 modes with echo media > /dev/ttyACM0 and echo zoom > /dev/ttyACM0.

In media mode it works exactly the same as before, but in zoom mode it toggles the camera on/off, toggles mute on/off and cycles through the available cameras.

This worked but keybow_serial_read() call added 1 second of latency to each call to the tick function which really wasn’t great as it was possible to miss key presses.

A bit of digging in the git rep turned up the file that implemented the serial access and this bit of code:

int serial_open(){
    if(port_fd > -1) return 0;

    port_fd = open(KEYBOW_SERIAL, O_RDWR);

    if(port_fd > -1){
        printf("Open success\n");
        tcgetattr(port_fd, &termios);
        termios.c_lflag &= ~ICANON;
        termios.c_cc[VTIME] = 10;
        termios.c_cc[VMIN] = 0;
        tcsetattr(port_fd, TCSANOW, &termios);
    }
    return 0;
}

The termios.c_cc[VTIME] = 10; was what was causing the delay. I rebuilt the library changing the value to 1 and 0. The value is in deciseconds (1/10 seconds)

With 1 the delay was cut to a tenth of a second, which was OK, but meant you had to be very deliberate in pushing the button to make sure it didn’t get missed, which with a mute toggle is a little risky.

With 0 it worked perfectly.

The script also changes the backlight colour for the keys based on mode so I can see which is active. It should be possible to add more modes as needed.

Next up is to see if I can script the toggling the mode based on if Zoom is the currently active window. Looks like it should be possible with tools like xprop or xdotool.

Logging request & response body and headers with nginx

I’ve been working a problem to do with oAuth token refresh with the Amazon Alexa team recently and one of the things they have asked for is a log of the entire token exchange stage.

Normally I’d do this with something like Wireshark but as the server is running on a Amazon EC2 instance I didn’t have easy access to somewhere to tap the network so I decided to look for another way.

The actual oAuth code is all in NodeJS + Express but the whole thing is fronted by nginx. You can get nginx to log the incoming request body relatively simply, there is a $request_body variable that can be included in the logs, but there is no equivalent $resp_body.

To solve this I turned to Google and it turned up this answer on Server Fault which introduced me to the embedded lua engine in nginx. I’ve been playing with lua for some things at work recently so I’ve managed to get my head round the basics.

The important bit of the answer is:

lua_need_request_body on;

set $resp_body "";
body_filter_by_lua '
  local resp_body = string.sub(ngx.arg[1], 1, 1000)
  ngx.ctx.buffered = (ngx.ctx.buffered or "") .. resp_body
  if ngx.arg[2] then
     ngx.var.resp_body = ngx.ctx.buffered
  end
';

I also wanted the request and response headers logging so a little bit more lua got me those as well:

set $req_header "";
set $resp_header "";
header_filter_by_lua ' 
  local h = ngx.req.get_headers()
  for k, v in pairs(h) do
      ngx.var.req_header = ngx.var.req_header .. k.."="..v.." "
  end
  local rh = ngx.resp.get_headers()
  for k, v in pairs(rh) do
      ngx.var.resp_header = ngx.var.resp_header .. k.."="..v.." "
  end
';

This combined with a custom log format string gets me everything I need.

log_format log_req_resp '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'$request_time req_header:"$req_header" req_body:"$request_body" '
'resp_header:"$resp_header" resp_body:"$resp_body"';