Created
September 15, 2020 04:14
-
-
Save cinderblock/3a495618b1aa3152f5326c346851cad9 to your computer and use it in GitHub Desktop.
Extracted javascript interface for talking to pixelblaze expander boards
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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