UNPKG

iobroker.x-touch

Version:

Communicate with a Behringer X-Touch Control Surface (DAW Controller)

1,072 lines (960 loc) 126 kB
/** * * iobroker x-touch Adapter * * Copyright (c) 2020-2025, Bannsaenger <bannsaenger@gmx.de> * * MIT License * */ /* * ToDo: */ // The adapter-core module gives you access to the core ioBroker functions // you need to create an adapter const utils = require('@iobroker/adapter-core'); // Load your modules here, e.g.: const fs = require('fs'); const udp = require('dgram'); // const { debug } = require('console'); const POLL_REC = 'F0002032585400F7'; const POLL_REPLY = 'F00000661400F7'; //const HOST_CON_QUERY = 'F000006658013031353634303730344539F7'; const HOST_CON_QUERY = 'F000006658013031353634303732393345F7'; const HOST_CON_REPLY = 'F0000066580230313536343037353D1852F7'; class XTouch extends utils.Adapter { /** * @param {Partial<utils.AdapterOptions>} [options] Options from js-controller */ constructor(options) { super({ ...options, name: 'x-touch', }); this.on('ready', this.onReady.bind(this)); this.on('stateChange', this.onStateChange.bind(this)); // this.on('objectChange', this.onObjectChange.bind(this)); this.on('message', this.onMessage.bind(this)); this.on('unload', this.onUnload.bind(this)); // read Objects template for object generation this.objectTemplates = JSON.parse(fs.readFileSync(`${__dirname}/lib/object_templates.json`, 'utf8')); // Midi mapping this.midi2Objects = JSON.parse(fs.readFileSync(`${__dirname}/lib/midi_mapping.json`, 'utf8')); this.objects2Midi = {}; // and layout this.consoleLayout = JSON.parse(fs.readFileSync(`${__dirname}/lib/console_layout.json`, 'utf8')); // mapping of the encoder modes to LED values this.encoderMapping = JSON.parse(fs.readFileSync(`${__dirname}/lib/encoder_mapping.json`, 'utf8')); // mapping of the characters in timecode display to 7-segment // coding is in Siekoo-Alphabet (https://fakoo.de/siekoo.html) // not as described in Logic Control Manual this.characterMapping = JSON.parse(fs.readFileSync(`${__dirname}/lib/character_mapping.json`, 'utf8')); // devices object, key is ip address. Values are connection and memberOfGroup this.devices = []; this.nextDevice = 0; // next device index for db creation this.deviceGroups = []; this.timers = {}; // a place to store timers this.timers.encoderWheels = {}; // e.g. encoder wheel reset timers by device group this.timers.sendDelay = undefined; // put the timer based on the configured sendDelay here // Send buffer (Array of sendData objects) // sendData = { // data: {buffer | array of buffers} // address : {string} // ipAddress // port: {string | number} // port to send back (normally 10111) // } this.sendBuffer = []; this.sendActive = false; // true if data sending is ongoing right now // creating a udp server this.server = udp.createSocket('udp4'); } /** * Is called when databases are connected and adapter received configuration. */ async onReady() { try { // Initialize your adapter here // Reset the connection indicator during startup this.setState('info.connection', false, true); // emits when any error occurs this.server.on('error', this.onServerError.bind(this)); // emits when socket is ready and listening for datagram msgs this.server.on('listening', this.onServerListening.bind(this)); // emits after the socket is closed using socket.close(); this.server.on('close', this.onServerClose.bind(this)); // emits on new datagram msg this.server.on('message', this.onServerMessage.bind(this)); // The adapters config (in the instance object everything under the attribute 'native' is accessible via // this.config: /* * create a vice versa mapping in object2Midi */ for (const mapping of Object.keys(this.midi2Objects)) { this.objects2Midi[this.midi2Objects[mapping]] = mapping; } /* * For every state in the system there has to be also an object of type state */ for (const element of this.objectTemplates.common) { await this.setObjectNotExistsAsync(element._id, element); } /* * create the database */ await this.createDatabaseAsync(); // Read all devices in the db let tempObj; let actDeviceNum = '-1'; const result_state = await this.getStatesOfAsync('devices'); for (const element of result_state) { const splitStringArr = element._id.split('.'); if (splitStringArr[3] !== actDeviceNum) { // next device detected actDeviceNum = splitStringArr[3]; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.deviceLocked`); const actDeviceLocked = tempObj && tempObj.val ? tempObj.val.toString() : ''; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.ipAddress`); const actIpAddress = tempObj && tempObj.val ? tempObj.val.toString() : ''; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.port`); const actPort = tempObj && tempObj.val ? tempObj.val.toString() : ''; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.memberOfGroup`); const actMemberOfGroup = tempObj && tempObj.val ? tempObj.val : 0; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.serialNumber`); const actSerialNumber = tempObj && tempObj.val ? tempObj.val.toString() : ''; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.activeBank`); const actActiveBank = tempObj && tempObj.val ? tempObj.val : 0; tempObj = await this.getStateAsync(`devices.${actDeviceNum}.activeBaseChannel`); const actActiveBaseChannel = tempObj && tempObj.val ? tempObj.val : 0; this.devices[actIpAddress] = { index: actDeviceNum, connection: false, // connection must be false on system start deviceLocked: actDeviceLocked, blankingFinished: false, // allow sending of updates while blanking runs. Will be true if blanking succeeded ipAddress: actIpAddress, port: actPort, memberOfGroup: actMemberOfGroup, serialNumber: actSerialNumber, activeBank: actActiveBank, activeBaseChannel: actActiveBaseChannel, }; this.log.debug( `X-Touch got device with ip address ${this.devices[actIpAddress].ipAddress} from the db`, ); } } this.nextDevice = Number(actDeviceNum) + 1; this.log.info( `X-Touch got ${Object.keys(this.devices).length} devices from the db. Next free device number: "${ this.nextDevice }"`, ); // read all states from the device groups to memory const device_states = await this.getStatesOfAsync('deviceGroups'); for (const device_state of device_states) { this.deviceGroups[device_state._id] = device_state; tempObj = await this.getStateAsync(device_state._id); this.deviceGroups[device_state._id].val = tempObj && tempObj.val !== undefined ? tempObj.val : ''; this.deviceGroups[device_state._id].helperBool = false; // used for e.g. autoToggle this.deviceGroups[device_state._id].helperNum = -1; // used for e.g. display of encoders } this.log.info(`X-Touch got ${Object.keys(this.deviceGroups).length} states from the db`); // In order to get state updates, you need to subscribe to them. The following line adds a subscription for our variable we have created above. // Or, if you really must, you can also watch all states. Don't do this if you don't need to. Otherwise this will cause a lot of unnecessary load on the system: this.subscribeStates('*'); // try to open open configured server port this.log.info(`Bind UDP socket to: "${this.config.bind}:${this.config.port}"`); this.server.bind(this.config.port, this.config.bind); // Set the connection indicator after startup // this.setState('info.connection', true, true); // set by onServerListening // create a timer to reset the encoder state for each device group for (let index = 0; index < this.config.deviceGroups; index++) { this.timers.encoderWheels[index] = setTimeout( this.onEncoderWheelTimeoutExceeded.bind(this, index.toString()), 1000, ); } // last action is to create the timer for the sendDelay and unref it immediately this.timers.sendDelay = setTimeout( this.deviceSendNext.bind(this, undefined, 'timer'), this.config.sendDelay || 1, ); //this.timers.sendDelay.unref(); } catch (err) { this.errorHandler(err, 'onReady'); } } /** * Is called to set the connection state in db and log * * @param {string} deviceAddress IP address of the device to handle * @param {number} port Source port of device * @param {boolean} status Status to set, online = true, offline = false */ async setConnection(deviceAddress, port, status) { try { if (status) { /* create new device if this is the first polling since start of adapter */ if (!(deviceAddress in this.devices)) { this.devices[deviceAddress] = { activeBank: 0, activeBaseChannel: 1, connection: true, ipAddress: deviceAddress, port: port, memberOfGroup: 0, serialNumber: '', deviceLocked: false, blankingFinished: false, index: this.nextDevice, }; let prefix = `devices.${this.nextDevice.toString()}`; this.setObjectNotExists(prefix, this.objectTemplates.device); prefix += '.'; this.nextDevice++; for (const element of this.objectTemplates.devices) { await this.setObjectNotExistsAsync(prefix + element._id, element); } this.log.info(`X-Touch device with IP <${deviceAddress}> created. Is now online.`); this.setState(`${prefix}ipAddress`, deviceAddress, true); this.setState(`${prefix}port`, port, true); this.setState(`${prefix}memberOfGroup`, 0, true); this.setState(`${prefix}connection`, true, true); this.setState(`${prefix}deviceLocked`, false, true); this.deviceUpdateDevice(deviceAddress); if (this.devices[deviceAddress].timerDeviceInactivityTimeout) { this.devices[deviceAddress].timerDeviceInactivityTimeout.refresh(); } else { this.devices[deviceAddress].timerDeviceInactivityTimeout = setTimeout( this.onDeviceInactivityTimeoutExceeded.bind(this, deviceAddress), this.config.deviceInactivityTimeout, ); } } else { // object in db must exist. Only set state if connection changed to true // create all not existing objects if device was created before const prefix = `devices.${this.devices[deviceAddress].index}.`; for (const element of this.objectTemplates.devices) { await this.setObjectNotExistsAsync(prefix + element._id, element); } if (!this.devices[deviceAddress].connection) { this.devices[deviceAddress].connection = true; this.devices[deviceAddress].port = port; this.log.info(`X-Touch device with IP <${deviceAddress}> is now online.`); this.setState(`devices.${this.devices[deviceAddress].index}.connection`, true, true); this.setState(`devices.${this.devices[deviceAddress].index}.port`, port, true); // port can have changed this.deviceUpdateDevice(deviceAddress); } if (this.devices[deviceAddress].timerDeviceInactivityTimeout) { this.devices[deviceAddress].timerDeviceInactivityTimeout.refresh(); } else { this.devices[deviceAddress].timerDeviceInactivityTimeout = setTimeout( this.onDeviceInactivityTimeoutExceeded.bind(this, deviceAddress), this.config.deviceInactivityTimeout, ); } } } else { this.devices[deviceAddress].connection = false; this.log.info(`X-Touch device with IP <${deviceAddress}> now offline.`); this.setState(`devices.${this.devices[deviceAddress].index}.connection`, false, true); if (this.devices[deviceAddress].timerDeviceInactivityTimeout) { clearTimeout(this.devices[deviceAddress].timerDeviceInactivityTimeout); this.devices[deviceAddress].timerDeviceInactivityTimeout = undefined; } } } catch (err) { this.errorHandler(err, 'setConnection'); } } // Methods related to Server events /** * Is called if a server error occurs * * @param {any} error detected server error */ onServerError(error) { this.log.error(`Server got Error: <${error}> closing server.`); // Reset the connection indicator this.setState('info.connection', false, true); this.server.close(); } /** * Is called when the server is ready to process traffic */ onServerListening() { const addr = this.server.address(); this.log.info(`X-Touch server ready on <${addr.address}> port <${addr.port}> proto <${addr.family}>`); // Set the connection indicator after server goes for listening this.setState('info.connection', true, true); } /** * Is called when the server is closed via server.close */ onServerClose() { this.log.info('X-Touch server is closed'); } /** * Is called when the activity timer of a device expires * * @param {string} deviceAddress IP address of the device to handle */ onDeviceInactivityTimeoutExceeded(deviceAddress) { this.log.debug(`X-Touch device "${deviceAddress}" reached inactivity timeout`); this.setConnection(deviceAddress, 0, false); } /** * Is called when the encoder wheel values must be resetted to false * * @param {string} deviceGroup the device group to handle */ onEncoderWheelTimeoutExceeded(deviceGroup) { this.log.debug(`X-Touch encoder wheel from device group ${deviceGroup}" reached inactivity timeout`); this.setState(`deviceGroups.${deviceGroup}.transport.encoder.cw`, false, true); // reset the this.setState(`deviceGroups.${deviceGroup}.transport.encoder.ccw`, false, true); // state values } /** * Is called on new datagram msg from server * * @param {Buffer} msg the message content received by the server socket * @param {object} info the info for e.g. address of sending host */ async onServerMessage(msg, info) { try { const msg_hex = msg.toString('hex').toUpperCase(); const memberOfGroup = this.devices[info.address] ? this.devices[info.address].memberOfGroup : '0'; const deviceLocked = this.devices[info.address] ? this.devices[info.address].deviceLocked : false; let midiMsg; let stepsTaken; let direction; // If a polling is received then answer the polling to hold the device online if (msg_hex === POLL_REC) { this.log.silly( `X-Touch received Polling from device ${info.address}, give an reply "${this.logHexData(POLL_REPLY)}"`, ); this.setConnection(info.address, info.port, true); this.deviceSendData(this.fromHexString(POLL_REPLY), info.address, info.port, true); } else if (msg_hex === HOST_CON_QUERY) { this.log.silly( `X-Touch received Host Connection Query, give no reply, probably "${this.logHexData( HOST_CON_REPLY, )}" in the future`, ); } else { // other than polling and connection setup this.log.debug( `-> ${msg.length} bytes from ${info.address}:${info.port}: <${this.logHexData( msg_hex, )}> org: <${msg.toString()}>`, ); midiMsg = this.parseMidiData(msg); let baseId; const actPressed = midiMsg.value === '127' ? true : false; // check wheter desk is locked, let SysEx pass if (deviceLocked && midiMsg.msgType != 'SysEx') { this.log.info(`X-Touch with address: ${info.address} is locked.`); return; } switch (midiMsg.msgType) { case 'NoteOff': // No NoteOff events for now, description wrong. Only NoteOn with dynamic 0 break; case 'NoteOn': // NoteOn baseId = this.midi2Objects[midiMsg.note] ? `${this.namespace}.deviceGroups.${memberOfGroup}.${this.midi2Objects[midiMsg.note]}` : ''; if (Number(midiMsg.note) >= 104 && Number(midiMsg.note) <= 112) { // Fader touched, Fader 1 - 8 + Master await this.handleFader( baseId, undefined, actPressed ? 'touched' : 'released', info.address, ); } else if (Number(midiMsg.note) >= 46 && Number(midiMsg.note) <= 49) { // fader or channel switch if (actPressed) { // only on butten press, omit release let action = ''; switch (Number(midiMsg.note)) { case 46: // fader bank down action = 'bankDown'; break; case 47: // fader bank up action = 'bankUp'; break; case 48: // channel bank up action = 'channelDown'; break; case 49: // channel bank down action = 'channelUp'; break; } await this.deviceSwitchChannels(action, info.address); } } else { await this.handleButton( baseId, undefined, actPressed ? 'pressed' : 'released', info.address, ); } break; case 'Pitchbend': // Pitchbend (Fader value) baseId = `${this.namespace}.deviceGroups.${memberOfGroup}`; if (Number(midiMsg.channel) > 7) { // Master Fader baseId += '.masterFader'; } else { baseId += `.banks.0.channels.${Number(midiMsg.channel) + 1}.fader`; } await this.handleFader(baseId, midiMsg.value, 'fader', info.address); break; case 'ControlChange': // Encoders do that baseId = `${this.namespace}.deviceGroups.${memberOfGroup}`; if (Number(midiMsg.controller) >= 16 && Number(midiMsg.controller) <= 23) { // Channel encoder baseId += `.banks.0.channels.${Number(midiMsg.controller) - 15}.encoder`; } else { baseId += '.transport.encoder'; } //this.log.info(`midi message controller ${midiMsg.controller} value ${midiMsg.value}`); stepsTaken = 1; direction = 'cw'; if (midiMsg.value < 65) { stepsTaken = midiMsg.value; } else { stepsTaken = midiMsg.value - 64; direction = 'ccw'; } await this.handleEncoder(baseId, stepsTaken, direction, info.address); break; } } } catch (err) { this.errorHandler(err, 'onServerMessage'); } } /******************************************************************************** * handler functions to handle the values coming from the database or the device ******************************************************************************** * only the fader is not allowed to be transmitted to the sending device * primary behaviour is correction of values and the processing of the * autofunction process ********************************************************************************/ /** * handle the button events and call the sendback if someting is changed * * @param {string} buttonId full button id via onStateChange * @param {any | null | undefined} value the value of the element * @param {string} event pressed, released, fader or value (value = when called via onStateChange) * @param {string} deviceAddress only chen called via onServerMessage */ async handleButton(buttonId, value = undefined, event = 'value', deviceAddress = '') { try { let baseId; let stateName = ''; // the name of the particular state when called via onStateChange const buttonArr = buttonId.split('.'); let activeBank = 0; let activeBaseChannel = 1; let actStatus; let isDirty = false; // if true the button states has changed and must be sent if (buttonId === '') { this.log.debug('X-Touch button not supported'); return; } if (event === 'value') { // when called via onStateChange there is the full button id, cut the last part for baseId baseId = buttonId.substring(0, buttonId.lastIndexOf('.')); stateName = buttonId.substring(buttonId.lastIndexOf('.') + 1); if (stateName === '') { this.log.error('handleButton called with value and only baseId'); return; // if no value part provided throw an error } switch (stateName) { case 'autoToggle': // ToDo: check values and write back this.deviceGroups[`${baseId}.autoToggle`].val = value; // only update the internal db return; case 'syncGlobal': this.deviceGroups[`${baseId}.syncGlobal`].val = Boolean(value); // only update the internal db return; case 'flashing': if (this.deviceGroups[`${baseId}.flashing`].val != Boolean(value)) { // if changed send this.deviceGroups[`${baseId}.flashing`].val = Boolean(value); isDirty = true; } break; case 'pressed': event = value ? 'pressed' : 'released'; // if button press is simulated via state db break; default: if (this.deviceGroups[`${baseId}.status`].val != Boolean(value)) { // if changed send this.deviceGroups[`${baseId}.status`].val = Boolean(value); isDirty = true; } } } else { // when called by midiMsg determine the real channel if (deviceAddress !== '' && this.devices[deviceAddress]) { activeBank = this.devices[deviceAddress].activeBank; activeBaseChannel = this.devices[deviceAddress].activeBaseChannel; } if (buttonArr[4] === 'banks') { // replace bank and baseChannel on channel buttons buttonArr[5] = activeBank.toString(); buttonArr[7] = (Number(buttonArr[7]) + activeBaseChannel - 1).toString(); } baseId = buttonArr.join('.'); } const buttonName = buttonArr.length > 8 ? buttonArr[8] : ''; const actPressed = event === 'pressed' ? true : false; if (buttonName === 'encoder') { // encoder is only pressed event this.setState(`${baseId}.pressed`, actPressed, true); } else { actStatus = this.deviceGroups[`${baseId}.status`].val; let setValue = actStatus; if (event === 'value') { setValue = Boolean(value); isDirty = true; } else { // handle the button auto mode if (this.deviceGroups[`${baseId}.pressed`].val !== actPressed) { // if status changed this.deviceGroups[`${baseId}.pressed`].val = actPressed; this.setState(`${baseId}.pressed`, actPressed, true); switch (this.deviceGroups[`${baseId}.autoToggle`].val) { case 0: // no auto function break; case 1: // tip setValue = actPressed ? true : false; break; case 2: // on press if (actPressed) { setValue = actStatus ? false : true; } break; case 3: // on release if (!actPressed) { setValue = actStatus ? false : true; } break; case 4: // on press / release if (actPressed && !actStatus) { setValue = true; this.deviceGroups[`${baseId}.autoToggle`].helperBool = true; } if (!actPressed && actStatus) { if (this.deviceGroups[`${baseId}.autoToggle`].helperBool) { this.deviceGroups[`${baseId}.autoToggle`].helperBool = false; } else { setValue = false; } } break; } } if (this.deviceGroups[`${baseId}.status`].val !== setValue) { // if status changed this.deviceGroups[`${baseId}.status`].val = setValue; this.setState(`${baseId}.status`, setValue, true); isDirty = true; } } if (isDirty) { this.sendButton(baseId); } } } catch (err) { this.errorHandler(err, 'handleButton'); } } /** * handle the fader events and call the sendback if someting is changed * * @param {string} faderId full fader id via onStateChange * @param {any | null | undefined} value the value to handle * @param {string} event pressed, released or value (value = when called via onStateChange) * @param {string} deviceAddress only chen called via onServerMessage */ async handleFader(faderId, value = undefined, event = 'value', deviceAddress = '') { try { let baseId; let stateName = ''; // the name of the particular state when called via onStateChange const faderArr = faderId.split('.'); let activeBank = 0; let activeBaseChannel = 1; let isDirty = false; // if true the fader states has changed and must be sent let locObj = this.calculateFaderValue(value, 'midiValue'); if (faderId === '') { this.log.debug('X-Touch fader not supported'); return; } if (event === 'value') { // if called via onStateChange there is the full fader id, cut the last part for baseId baseId = faderId.substring(0, faderId.lastIndexOf('.')); stateName = faderId.substring(faderId.lastIndexOf('.') + 1); switch (stateName) { case 'syncGlobal': this.deviceGroups[`${baseId}.syncGlobal`].val = Boolean(value); // only update the internal db return; case 'touched': this.deviceGroups[`${baseId}.touched`].val = Boolean(value); // only update the internal db return; case 'value': locObj = this.calculateFaderValue(value, 'linValue'); if (this.deviceGroups[`${baseId}.value`].val != locObj.linValue) { this.deviceGroups[`${baseId}.value`].val = locObj.linValue; this.deviceGroups[`${baseId}.value_db`].val = locObj.logValue; isDirty = true; } this.setState(`${baseId}.value`, Number(locObj.linValue), true); // maybe correct the format this.setState(`${baseId}.value_db`, Number(locObj.logValue), true); // update log value too break; case 'value_db': locObj = this.calculateFaderValue(value, 'logValue'); if (this.deviceGroups[`${baseId}.value_db`].val != locObj.logValue) { this.deviceGroups[`${baseId}.value_db`].val = locObj.logValue; this.deviceGroups[`${baseId}.value`].val = locObj.linValue; isDirty = true; } this.setState(`${baseId}.value_db`, Number(locObj.logValue), true); // maybe correct the format this.setState(`${baseId}.value`, Number(locObj.linValue), true); // update lin value too break; default: this.log.warn(`X-Touch unknown fader value: "${faderId}"`); return; } } else { // if called by midiMsg determine the real channel if (deviceAddress !== '' && this.devices[deviceAddress]) { activeBank = this.devices[deviceAddress].activeBank; activeBaseChannel = this.devices[deviceAddress].activeBaseChannel; } if (faderArr[4] === 'banks') { // replace bank and baseChannel faderArr[5] = activeBank.toString(); faderArr[7] = (Number(faderArr[7]) + activeBaseChannel - 1).toString(); } baseId = faderArr.join('.'); if (event === 'touched') { if (!this.deviceGroups[`${baseId}.touched`].val) { // if status changed this.deviceGroups[`${baseId}.touched`].val = true; this.setState(`${baseId}.touched`, true, true); } } else if (event === 'released') { if (this.deviceGroups[`${baseId}.touched`].val) { // if status changed this.deviceGroups[`${baseId}.touched`].val = false; this.setState(`${baseId}.touched`, false, true); } } else if (event === 'fader') { if (this.deviceGroups[`${baseId}.value`].val != locObj.linValue) { this.deviceGroups[`${baseId}.value`].val = locObj.linValue; this.setState(`${baseId}.value`, Number(locObj.linValue), true); isDirty = true; } if (this.deviceGroups[`${baseId}.value_db`].val != locObj.logValue) { this.deviceGroups[`${baseId}.value_db`].val = locObj.logValue; this.setState(`${baseId}.value_db`, Number(locObj.logValue), true); isDirty = true; } } else { this.log.error(`X-Touch handleFader received unknown event: "${event}"`); } } if (isDirty) { this.sendFader(baseId, deviceAddress, true); } } catch (err) { this.errorHandler(err, 'handleFader'); } } /** * handle the display status and call the send back if someting is changed * * @param {string} displayId only when called via onStateChange * @param {any | null | undefined} value the value to handle */ async handleDisplay(displayId, value = undefined) { try { const displayArr = displayId.split('.'); const stateName = displayArr.length > 9 ? displayArr[9] : ''; const baseId = displayId.substring(0, displayId.lastIndexOf('.')); if (value === undefined) { return; } // nothing to do if (stateName === '') { return; } // if only base id there is nothing to handle. only called via onStateChange. Sending is done via sendDisplay let color = Number(this.deviceGroups[`${baseId}.color`].val); let inverted = this.deviceGroups[`${baseId}.inverted`].val; let line1 = this.deviceGroups[`${baseId}.line1`].val || ''; let line1_ct = this.deviceGroups[`${baseId}.line1_ct`].val; let line2 = this.deviceGroups[`${baseId}.line2`].val || ''; let line2_ct = this.deviceGroups[`${baseId}.line2_ct`].val; switch ( stateName // correction of malformed values ) { case 'color': color = Number(value); if (color < 0 || color > 7) { color = 0; this.setState(`${baseId}.color`, color, true); } this.deviceGroups[`${baseId}.color`].val = color.toString(); break; case 'inverted': inverted = Boolean(value); this.deviceGroups[`${baseId}.inverted`].val = inverted; break; case 'line1': line1 = value.toString(); if (!this.isASCII(line1)) { line1 = ''; this.setState(`${baseId}.line1`, line1, true); } if (line1.length > 7) { line1 = line1.substring(0, 7); this.setState(`${baseId}.line1`, line1, true); } this.deviceGroups[`${baseId}.line1`].val = line1; break; case 'line1_ct': line1_ct = Boolean(value); this.deviceGroups[`${baseId}.line1_ct`].val = line1_ct; break; case 'line2': line2 = value.toString(); if (!this.isASCII(line2)) { line2 = ''; this.setState(`${baseId}.line2`, line2, true); } if (line1.length > 7) { line1 = line1.substring(0, 7); this.setState(`${baseId}.line1`, line1, true); } this.deviceGroups[`${baseId}.line2`].val = line2; break; case 'line2_ct': line2_ct = Boolean(value); this.deviceGroups[`${baseId}.line2_ct`].val = line2_ct; break; } this.sendDisplay(baseId); // ToDo: handle syncGlobal } catch (err) { this.errorHandler(err, 'handleDisplay'); } } /** * handle the encoder status and call the send back if someting is changed * * @param {string} encoderId only when called via onStateChange * @param {any | null | undefined} value the value to handle * @param {string} event pressed, released or value (value = when called via onStateChange) * @param {string} deviceAddress only chen called via onServerMessage */ async handleEncoder(encoderId, value = undefined, event = 'value', deviceAddress = '') { try { let baseId; let stateName = ''; // the name of the particular state when called via onStateChange const encoderArr = encoderId.split('.'); let activeBank = 0; let activeBaseChannel = 1; const deviceGroup = encoderArr[3]; let actVal; let isDirty = false; // if true the encoder states has changed and must be sent if (encoderId === '') { this.log.debug('X-Touch encoder not supported'); return; } if (event === 'value') { // when called via onStateChange there is the full encoder id, cut the last part for baseId baseId = encoderId.substring(0, encoderId.lastIndexOf('.')); stateName = encoderId.substring(encoderId.lastIndexOf('.') + 1); if (stateName === '') { this.log.error('handleEncoder called with value and only baseId'); return; // if no value part provided throw an error } switch (stateName) { case 'cw': // if wheel movement is simulated via database case 'ccw': // only on encoder wheel possible this.timers.devicegroup[deviceGroup].refresh(); // restart/refresh the timer return; case 'enabled': if (this.deviceGroups[`${baseId}.enabled`].val != Boolean(value)) { // if changed send this.deviceGroups[`${baseId}.enabled`].val = Boolean(value); isDirty = true; } break; case 'mode': if (value < 0 || value > 3 || !Number.isInteger(value)) { value = 0; } // correct ? if (this.deviceGroups[`${baseId}.mode`].val != value) { // if changed send this.deviceGroups[`${baseId}.mode`].val = value; isDirty = true; } break; case 'pressed': // reset if sent via database this.setState(`${baseId}.pressed`, false, true); return; case 'stepsPerTick': // check and correct actVal = value; if (value < 0) { actVal = 0; } if (value > 1000) { actVal = 1000; } if (!Number.isInteger(value)) { actVal = parseInt(value, 10); } if (value != actVal) { // value corrected ? this.setState(`${baseId}.stepsPerTick`, Number(actVal), true); } if (this.deviceGroups[`${baseId}.stepsPerTick`].val != actVal) { this.deviceGroups[`${baseId}.stepsPerTick`].val = actVal; this.log.info(`handleEncoder changed the stepsPerTick to "${actVal}"`); } return; case 'value': if (value < 0) { value = 0; } if (value > 1000) { value = 1000; } if (!Number.isInteger(value)) { value = parseInt(value, 10); } if (this.deviceGroups[`${baseId}.value`].val != value) { this.deviceGroups[`${baseId}.value`].val = value; this.setState(`${baseId}.value`, Number(value), true); } break; } } else { // when called by midiMsg determine the real channel if (deviceAddress !== '' && this.devices[deviceAddress]) { activeBank = this.devices[deviceAddress].activeBank; activeBaseChannel = this.devices[deviceAddress].activeBaseChannel; } if (encoderArr[4] === 'banks') { // replace bank and baseChannel on channel encoders encoderArr[5] = activeBank.toString(); encoderArr[7] = (Number(encoderArr[7]) + activeBaseChannel - 1).toString(); } baseId = encoderArr.join('.'); } if (encoderArr[5] === 'encoder') { // only on encoder wheel switch (event) { case 'cw': this.setState(`${baseId}.cw`, true, true); this.timers.encoderWheels[deviceGroup].refresh(); // restart/refresh the timer return; // nothing more to do case 'ccw': this.setState(`${baseId}.ccw`, true, true); this.timers.encoderWheels[deviceGroup].refresh(); // restart/refresh the timer return; // nothing more to do default: this.log.error(`handleEncoder called with unknown event ${event} on encoder wheel`); } } if (this.deviceGroups[`${baseId}.enabled`].val !== true && !isDirty) { return; } // no farther processing if encoder disabled, only to send the status disabled on value "enabled" changed actVal = this.deviceGroups[`${baseId}.value`].val; if (this.deviceGroups[`${baseId}.value`].helperNum == -1) { // first call this.deviceGroups[`${baseId}.value`].helperNum = this.calculateEncoderValue(actVal); } switch (event) { case 'cw': // rotate to increment value actVal += this.deviceGroups[`${baseId}.stepsPerTick`].val * value; // value contains the steps taken if (actVal > 1000) { actVal = 1000; } break; case 'ccw': // rotate to decrement value actVal -= this.deviceGroups[`${baseId}.stepsPerTick`].val * value; if (actVal < 0) { actVal = 0; } break; } this.deviceGroups[`${baseId}.value`].val = actVal; this.setState(`${baseId}.value`, actVal, true); if (this.deviceGroups[`${baseId}.value`].helperNum != this.calculateEncoderValue(actVal)) { this.deviceGroups[`${baseId}.value`].helperNum = this.calculateEncoderValue(actVal); // if display value changed send isDirty = true; } let logStr = `handleEncoder event: ${event} new value ${actVal} `; if (isDirty) { logStr += `going to send ${this.deviceGroups[`${baseId}.value`].helperNum}`; this.sendEncoder(baseId); } this.log.debug(logStr); // ToDo: handle syncGlobal } catch (err) { this.errorHandler(err, 'handleEncoder'); } } /** * handle the timecode display character status and call the send back if someting is changed * * @param {string} charId only when called via onStateChange * @param {any | null | undefined} value the value to handle */ async handleDisplayChar(charId, value = undefined) { try { const characterArr = charId.split('.'); const stateName = characterArr.length > 6 ? characterArr[6] : ''; const baseId = charId.substring(0, charId.lastIndexOf('.')); if (value === undefined) { return; } // nothing to do if (stateName === '') { return; } // if only base id there is nothing to handle. only called via onStateChange. Sending is done via sendDisplayChar let char = this.deviceGroups[`${baseId}.char`].val || ''; let dot = this.deviceGroups[`${baseId}.dot`].val || false; let enabled = this.deviceGroups[`${baseId}.enabled`].val || false; let extended = this.deviceGroups[`${baseId}.extended`].val; let mode = this.deviceGroups[`${baseId}.mode`].val; switch ( stateName // correction of malformed values ) { case 'char': char = value.toString(); if (!this.isASCII(char)) { char = ''; this.setState(`${baseId}.char`, char, true); } if (char.length > 1) { char = char.substring(0, 1); this.setState(`${baseId}.char`, char, true); } this.deviceGroups[`${baseId}.char`].val = char; break; case 'dot': dot = Boolean(value); this.deviceGroups[`${baseId}.dot`].val = dot; break; case 'enabled': enabled = Boolean(value); this.deviceGroups[`${baseId}.enabled`].val = enabled; break; case 'extended': exten