Skip to content

Commit

Permalink
Improve DMX Usb device type detection. Add customizable OpenDMXUsb re…
Browse files Browse the repository at this point in the history
…fresh rate
  • Loading branch information
spensbot committed Dec 11, 2023
1 parent 9239b45 commit 96338f3
Show file tree
Hide file tree
Showing 16 changed files with 255 additions and 121 deletions.
34 changes: 21 additions & 13 deletions src/main/engine/connections/ConnectionManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,17 @@ import {
import { DmxConnection, createDmxConnection } from './dmx/DmxConnection'
import { getDmxDevice } from './dmx/DmxDevice_t'
import { ArtNetManager } from './art-net/ArtNetManager'
import { CleanReduxState } from 'renderer/redux/store'
import { RealtimeState } from 'renderer/redux/realtimeStore'
import { DmxConnectionUsb } from './dmx/DmxConnectionUsb'
import { EngineContext } from '../engineContext'

export class ConnectionManager {
private getControlState: () => CleanReduxState | null
private getRealtimeState: () => RealtimeState
private c: EngineContext
private dmxConnections: { [key: ConnectionId]: DmxConnection } = {}
private artNet: ArtNetManager

constructor(
getControlState: () => CleanReduxState | null,
getRealtimeState: () => RealtimeState
) {
this.getControlState = getControlState
this.getRealtimeState = getRealtimeState
this.artNet = new ArtNetManager(this.getRealtimeState, this.getControlState)
constructor(c: EngineContext) {
this.c = c
this.artNet = new ArtNetManager(this.c)
}

async updateConnections(
Expand All @@ -37,9 +32,22 @@ export class ConnectionManager {

this.pruneConnections(connectTo)

// The connection may learn additional info about the device (such as the type) once established.
const availableDevicesWithConnectionInfo = availableDevices.map(
(device) => {
const connection: DmxConnectionUsb | undefined =
this.dmxConnections[device.connectionId]
if (connection && connection.device.path === device.path) {
return connection.device
} else {
return device
}
}
)

let status: DmxConnectionInfo = {
connected: Object.keys(this.dmxConnections),
available: availableDevices,
available: availableDevicesWithConnectionInfo,
serialports,
artNet: this.artNet.updateConnections(),
}
Expand Down Expand Up @@ -74,7 +82,7 @@ export class ConnectionManager {
const newConnections = await Promise.all(
availableDevices
.filter((device) => shouldConnect(device))
.map((device) => createDmxConnection(device, this.getRealtimeState))
.map((device) => createDmxConnection(device, this.c))
)

for (const connection of newConnections) {
Expand Down
27 changes: 19 additions & 8 deletions src/main/engine/connections/SerialConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,18 @@ export interface SetOptions {
export class SerialConnection {
private readyToWrite = true
private connection: SerialPort
private onData: (data: Buffer) => void = () => {}

private constructor(connection: SerialPort) {
this.connection = connection
this.connection.on('data', (data) => this.onData(data))
// These don't really need to be used if we check if it's open
connection.on('disconnect', (_d) => {
console.log('Serial Disconnect')
})
connection.on('error', (e) => {
console.error('Error', e)
})
}

static async connect(path: string): Promise<SerialConnection> {
Expand All @@ -34,14 +43,6 @@ export class SerialConnection {
}
}
)

// These don't really need to be used if we check if it's open
connection.on('disconnect', (_d) => {
console.log('Serial Disconnect')
})
connection.on('error', (e) => {
console.error('Error', e)
})
})
}

Expand Down Expand Up @@ -76,4 +77,14 @@ export class SerialConnection {
})
}
}

writeAndAwaitReply(buffer: Buffer, timeoutMs = 500): Promise<Buffer | null> {
return new Promise((resolve, _reject) => {
this.onData = resolve
this.write(buffer)
setTimeout(() => {
resolve(null)
}, timeoutMs)
})
}
}
33 changes: 17 additions & 16 deletions src/main/engine/connections/art-net/ArtNetManager.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,39 @@
import dgram from 'node:dgram'
import { artDmxBuffer } from './artNetBuffers'
import { RealtimeState } from 'renderer/redux/realtimeStore'
import * as c from './constants'
import { CleanReduxState } from 'renderer/redux/store'
import { toIpBuffer } from '../ipUtil'
import * as constants from './constants'
import { ArtNetConnectionInfo } from 'shared/connection'
import { EngineContext } from 'main/engine/engineContext'

export class ArtNetManager {
private client: dgram.Socket
private intervalHandle: NodeJS.Timer

constructor(
getRealtimeState: () => RealtimeState,
getControlState: () => CleanReduxState | null
) {
constructor(c: EngineContext) {
this.client = dgram.createSocket('udp4')

this.intervalHandle = setInterval(() => {
const universe = getRealtimeState().dmxOut
const universe = c.realtimeState().dmxOut
const buffer = artDmxBuffer(universe, 0)
const controlState = getControlState()
const controlState = c.controlState()
const artNetIpOut: string | undefined = controlState
? controlState.control.device.connectable.artNet[0]
: undefined
if (artNetIpOut && toIpBuffer(artNetIpOut)) {
this.client.send(buffer, c.ARTNET_PORT, artNetIpOut, (err, _bytes) => {
if (err) {
console.error(`ArtNet UDP Error: ${err}`)
this.client.close()
this.client = dgram.createSocket('udp4')
this.client.send(
buffer,
constants.ARTNET_PORT,
artNetIpOut,
(err, _bytes) => {
if (err) {
console.error(`ArtNet UDP Error: ${err}`)
this.client.close()
this.client = dgram.createSocket('udp4')
}
}
})
)
}
}, c.DMX_PERIOD_MS)
}, constants.DMX_PERIOD_MS)
}

updateConnections(): ArtNetConnectionInfo {
Expand Down
6 changes: 3 additions & 3 deletions src/main/engine/connections/dmx/DmxConnection.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { DmxDevice_t } from 'shared/connection'
import { DmxConnectionUsb } from './DmxConnectionUsb'
import { RealtimeState } from 'renderer/redux/realtimeStore'
import { EngineContext } from 'main/engine/engineContext'

export type DmxConnection = DmxConnectionUsb

export async function createDmxConnection(
device: DmxDevice_t,
getRealtimeState: () => RealtimeState
c: EngineContext
): Promise<DmxConnection> {
return DmxConnectionUsb.create(device, getRealtimeState)
return DmxConnectionUsb.create(device, c)
}
54 changes: 37 additions & 17 deletions src/main/engine/connections/dmx/DmxConnectionUsb.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,71 @@
import { DmxDeviceUsb_t, DmxDevice_t } from 'shared/connection'
import { DmxDeviceUsb_t, DmxUsbDeviceType } from 'shared/connection'
import { SerialConnection } from '../SerialConnection'
import DmxUsbPro from './DmxUsbPro'
import DmxUsbPro, { isDmxUsbPro } from './DmxUsbPro'
import OpenDmxUsb from './OpenDmxUsb'
import { RealtimeState } from 'renderer/redux/realtimeStore'
import { EngineContext } from 'main/engine/engineContext'

export interface DmxUsbDeviceConfig {
sendUniverse: (
universe: number[],
connection: SerialConnection
) => Promise<void>
refreshHz: number
refreshHz: (c: EngineContext) => number
name: string
}

const configByDeviceType: { [key in DmxDevice_t['type']]: DmxUsbDeviceConfig } =
{
DmxUsbPro,
OpenDmxUsb,
}
const configByDeviceType: { [key in DmxUsbDeviceType]: DmxUsbDeviceConfig } = {
DmxUsbPro,
OpenDmxUsb,
}

export class DmxConnectionUsb {
type = 'DmxConnectionUsb'
device: DmxDeviceUsb_t
private serialConnection: SerialConnection
private intervalHandle: NodeJS.Timer
private config: DmxUsbDeviceConfig
private getRealtimeState: () => RealtimeState
private c: EngineContext
private lastHz: number = 0

private constructor(
device: DmxDeviceUsb_t,
serialConnection: SerialConnection,
getRealtimeState: () => RealtimeState
c: EngineContext
) {
this.c = c
this.device = device
this.serialConnection = serialConnection
this.config = configByDeviceType[this.device.type]
this.config = configByDeviceType[device.type as DmxUsbDeviceType]
this.lastHz = this.config.refreshHz(this.c)
this.device.name = this.config.name
this.intervalHandle = setInterval(() => {
this.sendDmx()
}, 1000 / this.config.refreshHz)
this.getRealtimeState = getRealtimeState
}, 1000 / this.config.refreshHz(c))
}

static async create(
device: DmxDeviceUsb_t,
getRealtimeState: () => RealtimeState
c: EngineContext
): Promise<DmxConnectionUsb> {
let serialConnection = await SerialConnection.connect(device.path)
return new DmxConnectionUsb(device, serialConnection, getRealtimeState)

let isPro = await isDmxUsbPro(serialConnection)

device.type = isPro ? 'DmxUsbPro' : 'OpenDmxUsb'

return new DmxConnectionUsb(device, serialConnection, c)
}

beginInterval() {
this.intervalHandle = setInterval(() => {
this.sendDmx()
const hz = this.config.refreshHz(this.c)
if (this.lastHz !== hz) {
this.lastHz = hz
clearInterval(this.intervalHandle)
this.beginInterval()
}
}, 1000 / this.config.refreshHz(this.c))
}

isOpen(): boolean {
Expand All @@ -59,7 +79,7 @@ export class DmxConnectionUsb {

private sendDmx() {
this.config.sendUniverse(
this.getRealtimeState().dmxOut,
this.c.realtimeState().dmxOut,
this.serialConnection
)
}
Expand Down
23 changes: 6 additions & 17 deletions src/main/engine/connections/dmx/DmxDevice_t.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,25 @@
import { SerialportInfo, DmxDevice_t, ConnectionId } from 'shared/connection'

const dmxDeviceNames: { [key in DmxDevice_t['type']]: string } = {
DmxUsbPro: 'Dmx Usb Pro',
OpenDmxUsb: 'Open Dmx Usb',
}

function getConnectionId(port: SerialportInfo): ConnectionId {
return port.path
return port.serialNumber ?? port.path
}

function getDmxDeviceType(port: SerialportInfo): DmxDevice_t['type'] | null {
const mfg = port.manufacturer

if (mfg?.includes('DMX') || mfg?.includes('ENTTEC')) return 'DmxUsbPro'
else if (mfg?.includes('FTDI')) return 'OpenDmxUsb'

return null
function isDmxDevice(port: SerialportInfo): boolean {
return port.productId === '6001'
}

export function getDmxDevice(port: SerialportInfo): DmxDevice_t | null {
let type = getDmxDeviceType(port)
if (type === null) return null
if (!isDmxDevice(port)) return null

return {
type,
type: null,
connectionId: getConnectionId(port),
path: port.path,
manufacturer: port.manufacturer,
pnpId: port.pnpId,
productId: port.productId,
serialNumber: port.serialNumber,
vendorId: port.vendorId,
name: dmxDeviceNames[type],
name: 'Dmx Usb Device',
}
}
Loading

0 comments on commit 96338f3

Please sign in to comment.