I recently picked up Ubiquiti's latest version of its UniFi Cloud Key that includes a nice LCD stats display.

While playing with it, I remembered that it used to be possible to control UniFi device LEDs via an SSH session:

echo 255 > /sys/class/leds/blue/brightness # LED on
echo 0 > /sys/class/leds/blue/brightness # LED off

If it was possible to control the LED, I wondered, could the LCD display be controlled too? That turned into a fun reverse-engineering exercise I'm documenting here for others to build on.

What's the display?

The Ubiquiti forum post contained another command to turn off the LCD display, which gives a first clue:

echo 0  > /sys/class/backlight/fb_sp8110/brightness

What is fb_sp8110? It seems specific to the LCD. When Googling it, I only found other Ubiquiti forum posts:

The other two results provide the next clue: logs containing error messages from fb_sp8110 that reference an interestingly named fbtft_update_display module.

fb_sp8110 spi3.0: fbtft_update_display: <some error>

If you Google fbtft_update_display, you'll find many more results, and a clearer picture forms:

  • fbtft is a Linux framebuffer driver for SPI TFT screens
  • TFT stands for "thin-film-transistor", a specific variant of LCD screen
  • SPI stands for "serial peripheral interface", a communication protocol
  • SPI TFT displays are common in hobbyist projects (ex. Raspberry Pi)

How can we control it?

This is a good start - let's do some more basic enumeration with this new information.

If you search the kernel logs for tft, you can find specifications of the display:

root@cloudkey:/sys/class# dmesg | grep -i tft
[    0.819476] fbtft_of_value: width = 160
[    0.819479] fbtft_of_value: height = 60
[    0.819483] fbtft_of_value: buswidth = 8
[    0.819486] fbtft_of_value: bpp = 16
[    0.819490] fbtft_of_value: rotate = 180
[    0.819494] fbtft_of_value: fps = 100

And if you search the kernel logs for graphics, you can find the block device that represents the display in the filesystem (fb0):

$ dmesg | grep graphics
[    0.945633] graphics fb0: fb_sp8110 frame buffer, 160x60, 18 KiB video memory, 16 KiB buffer memory, fps=100, spi3.0 at 19 MHz

Writing to this block device should cause something to happen on the display. As a quick test, we can write random bytes to the frame buffer:

cp /dev/urandom /dev/fb0
Ta-da! A display full of (our) static

Similarly: You can easily clear / blank the display with cp /dev/null /dev/fb0.

Persisting the Screen

In a few seconds, the static disappears as the original UI returns. It's periodically redrawn by the UniFi software. If the aim is to replace the screen with our own information, we probably don't want to fight for control of the framebuffer.

We can use lsof to find which process ID is drawing the UniFi UI and then disable it. The process begins again after a device reboot.

root@cloudkey:~# lsof /dev/fb0
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
ck-ui   685 root  mem    CHR   29,0          4441 /dev/fb0
ck-ui   685 root    4u   CHR   29,0      0t0 4441 /dev/fb0	

root@cloudkey:~# pgrep ck-ui
685

root@cloudkey:~# kill 685

root@cloudkey:~# watch pgrep ck-ui

Drawing Arbitrary Data

A post on wavesharejfs's blog has some starter code for driving these displays with Python. It leverages the Python Imaging Library ("PIL" or later "Pillow"):

#!/usr/bin/env python2
import os
import struct
from PIL import Image

im = Image.open('test.bmp')

w, h = im.size

with open('temp.fb', 'wb') as f:
    for j in range(0,h):
        for i in range(0,w):
            r,g,b =im.getpixel((i,j))
            rgb=struct.pack('H',((r >> 3) << 11)|((j >> 2) << 5)|(b >> 3))
            f.write(rgb);
os.system('cat temp.fb > /dev/fb0')

You can use this to feed 160x60 pixel images into the display. You can use ImageMagick's convert tool to shrink images on the command line.

From here, you could use matplotlib to generate custom infographics about your network(s) fed by the controller's API, and so on.