Skip to content

Latest commit

 

History

History
847 lines (644 loc) · 33.6 KB

README.md

File metadata and controls

847 lines (644 loc) · 33.6 KB

Local Network Cloudbit

Introduction

The Littlebits w20 cloudbit bit is a great idea: it allows your Littlebits circuits to be connected to the Internet, thus letting you integrate your contraptions with other services such as Google Home (via IFTTT) or even your own homegrown home automation systems. Unfortunately, the Littlebits Cloud is very unreliable. For something like this to work, you really need near-100% uptime, and in my experience the API has an uptime of about 70%, sometimes being down for weeks at a time.

In any case, it's a little crazy that a little circuit on your network should communicate with a remote server if all it wants to do is tell your Raspberry Pi on the other side of the room what the temperature is or when a button gets pushed or when your laundry washing machine has finished.

The cloudbit is just an embedded Linux computer, though, and Littlebits kindly left a C compiler and a Perl interpreter on it, so experimenting with the cloudbit is pretty straight-forward and solving the problem of having to talk to the Internet just to send a message six meters across the room is emminently addressable.

Communicating with the local network instead of the cloud

This repository contains a replacement for the default behaviour of the cloudbit, so that instead of reporting the current value of the cloudbit to the Littlebits servers, it just sends it to a specified local server.

There are three ways to install this program. In all three cases, you will need a machine where you can mount ext2 file systems (e.g. a Linux box, or a Mac using FUSE for macOS). Then, you either:

  • Use a bash shell to a script from this repository to complete the installation automatically.

  • Walk through the script's steps manually.

  • Enable SSH access on the cloudbit, then SSH into the cloudbit and install the software directly there using the script from the repository.

Commissioning the cloudbit

Before you do anything described below, first make sure your cloudbit is configured for your local network. The steps below disable the Littlebits code that allows you to configure the network by pushing the button on the cloudbit. To reconfigure the cloudbit after setting things up as below, you will either need to reinstall the firmware using the Littlebits updater, or manually figure it out.

You may also want to take note of the cloudbit's MAC address, since that is used to identify the cloudbit.

Mounting the SD card

The details for mounting the SD card vary from system to system. Consult your local documentation (e.g. the mount program on Unix systems) for instructions on doing this.

The SD card seems to have three partitions; you only need the third one (the biggest one) which is where the Linux userspace filesystem is stored.

Installing automatically

You will need: bash, sudo, curl, grep, git, mkdir, mv, ln

This has only been tested on Linux. It may work on other Unix-like systems such as macOS, but this is untested.

Once you have the SD card mounted, run the command below.

This command prompts for your root password (because of the sudo). This is necessary because the filesystem you have mounted has files that are only writable by root. If you have configured your SD card to be mounted so that it is user-writable on your host but will create files that have root as the owner when run by the cloudbit, then you can omit the sudo.

It's generally considered very sketchy to pipe a program from the network into sudo bash; even piping it into bash without the sudo isn't great since it can compromise your account. The shell script downloads a binary onto your cloudbit; that binary could also be compromised. I highly recommend going through the more tedious manual process below instead, that way you have much more control over what exactly is going on.

But if you're ok with trusting me and GitHub and are sure you're not experiencing a state-level attack on your communications or some other such thing, then run the following command, replacing server.example.com with the name of the host to which you want to send the network messages, in a shell open to the root directory of the mounted SD card filesystem:

curl https://raw.githubusercontent.com/Hixie/localbit/master/install.sh | sudo bash -s server.example.com

Then, unmount the SD card, install it back in your cloudbit, and boot up your cloudbit. If everything worked, the LED should turn off once it has booted, and network traffic will start flowing to port 2020 of the host you specified on the command line above. If the LED turns any colour other than white, then something went wrong (unless you are already sending your own packets to the cloudbit!).

Security considerations

The script above disables the routing tables that keep the cloudbit safe from being attacked (it has to, since the whole point is to listen to UDP connections). It's possible that this will expose you to some vulnerability in some software running on the cloudbit.

Installing manually

A technically safer alternative to the above is to walk through the following steps. You can either do it over SSH on the device itself, or on the SD card directly.

