const { SerialPort } = require('serialport'); const fs = require('fs'); const EventEmitter = require('events'); // add parser import const { ReadlineParser } = require('@serialport/parser-readline'); class ArduinoThread extends EventEmitter { constructor(comPort, baudRate = 9600, sliderCount = 3, yamlPath = 'config.json', verbose = false) { super(); if (typeof comPort !== 'string') throw new Error('comPort must be a string'); if (typeof baudRate !== 'number') throw new Error('baudRate must be a number'); if (typeof sliderCount !== 'number') throw new Error('sliderCount must be a number'); this.port = new SerialPort({ path: comPort, baudRate, autoOpen: true }); this.sliderCount = sliderCount; this.yamlPath = yamlPath; // now points to config.json by default this.verbose = !!verbose; this.sendBuffer = []; this.updateList = Array(sliderCount).fill(0); this.isRunning = true; this.pendingMessage = null; // pipe through a newline-based parser so we always get full lines try { this.parser = this.port.pipe(new ReadlineParser({ delimiter: '\n' })); this.parser.on('data', (line) => this._handleData(line)); } catch (err) { // fallback to raw data event if parser unavailable this.port.on('data', (data) => this._handleData(data)); } // keep error/close handlers on the port this.port.on('error', (err) => { this.emit('error', err); if (this.verbose) console.error('Serial error:', err); }); this.port.on('close', () => { this.isRunning = false; if (this.verbose) console.log('Serial port closed'); }); } _handleData(data) { // accept string or Buffer; trim newline/whitespace const input = (typeof data === 'string') ? data.trim() : data.toString().trim(); // acknowledgement handling if (input === 'OK' && this.pendingMessage) { if (this.verbose) console.log(`Message confirmed: ${this.pendingMessage}`); this.pendingMessage = null; // drain buffer if present if (this.sendBuffer.length > 0) { const next = this.sendBuffer.shift(); this._writeMessage(next); } return; } const numbers = input.split('|'); if (numbers.length === this.sliderCount) { try { const values = numbers.map((n) => parseInt(n, 10)); values.forEach((val, i) => { if (!Number.isFinite(val)) throw new Error('parse error'); if (val !== this.updateList[i]) { this._setVolume(val, i); } }); this.updateList = values; this.emit('update', values); // notify listeners (GUI) if (this.verbose) console.log('Slider update ->', values); } catch (err) { this.emit('warn', { msg: 'Invalid slider data', raw: input }); if (this.verbose) console.warn('Invalid slider data:', input); } } else { // Not an update; emit for consumers this.emit('raw', input); if (this.verbose) console.warn('Unexpected input:', input); } } send(message) { if (!message || typeof message !== 'string') return; if (this.pendingMessage) { // queue if another message is awaiting ACK this.sendBuffer.push(message); return; } this._writeMessage(message); } _writeMessage(message) { if (!this.port || !this.port.writable) { const err = new Error('Serial port not writable'); this.emit('error', err); if (this.verbose) console.error(err); return; } this.pendingMessage = message; this.port.write(message + '\n', (err) => { if (err) { this.emit('error', err); if (this.verbose) console.error('Error writing:', err.message); this.pendingMessage = null; } else if (this.verbose) { console.log('Wrote message:', message); } }); // fallback timeout if device never replies OK setTimeout(() => { if (this.pendingMessage) { this.emit('warn', { msg: "Did not receive OK from device", message: this.pendingMessage }); if (this.verbose) console.warn('Did not receive OK from device for', this.pendingMessage); this.pendingMessage = null; // try to drain next queued message if any if (this.sendBuffer.length > 0) { const next = this.sendBuffer.shift(); this._writeMessage(next); } } }, 100); // adjustable timeout } _setVolume(value, sliderNumber) { const normalized = value / 1023; if (!fs.existsSync(this.yamlPath)) { this.emit('warn', { msg: 'Config file missing', path: this.yamlPath }); if (this.verbose) console.warn('Config file missing:', this.yamlPath); return; } try { const file = fs.readFileSync(this.yamlPath, 'utf8'); const config = JSON.parse(file || '{}'); // parse JSON config.json const processes = config[sliderNumber] || []; processes.forEach((proc) => { // Emit an event so the application can perform the platform-specific change. this.emit('set-volume', { process: proc, value: normalized, slider: sliderNumber }); if (this.verbose) { console.log(`Would set volume for "${proc}" to ${normalized.toFixed(2)} (slider ${sliderNumber})`); } }); } catch (err) { this.emit('error', err); if (this.verbose) console.error('Config JSON error:', err); } } stop() { this.isRunning = false; try { if (this.port && this.port.isOpen) this.port.close(); } catch (e) { if (this.verbose) console.warn('Error closing port', e); } } getValues() { return this.updateList.slice(); } } module.exports = ArduinoThread;