forked from longears/pixelslinger
/
opc.go
451 lines (407 loc) · 14 KB
/
opc.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
/*
Package opc helps you send and receive Open Pixel Control messages.
*/
package opc
import (
"bufio"
"fmt"
"github.com/longears/pixelslinger/midi"
"math"
"net"
"os"
"strconv"
"strings"
"time"
)
//--------------------------------------------------------------------------------
// PATTERN REGISTRY
var PATTERN_REGISTRY map[string](func(locations []float64) ByteThread)
func init() {
// This has to happen in init() to avoid an initialization loop (circular dependency)
// because the midi-switcher pattern reads from this map.
PATTERN_REGISTRY = map[string](func(locations []float64) ByteThread){
"basic-midi": MakePatternBasicMidi,
"diamond": MakePatternDiamond,
"eye": MakePatternEye,
"fire": MakePatternFire,
"japan": MakePatternJapan,
"midi-switcher": MakePatternMidiSwitcher,
"moire": MakePatternMoire,
"off": MakePatternOff,
"raver-plaid": MakePatternRaverPlaid,
"sailor-moon": MakePatternSailorMoon,
"shield": MakePatternShield,
"spatial-stripes": MakePatternSpatialStripes,
"sunset": MakePatternSunset,
"test": MakePatternTest,
"test-gamma": MakePatternTestGamma,
"test-rgb": MakePatternTestRGB,
"white": MakePatternWhite,
}
}
//--------------------------------------------------------------------------------
// TYPES
// Most of this library is built from ByteThread functions which you can
// string together via channels.
// These are meant to be run as goroutines. They read in one
// byte slice at a time through the input channel, do something to it,
// then return it over the output channel when done.
// These are used both for sources and destinations of pixel data.
// They should loop forever until the input channel is closed, then return.
// The byte slice should hold values from 0 to 255 in [r g b r g b r g b ... ] order
// so its total length is 3 times the number of pixels in the LED strip.
// The MidiState object is shared with other threads and should be treated as read-only.
// It will be updated during the time when the ByteThread is not holding a byte slice.
type ByteThread func(chan []byte, chan []byte, *midi.MidiState)
//--------------------------------------------------------------------------------
// CONSTANTS
// How many bytes can be written to the SPI bus at once?
const SPI_CHUNK_SIZE = 2048
// Gamma for LPD chipset
const GAMMA = 2.2
const CONNECTION_TRIES = 1 // milliseconds
const WAIT_TO_RETRY = 1000 // milliseconds
const WAIT_BETWEEN_RETRIES = 1 // milliseconds
//--------------------------------------------------------------------------------
// OPC LAYOUT FORMAT
// Read locations from OPC-style JSON layout file into a slice of floats
func ReadLocations(fn string) []float64 {
locations := make([]float64, 0)
var file *os.File
var err error
if file, err = os.Open(fn); err != nil {
panic(fmt.Sprintf("[opc.ReadLocations] could not open layout file: %s", fn))
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 || line[0] == '[' || line[0] == ']' {
continue
}
line = strings.Split(line, "[")[1]
line = strings.Split(line, "]")[0]
coordStrings := strings.Split(line, ", ")
var x, y, z float64
x, err = strconv.ParseFloat(coordStrings[0], 64)
y, err = strconv.ParseFloat(coordStrings[1], 64)
z, err = strconv.ParseFloat(coordStrings[2], 64)
locations = append(locations, x, y, z)
}
fmt.Printf("[opc.ReadLocations] Read %v pixel locations from %s\n", len(locations), fn)
return locations
}
//--------------------------------------------------------------------------------
// NET HELPERS
// Try to connect to the given ipPort. Retry several times in a row if needed,
// waiting WAIT_BETWEEN_RETRIES each time. On failure, return nil.
func getConnection(ipPort string) net.Conn {
fmt.Printf("[opc.getConnection] connecting to %v...\n", ipPort)
triesLeft := CONNECTION_TRIES
var conn net.Conn
var err error
for {
conn, err = net.Dial("tcp", ipPort)
if err == nil {
// success
fmt.Println("[opc.getConnection] connected")
return conn
}
fmt.Println("[opc.getConnection]", triesLeft, err)
time.Sleep(WAIT_BETWEEN_RETRIES * time.Millisecond)
triesLeft -= 1
if triesLeft == 0 {
// failure
return nil
}
}
}
//--------------------------------------------------------------------------------
// SENDING GOROUTINES
// Return a ByteThread which passes byte slices from the input to
// the output channels without doing anything.
func MakeSendToDevNullThread() ByteThread {
return func(bytesIn chan []byte, bytesOut chan []byte, midiState *midi.MidiState) {
fmt.Println("[opc.SendToDevNullThread] starting up")
for bytes := range bytesIn {
bytesOut <- bytes
}
}
}
// Return a ByteThread which prints the bytes to the screen.
func MakeSendToScreenThread() ByteThread {
const MAX_LEN = 19
result := make([]string, MAX_LEN)
return func(bytesIn chan []byte, bytesOut chan []byte, midiState *midi.MidiState) {
fmt.Println("[opc.SendToDevNullThread] starting up")
for bytes := range bytesIn {
for ii := 0; ii < len(bytes) && ii < MAX_LEN; ii++ {
if ii%4 == 3 {
result[ii] = "|"
ii += 1
}
result[ii] = fmt.Sprintf("%3d", bytes[ii])
}
fmt.Printf("[ %s ...] %v px \n", strings.Join(result, " "), len(bytes)/3)
bytesOut <- bytes
}
}
}
// Return a ByteThread which writes bytes to SPI via the given filename (such as "/dev/spidev1.0").
// Format the outgoing bytes for LED strips which use the LPD8806 chipset.
// If the SPI device can't be opened, exit the whole program with exit status 1.
// This chipset expects colors in G R B order; this function is responsible for swapping from
// the usual R G B order.
func MakeSendToLPD8806Thread(spiFn string) ByteThread {
return func(bytesIn chan []byte, bytesOut chan []byte, midiState *midi.MidiState) {
fmt.Println("[opc.SendToLPD8806Thread] starting up")
// open output file and keep the file descriptor around
spiFile, err := os.Create(spiFn)
if err != nil {
fmt.Println("[opc.SendToLPD8806Thread] Error opening SPI file:")
fmt.Println(err)
os.Exit(1)
}
// close spiFile on exit and check for its returned error
defer func() {
if err := spiFile.Close(); err != nil {
panic(err)
}
}()
gamma_lookup := make([]byte, 256)
for ii := 0; ii < 256; ii++ {
floatVal := math.Pow(float64(ii)/255, GAMMA)
if floatVal >= 1 {
gamma_lookup[ii] = 255
} else {
gamma_lookup[ii] = byte(floatVal * 256)
}
}
// as we get byte slices over the channel...
for bytes := range bytesIn {
// build a new slice of bytes in the format the LED strand wants
// TODO: avoid allocating these bytes over and over
spiBytes := make([]byte, 0)
// leading zeros to begin a new frame of bytes
numZeroes := (len(bytes)+31)/32 + 2
for ii := 0; ii < numZeroes*5; ii++ {
spiBytes = append(spiBytes, 0)
}
// actual bytes
//for _, v := range bytes {
for ii := 0; ii < len(bytes)-2; ii += 3 {
// apply gamma lookup table
r := gamma_lookup[bytes[ii+0]]
g := gamma_lookup[bytes[ii+1]]
b := gamma_lookup[bytes[ii+2]]
// HACK
// white balance for the strips with white backing
// red needs a boost
// green and blue are too strong
if ii >= 160*3 {
//r = byte(math.Pow(float64(r)/256.0, 0.7) * 256.0)
g = byte(float64(g) * 0.8)
b = byte(float64(b) * 0.7)
}
// format for LPD8806
// high bit must be always on, remaining seven bits are data
r = 128 | (r >> 1)
g = 128 | (g >> 1)
b = 128 | (b >> 1)
// swap to [g r b] order
if ii < 160*3 {
// copper-colored strip
spiBytes = append(spiBytes, g)
spiBytes = append(spiBytes, r)
spiBytes = append(spiBytes, b)
} else {
// white strips
spiBytes = append(spiBytes, b)
spiBytes = append(spiBytes, r)
spiBytes = append(spiBytes, g)
}
}
// send some extra black pixels to make the last LEDs latch
for ii := 0; ii < 6; ii++ {
spiBytes = append(spiBytes, 128)
}
// write spiBytes to the wire in chunks
//fmt.Println("sending", len(bytes), " + ", numZeroes, " zeroes = ", len(spiBytes), "bytes")
bytesSent := 0
for ii := 0; ii < len(spiBytes); ii += SPI_CHUNK_SIZE {
endIndex := ii + SPI_CHUNK_SIZE
if endIndex > len(spiBytes) {
endIndex = len(spiBytes)
}
thisChunk := spiBytes[ii:endIndex]
bytesSent += len(thisChunk)
if _, err := spiFile.Write(thisChunk); err != nil {
panic(err)
}
}
//fmt.Println(bytesSent,len(spiBytes))
bytesOut <- bytes
}
}
}
// Return a ByteThread which sends the bytes out as OPC messages to the given ipPort.
// Create OPC headers for each byte slice it sends.
// Initiate and maintains a long-lived connection to ipPort. If the connection is bad at any point
// (or was never good to begin with), keep trying to reconnect whenever new bytes come in.
// Can sleep for WAIT_TO_RETRY during reconnection attempts; this blocks the input channel.
// Silently drop bytes if it's not possible to send them.
func MakeSendToOpcThread(ipPort string) ByteThread {
return func(bytesIn chan []byte, bytesOut chan []byte, midiState *midi.MidiState) {
fmt.Println("[opc.SendToOpcThread] starting up")
var conn net.Conn
var err error
gamma_lookup := make([]byte, 256)
for ii := 0; ii < 256; ii++ {
floatVal := math.Pow(float64(ii)/255, GAMMA)
if floatVal >= 1 {
gamma_lookup[ii] = 255
} else {
gamma_lookup[ii] = byte(floatVal * 256)
}
}
for bytes := range bytesIn {
// if the connection has gone bad, make a new one
if conn == nil {
conn = getConnection(ipPort)
}
// if that didn't work, wait a second and restart the loop
if conn == nil {
bytesOut <- bytes
fmt.Println("[opc.SendToOpcThread] waiting to retry")
time.Sleep(WAIT_TO_RETRY * time.Millisecond)
continue
}
// ok, at this point the connection is good
// gamma correct
// HACK: change this later when we decide if OPC should have
// pixels in perceptual or linear space
for ii := range bytes {
bytes[ii] = gamma_lookup[bytes[ii+0]]
}
// make and send OPC header
channel := byte(0)
command := byte(0)
lenLowByte := byte(len(bytes) % 256)
lenHighByte := byte(len(bytes) / 256)
header := []byte{channel, command, lenHighByte, lenLowByte}
_, err = conn.Write(header)
if err != nil {
// net error -- set conn to nil so we can try to make a new one
fmt.Println("[opc.SendToOpcThread]", err)
conn = nil
bytesOut <- bytes
continue
}
// send actual pixel values
_, err = conn.Write(bytes)
if err != nil {
// net error -- set conn to nil so we can try to make a new one
fmt.Println("[opc.SendToOpcThread]", err)
conn = nil
bytesOut <- bytes
continue
}
bytesOut <- bytes
}
}
}
//--------------------------------------------------------------------------------
// OPC SERVER
// A single OPC message
type OpcMessage struct {
Channel byte
Command byte
Bytes []byte
}
// Read a series of OPC messages as bytes from the net connection, convert them into OpcMessage
// objects, and push pointers to those objects over the channel.
func handleOpcConnection(conn net.Conn, incomingOpcMessageChan chan *OpcMessage) {
// OPC protocol:
// byte 0: channel number
// byte 1: command
// byte 2: length (high byte)
// byte 3: length (low byte)
// bytes 4...: data in R G B order
for {
// get header
headerBuf := make([]byte, 4)
n, err := conn.Read(headerBuf)
if err != nil {
return // err is EOF hopefully
}
if n != 4 {
panic(fmt.Sprintf("header should be 4 bytes long, got %v", n))
}
channel := headerBuf[0]
command := headerBuf[1]
length := int(headerBuf[2])<<8 + int(headerBuf[3])
// get data
dataBuf := make([]byte, length)
// TODO: test this with length == 0
n, err = conn.Read(dataBuf)
if err != nil {
panic(err)
}
if n != length {
panic(fmt.Sprintf("expected %v bytes of data, got %v", length, n))
}
incomingOpcMessageChan <- &OpcMessage{channel, command, dataBuf}
}
}
// Start a server at ipPort (or, for example, ":7890") and push received *OpcMessage pointers over
// the incomingOpcMessageChan.
// You should launch this in its own goroutine.
func OpcServerThread(ipPort string, incomingOpcMessageChan chan *OpcMessage) {
fmt.Println("[opc] OPC server thread is listening on", ipPort)
listen, err := net.Listen("tcp", ":7890")
if err != nil {
panic(err)
}
for {
conn, err := listen.Accept()
if err != nil {
panic(err)
}
go handleOpcConnection(conn, incomingOpcMessageChan)
}
}
// Launch the OPC server in its own goroutine and return the channel over which it
// will push incoming OPC messages.
func LaunchOpcServer(ipPort string) chan *OpcMessage {
incomingOpcMessageChan := make(chan *OpcMessage, 0)
go OpcServerThread(ipPort, incomingOpcMessageChan)
return incomingOpcMessageChan
}
// Return a ByteThread function which will start an OPC server and push out pixels from it in
// the usual way ByteThreads do. The channel field is ignored, so if you're piping OPC In to
// OPC Out be aware that the channel will be set to zero in the process.
// Only pays attention to OPC messages with command 0 (set pixels).
func MakeOpcServerThread(ipPort string) ByteThread {
incomingOpcMessageChan := make(chan *OpcMessage, 0)
go OpcServerThread(ipPort, incomingOpcMessageChan)
return func(bytesIn chan []byte, bytesOut chan []byte, midiState *midi.MidiState) {
// wait for ready signal from outside
for byteSlice := range bytesIn {
// wait for incoming opc message
opcMessage := <-incomingOpcMessageChan
// only accept command 0 (set pixels)
if opcMessage.Command != 0 {
continue
}
// copy opc message bytes into byteSlice and return it
// because byteSlice and opcMessage.Bytes might be different lengths,
// we reset byteSlice back to length 0 and then append all the bytes
// while keeping the same underlying array for efficiency.
byteSlice = byteSlice[0:0]
for _, b := range opcMessage.Bytes {
byteSlice = append(byteSlice, b)
}
bytesOut <- byteSlice
}
}
}