Hardware project for controlling legacy flipdot panels used by BVG
Introduction¶
Note
This project was exhibited at 36C3. A large display was constructed from many flipdots. Pictures of the installation.
fluepdot is a hardware and C project for controlling flip disc panels which where in use by the Berlin public transport operator BVG.
Hardware¶
Up to 5 flipdot panels are managed by one fluepboard. It is equipped with a dual core ESP32 that supports WLAN and Bluetooth. An RS485 transceiver can be used for wired installations. You only have to solder the THT (through hole technology) connectors which are shipped together with the fluepboard as a kit.
Top |
Bottom |
Software¶
The fluepboard comes pre-flashed with a firmware, that allows for various ways of interacting with the flipdot.
Features:
CLI for configuration and testing purposes
mDNS for simple service discovery
HTTP API for framebuffer manipulations
SNMP for monitoring and framebuffer manipulations
BT LE support for framebuffer manipulations
C flipdot library for custom applications
Getting started¶
This short guide explains how to get from receiving a fluepdot kit to flipping pixels the first time.
Hardware assembly¶
Warning
Electrons’n’stuff can be dangerous. Use your brain and a proper 12V power supply rated for at least 3 amperes.
Perform the following steps:
Solder the components J1 (USB port), J2 (10 pins, 2.54mm, power supply, RS485 and select) and J3 (34 pins, flat cable connector) to the board.
J3 notch must point to the right, to the PCB’s border.¶
Attach the flatband cable to the boards and the fluepboard.
Connect the panel’s select pins to the PCB. Pinout is given in the rendering. The order is left (0) to right (5), assuming panel’s “top” is where the very long connector with huge amounts of pins is.
Panel select pin (highlighted)¶
In case you mess with the panel order, there are CLI commands for changing the order in software.
Provide 12V and at least 3A to the PCB as displayed on the pinout above.
Connect the fluepboard via USB to some computer.
Software¶
The fluepboard has the USB vendor id 0x1209 and product id 0x4223 assigned by pid.codes.
Please configure your system (e.g. by writing a udev rule) to allow access to the serial port.
USB-to-serial chip in use is a CP2102N. Users of linux based operating systems don’t need to install any drivers.
Note
Serial interface params
Baudrate: 115200
Data Bits: 8
Parity: None
Stop Bits: 1
Connect to the serial interface
screen /dev/ttyUSB0 115200
Configure the fluepdot panel layout
TODO
Optional: Set a hostname
config_hostname yet_another_iot_device.local
Optional: Connect to a wireless LAN
config_wifi_station <ssid> [<password>] Configure station mode <ssid> WiFi SSID <password> WiFi Password
Warning
Absolutely no attempt to protect your wireless LAN’s credentials is made. Everyone having access to the fluepboard can retrieve them easily.
The fluepboard’s firmware intentionally does not provide means of authentication. It is up to you as a network operator to restrict access (e.g. by firewalling).
Save the configuration and reboot
config_show config_save reboot
Clear and set all pixels as a test, be prepared for noise
flipdot_clear --invert flipdot_clear
Commandline interface¶
For debugging and configuration purposes the fluepboard firmware provides a simple CLI.
The serial interface is available by connecting the fluepboard to a computer via USB.
Serial interface works at 115200/8-N-1 1.
Note
After performing configuration changes, please issue a config_save
followed by a reboot
in order to apply the configuration.
Available commands¶
Type help
to get a list of available commands:
help
Print the list of registered commands
ping [-W <t>] [-i <t>] [-s <n>] [-c <n>] [-Q <n>] <host>
send ICMP ECHO_REQUEST to network hosts
-W, --timeout=<t> Time to wait for a response, in seconds
-i, --interval=<t> Wait interval seconds between sending each packet
-s, --size=<n> Specify the number of data bytes to be sent
-c, --count=<n> Stop after sending count packets
-Q, --tos=<n> Set Type of Service related bits in IP datagrams
<host> Host address
host <host>
Perform a DNS lookup
<host> Host to look up
traceroute <host>
Perform a traceroute
<host> Host to traceroute
reboot
Perform a software reset
show_version
Get version of chip and SDK
show_tasks
Get information about running tasks
config_save
Save the current system configuration to flash
config_load
Load the system configuration from flash
config_show
Show the current system configuration
config_reset
Factory reset the system configuration
config_wifi_ap <ssid> [<password>]
Configure AP mode
<ssid> WiFi SSID
<password> WiFi Password
config_wifi_station <ssid> [<password>]
Configure station mode
<ssid> WiFi SSID
<password> WiFi Password
config_hostname <hostname>
Set system hostname
<hostname> Hostname
config_panel_layout <panel_size> [<panel_size>]...
Configure panel count and sizes
<panel_size> Panel size
flipdot_clear [--invert]
Clear the flipdot
--invert Set all pixels to white instead of black
show_fonts
List installed fonts
render_font [-x <int>] [-y <int>] [-f <font>] <text>
Render some text given some font
-x, --X=<int> Depending on aligned, either left, center or right edge of target.
-y, --Y=<int> Upper edge of the target area.
-f, --font=<font> Name of font to use
<text> Text to display
Usage examples¶
Connect to a wireless network
config_wifi_station NetworkName Password config_show config_save reboot
Connect to an unencrypted wireless network
config_wifi_station NetworkName config_show config_save reboot
MTU test
ping fritz.box -s 1472
Cycle all pixels
flipdot_clear flipdot_clear --invert
Footnotes¶
SNMP¶
To serious business people and for optimal integration into your existing enterprise network, the fluepboard firmware offers SNMP integration.
The SNMP implementation provides monitoring, full control over framebuffer and rendering options as well as an IF-MIB, IP-MIB and TCP/UDP-MIB implementation.
You might find the SNMP implementation useful in combination with the prometheus snmp_exporter.
You can find the MIB (Mangement information base) file in util/FLUEPDOT.mib
.
Communities¶
- Private (write-only) community
private
- Public (read-only) community
public
- Version
v2c
Tree¶
snmptranslate -m ./util/FLUEPDOT.mib -Tp .1.3.6.1.4.1.54722
+--fluepke(54722)
|
+--projects(1)
|
+--fluepdot(1)
|
+--framebuffer(1)
| |
| +-- -R-- Integer32 width(1)
| +-- -R-- Integer32 height(2)
| |
| +--pixelsTable(3)
| |
| +--pixelEntry(1)
| | Index: pixelX, pixelY
| |
| +-- -R-- Integer32 pixelX(1)
| | Range: 0..255
| +-- -R-- Integer32 pixelY(2)
| | Range: 0..255
| +-- -RW- EnumVal pixelState(3)
| Values: dark(0), bright(1)
|
+--panels(2)
| |
| +-- -R-- Integer32 panelCount(1)
| | Range: 0..5
| |
| +--panelTable(2)
| |
| +--panelTableEntry(1)
| | Index: panelIndex
| |
| +-- -R-- Integer32 panelIndex(1)
| | Range: 0..5
| +-- -R-- Integer32 panelWidth(2)
| | Range: 20..25
| +-- -R-- Integer32 panelX(3)
| Range: 0..255
|
+--renderingOptions(3)
| |
| +--delayTable(1)
| | |
| | +--delayEntry(1)
| | | Index: column
| | |
| | +-- -R-- Integer32 column(1)
| | | Range: 0..255
| | +-- -RW- Integer32 columnPreDelay(2)
| | +-- -RW- Integer32 columnSetDelay(3)
| | +-- -RW- Integer32 columnClearDelay(4)
| |
| +--panelOrderTable(2)
| | |
| | +--panelOrderEntry(1)
| | | Index: orderIndex
| | |
| | +-- -R-- Integer32 orderIndex(1)
| | | Range: 0..5
| | +-- -RW- Integer32 panelOrderIndex(2)
| | Range: 0..5
| |
| +-- -RW- EnumVal renderingMode(3)
| Values: full(0), differential(1)
|
+-- -R-- Counter64 pixelsFlipped(4)
+-- --W- Integer32 dirtyBit(69)
Usage examples¶
Set the pixel x=23, y=4 to bright
snmpset -v 2c -c private -m ./util/FLUEPDOT.mib fluepdot0.cluster.ap-south-1.yolo.network FLUEPDOT-MIB::pixelState.23.4 i bright
Set the dirty bit (aka tell the fluepboard to render framebuffer contents)
snmpset -v 2c -c private -m ./util/FLUEPDOT.mib fluepdot0.cluster.ap-south-1.yolo.network FLUEPDOT-MIB::dirtyBit.0 i 1
Get the number of flipped pixels
snmpget -v 2c -c public -m ../util/FLUEPDOT.mib 192.168.178.94 FLUEPDOT-MIB::pixelsFlipped.0
HTTP API¶
Framebuffer¶
For simple integration into your projects, the fluepboard firmwares ships with a simple to use HTTP API.
Framebuffer encoding¶
Framebuffers are ASCII-encoded. The only allowed characters are <space> 0x20
, X 0x58
and ``\n`` \0x0A
.
A bright (set) pixel is encoded as an X.
A dark (cleared) pixel is encoded as a space character.
Each line in a framebuffer has exactly the same amount of characters, which is exactly the fluepdot’s width plus one newline character.
Each framebuffer has exactly 16 lines.
Lines are terminated by a single \n
. No carriage return is used.
Framebuffer manipulations¶
GET /framebuffer¶
- Params
- GET
None
- POST
None
Gets the current framebuffer encoded as explained above. This endpoint can be used to calculate the framebuffer dimensions, thus there is no seperate endpoint for retrieving the geometry.
POST /framebuffer¶
- Params
- GET
None
- POST
Raw framebuffer encoded as explained above
Draw the posted framebuffer to the fluepdot.
GET /pixel¶
- Params
- GET
x - The x coordinate y - The y coordinate
Get the current pixel at the given coordinate encoded as above
POST /pixel¶
- Params
- GET
- x
The x coordinate (ascii encoded decimal value)
- y
The y coordinate (ascii encoded decimal value)
Sets the pixel at the given coordinate to bright.
DELETE /pixel¶
- Params
- GET
- x
The x coordinate (ascii encoded decimal value)
- y
The y coordinate (ascii encoded decimal value)
Set the pixel at the give coordinate to dark.
POST /framebuffer/text¶
- Params
- POST
Text to display
- GET
- x
The x coordinate to render text to
- y
The y coordinate to render text to
- font
The font’s name to use for rendering text. Retrieve a list of fonts at GET /fonts
Render a string
GET /fonts¶
Returns a list of installed fonts
For each font there are two lines: First the full_name and then short_name which is used with the POST /framebuffer/text eAPI endpoint.
Rendering options¶
Rendering mode¶
GET /rendering/mode¶
Retuns an ASCII printed integer which value is defined as follows:
- enum
flipdot_rendering_mode_t
Values:
FULL
= 0Always render the full framebuffer without skipping anything.
DIFFERENTIAL
= 1Redraw only those pixels that changed.
PUT /rendering/mode¶
- Params
- GET
None
- POST
ASCII printed integer, which value is to be interpreted as stated above.
Rendering timings encoding¶
Rendering timings are ASCII-encoded. The only allowed characters are 0-9 (0x30-0x39
) and \\n
.
For each fluepdot column, there are exactly 3 rows:
Pre delay
How long to wait (50 microseconds steps) before rendering to the column.
Clear delay
How long to power the coils in order to clear the column (in 50 microseconds steps).
Set delay
How long to power the coils in order to set the column (in 50 microseconds steps)
Each line has exactly 5 characters and one trailing \\n
. You have to pad with zeros.
Each line contains the decimal value in ascii-encoded form.
Warning
Powering a coil repeatedly for a long duration might cause the coil to overheat and or fail. Decreasing the timings might result in higher framerate, but might result in not all pixels flipping.
Usually 1600uS are enough to reliably flip all pixels. This is the default.
GET /rendering/timings¶
Returns the timing configuration encoded as explained above.
POST /rendering/timings¶
- Params
- GET
None
- POST
Timing configuration as explained above.
Set the timing configuration.
Firmware update¶
Using service utility¶
The fluepboard’s firmware can be easily updated using a standalone binary for GNU/Linux (amd64) that is generated by the Gitlab CI build process of this project.
You can get the latest version here.
Make sure your current user has permission to use the serial port (in this example /dev/ttyUSB0), then run:
./service_utility -serial.port /dev/ttyUSB0
This utility will flash: * the partition table * the bootloader * the factory application
Using esptool.py¶
You can also flash the firmware using esptool.py. Download and unpack the compiled firmware, then run:
esptool.py --chip esp32 --before=default_reset --after=hard_reset write_flash --flash_mode dio --flash_freq 40m --flash_size 4MB 0x8000 partition_table/partition-table.bin 0x1000 bootloader/bootloader.bin 0x10000 flipdot-firmware.bin
Compiling¶
A multistage Docker environment is used to build firmware and documentation.
Clone the repository recursively.
git clone --recursive https://gitlab.com/fluepke/fluepdot.git
Build or pull the build container.
If you have a high bandwidth internet connection downloading is fastest (~ 10 GB):
docker pull fluepke/fluepdot-build-environment
If you have a low bandwidth internet connection building is faster (still ~ 3 GB)
docker build --force-rm -t fluepke/fluepdot-build-environment -f Dockerfile .
Build the second stage container
docker build --force-rm -t fluepdot -f Dockerfile.build .
Run the second stage container and flash the image (assuming your flipdot’s serial device is at
/dev/ttyUSB0
)docker run -d --name fluepdot --device /dev/ttyUSB0:/fluepdot-device:rwm docker exec -it fluepdot \ -w "/fluepdot/software/firmware" \ -e ESPTOOL_PORT='/fluepdot-device' \ -e ESPTOOL_BAUD='480000' \ make flash
Flipdot API reference¶
Flipdot¶
Flipdot structs¶
-
struct
flipdot_t
¶ Main flipdot data structure Passed as a pointer to most of the flipdot functions.
Public Members
-
EventGroupHandle_t
event_group
¶ Used for inter task communication with the flipdot task.
-
TaskHandle_t *
task
¶ Flipdot rendering task Performs the following actions, when
FLIPDOT_FRAMEBUFFER_DIRTY_BIT
inevent_group
was set:Delete
framebuffer_internal_old
Copy
framebuffer_internal
toframebuffer_internal_old
Delete
framebuffer_internal
Copy
framebuffer
toframebuffer_internal
Delete
internal_rendering_options
Copy
rendering_options
tointernal_rendering_options
Render the flipdot as configured
-
framebuffer_t *
framebuffer
¶ You can manipulate this framebuffer as you please.
Once done, call
flipdot_set_dirty_flag
-
framebuffer_t *
framebuffer_internal
¶ Internal framebuffer, used to avoid race conditions when rendering.
-
framebuffer_t *
framebuffer_internal_old
¶ Previous internal framebuffer, used for differential rendering.
-
flipdot_panel_t *
panels
¶ Panel configuration.
-
uint8_t
panel_count
¶ Number of attached panels.
-
uint8_t
width
¶ Total flipdot width in pixels.
-
flipdot_rendering_options_t *
rendering_options
¶ Rendering options.
-
flipdot_rendering_options_t *
internal_rendering_options
¶ Internal rendering options, used internally to avoid race conditions when rendering.
-
bool
power_status
¶ Flipdot power status.
- Note
Use
flipdot_set_power
to manipulate this member- See
-
spi_device_handle_t
spi_device_handle
¶ SPI device handle.
- Note
SPI is used for controlling shift registers IC5, IC6 and IC7.
-
flipdot_io_state_t
io
¶ Flipdot GPIO state, used for shifting out IO states via SPI to IC5, IC6 and IC7.
-
unsigned long long
pixels_flipped
¶ Counts the pixels that changed their color since startup.
-
SemaphoreHandle_t
semaphore
¶ Used for mutual exclusive hardware access.
-
EventGroupHandle_t
-
struct
flipdot_panel_t
¶ Location and size of a flipdot panel.
A flipdot consists of up to 5 panels. Width of a panel is typically 20 or 25 pixels. Height is hardcoded to be 16.
-
struct
flipdot_configuration_t
¶ Used to initialize a flipdot.
Flipdot functions¶
-
esp_err_t
flipdot_initialize
(flipdot_t *flipdot, flipdot_configuration_t *flipdot_configuration)¶ Initializes a flipdot for use and starts the flipdot task.
- Note
To actually use the flipdot you have to power it up using flipdot_set_power.
- See
- Return
ESP_OK
: successESP_ERR_INVALID_ARG
: invalid arguments (i.e. null pointers)ESP_ERR_NO_MEM
: allocating internal buffers failedother: failure
- Parameters
flipdot
: Flipdot to initialize, must not be NULLflipdot_configuration
: Configuration to apply, must not be NULL
-
esp_err_t
flipdot_set_power
(flipdot_t *flipdot, bool power_status)¶ Power on / off the flipdot.
- Note
This does not stop the rendering task
- Return
ESP_OK success
ESP_TIMEOUT timeout waiting for flipdot->semaphore
- Parameters
flipdot
: The flipdot handlepower_status
:True power on the flipdot
False power off the flipdot
Framebuffer¶
Framebuffer struct¶
-
struct
framebuffer_t
¶ A framebuffer with bottom left corner as x=0, y=0.
-
esp_err_t
flipdot_framebuffer_init
(framebuffer_t *framebuffer, uint8_t width)¶ Initialize a framebuffer.
- Return
ESP_ERR_INVALID_ARG
: framebuffer was NULL or width was 0ESP_ERR_NO_MEM
: failed to allocate memory
- Parameters
framebuffer
: The framebuffer_t* to initializewidth
: Framebuffer width. Must be > 0
-
void
flipdot_framebuffer_free
(framebuffer_t *framebuffer)¶ Free a framebuffer_t* Framebuffer is freed and the pointer set to NULL to avoid use after free bugs.
- Parameters
framebuffer
: The framebuffer to free
Framebuffer manipulation¶
-
void
flipdot_framebuffer_clear
(framebuffer_t *framebuffer)¶ Clear a framebuffer (set all pixels dark)
- Parameters
framebuffer
: The framebuffer to clear
-
bool
flipdot_framebuffer_get_pixel
(framebuffer_t *framebuffer, uint8_t x, uint8_t y)¶ Get a single pixel value.
- See
framebuffer_t for an explanation of the coordinate system
- See
framebuffer_t for an explanation of the coordinate system
- Return
true Pixel is set (bright)
false Pixel is not set (dark) or out of bounds read or framebuffer was NULL
- Parameters
framebuffer
: The framebuffer to retrieve pixel value fromx
: X location,
- Parameters
y
: Y location,
-
esp_err_t
flipdot_framebuffer_set_pixel
(framebuffer_t *framebuffer, uint8_t x, uint8_t y, bool value)¶ Set a single pixel value.
- See
framebuffer_t for an explanation of the coordinate system
- See
framebuffer_t for an explanation of the coordinate system
- Return
ESP_OK success
ESP_ERR_INVALID_ARG out of bounds or framebuffer was NULL
- Parameters
framebuffer
: The framebuffer to manipulatex
: X location,
- Parameters
y
: Y location,
- Parameters
value
:true Pixel is set (bright)
false Pixel is not set (dark) or out of bounds read
Framebuffer de/encoding¶
-
char *
flipdot_framebuffer_encode_line
(framebuffer_t *framebuffer, char *dst, size_t dst_len, uint8_t y)¶ ASCII encode a horizontal framebuffer line.
- Return
NULL failure
char* Pointer to the encoded line
- Note
- See
flipdot_framebuffer_encode_pixel is used for ASCII encoding single pixels
- Parameters
framebuffer
: The framebuffer to encodedst
: Destination char bufferdst_len
: Destination buffer size, must be framebuffer->width + 1 to accomodate the trailing 0 character
-
esp_err_t
flipdot_framebuffer_decode_line
(framebuffer_t *framebuffer, char *src, size_t src_len, uint8_t y)¶ ASCII decode a horizontal framebuffer line.
- Return
ESP_OK success
ESP_INVALID_ARG framebuffer was null, or src_len != framebuffer->width + 1
- Note
- See
flipdot_framebuffer_decode_pixel is used for ASCII decoding single pixels
- Parameters
framebuffer
: The framebuffer to decode intosrc
: Source char buffersrc_len
: Source buffer size, must be framebuffer->width + 1 to accomodate the trailing 0 character
-
char
flipdot_framebuffer_encode_pixel
(bool value)¶ ASCII encode a single pixel.
- Return
- See
FLIPDOT_PIXEL_SET pixel is set
- See
FLIPDOT_PIXEL_CLEAR pixel is not set
- Parameters
value
: The pixel value to encode
-
bool
flipdot_framebuffer_decode_pixel
(char src)¶ ASCII decode a single pixel.
- Return
true pixel is set (src == FLIPDOT_PIXEL_SET)
false pixel is not set (all other cases)
- Parameters
src
: Character to decode into a pixel value
-
esp_err_t
flipdot_framebuffer_printf
(framebuffer_t *framebuffer)¶ Printf the given framebuffer to STDOUT.
- Return
ESP_OK success
ESP_ERR_INVALID_ARG framebuffer was NULL
ESP_ERR_NO_MEM could not allocate enough memory
- Parameters
framebuffer
: The framebuffer to print
Framebuffer diffing¶
-
esp_err_t
flipdot_framebuffer_compare
(framebuffer_t *a, framebuffer_t *b, unsigned int *diff)¶ Compares two framebuffers.
- Return
ESP_INVALID_ARG a was NULL and / or b was NULL and / or diff was NULL and / or a->width != b->width
ESP_OK success
- Parameters
a
: Framebuffer 1b
: Framebuffer 2diff
: Pointer to an integer to store the number of changed pixels
-
esp_err_t
flipdot_framebuffer_compare_partial
(framebuffer_t *a, framebuffer_t *b, uint8_t start_x, uint8_t width, unsigned int *diff)¶ Compares two framebuffers partially.
- Return
ESP_INVALID_ARG a was NULL and / or b was NULL and / or diff was NULL and / or a->width != b->width and / or a->width < start_x + width
ESP_OK success
- Parameters
a
: Framebuffer 1b
: Framebuffer 2start_x
: X offset from where to start comparingwidth
: Width of the view port to comparediff
: Pointer to an integer to store the number of changed pixels
Rendering options¶
-
struct
flipdot_rendering_options_t
¶ Flipdot rendering options.
Public Members
-
uint8_t
width
¶ Flipdot width.
-
uint8_t
panel_count
¶ Number of panels attached to flipdot.
-
flipdot_rendering_mode_t
mode
¶ Flipdot rendering mode.
-
flipdot_rendering_delay_options_t *
delay_options
¶ Flipdot timing options.
-
uint8_t *
panel_order
¶ Order in which panels are rendered.
-
uint8_t
-
struct
flipdot_rendering_delay_options_t
¶ Flipdot timing options.
-
enum
flipdot_rendering_mode_t
¶ Values:
-
FULL
= 0¶ Always render the full framebuffer without skipping anything.
-
DIFFERENTIAL
= 1¶ Redraw only those pixels that changed.
-
-
esp_err_t
flipdot_rendering_options_initialize
(flipdot_rendering_options_t *rendering_options, uint8_t panel_count, uint8_t width)¶ Allocates internal data structures and initializes them with reasonable defaults.
- Return
ESP_ERR_INVALID_ARG
: rendering_options was NULL, panel_count was 0 or width was 0ESP_ERR_NO_MEM
: could not allocate memory for internal data structuresESP_OK
: success
- Parameters
options
: The flipdot_rendering_options_t to initialize, must not be NULL.panel_count
: Number of panels available, must be larger than 0width
: Number of columns available, must be larger than 0
-
esp_err_t
flipdot_rendering_options_copy
(flipdot_rendering_options_t *dest, flipdot_rendering_options_t *src)¶ Copies rendering options from src to dest.
- Parameters
dest
: Allocate usingcalloc(1, sizeof(flipdot_rendering_options_t))
src
: Source
-
void
flipdot_rendering_options_free
(flipdot_rendering_options_t *rendering_options)¶ Free a flipdot_rendering_options_t*.
- Parameters
rendering_options
: flipdot_rendering_options_t* to free.