-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathprolink.go
571 lines (462 loc) · 16.6 KB
/
prolink.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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
package prolink
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"time"
"github.com/inconshreveable/log15"
)
var be = binary.BigEndian
// Log specifies the logger that should be used for capturing information. May
// be disabled by replacing with the logrus.test.NullLogger.
var Log = log15.New("module", "prolink")
func init() {
Log.SetHandler(log15.LvlFilterHandler(
log15.LvlInfo,
log15.StdoutHandler,
))
}
// We wait a second and a half to send keep alive packets for the virtual CDJ
// we create on the PRO DJ LINK network.
const keepAliveInterval = 1500 * time.Millisecond
// How long to wait after before considering a device off the network.
const deviceTimeout = 10 * time.Second
// Length of device announce packets
const announcePacketLen = 54
const announceDeadline = 5 * time.Second
// The UDP address on which device announcements are received.
var announceAddr = &net.UDPAddr{
IP: net.IPv4zero,
Port: 50000,
}
// The UDP address on which device information is received.
var listenerAddr = &net.UDPAddr{
IP: net.IPv4zero,
Port: 50002,
}
// All UDP packets on the PRO DJ LINK network start with this header.
var prolinkHeader = []byte{
0x51, 0x73, 0x70, 0x74, 0x31,
0x57, 0x6d, 0x4a, 0x4f, 0x4c,
}
// playerIDrange is the normal set of player IDs that may exist on one prolink
// network.
var prolinkIDRange = []DeviceID{0x01, 0x02, 0x03, 0x04}
func makeNameBytes(dev *Device) []byte {
// The name is a 20 byte string
name := make([]byte, 20)
copy(name[:], []byte(dev.Name))
return name
}
// getAnnouncePacket constructs the announce packet that is sent on the PRO DJ
// LINK network to announce a devices existence.
func getAnnouncePacket(dev *Device) []byte {
// unknown padding bytes
unknown1 := []byte{0x01, 0x02, 0x00, 0x36}
unknown2 := []byte{0x01, 0x00, 0x00, 0x00}
parts := [][]byte{
prolinkHeader, // 0x00: 10 byte header
{0x06, 0x00}, // 0x0A: 02 byte announce packet type
makeNameBytes(dev), // 0x0c: 20 byte device name
unknown1, // 0x20: 04 byte unknown
{byte(dev.ID)}, // 0x24: 01 byte for the player ID
{byte(dev.Type)}, // 0x25: 01 byte for the player type
dev.MacAddr[:6], // 0x26: 06 byte mac address
dev.IP.To4(), // 0x2C: 04 byte IP address
unknown2, // 0x30: 04 byte unknown
{byte(dev.Type)}, // 0x34: 01 byte for the player type
{0x00}, // 0x35: 01 byte final padding
}
return bytes.Join(parts, nil)
}
// getStatusPacket returns a mostly empty-state status packet. This is
// currently used to report the virtual CDJs status, which *seems* to be
// required for the CDJ to send metadata about some unanalyzed mp3 files.
func getStatusPacket(dev *Device) []byte {
// NOTE: It seems that byte 0x68 and 0x75 MUST be 1 in order for the CDJ to
// correctly report mp3 metadata (again, only for some files).
// See https://github.com/brunchboy/dysentery/issues/15
// NOTE: Byte 0xb6 MUST be 1 in order for the CDJ to not think that our
// device is "running an older firmware".
b := []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x03, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9c, 0xff, 0xfe, 0x00, 0x10, 0x00, 0x00,
0x7f, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff, 0xff, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff,
0xff, 0xff, 0xff, 0xff, 0x01, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0f, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
// Replace key values into template packet
copy(b, prolinkHeader) // 0x00: 10 byte header
copy(b[0x0B:], makeNameBytes(dev)) // 0x0B: 20 byte device name
copy(b[0x21:], string(dev.ID)) // 0x21: 01 byte device ID
copy(b[0x24:], string(dev.ID)) // 0x24: 01 byte device ID
copy(b[0x7C:], VirtualCDJFirmware) // 0x7C: 04 byte firmware string
return b
}
// deviceFromAnnouncePacket constructs a device object given a device
// announcement packet.
func deviceFromAnnouncePacket(packet []byte) (*Device, error) {
if !bytes.HasPrefix(packet, prolinkHeader) {
return nil, fmt.Errorf("Announce packet does not start with expected header")
}
if packet[0x0A] != 0x06 {
return nil, fmt.Errorf("Packet is not an announce packet")
}
dev := &Device{
Name: string(bytes.TrimRight(packet[0x0C:0x0C+20], "\x00")),
ID: DeviceID(packet[0x24]),
Type: DeviceType(packet[0x34]),
MacAddr: net.HardwareAddr(packet[0x26 : 0x26+6]),
IP: net.IP(packet[0x2C : 0x2C+4]),
}
dev.LastActive = time.Now()
return dev, nil
}
// getMatchingInterface determines the interface that routes the given address
// by comparing the masked addresses. This type of information is generally
// determined through the kernels routing table, but for sake of cross-platform
// compatibility, we do some rudimentary lookup.
func getMatchingInterface(ip net.IP) (*net.Interface, error) {
Log.Debug("Matching IP route interface", "ip", ip)
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for _, possibleIface := range ifaces {
if possibleIface.Flags&net.FlagUp == 0 {
continue
}
addrs, err := possibleIface.Addrs()
if err != nil {
return nil, err
}
var matchedIface *net.Interface
matchedSubnetLen := 0x00
for _, addr := range addrs {
ifaceIP, ok := addr.(*net.IPNet)
if !ok {
continue
}
subnetLen, _ := ifaceIP.Mask.Size()
if ifaceIP.Contains(ip) && subnetLen > matchedSubnetLen {
matchedIface = &possibleIface
matchedSubnetLen = subnetLen
}
Log.Debug("Checking iface addr", "iface", possibleIface.Name, "addr", ifaceIP)
}
if matchedIface != nil {
return matchedIface, nil
}
}
return nil, fmt.Errorf("Failed to find matching interface for %s", ip)
}
// getV4IPNetOfInterface finds the first Ipv4 address on an interface that is
// not a loopback address.
func getV4IPNetOfInterface(iface *net.Interface) (*net.IPNet, error) {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if ok && ipNet.IP.To4() != nil && !ipNet.IP.IsLoopback() {
return ipNet, nil
}
}
return nil, nil
}
// getBroadcastAddress determines the broadcast address to use for
// communicating with the device.
func getBroadcastAddress(dev *Device) *net.UDPAddr {
iface, _ := getMatchingInterface(dev.IP)
ipNet, _ := getV4IPNetOfInterface(iface)
mask := ipNet.Mask
bcastIPAddr := make(net.IP, net.IPv4len)
for i, b := range dev.IP.To4() {
bcastIPAddr[i] = b | ^mask[i]
}
broadcastAddr := net.UDPAddr{
IP: bcastIPAddr,
Port: announceAddr.Port,
}
return &broadcastAddr
}
// newVirtualCDJDevice constructs a Device that can be bound to the network
// interface provided.
func newVirtualCDJDevice(iface *net.Interface, id DeviceID) (*Device, error) {
ipNet, err := getV4IPNetOfInterface(iface)
if err != nil {
return nil, err
}
if ipNet == nil {
return nil, fmt.Errorf("No IPv4 broadcast interface available")
}
virtualCDJ := &Device{
Name: VirtualCDJName,
ID: id,
Type: DeviceTypeCDJ,
MacAddr: iface.HardwareAddr,
IP: ipNet.IP,
}
return virtualCDJ, nil
}
// cdjAnnouncer manages announcing a CDJ device on the network. This is usually
// used to announce a "virtual CDJ" which allows the prolink library to receive
// more details from real CDJs on the network.
type cdjAnnouncer struct {
cancel chan bool
running bool
}
// start creates a goroutine that will continually announce a virtual CDJ
// device on the host network.
func (a *cdjAnnouncer) activate(vCDJ *Device, dm *DeviceManager, announceConn *net.UDPConn) {
if a.running {
return
}
Log.Info("Announcing vCDJ on network", "vCDJ", vCDJ)
announceTicker := time.NewTicker(keepAliveInterval)
broadcastAddrs := getBroadcastAddress(vCDJ)
announcePacket := getAnnouncePacket(vCDJ)
statusPacket := getStatusPacket(vCDJ)
Log.Info("Broadcast address detected", "addr", broadcastAddrs)
announceStatus := func() {
for _, device := range dm.ActiveDevices() {
addr := &net.UDPAddr{
IP: device.IP,
Port: listenerAddr.Port,
}
announceConn.SetWriteDeadline(time.Now().Add(announceDeadline))
announceConn.WriteToUDP(statusPacket, addr)
}
}
go func() {
for {
select {
case <-a.cancel:
a.running = false
return
case <-announceTicker.C:
announceConn.SetWriteDeadline(time.Now().Add(announceDeadline))
announceConn.WriteToUDP(announcePacket, broadcastAddrs)
announceStatus()
}
}
}()
a.running = true
}
// stop stops the running announcer
func (a *cdjAnnouncer) deactivate() {
if a.running {
a.cancel <- true
}
Log.Info("Announcer stopped")
}
func newCDJAnnouncer() *cdjAnnouncer {
return &cdjAnnouncer{
cancel: make(chan bool),
}
}
// Network is the primary API to the PRO DJ LINK network.
type Network struct {
announceConn *net.UDPConn
listenerConn *net.UDPConn
announcer *cdjAnnouncer
cdjMonitor *CDJStatusMonitor
devManager *DeviceManager
remoteDB *RemoteDB
// TargetInterface specifies what network interface to broadcast announce
// packets for the virtual CDJ on.
//
// This field should not be reconfigured, use SetInterface instead to
// ensure the announce is correctly restarted on the new interface.
TargetInterface *net.Interface
// VirtualCDJID specifies the CDJ Device ID (Player ID) that should be used
// when announcing the device.
//
// This field should not be reconfigured, use SetVirtualCDJID instead to
// ensure the announce is correctly restarted on the new interface.
VirtualCDJID DeviceID
}
// CDJStatusMonitor obtains the CDJStatusMonitor for the network.
func (n *Network) CDJStatusMonitor() *CDJStatusMonitor {
return n.cdjMonitor
}
// DeviceManager returns the DeviceManager for the network.
func (n *Network) DeviceManager() *DeviceManager {
return n.devManager
}
// RemoteDB returns the remote database client for the network.
func (n *Network) RemoteDB() *RemoteDB {
return n.remoteDB
}
// SetVirtualCDJID configures the CDJ ID (Player ID) that the prolink library
// should use to identify itself on the network. To correctly access metadata
// on the network this *must* be in the range from 1-4, and should *not* be a
// player ID that is already in use by a CDJ, otherwise the CDJ simply will not
// respond. This is a known issue [1]
//
// [1]: https://github.com/evanpurkhiser/prolink-go/issues/6
func (n *Network) SetVirtualCDJID(id DeviceID) error {
n.VirtualCDJID = id
n.remoteDB.setRequestingDeviceID(id)
Log.Info("VirtualCDJ ID updated", "ID", id)
return n.reloadAnnouncer()
}
// SetInterface configures what network interface should be used when
// announcing the Virtual CDJ.
func (n *Network) SetInterface(iface *net.Interface) error {
lastInterface := n.TargetInterface
n.TargetInterface = iface
Log.Info("PROLINK interface updated", "iface", iface.Name)
err := n.reloadAnnouncer()
if err != nil {
Log.Warn("Bad interface, restoring previous interface", "err", err)
n.TargetInterface = lastInterface
n.reloadAnnouncer()
}
return err
}
// AutoConfigure attempts to configure the two configuration parameters of the
// network.
//
// - Determine which interface to announce the Virtual CDJ over by finding
// the interface which has a matching net mask to the first CDJ detected on the
// network.
//
// - Determine the Virtual CDJ ID to assume by looking for the first unused CDJ
// ID on the network.
//
// wait specifies how long to wait before checking what devices have appeared
// on the network to determine auto configuration values from.
func (n *Network) AutoConfigure(wait time.Duration) error {
Log.Debug("Waiting to autoconfigure", "wait", wait)
time.Sleep(wait)
Log.Debug("Starting autoconfigure")
playerIDs := []DeviceID{}
var CDJAddr net.IP
for _, device := range n.devManager.ActiveDevices() {
if device.Type != DeviceTypeCDJ {
continue
}
playerIDs = append(playerIDs, device.ID)
CDJAddr = device.IP
}
if len(playerIDs) == 0 {
return fmt.Errorf("Could not autoconfigure network: no CDJs on network")
}
var unusedDeviceID DeviceID
// Choose an unused ID from the 4 available CDJ slots
for _, id := range prolinkIDRange {
isUnused := true
for _, usedID := range playerIDs {
if id == usedID {
isUnused = false
}
}
if isUnused {
unusedDeviceID = id
break
}
}
if unusedDeviceID == 0x0 {
return fmt.Errorf("Could not autoconfigure network: No available Virtual CDJ slots")
}
n.SetVirtualCDJID(unusedDeviceID)
// Determine the matching interface for the CDJ
iface, err := getMatchingInterface(CDJAddr)
if err != nil {
return fmt.Errorf("Could not autoconfigure network: %s", err)
}
n.SetInterface(iface)
return nil
}
func (n *Network) reloadAnnouncer() error {
if n.TargetInterface == nil || n.VirtualCDJID == 0x0 {
return nil
}
vCDJ, err := newVirtualCDJDevice(n.TargetInterface, n.VirtualCDJID)
if err != nil {
return fmt.Errorf("Failed to construct virtual CDJ: %s", err)
}
Log.Info("Reloading announcer")
n.announcer.deactivate()
n.announcer.activate(vCDJ, n.devManager, n.announceConn)
// Reload the remote remote DB service since we may now be announcing as a
// different device, we need to re-associate ourselves with the devices
// serving the remote database.
n.remoteDB.deactivate(n.devManager)
n.remoteDB.activate(n.devManager)
return nil
}
// openUDPConnection connects to the minimum required UDP sockets needed to
// communicate with the Prolink network.
func (n *Network) openUDPConnections() error {
listenerConn, err := net.ListenUDP("udp", listenerAddr)
if err != nil {
return fmt.Errorf("Failed to open listener connection: %s", err)
}
n.listenerConn = listenerConn
Log.Debug("UDP socket open -> packet listening", "addr", listenerAddr)
announceConn, err := net.ListenUDP("udp", announceAddr)
if err != nil {
return fmt.Errorf("Cannot open UDP announce connection: %s", err)
}
n.announceConn = announceConn
Log.Debug("UDP socket open -> announce listening", "addr", announceAddr)
return nil
}
// activeNetwork keeps a reference to the currently connected network.
var activeNetwork *Network
// Connect connects to the Pioneer PRO DJ LINK network, returning the singleton
// Network object to interact with the connection.
//
// Note that after connecting you must configure the virtual CDJ ID and network
// interface to announce the virtual CDJ on before all functionality of the
// prolink network will be available, specifically:
//
// - CDJs will not broadcast detailed payer information until they receive the
// announce packet and recognize the libraries virtual CDJ as being on the
// network.
//
// - Any remote DB devices will not respond to metadata queries.
//
// Both values may be autodetected or manually configured.
func Connect() (*Network, error) {
if activeNetwork != nil {
return activeNetwork, nil
}
Log.Info("Connecting to Prolink network")
n := &Network{
announcer: newCDJAnnouncer(),
remoteDB: newRemoteDB(),
devManager: newDeviceManager(),
cdjMonitor: newCDJStatusMonitor(),
}
activeNetwork = n
err := n.openUDPConnections()
if err != nil {
return nil, err
}
// We can start the device manager and CDJ monitor immediately as neither
// of these have any type of reconfiguration options other than then
// network connection.
n.devManager.activate(n.announceConn)
n.cdjMonitor.activate(n.listenerConn)
// NOTE: We cannot start the remoteDB service until the Virtual CDJ has
// been announced on the network.
return n, nil
}