Understanding the Luminator protocol

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.

Connector

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):

1 3 6 7 4 5 2
Pin Function
1 Main power (24V DC)
2 Switched power
3 LED power
4 Ground
5 RS-485 +
6 RS-485 −
7 Shield
Figure 1: Pinout of the sign's 7-pin circular connector

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.

Serial communication

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.

Wire Format

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:

Data bytes # of : DataLen Address MsgType Data 0 Data N Chksum \r \n
Figure 2: Layout of an Intel HEX data frame

Here's an example: :01000702FFF7\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.

Protocol

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

State Machine

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:

  1. Discover a sign on the bus
  2. If it's not already in the Unconfigured state, reset it
  3. Send config data
  4. Send pixel data
  5. If the sign flips automatically, we're done, otherwise:
  6. Show the first page
  7. Cycle through loading and showing additional pages
  8. When pages need to be updated, go to step 4

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 :01000302FFFB Hello to sign 3
Sign :010003040FE9 Report State (Unconfigured)
Configuration
Controller :01000303A158 Request Operation (Receive Config)
Sign :010003059562 Acknowledge Operation (Receive Config)
Controller :1000000004200006071E1E1E00080000000000005D Send Data at offset 0
Controller :00000101FE Data Chunks Sent (1)
Controller :0100030200FA Query State
Sign :0100030407F1 Report State (Config Received)
Pixel Data
Controller :01000303A257 Request Operation (Receive Pixels)
Sign :010003059166 Acknowledge Operation (Receive Pixels)
Controller :100000000010000011224408112244081122440863 Send Data at offset 0
Controller :1000100011224408112244081122440811224408E4 Send Data at offset 16
Controller :1000200011224408112244081122440811224408D4 Send Data at offset 32
Controller :1000300011224408112244081122440811224408C4 Send Data at offset 48
Controller :1000400011224408112244081122440811224408B4 Send Data at offset 64
Controller :100050001122440811224408112244081122FFFFF2 Send Data at offset 80
Controller :00000601F9 Data Chunks Sent (6)
Controller :0100030200FA Query State
Sign :0100030401F7 Report State (Pixels Received)
Controller :0100030600F6 Pixels Complete
Page Load/Show Cycle (Manual Flip Signs)
Controller :0100030200FA Query State
Sign :0100030410E8 Report State (Page Loaded)
Controller :01000303A950 Request Operation (Show Loaded Page)
Sign :010003059661 Acknowledge Operation (Show Loaded Page)
Controller :0100030200FA Query State
Sign :0100030411E7 Report State (Page Show in Progress)
Controller :0100030200FA Query State
Sign :0100030412E6 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 :0100030200FA Query State
Sign :0100030400F8 Report State (Showing Pages)

The full state machine is as follows:

Unconfigured (Any state) Config In Progress Config Received Config Failed Pixels In Progress Pixels Received Page Loaded Showing Pages Page Shown Ready To Reset Page Show In Progress Page Load In Progress Pixels Failed (Power on sign) (Sign blanks and shuts down) * ** Receive Config Receive Config Receive Pixels Receive Pixels Receive Pixels Start Reset Goodbye Finish Reset Hello Pixels Complete (If sign is manual flip) (If sign is auto flip) Load Next Page (Sign loads page) (Sign shows page) Data Chunks Sent (OK) Data Chunks Sent (OK) Data Chunks Sent (Error) Data Chunks Sent (Error) Hello/ Query State Send Data Send Data Show Loaded Page
Figure 3: Sign's state machine

* 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.

Timings

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.

Configuration Data

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.

0x04 0 1 2 3 4 5 6 7 8 Bytes 9 10 11 12 13 14 15 H W1 W2 W3 ID 0x00 ?? W4 0x00 0x00 0x00 0x00 B 0x00 0x00
Figure 4: Configuration format for MAX 3000 signs

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.

0x08 0 1 2 3 4 5 6 7 8 Bytes 9 10 11 12 13 14 15 ?? H 0x00 W ID 0x00 ?? A1 ?? 0x00 0x00 0x00 A2 B1 B2
Figure 5: Configuration format for Horizon signs

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 0447000F101C1C1C1C10000000000000
MAX 3000 Front 98 × 16 044D000D100E1C1C1C10000000000000
MAX 3000 Side 90 × 7 04200006071E1E1E0008000000000000
MAX 3000 Rear 23 × 10 046100040A1700000010000000000000
MAX 3000 Rear 30 × 10 046200040A1E00000010000000000000
MAX 3000 Dash 30 × 7 04260003071E00000008000000000000
Horizon Front 160 × 16 08B100150C1000A00400280000000000
Horizon Front 140 × 16 08B200120410008C0103142800000000
Horizon Side 96 × 8 08B400070C0800600200300000000000
Horizon Rear 48 × 16 08B500070C1000300100300000000000
Horizon Dash 40 × 12 08B900068C0C00280100280004000000

Pixel Data

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.

4-byte header Data bytes Padding ID ?? ?? ?? 0 1 2 3 4 5 0xFF 0xFF To multiple of 16 7 6 5 4 3 2 1 0 Bits Most significant Least significant
Figure 6: Logical layout of one page of pixel data

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:

Columns (bit 7 unused) Rows ... ... ... 0 0 1 2 3 4 5 6 0 1 2 3 4 5 Columns Rows ... ... ... ... ... 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 1 2 3 4 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
Figure 7: Mapping of data bits from Figure 6 to a 7-row sign (left) and a 16-row sign (right)

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.

License

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.