Note: This information is intended only for hobbyist and educational purposes. I am not affiliated with Luminator in any way.
This document applies to newer signs such as the MAX 3000 and Horizon models, which use a 7-pin circular connector. I believe older signs use a similar protocol, but I haven't had a chance to study it.
This represents my best guess at how everything works, but no guarantees as to the accuracy. Rely on at your own risk.
If you'd prefer running code, my flipdot library (written in Rust) implements this protocol and provides a high-level interface for interacting with signs. For a concrete example that uses the sign as a clock, check out dotclock.
First things first: how to connect to the sign? It uses a TE Connectivity Circular Plastic Connector (CPC) model 788194-1 with the following pinout (looking at the connector on the sign):
Pin | Function |
---|---|
1 |
Main power (24V DC) |
2 |
Switched power |
3 |
LED power |
4 |
Ground |
5 |
RS-485 + |
6 |
RS-485 − |
7 |
Shield |
I'm not sure what specific pin inserts they're using (there are a lot to choose from), but there are definitely two types. Pins 1 and 4 are beefier since they'll be carrying a few amps. In theory you could build your own mating connector, but I opted to buy original Luminator cables instead to avoid needing to figure that out.
At minimum, you need to connect main power, switched power, ground, and RS-485 +/−. Switched power turns the sign on, and LED power enables the LEDs/backlight (I guess I'm not sure what that does on a Horizon sign). Main power is definitely 24V DC. You're going to want a capable power supply; my sign draws ~2.5A to flip the dots, and another amp or so for the LEDs. I've seen conflicting information on whether switched power and LED power are 12V or 24V. They're basically just switches (draw negligible current), and I have run the sign using 24V on them, but that might not have been wise. You should connect the shield pin to ground in one place (probably the controller) to avoid ground loops.
Signs communicate over an RS-485 bus, configured as 19200 8-N-1 (19200 bps data rate, 8 data bits, no parity bit, one stop bit). Cheap USB adapters are readily available to connect to a PC. Each sign has an address to uniquely identify it on the bus, which is set via DIP switches.
Data is transmitted on the bus using the Intel HEX format (but not its semantics; we'll get into that in a second). The layout looks like this:
Here's an example: :
01
0007
02
FF
F7
\r\n
(though for brevity, I'm going to omit
the carriage return/newline sequence from here on). This is a message of type 2
with
address 7
, containing 1
data byte: 0xFF
.
Note that since we're spelling everything out in ASCII, one byte of numeric data requires two bytes to actually encode.
The DataLen
field describes how many such two-character data byte sequences are present.
Note that since it is represented as a single byte, the data length cannot exceed 255 (0xFF), though in practice
the Luminator protocol only sends 16-byte chunks anyway. If DataLen
is 0, there are
no data bytes, and MsgType
is followed directly by Chksum
.
The checksum is a longitudinal redundancy check
calculated on all numeric fields.
A handful of specific HEX message types make up the protocol:
Name | Address | MsgType | Data | Notes |
---|---|---|---|---|
Send Data | Data offset | 0x00 |
16 data bytes | Used to send configuration and pixel data in chunks |
Data Chunks Sent | # of chunks | 0x01 |
— | Number of 16-byte chunks sent via Send Data messages |
Hello | Sign address | 0x02 |
0xFF |
Initially discovers signs on the bus and queries their state |
Goodbye | Sign address | 0x02 |
0x55 |
Tells a sign to blank itself and shut down |
Query State | Sign address | 0x02 |
0x00 |
Queries a sign for its current state |
Report State | Sign address | 0x04 |
State | Sent by the sign in reply to Query State |
Request Operation | Sign address | 0x03 |
Operation | Requests that the sign perform an operation |
Acknowledge Operation | Sign address | 0x05 |
Operation | Sent by the sign in reply to Request Operation |
Pixels Complete | Sign address | 0x06 |
0x00 |
Notifies the sign to begin normal operation |
Note that the Address field often refers to the sign's bus address, but is repurposed for the SendData and Data Chunks Sent messages (as prior messages will have already indicated which sign should be paying attention to the data).
The sign's possible states (communicated via Report State) are as follows:
Name | Value | Notes |
---|---|---|
Unconfigured | 0x0F |
The sign's initial state after power on or reset |
Config in Progress | 0x0D |
The sign is waiting to receive configuration data |
Config Received | 0x07 |
The sign has successfully received and understood the config data |
Config Failed | 0x0C |
There was an error receiving the configuration data, or it was malformed |
Pixels in Progress | 0x03 |
The sign is waiting to receive pixel data |
Pixels Received | 0x01 |
The sign has successfully received and understood the pixel data |
Pixels Failed | 0x0B |
There was an error receiving the pixel data, or it was malformed |
Page Loaded | 0x10 |
The sign has loaded a page into memory and is ready to display it |
Page Load in Progress | 0x13 |
The sign is in the process of loading a page into memory |
Page Shown | 0x12 |
The sign has shown the last loaded page (so memory is empty) |
Page Show in Progress | 0x11 |
The sign is in the process of showing the last loaded page |
Showing Pages | 0x00 |
The sign is independently showing and flipping pages (mutually exclusive with the above Page-related states) |
Ready to Reset | 0x08 |
The sign has begun the reset process and is ready to return to Unconfigured |
The following operations can be requested via Request Operation (and are acknowledged with a different value in an Acknowledge Operation):
Name | Request | Ack | Notes |
---|---|---|---|
Receive Config | 0xA1 |
0x95 |
Prepare to receive config data; switch to Config in Progress |
Receive Pixels | 0xA2 |
0x91 |
Prepare to receive pixel data; switch to Pixels in Progress |
Show Loaded Page | 0xA9 |
0x96 |
Show the page currently loaded in memory; switch to Page Show in Progress |
Load Next Page | 0xAA |
0x97 |
Begin loading the next page into memory; switch to Page Load in Progress |
Start Reset | 0xA6 |
0x93 |
Begin the reset process; switch to Ready to Reset |
Finish Reset | 0xA7 |
0x94 |
Finish resetting; switch to Unconfigured |
The system is driven by a controller, which in a real bus would be an ODK (Operator's Display and Keyboard). Signs don't initiate communication; they only respond to messages from the controller. Each sign maintains a state machine which is driven by the controller. The typical sequence goes like this:
Here's a concrete example of a typical exchange between a controller and a sign with address 3 comprising discovery, configuration, sending one page of pixels, then showing that page:
Sender | Raw Message | Decoded Message |
---|---|---|
Discovery | ||
Controller | : 01 0003 02 FF FB |
Hello to sign 3 |
Sign | : 01 0003 04 0F E9 |
Report State (Unconfigured) |
Configuration | ||
Controller | : 01 0003 03 A1 58 |
Request Operation (Receive Config) |
Sign | : 01 0003 05 95 62 |
Acknowledge Operation (Receive Config) |
Controller | : 10 0000 00 04200006071E1E1E0008000000000000 5D |
Send Data at offset 0 |
Controller | : 00 0001 01 FE |
Data Chunks Sent (1) |
Controller | : 01 0003 02 00 FA |
Query State |
Sign | : 01 0003 04 07 F1 |
Report State (Config Received) |
Pixel Data | ||
Controller | : 01 0003 03 A2 57 |
Request Operation (Receive Pixels) |
Sign | : 01 0003 05 91 66 |
Acknowledge Operation (Receive Pixels) |
Controller | : 10 0000 00 00100000112244081122440811224408 63 |
Send Data at offset 0 |
Controller | : 10 0010 00 11224408112244081122440811224408 E4 |
Send Data at offset 16 |
Controller | : 10 0020 00 11224408112244081122440811224408 D4 |
Send Data at offset 32 |
Controller | : 10 0030 00 11224408112244081122440811224408 C4 |
Send Data at offset 48 |
Controller | : 10 0040 00 11224408112244081122440811224408 B4 |
Send Data at offset 64 |
Controller | : 10 0050 00 1122440811224408112244081122FFFF F2 |
Send Data at offset 80 |
Controller | : 00 0006 01 F9 |
Data Chunks Sent (6) |
Controller | : 01 0003 02 00 FA |
Query State |
Sign | : 01 0003 04 01 F7 |
Report State (Pixels Received) |
Controller | : 01 0003 06 00 F6 |
Pixels Complete |
Page Load/Show Cycle (Manual Flip Signs) | ||
Controller | : 01 0003 02 00 FA |
Query State |
Sign | : 01 0003 04 10 E8 |
Report State (Page Loaded) |
Controller | : 01 0003 03 A9 50 |
Request Operation (Show Loaded Page) |
Sign | : 01 0003 05 96 61 |
Acknowledge Operation (Show Loaded Page) |
Controller | : 01 0003 02 00 FA |
Query State |
Sign | : 01 0003 04 11 E7 |
Report State (Page Show in Progress) |
Controller | : 01 0003 02 00 FA |
Query State |
Sign | : 01 0003 04 12 E6 |
Report State (Page Shown) |
There is also a simpler version that somes signs use where the sign shows and flips pages independently without input from the controller after receiving Pixels Complete:
Page Load/Show Cycle (Automatic Flip Signs) | ||
Controller | : 01 0003 02 00 FA |
Query State |
Sign | : 01 0003 04 00 F8 |
Report State (Showing Pages) |
The full state machine is as follows:
* The green box with the dashed border represents the set of "running" states. It is legal to request Receive Pixels from any of these states in order to update the set of pages.
** The light gray box with the dashed border represents any state. You can always request Query State (or Hello) to see what the current state is, and you can always start fresh by issuing a Start Reset. This is helpful to get back into a known state if you connect to a sign that's been already been running for a while. You can also blank the sign and shut down from any state, but I don't find this very useful since turning off switched power will also blank my sign.
To avoid overloading the sign, a small delay is recommended after each Send Data message, e.g. 30ms.
When loading or showing a page, the sign can take a second or more to complete the operation, depending on factors like how many dots need to flip. While checking if that operation has completed (by polling Query State messages) before moving onto the next one, you may want to insert a delay (say 100ms or so) after each Page Load/Show in Progress response in order to avoid spamming the sign with status requests.
It's not exactly clear to me if this data actually configures the sign in some way, is verified by the sign to ensure the controller has properly identified it, or something else entirely. I've haven't wanted to risk doing any experiments along these lines on my sign. Regardless, you need to send this sign-specific block of 16 bytes as part of establishing communication. The first byte indicates the family of sign, and different families have different formats for the rest of the configuration block.
Note: I'm going to describe sign dimensions using the computer graphics convention of width × height since that's the direction I'm approaching this from.
MAX 3000 signs have an initial byte of 0x04
. ID
is a
unique ID for the particular sign type within the family, e.g. the 90 × 7 side sign has ID 0x20
.
Byte 2 seems to always be zero, and byte 3 is unknown. H
is the height in pixels,
and W1 + W2 + W3 + W4
is the total width. B
indicates
the number of bits per column (either 8 or 16). The remaining bytes appear unused and are always zero.
Horizon signs have an initial byte of 0x08
. ID
is a
unique ID for the particular sign type within the family, e.g. the 96 × 8 side sign has ID 0xB4
.
Byte 2 seems to always be zero, and bytes 3 and 4 are unknown. H
is the height
in pixels, and W
is the width. The next four bytes seem to indicate the arrangement
of sub-panels to create the final width: W =
A1 × B1
+
A2 × B2
. Byte 12 is unknown (generally zero but 0x04
for the 40 ×
12 dash sign). The remaining bytes appear unused and are always zero.
These are the configuration blocks for all the signs I'm aware of:
Family | Type | Size | Data (hex) |
---|---|---|---|
MAX 3000 | Front | 112 × 16 | 04 47 00 0F 10 1C 1C 1C 1C 10 00 00 00 00 00 00 |
MAX 3000 | Front | 98 × 16 | 04 4D 00 0D 10 0E 1C 1C 1C 10 00 00 00 00 00 00 |
MAX 3000 | Side | 90 × 7 | 04 20 00 06 07 1E 1E 1E 00 08 00 00 00 00 00 00 |
MAX 3000 | Rear | 23 × 10 | 04 61 00 04 0A 17 00 00 00 10 00 00 00 00 00 00 |
MAX 3000 | Rear | 30 × 10 | 04 62 00 04 0A 1E 00 00 00 10 00 00 00 00 00 00 |
MAX 3000 | Dash | 30 × 7 | 04 26 00 03 07 1E 00 00 00 08 00 00 00 00 00 00 |
Horizon | Front | 160 × 16 | 08 B1 00 15 0C 10 00 A0 04 00 28 00 00 00 00 00 |
Horizon | Front | 140 × 16 | 08 B2 00 12 04 10 00 8C 01 03 14 28 00 00 00 00 |
Horizon | Side | 96 × 8 | 08 B4 00 07 0C 08 00 60 02 00 30 00 00 00 00 00 |
Horizon | Rear | 48 × 16 | 08 B5 00 07 0C 10 00 30 01 00 30 00 00 00 00 00 |
Horizon | Dash | 40 × 12 | 08 B9 00 06 8C 0C 00 28 01 00 28 00 04 00 00 00 |
When sending pixel data, the following format is used (split into 16-byte chunks per the overall protocol). Note that the offset in the SendData message is relative to the current page, not the total amount of data to send.
The purpose of the 4-byte header isn't clear. The first byte seems to be a page number or ID, though I don't think
it affects sign operation. The other bytes are most frequently 0x10 0x00 0x00
.
The interpretation of the data bytes in Figure 6 depends on the dimensions of the sign they're meant for. For example, consider a 7-pixel tall sign and a 16-pixel tall sign. The former needs only one byte for each column, while the latter needs two, so the same data will be interpreted differently depending on the sign:
The bytes are arranged with their least significant bits toward the top of the sign, proceeding top down and then left to right. If the number of pixels in a column isn't a multiple of 8, the extra bits are ignored to maintain alignment.