The file /usr/local/lb/etc/lb_scripts.conf on the SD card ends with a line that identifies the firmware version you are using. This repository assumes you are using version 1.0.150603a, which as of the time of writing is the latest version. If necessary, you can download updates from littlebits.com.

Setting up SSH access for a w20 cloudbit

If you are going to set everything up on the SD card directly, you can skip this step.

Prior art: Chris Wilson, chobie, rhomel.

To enable SSH, you need to make three changes: changing the root password, enabling the SSH service, and turning off iptables.

There's one user account set up, the root account. To change its password, use mkpasswd -m sha-512 to generate a new hash, and then edit /etc/shadow and replace the hash (between the first two colons) for the root user with the new hash. The password you give mkpasswd will be the password you use to log in to your cloudbit.

To enable SSH, first edit /etc/ssh/sshd_config. There are two lines to change. First, uncomment the line that says PermitRootLogin yes, otherwise it will default to "no", which, as you might guess, disables login. The other line to change is the one that says UsePAM yes; change that to UsePAM no. The Pluggable Authentication Module (PAM) interface overrides the root login setting.

Next, enable it by setting up symlinks in the /etc/systemd/system/multi-user.target.wants/ directory as follows:

sudo ln -s /usr/lib/systemd/system/sshdgenkeys.service sshdgenkeys.service
sudo ln -s /usr/lib/systemd/system/sshd.service sshd.service

Finally, you have to turn off iptables. Create a disabled subdirectory in the /etc/systemd/system/multi-user.target.wants/ directory, and then move the iptables.service file in /etc/systemd/system/multi-user.target.wants/ into this new disabled directory.

Once you are SSH'ed into the cloudbit, you can either continue following these steps, or you can run the install.sh script mentioned above (using the same instructions as above) to have the script do them for you. This is slightly safer than running the script on your machine, since if your cloudbit gets compromised, you only lose your cloudbit, instead of your workstation.

Configuring the local control client on the cloudbit device

You must specify the host name of the server to which messages will be sent in /root/localbit_server.cfg. This file should contain only the host name optionally followed by a newline character, and nothing else.

From the root of the SD card filesystem (either after SSHing into the cloudbit, or on your host with the SD card mounted), run the following, replacing "server.example.com" with the host name that you will be running your server on:

echo server.example.com > root/localbit_server.cfg

Fetching the code

The code for the localbit software is on GitHub.

From the root of the SD card filesystem (either after SSHing into the cloudbit, or on your host with the SD card mounted), run the following to download it to your SD card:

git clone https://github.com/Hixie/localbit.git root/localbit

If you are running this on your host and have mounted the SD card normally (i.e. honouring the permissions on the SD card), you may need to run this with sudo since the SD card filesystem's /root/ directory is only writable by root.

The code for the program that sits between the cloudbit hardware and the network is in localbit.c.

Compiling the code

A compiled binary of localbit.c suitable for use on machines with the architecture of cloudbits is also in this repository. However, if you would prefer to compile it yourself, you may do so by SSHing into the cloudbit and using the conveniently preinstalled gcc that Littlebits provides.

Run this command in the /root/localbit directory (the one you downloaded from git above) to regenerate the binary. You must be SSHed into the cloudbit for this to work. In theory you could also cross-compile from your host to the architecture of the cloudbit, but that is left as an exercise for the reader.

gcc localbit.c -lm -o localbit

If you are compiling your own version, consider using -O3 or -Ofast. The version provided in the repository is currently compiled with -Ofast, mostly to conserve power so that the bit doesn't heat up too much.

Turning off the built-in programs

If you followed the instructions earlier for enabling SSH, you will have created a disabled subdirectory in the /etc/systemd/system/multi-user.target.wants/ directory on the cloudbit SD card. If you did not do that, then do it now.

Then, move the following files from /etc/systemd/system/multi-user.target.wants/ into this disabled directory:

  • iptables.service (you will already have moved this if you enabled SSH)
  • ADC.service
  • LEDcolor.service
  • button.service
  • dac_init.service
  • dac.service
  • netctl-monitor.service
  • onboot.service

