pxt-jacdac/routing.ts

1035 строки
34 KiB
TypeScript

namespace jacdac {
// common logging level for jacdac services
export let consolePriority = ConsolePriority.Debug;
let _hostServices: Host[]
export let _unattachedClients: Client[]
export let _allClients: Client[]
let _myDevice: Device
//% whenUsed
export let _devices: Device[] = []
//% whenUsed
let _announceCallbacks: (() => void)[] = [];
let _newDeviceCallbacks: (() => void)[];
let _pktCallbacks: ((p: JDPacket) => void)[];
let restartCounter = 0
let autoBindCnt = 0
export let autoBind = true
function log(msg: string) {
console.add(consolePriority, msg);
}
function mkEventCmd(evCode: number) {
if (!_myDevice._eventCounter)
_myDevice._eventCounter = 0
_myDevice._eventCounter = (_myDevice._eventCounter + 1) & CMD_EVENT_COUNTER_MASK
if (evCode >> 8)
throw "invalid evcode"
return CMD_EVENT_MASK | (_myDevice._eventCounter << CMD_EVENT_COUNTER_POS) | evCode
}
//% fixedInstances
export class Host {
protected supressLog: boolean;
running: boolean
serviceIndex: number
protected stateUpdated: boolean;
private _statusCode: number = 0; // u16, u16
constructor(
public name: string,
public readonly serviceClass: number
) { }
get statusCode() {
return this._statusCode;
}
setStatusCode(code: number, vendorCode: number) {
const c = ((code & 0xffff) << 16) | (vendorCode & 0xffff)
if (c !== this._statusCode) {
this._statusCode = c;
this.sendChangeEvent();
}
}
handlePacketOuter(pkt: JDPacket) {
// status code support
if (this.handleStatusCode(pkt))
return;
if (pkt.serviceCommand == SystemCmd.Announce) {
this.sendReport(
JDPacket.from(SystemCmd.Announce, this.advertisementData()))
} else {
this.stateUpdated = false
this.handlePacket(pkt)
}
}
handlePacket(pkt: JDPacket) { }
isConnected() {
return this.running
}
advertisementData() {
return Buffer.create(0)
}
protected sendReport(pkt: JDPacket) {
pkt.serviceIndex = this.serviceIndex
pkt._sendReport(_myDevice)
}
protected sendEvent(eventCode: number, data?: Buffer) {
const pkt = JDPacket.from(mkEventCmd(eventCode), data || Buffer.create(0))
this.sendReport(pkt)
const now = control.millis()
delayedSend(pkt, now + 20)
delayedSend(pkt, now + 100)
}
protected sendChangeEvent(): void {
this.sendEvent(SystemEvent.Change);
}
private handleStatusCode(pkt: JDPacket): boolean {
this.handleRegUInt32(pkt, SystemReg.StatusCode, this._statusCode)
return pkt.serviceCommand == (SystemReg.StatusCode | SystemCmd.GetRegister)
}
protected handleRegFormat<T extends any[]>(pkt: JDPacket, register: number, fmt: string, current: T): T {
const getset = pkt.serviceCommand >> 12
if (getset == 0 || getset > 2)
return current
const reg = pkt.serviceCommand & 0xfff
if (reg != register)
return current
if (getset == 1) {
this.sendReport(JDPacket.jdpacked(pkt.serviceCommand, fmt, current))
} else {
if (register >> 8 == 0x1)
return current // read-only
const v = pkt.jdunpack<T>(fmt)
if (!jdpackEqual<T>(fmt, v, current)) {
this.stateUpdated = true
current = v
}
}
return current
}
// only use for numbers
protected handleRegValue<T>(pkt: JDPacket, register: number, fmt: string, current: T): T {
const getset = pkt.serviceCommand >> 12
if (getset == 0 || getset > 2)
return current
const reg = pkt.serviceCommand & 0xfff
if (reg != register)
return current
// make sure there's no null/undefined
if (getset == 1) {
this.sendReport(JDPacket.jdpacked(pkt.serviceCommand, fmt, [current]))
} else {
if (register >> 8 == 0x1)
return current // read-only
const v = pkt.jdunpack(fmt);
if (v[0] !== current) {
this.stateUpdated = true
current = v[0]
}
}
return current
}
protected handleRegBool(pkt: JDPacket, register: number, current: boolean): boolean {
const res = this.handleRegValue(pkt, register, "u8", current ? 1 : 0);
return !!res;
}
protected handleRegInt32(pkt: JDPacket, register: number, current: number): number {
const res = this.handleRegValue(pkt, register, "i32", current >> 0);
return res;
}
protected handleRegUInt32(pkt: JDPacket, register: number, current: number): number {
const res = this.handleRegValue(pkt, register, "u32", current >>> 0);
return res;
}
protected handleRegBuffer(pkt: JDPacket, register: number, current: Buffer): Buffer {
const getset = pkt.serviceCommand >> 12
if (getset == 0 || getset > 2)
return current
const reg = pkt.serviceCommand & 0xfff
if (reg != register)
return current
if (getset == 1) {
this.sendReport(JDPacket.from(pkt.serviceCommand, current))
} else {
if (register >> 8 == 0x1)
return current // read-only
let data = pkt.data
const diff = current.length - data.length
if (diff == 0) { }
else if (diff < 0)
data = data.slice(0, current.length)
else
data = data.concat(Buffer.create(diff))
if (!data.equals(current)) {
current.write(0, data)
this.stateUpdated = true
}
}
return current
}
/**
* Registers and starts the driver
*/
start() {
if (this.running)
return
this.running = true
jacdac.start();
this.serviceIndex = _hostServices.length
_hostServices.push(this)
this.log("start");
}
/**
* Unregister and stops the service
*/
stop() {
if (!this.running)
return
this.running = false
this.log("stop")
}
protected log(text: string) {
if (this.supressLog || consolePriority < console.minPriority)
return
let dev = selfDevice().toString()
console.add(consolePriority, `${dev}:${this.serviceClass}>${this.name}>${text}`);
}
}
class ClientPacketQueue {
private pkts: Buffer[] = []
constructor(public readonly parent: Client) { }
private updateQueue(pkt: JDPacket) {
const cmd = pkt.serviceCommand
for (let i = 0; i < this.pkts.length; ++i) {
if (this.pkts[i].getNumber(NumberFormat.UInt16LE, 2) == cmd) {
this.pkts[i] = pkt.withFrameStripped()
return
}
}
this.pkts.push(pkt.withFrameStripped())
}
clear() {
this.pkts = []
}
send(pkt: JDPacket) {
if (pkt.isRegSet || this.parent.serviceIndex == null)
this.updateQueue(pkt)
this.parent.sendCommand(pkt)
}
resend() {
const sn = this.parent.serviceIndex
if (sn == null || this.pkts.length == 0)
return
let hasNonSet = false
for (let p of this.pkts) {
p[1] = sn
if ((p[3] >> 4) != (CMD_SET_REG >> 12))
hasNonSet = true
}
const pkt = JDPacket.onlyHeader(0)
pkt.compress(this.pkts)
this.parent.sendCommand(pkt)
// after re-sending only leave set_reg packets
if (hasNonSet)
this.pkts = this.pkts.filter(p => (p[3] >> 4) == (CMD_SET_REG >> 12))
}
}
interface SMap<T> {
[index: string]: T;
}
export class RegisterClient<TValues extends PackSimpleDataType[]> {
private data: Buffer;
private _localTime: number;
private _dataChangedHandler: () => void;
constructor(
public readonly service: Client,
public readonly code: number,
public readonly packFormat: string,
defaultValue?: TValues) {
this.data = defaultValue && jdpack(this.packFormat, defaultValue) || Buffer.create(0);
this._localTime = control.millis()
}
hasValues(): boolean {
this.service.start();
return !!this.data;
}
pauseUntilValues(timeOut?: number) {
if (!this.hasValues())
pauseUntil(() => this.hasValues(), timeOut || 2000)
return this.values;
}
get values(): TValues {
this.service.start();
return jdunpack(this.data, this.packFormat) as TValues;
}
set values(values: TValues) {
this.service.start();
const d = jdpack(this.packFormat, values);
this.data = d;
// send set request to the service
this.service.setReg(this.code, this.packFormat, values);
}
get lastGetTime() {
return this._localTime;
}
onDataChanged(handler: () => void) {
this._dataChangedHandler = handler;
}
handlePacket(packet: JDPacket): void {
if (packet.isRegGet && this.code !== packet.regCode) {
const d = packet.data
const changed = !d.equals(this.data);
this.data = d;
this._localTime = control.millis();
if (changed && this._dataChangedHandler)
this._dataChangedHandler();
}
}
}
//% fixedInstances
export class Client {
device: Device
currentDevice: Device
protected readonly eventId: number
broadcast: boolean // when true, this.device is never set
serviceIndex: number;
protected supressLog: boolean;
started: boolean;
protected advertisementData: Buffer;
private handlers: SMap<(idx?: number) => void>;
protected systemActive = false;
protected readonly config: ClientPacketQueue
private readonly registers: RegisterClient<PackSimpleDataType[]>[] = [];
constructor(
public readonly serviceClass: number,
public role: string
) {
this.eventId = control.allocateNotifyEvent();
this.config = new ClientPacketQueue(this)
if (!this.role)
throw "no role"
}
protected addRegister<TValues extends PackSimpleDataType[]>(code: number, packFormat: string, defaultValues?: TValues): RegisterClient<TValues> {
let reg = this.registers.find(reg => reg.code === code);
if (!reg) {
reg = new RegisterClient<TValues>(this, code, packFormat, defaultValues);
this.registers.push(reg);
}
return reg as RegisterClient<TValues>;
}
register(code: number) {
return this.registers.find(reg => reg.code === code);
}
broadcastDevices() {
return devices().filter(d => d.clients.indexOf(this) >= 0)
}
isConnected() {
return !!this.device
}
requestAdvertisementData() {
this.sendCommand(JDPacket.onlyHeader(SystemCmd.Announce))
}
handlePacketOuter(pkt: JDPacket) {
if (pkt.serviceCommand == SystemCmd.Announce)
this.advertisementData = pkt.data
if (pkt.isEvent) {
const code = pkt.eventCode
if (code == SystemEvent.Active) this.systemActive = true
else if (code == SystemEvent.Inactive) this.systemActive = false
this.raiseEvent(code, pkt.intData)
}
for(const register of this.registers)
register.handlePacket(pkt);
this.handlePacket(pkt)
}
handlePacket(pkt: JDPacket) { }
_attach(dev: Device, serviceNum: number) {
if (this.device) throw "Invalid attach"
if (!this.broadcast) {
if (!dev.matchesRoleAt(this.role, serviceNum))
return false // don't attach
this.device = dev
this.serviceIndex = serviceNum
_unattachedClients.removeElement(this)
}
log(`attached ${dev.toString()}/${serviceNum} to client ${this.role}`)
dev.clients.push(this)
this.onAttach()
this.config.resend()
return true
}
_detach() {
log(`dettached ${this.role}`)
this.serviceIndex = null
if (!this.broadcast) {
if (!this.device) throw "Invalid detach"
this.device = null
_unattachedClients.push(this)
clearAttachCache()
}
this.onDetach()
}
protected onAttach() { }
protected onDetach() { }
sendCommand(pkt: JDPacket) {
this.start()
if (this.serviceIndex == null)
return
pkt.serviceIndex = this.serviceIndex
pkt._sendCmd(this.device)
}
sendCommandWithAck(pkt: JDPacket) {
this.start()
if (this.serviceIndex == null)
return
pkt.serviceIndex = this.serviceIndex
if (!pkt._sendWithAck(this.device.deviceId))
throw "No ACK"
}
// this will be re-sent on (re)attach
setReg(reg: number, format: string, values: PackSimpleDataType[]) {
this.start();
const payload = JDPacket.jdpacked(CMD_SET_REG | reg, format, values);
this.config.send(payload);
}
setRegBuffer(reg: number, value: Buffer) {
this.start()
this.config.send(JDPacket.from(CMD_SET_REG | reg, value))
}
protected raiseEvent(value: number, argument: number) {
control.raiseEvent(this.eventId, value)
if (this.handlers) {
const h = this.handlers[value + ""]
if (h)
h(argument)
}
}
protected registerEvent(value: number, handler: () => void) {
this.start()
control.onEvent(this.eventId, value, handler);
}
protected registerHandler(value: number, handler: (idx: number) => void) {
this.start()
if (!this.handlers) this.handlers = {}
this.handlers[value + ""] = handler
}
protected log(text: string) {
if (this.supressLog || consolePriority < console.minPriority)
return
let dev = selfDevice().toString()
let other = this.device ? this.device.toString() : "<unbound>"
console.add(consolePriority, `${dev}/${other}:${this.serviceClass}>${this.role}>${text}`);
}
start() {
if (this.started) return
this.started = true
jacdac.start()
_unattachedClients.push(this)
_allClients.push(this)
clearAttachCache()
}
destroy() {
if (this.device)
this.device.clients.removeElement(this)
_unattachedClients.removeElement(this)
_allClients.removeElement(this)
this.serviceIndex = null
this.device = null
clearAttachCache()
}
announceCallback() { }
}
// 2 letter + 2 digit ID; 1.8%/0.3%/0.07%/0.015% collision probability among 50/20/10/5 devices
export function shortDeviceId(devid: string) {
const h = Buffer.fromHex(devid).hash(30)
return String.fromCharCode(0x41 + h % 26) +
String.fromCharCode(0x41 + Math.idiv(h, 26) % 26) +
String.fromCharCode(0x30 + Math.idiv(h, 26 * 26) % 10) +
String.fromCharCode(0x30 + Math.idiv(h, 26 * 26 * 10) % 10)
}
class RegQuery {
lastQuery = 0
value: Buffer
constructor(public reg: number) { }
}
export class Device {
services: Buffer
lastSeen: number
clients: Client[] = []
_eventCounter: number
private _shortId: string
private queries: RegQuery[]
_score: number
constructor(public deviceId: string) {
_devices.push(this)
}
get isConnected() {
return this.clients != null
}
get shortId() {
// TODO measure if caching is worth it
if (!this._shortId)
this._shortId = shortDeviceId(this.deviceId)
return this._shortId;
}
toString() {
return this.shortId
}
matchesRoleAt(role: string, serviceIdx: number) {
if (!role)
return true
if (role == this.deviceId)
return true
if (role == this.deviceId + ":" + serviceIdx)
return true
return jacdac._rolemgr.getRole(this.deviceId, serviceIdx) == role
}
private lookupQuery(reg: number) {
if (!this.queries) this.queries = []
return this.queries.find(q => q.reg == reg)
}
queryInt(reg: number, refreshRate = 1000) {
const v = this.query(reg, refreshRate)
if (!v) return undefined
return intOfBuffer(v)
}
query(reg: number, refreshRate = 1000) {
let q = this.lookupQuery(reg)
if (!q)
this.queries.push(q = new RegQuery(reg))
const now = control.millis()
if (!q.lastQuery ||
(q.value === undefined && now - q.lastQuery > 500) ||
(refreshRate != null && now - q.lastQuery > refreshRate)) {
q.lastQuery = now
this.sendCtrlCommand(CMD_GET_REG | reg)
}
return q.value
}
get mcuTemperature() {
return this.queryInt(ControlReg.McuTemperature)
}
get firmwareVersion() {
const b = this.query(ControlReg.FirmwareVersion, null)
if (b) return b.toString()
else return ""
}
get firmwareUrl() {
const b = this.query(ControlReg.FirmwareUrl, null)
if (b) return b.toString()
else return ""
}
get deviceUrl() {
const b = this.query(ControlReg.DeviceUrl, null)
if (b) return b.toString()
else return ""
}
handleCtrlReport(pkt: JDPacket) {
if ((pkt.serviceCommand & CMD_TYPE_MASK) == CMD_GET_REG) {
const reg = pkt.serviceCommand & CMD_REG_MASK
const q = this.lookupQuery(reg)
if (q)
q.value = pkt.data
}
}
hasService(serviceClass: number) {
for (let i = 4; i < this.services.length; i += 4)
if (this.services.getNumber(NumberFormat.UInt32LE, i) == serviceClass)
return true
return false
}
clientAtServiceIndex(serviceIndex: number) {
for (const c of this.clients) {
if (c.device == this && c.serviceIndex == serviceIndex)
return c
}
return null
}
sendCtrlCommand(cmd: number, payload: Buffer = null) {
const pkt = !payload ? JDPacket.onlyHeader(cmd) : JDPacket.from(cmd, payload)
pkt.serviceIndex = JD_SERVICE_INDEX_CTRL
pkt._sendCmd(this)
}
static clearNameCache() {
clearAttachCache()
}
_destroy() {
log("destroy " + this.shortId)
for (let c of this.clients)
c._detach()
this.clients = null
}
}
/**
* Raised when an identity command request is received
*/
//% whenUsed
export let onIdentifyRequest = () => {
if (!pins.pinByCfg(DAL.CFG_PIN_LED))
return
for (let i = 0; i < 7; ++i) {
setPinByCfg(DAL.CFG_PIN_LED, true)
pause(50)
setPinByCfg(DAL.CFG_PIN_LED, false)
pause(150)
}
}
class ControlService extends Host {
constructor() {
super("ctrl", 0)
}
handlePacketOuter(pkt: JDPacket) {
switch (pkt.serviceCommand) {
case SystemCmd.Announce:
queueAnnounce()
break
case ControlCmd.Identify:
control.runInParallel(onIdentifyRequest)
break
case ControlCmd.Reset:
control.reset()
break
case CMD_GET_REG | ControlReg.DeviceDescription:
this.sendReport(JDPacket.from(pkt.serviceCommand, Buffer.fromUTF8("PXT: " + control.programName())))
break
}
}
}
/**
* Gets the list of devices currently detected on the bus
*/
export function devices() {
return _devices.slice()
}
/**
* Gets the Jacdac device representing the running device
*/
export function selfDevice() {
if (!_myDevice) {
_myDevice = new Device(control.deviceLongSerialNumber().toHex())
_myDevice.services = Buffer.create(4)
}
return _myDevice
}
/**
* Raised when services from a device are announced
* @param cb
*/
export function onAnnounce(cb: () => void) {
_announceCallbacks.push(cb)
}
/**
* Raised when a new device is detected on the bus
* @param cb
*/
export function onNewDevice(cb: () => void) {
if (!_newDeviceCallbacks) _newDeviceCallbacks = []
_newDeviceCallbacks.push(cb)
}
export function onRawPacket(cb: (pkt: JDPacket) => void) {
if (!_pktCallbacks) _pktCallbacks = []
_pktCallbacks.push(cb)
}
function queueAnnounce() {
const ids = _hostServices.map(h => h.running ? h.serviceClass : -1)
if (restartCounter < 0xf) restartCounter++
ids[0] = restartCounter | 0x100
const buf = Buffer.create(ids.length * 4)
for (let i = 0; i < ids.length; ++i)
buf.setNumber(NumberFormat.UInt32LE, i * 4, ids[i]);
JDPacket.from(SystemCmd.Announce, buf)
._sendReport(selfDevice())
_announceCallbacks.forEach(f => f())
for (const cl of _allClients)
cl.announceCallback()
gcDevices()
// only try autoBind we see some devices online
if (autoBind && _devices.length > 1) {
autoBindCnt++
// also, only do it every two announces (TBD)
if (autoBindCnt >= 2) {
autoBindCnt = 0
jacdac.roleManagerHost.autoBind();
}
}
}
function clearAttachCache() {
for (let d of _devices) {
// add a dummy byte at the end (if not done already), to force re-attach of services
if (d.services && (d.services.length & 3) == 0)
d.services = d.services.concat(Buffer.create(1))
}
}
function newDevice() {
if (_newDeviceCallbacks)
for (let f of _newDeviceCallbacks)
f()
}
function reattach(dev: Device) {
log(`reattaching services to ${dev.toString()}; cl=${_unattachedClients.length}/${_allClients.length}`)
const newClients: Client[] = []
const occupied = Buffer.create(dev.services.length >> 2)
for (let c of dev.clients) {
if (c.broadcast) {
c._detach()
continue // will re-attach
}
const newClass = dev.services.getNumber(NumberFormat.UInt32LE, c.serviceIndex << 2)
if (newClass == c.serviceClass && dev.matchesRoleAt(c.role, c.serviceIndex)) {
newClients.push(c)
occupied[c.serviceIndex] = 1
} else {
c._detach()
}
}
dev.clients = newClients
newDevice()
if (_unattachedClients.length == 0)
return
for (let i = 4; i < dev.services.length; i += 4) {
if (occupied[i >> 2])
continue
const serviceClass = dev.services.getNumber(NumberFormat.UInt32LE, i)
for (let cc of _unattachedClients) {
if (cc.serviceClass == serviceClass) {
if (cc._attach(dev, i >> 2))
break
}
}
}
}
function serviceMatches(dev: Device, serv: Buffer) {
const ds = dev.services
if (!ds || ds.length != serv.length)
return false
for (let i = 4; i < serv.length; ++i)
if (ds[i] != serv[i])
return false
return true
}
export function routePacket(pkt: JDPacket) {
// log("route: " + pkt.toString())
const devId = pkt.deviceIdentifier
const multiCommandClass = pkt.multicommandClass
// TODO implement send queue for packet compression
if (pkt.requiresAck) {
pkt.requiresAck = false // make sure we only do it once
if (pkt.deviceIdentifier == selfDevice().deviceId) {
const crc = pkt.crc
const ack = JDPacket.onlyHeader(crc)
ack.serviceIndex = JD_SERVICE_INDEX_CRC_ACK
ack._sendReport(selfDevice())
}
}
if (_pktCallbacks)
for (let f of _pktCallbacks)
f(pkt)
if (multiCommandClass != null) {
if (!pkt.isCommand)
return // only commands supported in multi-command
const h = _hostServices.find(s => s.serviceClass == multiCommandClass);
if (h && h.running) {
// pretend it's directly addressed to us
pkt.deviceIdentifier = selfDevice().deviceId
pkt.serviceIndex = h.serviceIndex
h.handlePacketOuter(pkt)
}
} else if (devId == selfDevice().deviceId) {
if (!pkt.isCommand) {
// control.dmesg(`invalid echo ${pkt}`)
return // huh? someone's pretending to be us?
}
const h = _hostServices[pkt.serviceIndex]
if (h && h.running) {
// log(`handle pkt at ${h.name} cmd=${pkt.service_command}`)
h.handlePacketOuter(pkt)
}
} else {
if (pkt.isCommand)
return // it's a command, and it's not for us
let dev = _devices.find(d => d.deviceId == devId)
if (pkt.serviceIndex == JD_SERVICE_INDEX_CTRL) {
if (pkt.serviceCommand == SystemCmd.Announce) {
if (dev && (dev.services[0] & 0xf) > (pkt.data[0] & 0xf)) {
// if the reset counter went down, it means the device resetted; treat it as new device
_devices.removeElement(dev)
dev._destroy()
dev = null
}
if (!dev)
dev = new Device(pkt.deviceIdentifier)
const matches = serviceMatches(dev, pkt.data)
dev.services = pkt.data
if (!matches) {
dev.lastSeen = control.millis()
reattach(dev)
}
}
if (dev) {
dev.handleCtrlReport(pkt)
dev.lastSeen = control.millis()
}
return
} else if (pkt.serviceIndex == JD_SERVICE_INDEX_CRC_ACK) {
_gotAck(pkt)
}
if (!dev)
// we can't know the serviceClass, no announcement seen yet for this device
return
dev.lastSeen = control.millis()
const serviceClass = dev.services.getNumber(NumberFormat.UInt32LE, pkt.serviceIndex << 2)
if (!serviceClass || serviceClass == 0xffffffff)
return
if (pkt.isEvent) {
let ec = dev._eventCounter
// if ec is undefined, it's the first event, so skip processing
if (ec !== undefined) {
ec++
// how many packets ahead and behind current are we?
const ahead = (pkt.eventCounter - ec) & CMD_EVENT_COUNTER_MASK
const behind = (ec - pkt.eventCounter) & CMD_EVENT_COUNTER_MASK
// ahead == behind == 0 is the usual case, otherwise
// behind < 60 means this is an old event (or retransmission of something we already processed)
// ahead < 5 means we missed at most 5 events, so we ignore this one and rely on retransmission
// of the missed events, and then eventually the current event
if (ahead > 0 && (behind < 60 || ahead < 5))
return
}
dev._eventCounter = pkt.eventCounter
}
const client = dev.clients.find(c =>
c.broadcast
? c.serviceClass == serviceClass
: c.serviceIndex == pkt.serviceIndex)
if (client) {
// log(`handle pkt at ${client.name} rep=${pkt.service_command}`)
client.currentDevice = dev
client.handlePacketOuter(pkt)
}
}
}
function gcDevices() {
const now = control.millis()
const cutoff = now - 2000
selfDevice().lastSeen = now // make sure not to gc self
let numdel = 0
for (let i = 0; i < _devices.length; ++i) {
const dev = _devices[i]
if (dev.lastSeen < cutoff) {
_devices.splice(i, 1)
i--
dev._destroy()
numdel++
}
}
if (numdel)
newDevice()
}
const EVT_DATA_READY = 1
const CFG_PIN_JDPWR_OVERLOAD_LED = 1103
const CFG_PIN_JDPWR_ENABLE = 1104
const CFG_PIN_JDPWR_FAULT = 1105
function setPinByCfg(cfg: number, val: boolean) {
const pin = pins.pinByCfg(cfg)
if (!pin)
return
if (control.getConfigValue(cfg, 0) & DAL.CFG_PIN_CONFIG_ACTIVE_LO)
val = !val
pin.digitalWrite(val)
}
function enablePower(enabled = true) {
// EN active-lo, AP2552A, AP22652A, TPS2552-1
// EN active-hi, AP2553A, AP22653A, TPS2553-1
setPinByCfg(CFG_PIN_JDPWR_ENABLE, enabled)
}
/**
* Starts the Jacdac service
*/
export function start(options?: {
disableLogger?: boolean,
disableRoleManager?: boolean
}): void {
if (_hostServices)
return // already started
log("jacdac starting")
options = options || {};
_hostServices = []
new ControlService().start()
_unattachedClients = []
_allClients = []
jacdac.__physStart();
control.internalOnEvent(jacdac.__physId(), EVT_DATA_READY, () => {
let buf: Buffer;
while (null != (buf = jacdac.__physGetPacket())) {
const pkt = JDPacket.fromBinary(buf)
pkt.timestamp = jacdac.__physGetTimestamp()
routePacket(pkt)
}
});
control.internalOnEvent(jacdac.__physId(), 100, queueAnnounce);
enablePower(true)
const faultpin = pins.pinByCfg(CFG_PIN_JDPWR_FAULT)
if (faultpin) {
// FAULT is always assumed to be active-low; no external pull-up is needed
// (and you should never pull it up to +5V!)
faultpin.setPull(PinPullMode.PullUp)
faultpin.digitalRead()
onAnnounce(() => {
if (faultpin.digitalRead() == false) {
control.runInParallel(() => {
control.dmesg("jacdac power overload; restarting power")
enablePower(false)
setPinByCfg(CFG_PIN_JDPWR_OVERLOAD_LED, true)
pause(200) // wait some time for the LED to be noticed; also there's some de-glitch time on EN
setPinByCfg(CFG_PIN_JDPWR_OVERLOAD_LED, false)
enablePower(true)
})
}
})
}
if (!options.disableLogger) {
console.addListener(function (pri, msg) {
if (msg[0] != ":")
loggerHost.add(pri as number, msg);
});
loggerHost.start()
}
if (!options.disableRoleManager)
roleManagerHost.start();
// and we're done
log("jacdac started");
}
// start after main
control.runInParallel(() => start());
}