church_streamer/api/serial/serial.mjs

222 lines
5.4 KiB
JavaScript
Raw Permalink Normal View History

import { SerialPort } from 'serialport'
import { autoDetect } from '@serialport/bindings-cpp'
import { effect, signal } from '@preact/signals-core'
class SerialManager {
constructor() {
this.serial = signal(null)
this.currentDisplay = signal({
first: 'Filadelfia streamer ',
second: ' ',
})
this.queue = null
this.attempting = null
this.data = ''
this.failues = 0
// this.serial = null
this.gotPing = false
this.logHistory = []
this.setup()
}
setup() {
this.binding = autoDetect()
setInterval(() => {
if (!this.serial.value) return
this.serial.value.write('~')
}, 15000)
effect(() => {
if (!this.serial.value) {
this.gotPing = false
this.displayUpdated = false
this.safeStartDelay(5)
return
}
this.log('info', `Connection with display established.`)
this.updateDisplay(
'Filadelfia streamer',
'Version ' + this.core.version,
true
)
if (this.queue) {
setTimeout(() => {
this.updateDisplay(
this.queue[0],
this.queue[1],
)
}, 1000)
}
})
effect(() => {
let display = this.currentDisplay.value
this.io?.io.emit('serial.display', display)
})
}
init(core, io) {
this.core = core
this.io = io
this.safeStartDelay(1)
}
log(level, message) {
this.logHistory.unshift(`[${(new Date()).toISOString().replace('T', ' ').split('.')[0]}] ${message}`)
this.logHistory = this.logHistory.slice(0, 40)
this.core.log[level]('DISPLAY: ' + message)
this.io.io.emit('serial.status', this.status())
}
bufferToString(buf) {
if (typeof buf === 'string') return buf
let out = Buffer.from(buf)
for (let i = 0; i < out.length; i++) {
if (out[i] === 255) {
out.writeUInt8('#'.charCodeAt(0), i)
}
}
return out.toString().replace(/#/g, '█')
}
updateDisplay(firstLine, secondLine, local = false) {
if (!local) {
this.queue = [firstLine, secondLine]
}
let first = Buffer.from(firstLine)
let second = Buffer.from(secondLine)
if (first.length > 20) {
first = first.subarray(0, 20)
} else if (first.length < 20) {
first = Buffer.concat([first, Buffer.from(new Array(20 - first.length).fill(32))])
}
if (second.length > 20) {
second = second.subarray(0, 20)
} else if (second.length < 20) {
second = Buffer.concat([second, Buffer.from(new Array(20 - second.length).fill(32))])
}
if (this.serial.value) {
this.serial.value.write(Buffer.concat([
Buffer.from('^'),
first,
second,
]))
}
this.currentDisplay.value = {
first: this.bufferToString(firstLine).slice(0, 20),
second: this.bufferToString(secondLine).slice(0, 20),
}
}
safeStartDelay(sec) {
2024-02-25 21:55:52 +00:00
return
if (this.attempting || !this.core) return
this.log('info', `Attempting to connect with display in ${sec} second${sec > 1 ? 's': ''}.`)
this.attempting = setTimeout(this.safeStart.bind(this), sec * 1000)
}
safeStart() {
2024-02-25 21:55:52 +00:00
return false
this.start()
.then(
serial => {
this.attempting = null
this.failues = 0
this.serial.value = serial
},
err => {
this.attempting = null
this.failues = Math.min(this.failues + 1, 4)
this.log('error', err.message)
this.safeStartDelay(this.failues * 15)
}
)
}
async start() {
let list = await this.binding.list()
if (!list.length) {
throw new Error('No com ports were found')
}
for (let item of list) {
this.log('info', `Found ${item.path} (${item.friendlyName})`)
}
for (let item of list) {
this.log('info', `Testing out ${item.path} (${item.friendlyName})`)
let serial = new SerialPort({
path: item.path,
baudRate: 9600,
autoOpen: false,
})
await this.openSerial(serial)
if (!serial.port) continue
this.listen(serial)
let index = 0
while (index < 50 && !this.gotPing) {
if (index % 10 === 0) serial.write('~')
await new Promise(res => setTimeout(res, 100))
index++
}
if (this.gotPing) return serial
serial.destroy()
}
throw new Error('Failed to find display device')
}
listen(serial) {
serial.on('data', this.onData.bind(this))
serial.on('error', err => {
this.log('error', 'serial on error: ' + err.message)
})
serial.once('close', () => {
serial.destroy()
if (serial === this.serial.value) {
this.log('warn', 'Serial was closed')
this.serial.value = null
}
})
}
onData(data) {
this.data = (this.data + data).slice(-1000)
if (this.data.indexOf('^') < 0) return
let split = this.data.split('^')
this.data = split[split.length - 1]
this.gotPing = this.gotPing || split.includes('pong')
}
openSerial(serial) {
return new Promise(res => {
serial.open(err => {
if (err) {
this.log('error', 'Failed to open serial connection: ' + err.message)
serial.destroy()
}
res()
})
})
}
getDisplay() {
return this.currentDisplay.value
}
status() {
return {
running: Boolean(this.serial.value),
log: this.logHistory.join('\n'),
}
}
}
const serial = new SerialManager()
export default serial;