Be aware that this will disable a number of features of the cloudbit:

Removing button.service disables the commissioning script (holding down the button won't do anything anymore; normally button.service runs a program that watches the button and triggers the commissioning script if the button is help for long enough). If you ever need to update the wifi configuration of this device, move these files back into /etc/systemd/system/multi-user.target.wants/ first.

Removing netctl-monitor.service will prevent monitorNetctl.pl from running. That script is mainly in charge of managing the program that sends the status of the cloudbit to the cloud, but, among other things, it also runs the command that enables the network and sets the system clock (there's no clock battery on the cloudbit).

Removing dac_init.service will prevent DAC_init from running at startup, and that program must be run before any code uses the DAC.

The other services all implement various features mostly used by the three called out explicitly above.

As a replacement for all the above, this repository comes with a short shell script that enables the network, sets the date and time, and calls the DAC_init program. To install it, run the following command inside the /etc/systemd/system/multi-user.target.wants/ directory:

sudo ln -s /root/localbit/localbit-startup.service localbit-startup.service

This script will log to /var/log/localbit-startup if you are curious to see how successful it is.

This script does require internet access. It tries to ping 8.8.8.8 (a Google DNS server) to test if the network is operational, and uses NTP time servers to update the clock. (It honours the /usr/local/lb/etc/lb_scripts.conf script, in particular to determine which NTP server to use (NTP_SERVER), which IP address to ping to test the network (PING_DEST), and the directories to use to find the Littlebits binaries.)

Turning on the localbit program

To automatically run the localbit binary and keep it running, run the following inside the /etc/systemd/system/multi-user.target.wants/ directory:

sudo ln -s /root/localbit/localbit-daemon.service localbit-daemon.service

This script runs the localbit binary compiled above, which listens to the ADC and the button and reports their status to the network, and listens to the network for commands for changing the status of the output and the LED.

If you want to turn it off, e.g. so that you can hack on it, run systemctl stop localbit-daemon.

The localbit network protocol

The protocol uses UDP.

The localbit program running on the cloudbit periodically sends the following packet to the configured host on UDP port 2020:

  • 6 bytes of the cloudbit's MAC address.
  • 2 byte word in network byte order (big-endian) which is 0x0001 if the button is pressed and otherwise 0x0000.
  • 2 byte word in network byte order (big-endian) representing the input (0x0000 to 0xFFFF). The value is only significant to about about 10 bits, and even the last two of these "significant" bits fluctate a lot, so if 8 bit precision is enough, you can just treat the first of these two bytes as the value in the range 0 to 255 and ignore the second byte.
 00 01 02 03 04 05 06 07 08 09
+--+--+--+--+--+--+--+--+--+--+
|   MAC ADDRESS   |00| B| VAL |
+--+--+--+--+--+--+--+--+--+--+
  Button state --------'   |
  Input value -------------'     16 bit word, big-endian.

Button state:
MSB                   LSB
+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |SS|
+--+--+--+--+--+--+--+--+
  State --------------'          Whether the button is set.
                                 0x00 = no, 0x01 = pressed

The localbit program listens for UDP packets on port 2021. If they start with 6 bytes that match the cloudbit's MAC address, then the next byte is examined. This is the LED state byte. If this byte's 8th bit is set (0x80), then then bits 1 through 3 of the LED state byte are interpreted as the red, green, and blue channels for the LED respectively. The byte after this is the output state byte. If the high bit is set, then the next two bytes are interpreted as the 16 bit value in network byte order (big-endian) to which to set the output (0x0000 to 0xFFFF).

 00 01 02 03 04 05 06 07 08 09
+--+--+--+--+--+--+--+--+--+--+
|   MAC ADDRESS   |LL|OO| VAL |
+--+--+--+--+--+--+--+--+--+--+
  LED state -------'  |    |
  Output state -------'    |
  Output value ------------'     16 bit word, big-endian.

LED state:
MSB                   LSB
+--+--+--+--+--+--+--+--+
|CC|  |  |  |  |RR|GG|BB|
+--+--+--+--+--+--+--+--+
 '---- Control  |  |  |          Whether to change the LED.
  Red ----------'  |  |          If CC is set, new red channel state.
  Green -----------'  |          If CC is set, new green channel state.
  Blue ---------------'          If CC is set, new blue channel state.

Output state:
MSB                   LSB
+--+--+--+--+--+--+--+--+
|CC|  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+
 '---- Control                   Whether to change the output value.

The rate of messages being sent from the cloudbit increases if the input's value fluctuates or if the button is pressed, and decreases if the input's value remains steady. This is implemented by inserting a delay in the program loop. If the input's value is seen to fluctate dramatically, the delay is set to its minimum value immediately; if the rate of change is slow, then the delay is decreased by 10ms; if the value barely changes at all, then the rate is increased by 10ms up to the maximum delay.

The fastest rate at which messages are being sent (e.g. while the button is being held down), is 100Hz (one message every 10ms). The slowest rate is about 0.5Hz (a delay of 2010ms).

A new message is always sent immediately when one is received, without affecting the steady state rate, so the value may be polled by sending a message with the two control bits set to zero (i.e. a ten byte message consisting of the six MAC address bytes followed by four zero bytes). The minimum delay is still enforced; at most one packet is consumed every 10ms.

This variable rate is intended to keep the load on the cloudbit CPU to a reasonable level so that the cloudbit can run continually with minimal risk of damage. Be wary of sending polling messages at a high rate continually since doing so defeats this mitigation.

Practical considerations

On a local network, realistically, the highest signal frequency you can expect to send to the cloudbit with any sort of accuracy is about 2Hz, with a latency of anything from 20ms to 100ms.

The following images show some oscilloscope traces of the input going into the cloudbit and the output coming from the cloudbit, for a square wave signal that is being reflected back to the cloudbit over the local network, at the highest rate that localbit will operate (100Hz), in close-to-ideal network conditions. The first is for an input wave of 2Hz, the second for an input wave of 15Hz.

At 2Hz, the output signal is delayed but vaguely resembles the input. At 15Hz, the output signal has much more noticeable gaps and irregularities.

Notes on the w20 cloudbit

The remainder of this file consists of notes I made while reverse-engineering the cloudbit.

Hardware

According to /proc/cpuinfo and /proc/meminfo, the cloudbit has a cARM926EJ-S rev 5 (v5l) processor (113.04 BogoMIPS), 59844 kB of RAM, and a ~2.6GB SD card.

The schematic for the cloudbit is available on GitHub.

The hardware is controlled from code by manipulating memory locations, as described below. The hardware is physically connected to pins on the chip on the cloudbit. It's not clear to me how the pins map to memory locations.

The cloudbit ships with some proprietary Littlebits programs. To determine their behaviour, a program such as retdec is invaluable.

The LED

The program /usr/local/lb/LEDcolor/bin/LEDcolor.d, which is run when the cloudbit boots, opens a Unix domain socket (/var/lb/SET_COLOR_socket), and executes commands written into that socket to change the colour of the LED.

The program /usr/local/lb/LEDcolor/bin/setColor writes commands given on its command line to that socket.

The commands are colours, e.g. specifically red, green, blue, yellow, teal, purple (or violet), and white, the command off which turns off the LED entirely, the two commands blink and hold which stop and start a periodic timer that switches between the current colour and off, and the command clownbarf which I'll leave as a surprise. The command off has no effect if a blink is in effect, and blink has no effect if off was the last colour triggered. Setting a colour seems to reset the blink timer, with the LED enabled.

The 2KB page starting at 0x80018000 is used by the LEDcontrol.d program to control the LED. This will be called gpioPage below.

When the program starts, it sets the following memory locations to the following values, in the following order:

Address Value
gpioPage + 0x0114 0xF0000000
gpioPage + 0x0134 0x03000000
gpioPage + 0x0704 0x40000000
gpioPage + 0x0714 0x10000000

I've no idea what these values do.

The LED is controlled by three lines, red, green, and blue, which can be turned on or off. To turn one of the lines on, you send the value to the address to turn on, and to turn it off, you send the value to the address to turn it off.

Color Address to turn on Address to turn off Value
Red gpioPage + 0x0508 gpioPage + 0x0504 0x80000000
Green gpioPage + 0x0508 gpioPage + 0x0504 0x40000000
Blue gpioPage + 0x0518 gpioPage + 0x0514 0x10000000

These channels correspond to pins 128, 127, and 91 respectively on the big box on the schematic.

Areas for further research

I haven't tested to see if you can read the addresses listed above. I haven't tested what happens if you try to read or write nearby addresses, or set values other than those listed above (except I did once try to set one of those addresses to 0xFFFFFFFF and the wifi dropped until I rebooted the device, though the device itself was still executing code, which I could tell because the LED was still blinking).

Analog-to-digital convertor (input)

As with the LED, the ADC is controlled by a program /usr/local/lb/ADC/bin/ADC.d, which is run when the cloudbit boots, and which opens a Unix domain socket (/var/lb/ADC_socket). When a message is written to the socket, it outputs the current value of the input to the socket.

The program /usr/local/lb/ADC/bin/getADC interacts with this socket. You give it an arbitrary argument, such as -1, and it outputs two lines each containing a number. The first is a number in the range 0 to 255 which corresponds to the input being in the range 0.0V to 5.0V. The second is a number if the range 0 to 15 and I've no idea what it corresponds to, except that when the input is off it's 0, when the input is on full it's 15, and when the input is between those it varies wildly between those two numbers (seemingly not with any correlation to the input value).

The 2KB page starting at 0x80050000 is used by the ADC.d program to read the input. This will be called adcPage below.

When the program starts, it sets the following memory locations to the following values, in the following order (note that the values are not in ascending order):

Address Value
adcPage + 0x0008 0x40000000
adcPage + 0x0004 0x00000001
adcPage + 0x0028 0x01000000
adcPage + 0x0014 0x00010000
adcPage + 0x0034 0x00000001
adcPage + 0x0024 0x01000000
adcPage + 0x0144 0x00000000

I've no idea what these values do. It's worth noting that there's also code in ADC.d to do something with memory at address 0x80068000, though it doesn't appear to actually be used.

Updating the system memory with the values above is not sufficient to make the ADC work. If you set the values above then read the ADC values as described below, the program will hang. However, if you run ADC.d, then kill it, then read the value as described below, it works fine. I have no explanation.

To read the ADC value, it sets the value at adcPage + 0x0004 to 0x00000001, waits until the high bit of the 32 bit word at adcPage + 0x0050 changes value, then sets the value at adcPage + 0x0018 to 0x00000001. The value of the ADC is the low 31 bits of the 32 bit word at adcPage + 0x0050. (Actually, more like the low 11 bits.)

The value appears to be a number in the range ~200±1 (when the input is at 0V) to ~1700±10 (when the input is at a nominal 5V, though it sometimes dips as far as ~1660 or so even when the input is supposedly fully on).

Pins 115 and 108 on the schematic, labeled LINE1_INL and LRADC0 respectively, are connected to SIGIN1, which corresponds to the input side bitSnap, which is the input to the ADC.

Areas for further research

I haven't determined what ADC.d does that needs to be done to actually read the ADC.

Digital-to-analog convertor (output)

The pattern repeats itself with the DAC, but with a bit of a twist.

When the cloudbit boots, it first runs /usr/local/lb/DAC/bin/DAC_init, which does a bunch of memory manipulation that I haven't tried to understand.

There is a socket-listening program, /usr/local/lb/DAC/bin/DAC.d, which is run when the cloudbit boots, after DAC_init. It opens a Unix domain socket (/var/lb/DAC_socket) and listens to it. When a message is written to the socket, it tries to parse it as up to four hexadecimal digits (0-9, a-f; case-insensitive), and then sets the output of the cloudbit to a voltage by doing a linear mapping from the range 0x0000 to 0xFFFF to the range 0.0V to 5.0V. If the message is not one to four hexadecimal characters, it is ignored.

The program appears to try to log errors to /var/lb/dacerrorlog though I haven't ever seen that file be non-empty.

The program /usr/local/lb/ADC/bin/setDAC interacts with the aforementioned socket by just passing whatever argument it is given to the socket (unless it's --help, in which case it outputs a mostly useless help message instead).

The 2KB page starting at 0x80050000 is used by the DAC.d program to set the output. This will be called dacPage below. (The DAC_init program also uses that page as well as two other pages.)

There are two addresses that are used by DAC.d as far as I can tell. The first is dacPage + 0x00F0, which is used to set the value. The range 0x8000 to 0xFFFF seems to represent voltages in the range 0.0V to 2.5V, and the range 0x0000 to 0x7FFF seems to represents voltages in the range 2.5V to 5.0V. Thus, if value is in the range 0x0000 to 0xFFFF, set the memory location at dacPage + 0x00F0 to (value + 0x8000) % 0x10000.

The second address used by DAC.d is dacPage + 0x0040, which is used as a signalling mechanism; specifically it appears that the second bit at that address toggles when the DAC is done dealing with the last value that was set.

In practice, setting the value at dacPage + 0x00F0 doesn't take until you've actually set it three or four times, so DAC.d tries to set it up to 20 times in a row each time it is supposed to update the DAC.

Pin 113 on the schematic, labeled HPL, corresponds to the DAC's output, the output side bitSnap.

Areas for further research

I haven't determined what DAC_init does.

The Button

The button is managed by /usr/local/lb/Button/bin/button.d. It uses the socket /var/lb/BUTTON_socket. If the button is pressed for long enough, button.d itself will run /usr/local/lb/comm-util/signal_netmon.sh directly.

The /usr/local/lb/Button/bin/getButton program communicates via the socket to report the state of the button.

The button.d program operates in a manner similar to the other programs, reading memory locations to find the state of the hardware. Specifically, it uses the same 2KB page starting at 0x80018000 the LEDcontrol.d program uses to control the LED. This will again be called gpioPage below.

When the program starts, it sets the following memory locations to the following values, in the following order:

Address Value
gpioPage + 0x0124 0x0000C000
gpioPage + 0x0718 0x00000080

The actual status of the button is then available as bit 8 of the 32 bit word at gpioPage + 0x0610. (The other bytes seem unrelated to the button, but were not set to zero when I was testing this.)

The button is wired to pin 9 on the schematic, labeled LCD_D07 and TACTSWITCH.

Original software

The cloudbit uses a Linux distribution with systemd as the init, and pacman as the package manager. I presume it's an archlinux variant.

uname -a reports the following:

Linux noctilucent 3.7.2-8-littleARCH #1 PREEMPT Fri May 16 11:39:57 EDT 2014 armv5tejl GNU/Linux

Some interesting unique aspects of the configuration:

  • /var/lb/mac contains the MAC address of the cloudbit. It is created by /usr/local/lb/bit-util/onBoot.sh which runs early on boot.

  • /etc/wpa_supplicant/cloudbit.conf contains the network configuration last provided by the user when commissioning the device (including the password unencrypted).

  • There's a pair of shell scripts that set the CPU speed: /usr/local/lb/bit-util/hiPowerSet.sh and /usr/local/lb/bit-util/lowPowerSet.sh.

Notes on the o28 LED matrix (as driven by the w26 codebit)

The w26 codebit sends serial binary data to the o28 LED matrix to show images and scrolling text. It uses an RS-232-like wire encoding at 58K/8-N-1; that is, 57600 baud, 8 data bits, no parity bit, and one stop bit (whose width is usually slightly more than one bit's width).

Specifically, data is sent in packets of eight-bit bytes. Each byte takes about 175.5μs. Each byte begins with 17.3μs at 0V, then eight bits, least significant bit first, each 17.3μs long, at 0V for 0 (clear) and 5V for 1 (mark), and finally a terminating 5V for 19.5μs. When no data is being sent, the voltage of the data line is kept at 5V.

For reasons I cannot explain, pulses from the w26 codebit are sometimes arbitrarily longer than they should be according to the description above. The o28 LED matrix appears to either ignore bogus data or somehow correct for it; either way, I don't see glitches on the LED matrix when glitches happen in the output of the codebit.

Data format

Each packet starts with an 0x1C byte, a message type byte, a presentation configuration byte, and a length byte. This is followed by message-specific byte data whose length equals the length byte. Finally, the message ends with a trailing 0x26 byte. Messages are therefore a maximum of 260 bytes long.

 00 01 02 03  04...    04+LL
+--+--+--+--+---------+-----+
|1C|TT|PP|LL| data... | 26  |
+--+--+--+--+---------+-----+
    |  |  |
    |  |  `----------- Length byte
    |  `-------------- Presentation, see section below
    `----------------- Message type

There are two message types that seem to be used by the w26 codebit and corresponding software: 0x00 for images, and 0x01 for scrolling text.

Presentation

The third byte of every message represents the LED matrix display presentation configuration, and the intended audience for the message.

The high 4 bits represent the number of displays in the configuration. The o28 LED matrix uses this to know how many channels to allow the user to cycle through when they press the channel button. Values from 1 to 4 are valid. I haven't yet tested what happens with invalid values.

The low 4 bits represent which channel the message is intended for. For images in a four-display configuration, four packets are sent, each containing a different 8x8 image and assigned to a separate channel. The four images for display channels 1 through 4 are transmitted with the presentation configuration bytes set to 0x41, 0x42, 0x44, and 0x48 respectively. For text, all the channels are enabled simultaneously, so in a horizontal four-display configuration, the layout configuration byte is sent as 0x4F. (In a vertical configuration, only one display is enabled, using 0x1F.)

The o28 LED matrix ignores messages sent for channels beyond the number of channels declared in the high four bits.

Image messages (0x00)

Each image is a sent as a 12.1ms burst of data, consisting of 4 leading preamble bytes, 64 pixel bytes, and 1 trailing postamble byte.

When the image is configured to target a single LED matrix, the preamble is four bytes, 0x1C001140 (in network byte order), and the postamble is one byte, 0x26.

The image data is 8 bit RGB. On the wire, the first two bits of each byte represent the blue signal, the next three bytes represent the green signal, and final three bytes represent the red signal. This is equivalent to the low two bits being blue, the next three bits being green, and the high three bits being red.

 00 01 02 03  04...67      68
+--+--+--+--+-------------+--+
|1C|00|PP|LL| RGB data... |26|
+--+--+--+--+-------------+--+
    |  |  |
    |  |  `----------- Length byte, always 0x40 for a single image (64 pixels)
    |  `-------------- Presentation, see section above
    `----------------- Message type, 0x00 for images

Scrolling text messages (0x01)

Text is actually sent as ASCII text, not as a series of images, though each animation frame is sent as an individual message.

The data section of each message (following the preamble described above) consists of a colour byte, a big-endian 16 bit value representing how much the offset to apply to the text (in a multiple display configuration, the offset is affected by the display's selected channel, so that adjacent displays appear as one long display scrolling the same text), and the text.

The full packet is sent as follows:

 00 01 02 03 04  05 06 07...           04+LL
+--+--+--+--+---+-----+---------------+-----+
|1C|01|PP|LL|RGB|SS SS| ASCII data... | 26  |
+--+--+--+--+---+-----+---------------+-----+
    |  |  |   |    |
    |  |  |   |    `-- Scroll offset, 16 bit word, big-endian
    |  |  |   `------- Color byte, 8 bit RGB: RRRGGGBB
    |  |  `----------- Length byte, 3 plus the length of the string
    |  `-------------- Presentation, see section above
    `----------------- Message type, 0x01 for scrolling text

The font is monospace; characters are 6 pixels wide including a 1 pixel wide margin between characters. The scroll offset appears to be in pixels; a scroll offset of zero indicates that the text should be flush left with the first display, and positive numbers move the text further to the left. Typically a message would be scrolled from offset zero to offset LL*5 (the width of the message).

The w26 codebit's software always prefaces scrolling text messages with two 0x20 bytes (ASCII space characters). The two space characters are probably intended to provide a gap between subsequent scrolls of the message, and to start the message a little off the edge of the first display.

Sending bytes above 0x7F (i.e. non-ASCII characters) results in garbage on the display at that character position; I speculate that this is reading data from outside the font array in the LED matrix.