pxt-jacdac/routing.ts

1336 строки
43 KiB
TypeScript

namespace jacdac {
export const enum StatusEvent {
ProxyStarted = 200,
ProxyPacketReceived = 201,
Identify = 202,
}
export let onStatusEvent: (event: StatusEvent) => void
// common logging level for jacdac services
export let consolePriority = ConsolePriority.Debug
let _hostServices: Server[]
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
let resetIn = 2000000 // 2s
export let autoBind = true
function log(msg: string) {
console.add(consolePriority, msg)
}
function mkEventCmd(evCode: number) {
// protect access to _myDevice
let myDevice = selfDevice()
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 Server {
protected supressLog: boolean
running: boolean
serviceIndex: number
protected stateUpdated: boolean
private _statusCode = 0 // u16, u16
constructor(
public readonly 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) {
switch (pkt.serviceCommand) {
case jacdac.SystemCmd.Announce:
this.handleAnnounce(pkt)
break
case SystemReg.StatusCode | SystemCmd.GetRegister:
this.handleStatusCode(pkt)
break
case SystemReg.InstanceName | SystemCmd.GetRegister:
this.handleInstanceName(pkt)
break
default:
this.stateUpdated = false
this.handlePacket(pkt)
break
}
}
handlePacket(pkt: JDPacket) {}
isConnected() {
return this.running
}
advertisementData() {
return Buffer.create(0)
}
protected sendReport(pkt: JDPacket) {
pkt.serviceIndex = this.serviceIndex
pkt._sendReport(selfDevice())
}
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 handleAnnounce(pkt: JDPacket) {
this.sendReport(
JDPacket.from(
jacdac.SystemCmd.Announce,
this.advertisementData()
)
)
}
private handleStatusCode(pkt: JDPacket) {
this.handleRegUInt32(pkt, SystemReg.StatusCode, this._statusCode)
}
private handleInstanceName(pkt: JDPacket) {
this.handleRegValue(pkt, SystemReg.InstanceName, "s", this.name)
}
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
const dev = selfDevice().toString()
console.add(
consolePriority,
`${dev}[${this.serviceIndex}]>${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 (const 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
private _onConnected: () => void;
private _onDisconnected: () => void;
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)
}
/**
* Indicates if the client is bound to a server
*/
//% blockId=jd_client_is_connected block="is %client connected"
//% group="Services" weight=50
//% blockNamespace="modules"
isConnected() {
return this.broadcast || !!this.device
}
/**
* Raised when a server is connected.
*/
//% blockId=jd_client_on_connected block="on %client connected"
//% group="Services" weight=49
//% blockNamespace="modules"
onConnected(handler: () => void) {
this._onConnected = handler
if (this._onConnected && this.isConnected())
this.handleConnected()
}
/**
* Raised when a server is connected.
*/
//% blockId=jd_client_on_disconnected block="on %client disconnected"
//% group="Services" weight=48
//% blockNamespace="modules"
onDisconnected(handler: () => void) {
this._onDisconnected = handler
if (this._onDisconnected && !this.isConnected())
this._onDisconnected()
}
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.handleConnected()
return true
}
private handleConnected() {
// refresh registers
this.config.resend()
// if the device has any status light (StatusLightRgbFade is 0b..11.. mask)
if (this.device) {
const flags = this.device.announceflags
if (flags & ControlAnnounceFlags.StatusLightRgbFade)
control.runInParallel(() => this.connectedBlink())
}
// user handler
if (this._onConnected)
this._onConnected()
}
private connectedBlink() {
// double quick blink, pause, 4x
const g = 0xff >> 2
const og = 0x01
const tgreen = 96
const tgreenoff = 192
const toff = 512
const greenRepeat = 2
const repeat = 3
const green = JDPacket.from(ControlCmd.SetStatusLight, jdpack<[number, number, number, number]>("u8 u8 u8 u8", [0, g, 0, 0]))
green.serviceIndex = 0
const greenoff = JDPacket.from(ControlCmd.SetStatusLight, jdpack<[number, number, number, number]>("u8 u8 u8 u8", [0, og, 0, 0]))
greenoff.serviceIndex = 0
const off = JDPacket.from(ControlCmd.SetStatusLight, jdpack<[number, number, number, number]>("u8 u8 u8 u8", [0, 0, 0, 0]))
off.serviceIndex = 0
for(let i = 0; i < repeat; ++i) {
for(let j = 0; j < greenRepeat; ++j) {
green._sendCmd(this.device)
pause(tgreen)
greenoff._sendCmd(this.device)
pause(tgreenoff)
}
pause(toff - tgreenoff)
}
off._sendCmd(this.device)
}
_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()
if (this._onDisconnected)
this._onDisconnected()
}
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
lastReport = 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 announceflags(): ControlAnnounceFlags {
return this.services ? this.services.getNumber(NumberFormat.UInt16LE, 0) : 0
}
get resetCount() {
return this.announceflags & ControlAnnounceFlags.RestartCounterSteady
}
get packetCount() {
return this.services ? this.services[2] : 0
}
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)
}
get serviceClassLength() {
return !this.services ? 0 : this.services.length >> 2
}
serviceClassAt(serviceIndex: number) {
return serviceIndex == 0 ? 0
: this.services ? this.services.getNumber(NumberFormat.UInt32LE, serviceIndex << 2)
: 0
}
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 uptime(): number {
// create query
this.query(ControlReg.Uptime, 60000)
const q = this.lookupQuery(ControlReg.Uptime)
if (q.value) {
const up = q.value.getNumber(NumberFormat.UInt32LE, 0)
const offset = (control.millis() - q.lastReport) * 1000
return up + offset
}
return undefined
}
get mcuTemperature(): number {
return this.queryInt(ControlReg.McuTemperature)
}
get firmwareVersion(): string {
const b = this.query(ControlReg.FirmwareVersion, null)
if (b) return b.toString()
else return ""
}
get firmwareUrl(): string {
const b = this.query(ControlReg.FirmwareUrl, null)
if (b) return b.toString()
else return ""
}
get deviceUrl(): string {
const b = this.query(ControlReg.DeviceUrl, null)
if (b) return b.toString()
else return ""
}
handleCtrlReport(pkt: JDPacket) {
if (pkt.isRegGet) {
const reg = pkt.regCode
const q = this.lookupQuery(reg)
if (q) {
q.value = pkt.data
q.lastReport = control.millis()
}
}
}
hasService(serviceClass: number) {
const n = this.serviceClassLength
for (let i = 0; i < n; ++i)
if (this.serviceClassAt(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)) {
for (let i = 0; i < 7; ++i) {
setPinByCfg(DAL.CFG_PIN_LED, true)
pause(50)
setPinByCfg(DAL.CFG_PIN_LED, false)
pause(150)
}
}
}
function doNothing() {}
class ControlServer extends Server {
constructor() {
super("ctrl", 0)
}
sendUptime() {
const buf = Buffer.create(4)
buf.setNumber(NumberFormat.UInt32LE, 0, control.micros())
this.sendReport(JDPacket.from(CMD_GET_REG | ControlReg.Uptime, buf))
}
private handleFloodPing(pkt: JDPacket) {
let [numResponses, counter, size] = pkt.jdunpack<
[number, number, number]
>("u32 u32 u8")
const payload = Buffer.create(4 + size)
for (let i = 0; i < size; ++i) payload[4 + i] = i
const queuePing = () => {
if (numResponses <= 0) {
control.internalOnEvent(
jacdac.__physId(),
EVT_TX_EMPTY,
doNothing
)
} else {
payload.setNumber(NumberFormat.UInt32LE, 0, counter)
this.sendReport(
JDPacket.from(ControlCmd.FloodPing, payload)
)
numResponses--
counter++
}
}
control.internalOnEvent(jacdac.__physId(), EVT_TX_EMPTY, queuePing)
queuePing()
}
handlePacketOuter(pkt: JDPacket) {
if (pkt.isRegGet) {
switch (pkt.regCode) {
case ControlReg.Uptime: {
this.sendUptime()
break
}
case ControlReg.DeviceDescription: {
this.sendReport(
JDPacket.from(
pkt.serviceCommand,
Buffer.fromUTF8(control.programName())
)
)
break
}
}
} else {
switch (pkt.serviceCommand) {
case SystemCmd.Announce:
queueAnnounce()
break
case ControlCmd.Identify:
if (onIdentifyRequest)
control.runInParallel(onIdentifyRequest)
if (onStatusEvent) onStatusEvent(StatusEvent.Identify)
break
case ControlCmd.Reset:
control.reset()
break
case ControlCmd.FloodPing:
this.handleFloodPing(pkt)
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 |
ControlAnnounceFlags.IsClient |
ControlAnnounceFlags.SupportsACK |
ControlAnnounceFlags.SupportsBroadcast |
ControlAnnounceFlags.SupportsFrames
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()
// send resetin to whoever wants to listen for it
if (resetIn)
JDPacket.from(ControlReg.ResetIn | CMD_SET_REG, jdpack("u32", [resetIn]))
.sendAsMultiCommand(SRV_CONTROL)
// only try autoBind, proxy we see some devices online
if (_devices.length > 1) {
// check for proxy mode
jacdac.roleManagerServer.checkProxy()
// auto bind
if (autoBind) {
autoBindCnt++
// also, only do it every two announces (TBD)
if (autoBindCnt >= 2) {
autoBindCnt = 0
jacdac.roleManagerServer.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
for (const h of _hostServices) {
if (h.serviceClass == multiCommandClass && 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.resetCount > (pkt.data[0] & 0xf)) {
// if the reset counter went down, it means the device resetted; treat it as new device
log(`device ${dev.shortId} resetted`)
_devices.removeElement(dev)
dev._destroy()
dev = null
}
if (!dev) {
dev = new Device(pkt.deviceIdentifier)
// ask for uptime
dev.sendCtrlCommand(CMD_GET_REG | ControlReg.Uptime)
}
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.serviceClassAt(pkt.serviceIndex)
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 EVT_QUEUE_ANNOUNCE = 100
const EVT_TX_EMPTY = 101
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)
}
export const JACDAC_PROXY_SETTING = "__jacdac_proxy"
function startProxy() {
// check if a proxy restart was requested
if (!settings.exists(JACDAC_PROXY_SETTING)) return
log(`jacdac starting proxy`)
// clear proxy flag
settings.remove(JACDAC_PROXY_SETTING)
// start jacdac in proxy mode
control.internalOnEvent(jacdac.__physId(), EVT_DATA_READY, () => {
let buf: Buffer
while (null != (buf = jacdac.__physGetPacket())) {
if (onStatusEvent)
onStatusEvent(StatusEvent.ProxyPacketReceived)
}
})
// start animation
if (onStatusEvent) onStatusEvent(StatusEvent.ProxyStarted)
// don't allow main to run until next reset
while (true) {
pause(100)
}
}
/**
* Starts the Jacdac service
*/
export function start(options?: {
disableLogger?: boolean
disableRoleManager?: boolean,
disableSettings?: boolean
}): void {
if (_hostServices) return // already started
// make sure we prevent re-entering this function (potentially even log() can call us)
_hostServices = []
log("jacdac starting")
options = options || {}
const controlServer = new ControlServer()
controlServer.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(),
EVT_QUEUE_ANNOUNCE,
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] != ":") jacdac.loggerServer.add(pri as number, msg)
})
jacdac.loggerServer.start()
}
if (!options.disableRoleManager) {
roleManagerServer.start()
}
if (!options.disableSettings) {
jacdac.settingsServer.start()
}
controlServer.sendUptime()
// and we're done
log("jacdac started")
}
// make sure physical is started deterministically
// on micro:bit it allocates a buffer that should stay in the same place in memory
jacdac.__physStart()
// check for proxy mode
startProxy()
// start after main
control.runInParallel(() => start())
}