Skip to content

Instantly share code, notes, and snippets.

@cinderblock
Created September 15, 2020 04:14
Show Gist options
  • Save cinderblock/3a495618b1aa3152f5326c346851cad9 to your computer and use it in GitHub Desktop.
Save cinderblock/3a495618b1aa3152f5326c346851cad9 to your computer and use it in GitHub Desktop.
Extracted javascript interface for talking to pixelblaze expander boards
import { State } from '../shared/State';
import SerialPort from 'serialport';
import { promisify } from 'util';
/**
* This file is for communicating with a PixelBlaze Expander board.
*
* This should basically match what is found here: https://github.com/simap/pixelblaze_output_expander
*
* Cameron's summary of the packet format:
*
* Expander boards are wired up, up to eight devices, on a single directional shared 2M baud serial line.
*
* Each expander board has up to eight output channels.
*
* Standard sequence for drawing one frame to a display
*
* 1. Get pixel data
* 2. Split data into "channels". Up to 2400 bytes - 800 RGB or 600 RGBW. Must match hardware channels.
* 3. Send all channel packets on wire as separate packets (aka serial signal "frames", not visual frames)
* 4. Send a "DRAW_ALL" packet ("frame") to any address. Tells all expander boards to synchronously update all pixels with latest values in their buffers.
*
* A packet ("frame") is always:
*
* MAGIC (4 bytes) + CHANNEL (1 byte) + COMMAND (1 byte) + DATA (length depends on COMMAND) + CRC
*
* MAGIC is always "UPXL"
*
* CHANNEL
* - The top 2-bits are currently never set
* - The middle 3-bits match solder switches on each expander board
* - The lowest 3-bits are the channel number (which plug) on a specific board
*
* COMMAND
* - 0x01 - Update buffers (SET_CHANNEL_WS2812)
* - 0x02 - Draw buffers (DRAW_ALL) - No DATA
*
* DATA for SET_CHANNEL_WS2812
* - number colors per pixel - 1 unsigned byte - 0x00 = disable. 0x03 = RGB. 0x04 = RGBW.
* - color packing order - 1 unsigned byte - 4 sets of 2 bits. Allows reordering of colors. Suggested value: 0b00011011
* - number of pixels - 2 unsigned bytes
* - color information - [number of pixels * number of colors per pixel] unsigned bytes
*/
let drawing = true;
const serial = new SerialPort('/dev/serial0', { baudRate: 2e6 }, () => {
drawing = false;
});
let bytesReceived = 0;
serial.on('data', data => data && (bytesReceived += data.length));
serial.on('error', console.log.bind(0, 'Serialport Err:'));
enum RecordType {
SET_CHANNEL_WS2812 = 1,
DRAW_ALL = 2,
}
const crcTable = [
0x00000000,
0x1db71064,
0x3b6e20c8,
0x26d930ac,
0x76dc4190,
0x6b6b51f4,
0x4db26158,
0x5005713c,
0xedb88320,
0xf00f9344,
0xd6d6a3e8,
0xcb61b38c,
0x9b64c2b0,
0x86d3d2d4,
0xa00ae278,
0xbdbdf21c,
];
const stringLength = 50;
let crc: number;
function crcReset(): void {
crc = 0xffffffff;
}
function crcUpdate(byte: number): void {
byte &= 0xff;
crc = crcTable[(crc ^ byte) & 0x0f] ^ (crc >> 4);
crc = crcTable[(crc ^ (byte >> 4)) & 0x0f] ^ (crc >> 4);
crc &= 0xffffffff;
}
function crcUpdateBlock(data: Buffer): void {
for (let i = 0; i < data.length; i++) crcUpdate(data[i]);
}
const crcSendBuff = Buffer.allocUnsafe(4);
function sendCRC(): void {
// Super un-standard CRC
crcSendBuff.writeInt32LE(crc ^ 0xffffffff, 0);
sendRaw(crcSendBuff);
}
const header = Buffer.allocUnsafe(6);
// Magic header
header.write('UPXL', 0);
type String = {
/**
* Which driver are we talking to
*/
channel: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
buff: Buffer;
};
enum ColorOrder {
R = 1,
G = 2,
B = 3,
}
const pixelDataSize = 3;
const channelHeader = Buffer.allocUnsafe(4);
// Using RGB pixels
channelHeader[0] = pixelDataSize;
// Constant color order
channelHeader[1] = (ColorOrder.R << 0) | (ColorOrder.G << 2) | (ColorOrder.R << 4);
// Fix string length
channelHeader.writeUInt16LE(stringLength, 2);
function sendRaw(data: Buffer): void {
serial.write(data, e => {
if (e) throw e;
});
crcUpdateBlock(data);
}
function sendComplete(): Promise<void> {
return promisify(serial.drain.bind(serial))();
}
function sendPayload(channel: number, payload: Buffer): void {
if (payload.length != stringLength * pixelDataSize) throw new RangeError('Invalid number of bytes specified');
crcReset();
header[4] = channel;
header[5] = RecordType.SET_CHANNEL_WS2812;
sendRaw(header);
sendRaw(channelHeader);
sendRaw(payload);
sendCRC();
}
/**
* Latch a frame on all controllers
*/
async function sendDrawAll(): Promise<void> {
crcReset();
// Channel is ignored for "DRAW" command
header[4] = 0xff;
header[5] = RecordType.DRAW_ALL;
sendRaw(header);
sendCRC();
return sendComplete();
}
type LEDString = {
channel: number;
buff: Buffer;
};
let index = 0;
function makeString(startIndex?: number): LEDString {
if (startIndex !== undefined) index = startIndex;
const ret = {
channel: index,
buff: Buffer.allocUnsafe(stringLength * pixelDataSize).fill(20),
};
index++;
return ret;
}
const display: LEDString[] = [
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
// Second controller
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
makeString(),
];
async function drawDisplay(): Promise<void> {
for (const { channel, buff } of display) sendPayload(channel, buff);
return sendDrawAll();
}
function updateDisplay(state: State): void {
for (const polar in display) {
const { buff } = display[polar];
for (const vert in buff) {
buff[Number(vert)] = 0;
}
const t = Math.round(state.time * 100);
buff[(t + Number(polar)) % buff.length] = 255;
}
}
let skippedDraws = 0;
let drawsDone = 0;
setInterval(() => {
if (false && skippedDraws) {
console.log('Skipped Draws:', skippedDraws);
skippedDraws = 0;
}
if (false && drawsDone) {
console.log('Draws done:', drawsDone);
drawsDone = 0;
}
if (bytesReceived) {
console.log('Bytes Received:', bytesReceived);
bytesReceived = 0;
}
}, 1000);
/*
Theoretical max FPS
Frame Size = 16 * (50 * 3 + 6 + 4 + 4) + 6 + 4 bytes
Frame Tx Duration = ( (10 bits times / byte) * Frame Size ) / 2Mbps
= (10 * (16 * (50 * 3 + 6 + 4 + 4) + 6 + 4)) / 2e6
= 13.2ms
However, it is suggested to wait 3.6ms between starting new frame draw to prevent tearing
Max FPS = 1 / (Frame Tx Duration + 3.6ms)
= 1 / (((10 * (16 * (50 * 3 + 6 + 4 + 4) + 6 + 4)) / 2e6) seconds + (3.6 milliseconds))
= 60Hz
*/
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export async function doDisplay(state: State): Promise<void> {
if (drawing) {
skippedDraws++;
return;
}
drawing = true;
drawsDone++;
updateDisplay(state);
try {
await drawDisplay();
} catch (e) {
console.log('why error?');
console.log(e);
}
// await delay(3.6);
drawing = false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment