// A stand-in for an esp324xlight unit: greets "<name> ready", records every
// line it receives, answers `state`, and can drop its clients on demand.
import net from 'bare-net'
export class FakeUnit {
constructor (name = 'fake') {
this.name = name
this.lines = []
this.conns = new Set()
this.accepted = 0
this.values = new Array(12).fill(0)
this.server = net.createServer((c) => this._onConn(c))
}
listen () {
return new Promise((resolve) => {
this.server.listen(0, '127.0.0.1', () => {
this.port = this.server.address().port
resolve(this)
})
})
}
_onConn (c) {
this.accepted++
this.conns.add(c)
c.on('close', () => this.conns.delete(c))
c.on('error', () => {})
c.write(this.name + ' ready\r\n')
let buf = ''
c.on('data', (d) => {
buf += d.toString()
let i
while ((i = buf.indexOf('\n')) !== -1) {
const line = buf.slice(0, i).trim()
buf = buf.slice(i + 1)
if (!line) continue
this.lines.push(line)
if (line === 'state') {
c.write('state 0 1000 1.00 1.00 1.00 ' + this.values.join(' ') + '\r\n')
}
}
})
}
dropClients () {
for (const c of this.conns) c.destroy()
}
close () {
this.dropClients()
return new Promise((resolve) => this.server.close(resolve))
}
}
export function waitFor (pred, timeout = 3000) {
return new Promise((resolve) => {
const end = Date.now() + timeout
const tick = () => {
if (pred()) return resolve(true)
if (Date.now() > end) return resolve(false)
setTimeout(tick, 10)
}
tick()
})
}
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
// A proto-3 unit: referees per-slot grants like esp324xlight's grants.h, so the
// web UI's grant handling is tested against the same rules the firmware applies.
// Signatures are a fixed dummy (signature checks are the bee's job, phase 3).
export class FakeUnitV3 {
constructor (name = 'fake3', { epoch = 5 } = {}) {
this.name = name
this.epoch = epoch
this.counter = 0
this.lines = [] // every line received, in order: [connId, line]
this.values = new Array(12).fill(0)
this.holds = [null, null, null, null] // { conn, holder, n }
this.conns = new Map() // id -> socket
this.nextId = 1
this.server = net.createServer((c) => this._onConn(c))
}
listen () {
return new Promise((resolve) => {
this.server.listen(0, '127.0.0.1', () => {
this.port = this.server.address().port
resolve(this)
})
})
}
close () {
for (const c of this.conns.values()) c.destroy()
return new Promise((resolve) => this.server.close(resolve))
}
received (pred) {
return this.lines.filter(([, l]) => pred(l)).map(([, l]) => l)
}
_broadcast (line) {
for (const c of this.conns.values()) c.write(line + '\r\n')
}
_free (s, why) {
const h = this.holds[s]
this.holds[s] = null
const v = this.values.slice(s * 3, s * 3 + 3)
this._broadcast(['free', s, this.epoch, h.n, ...v, why, h.holder, 'ab'.repeat(64)].join(' '))
}
_setValues (from, vals, skipHeld) {
let changed = false
vals.forEach((v, i) => {
const c = from + i
if (skipHeld && this.holds[Math.floor(c / 3)]) return
if (this.values[c] !== v) { this.values[c] = v; changed = true }
})
if (changed) this._broadcast('values ' + this.values.join(' '))
}
_onConn (c) {
const id = this.nextId++
this.conns.set(id, c)
c.on('error', () => {})
// bare-net is half-open by default: 'close' only follows an explicit destroy.
c.on('end', () => c.destroy())
c.on('close', () => {
this.conns.delete(id)
for (let s = 0; s < 4; s++) if (this.holds[s] && this.holds[s].conn === id) this._free(s, 'disconnect')
})
c.write(this.name + ' ready\r\n')
c.write('hello proto=3 unit=' + this.name + ' epoch=' + this.epoch + ' pk=' + 'cd'.repeat(32) + '\r\n')
let buf = ''
c.on('data', (d) => {
buf += d.toString()
let i
while ((i = buf.indexOf('\n')) !== -1) {
const line = buf.slice(0, i).trim()
buf = buf.slice(i + 1)
if (line) this._command(id, c, line)
}
})
}
_command (id, c, line) {
this.lines.push([id, line])
const p = line.split(/\s+/)
const range = (a) => (a === 'all' ? [0, 1, 2, 3] : [Number(a)])
if (p[0] === 'state') {
c.write('state 0 1000 1.00 1.00 1.00 ' + this.values.join(' ') + '\r\n')
this.holds.forEach((h, s) => { if (h) c.write(['held', s, h.holder, this.epoch, h.n].join(' ') + '\r\n') })
c.write('stats epoch=' + this.epoch + ' dropped=0 pk=' + 'cd'.repeat(32) + '\r\n')
} else if (p[0] === 'grant') {
const slots = range(p[1])
const other = slots.find((s) => this.holds[s] && this.holds[s].conn !== id)
if (other !== undefined) { c.write('busy ' + other + ' ' + this.holds[other].holder + '\r\n'); return }
for (const s of slots) {
if (!this.holds[s]) {
this.holds[s] = { conn: id, holder: p[2], n: ++this.counter }
this._broadcast(['held', s, p[2], this.epoch, this.holds[s].n].join(' '))
}
c.write(['granted', s, this.epoch, this.holds[s].n].join(' ') + '\r\n')
}
} else if (p[0] === 'slot') {
const [s, e, n, b, g, r] = p.slice(1, 7).map(Number)
const h = this.holds[s]
if (!h || h.conn !== id || e !== this.epoch || n !== h.n) { c.write('ERR notheld ' + s + '\r\n'); return }
this._setValues(s * 3, [b, g, r], false)
} else if (p[0] === 'release') {
for (const s of range(p[1])) if (this.holds[s] && this.holds[s].conn === id) this._free(s, 'release')
} else if (p[0] === 'pwm') {
this._setValues(0, p.slice(1).map(Number), true)
} else if (p[0] === 'pwm2') {
this._setValues(0, p.slice(3).map(Number), true)
} else if (p[0] === 'stopfade') {
for (let s = 0; s < 4; s++) if (this.holds[s]) this._free(s, 'blackout')
}
}
// Another controller (e.g. Blender) grabbing a slot on its own socket.
async externalGrab (slot, holder) {
const sock = net.createConnection({ port: this.port, host: '127.0.0.1' })
await new Promise((resolve) => sock.on('connect', resolve))
sock.on('error', () => {})
sock.write('grant ' + slot + ' ' + holder + '\n')
await waitFor(() => this.holds[slot] && this.holds[slot].holder === holder)
return sock
}
}