iobroker.x-touch
Version:
Communicate with a Behringer X-Touch Control Surface (DAW Controller)
1,072 lines (960 loc) • 126 kB
JavaScript
/**
*
* 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