feat: implement new role manager (#30)
* Start on v2 of role mgr * Implement the new autobind * Add missing file * autobinder fixes * Check CRCs in sim * Packet spliting in sim * fixes in packet splitting * more testing and logging * fixing autobinding * Fix packet sizing * alpha-sort for role names * Update constants for role-mgr * potentially log loopback packets * Run auto-bind automatically * Correct initial binding occupancy * fix AllRolesAllocated * Remove TODO
This commit is contained in:
Родитель
b3c0b3f610
Коммит
70d9cea37a
30
jdsim.ts
30
jdsim.ts
|
@ -69,15 +69,35 @@ namespace jacdac {
|
||||||
if (__physIsRunning()) return
|
if (__physIsRunning()) return
|
||||||
recvQ = []
|
recvQ = []
|
||||||
control.simmessages.onReceived("jacdac", buf => {
|
control.simmessages.onReceived("jacdac", buf => {
|
||||||
recvQ.push(buf)
|
if (buf[2] + 12 != buf.length) {
|
||||||
control.raiseEvent(__physId(), 1)
|
control.dmesg("bad size in sim jdpkt: " + buf.toHex())
|
||||||
|
buf = buf.slice(0, buf[2] + 12)
|
||||||
|
}
|
||||||
|
const crc = jdCrc16(buf.slice(2));
|
||||||
|
if (buf.getNumber(NumberFormat.UInt16LE, 0) != crc) {
|
||||||
|
control.dmesg("bad crc in sim")
|
||||||
|
} else {
|
||||||
|
let num = 0
|
||||||
|
const b0 = buf.slice(0)
|
||||||
|
while (buf[2] >= 4) {
|
||||||
|
const tmp = buf.slice(0, buf[12] + 16)
|
||||||
|
recvQ.push(tmp)
|
||||||
|
const nextoff = (buf[12] + 16 + 3) & ~3
|
||||||
|
buf.write(12, buf.slice(nextoff))
|
||||||
|
const skip = nextoff - 12
|
||||||
|
if (buf[2] <= skip)
|
||||||
|
break
|
||||||
|
buf[2] -= skip
|
||||||
|
}
|
||||||
|
control.raiseEvent(__physId(), 1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
// announce packet, don't rely on forever
|
// announce packet, don't rely on forever
|
||||||
control.runInParallel(function() {
|
control.runInParallel(function () {
|
||||||
while(true) {
|
while (true) {
|
||||||
control.raiseEvent(__physId(), 100);
|
control.raiseEvent(__physId(), 100);
|
||||||
pause(500)
|
pause(500)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ namespace jacdac {
|
||||||
static fromBinary(buf: Buffer) {
|
static fromBinary(buf: Buffer) {
|
||||||
const p = new JDPacket()
|
const p = new JDPacket()
|
||||||
p._header = buf.slice(0, JD_SERIAL_HEADER_SIZE)
|
p._header = buf.slice(0, JD_SERIAL_HEADER_SIZE)
|
||||||
p._data = buf.slice(JD_SERIAL_HEADER_SIZE)
|
p._data = buf.slice(JD_SERIAL_HEADER_SIZE, p._header[12])
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ namespace jacdac {
|
||||||
data.write(sz, s)
|
data.write(sz, s)
|
||||||
sz += s.length
|
sz += s.length
|
||||||
}
|
}
|
||||||
this._data = data
|
this.data = data
|
||||||
}
|
}
|
||||||
|
|
||||||
withFrameStripped() {
|
withFrameStripped() {
|
||||||
|
@ -195,7 +195,7 @@ namespace jacdac {
|
||||||
}
|
}
|
||||||
|
|
||||||
jdpack(fmt: string, nums: any[]) {
|
jdpack(fmt: string, nums: any[]) {
|
||||||
this._data = jdpack(fmt, nums)
|
this.data = jdpack(fmt, nums)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isCommand() {
|
get isCommand() {
|
||||||
|
@ -214,6 +214,8 @@ namespace jacdac {
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendCore() {
|
_sendCore() {
|
||||||
|
if (this._data.length != this._header[12])
|
||||||
|
throw "jdsize mismatch"
|
||||||
jacdac.__physSendPacket(this._header, this._data)
|
jacdac.__physSendPacket(this._header, this._data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
3
pxt.json
3
pxt.json
|
@ -43,6 +43,7 @@
|
||||||
"sensor/client.ts",
|
"sensor/client.ts",
|
||||||
"sensor/host.ts",
|
"sensor/host.ts",
|
||||||
"role-manager/constants.ts",
|
"role-manager/constants.ts",
|
||||||
|
"rolemgr.ts",
|
||||||
"proto-test/constants.ts",
|
"proto-test/constants.ts",
|
||||||
"proto-test/host.ts"
|
"proto-test/host.ts"
|
||||||
],
|
],
|
||||||
|
@ -58,7 +59,7 @@
|
||||||
},
|
},
|
||||||
"public": true,
|
"public": true,
|
||||||
"targetVersions": {
|
"targetVersions": {
|
||||||
"target": "3.1.39",
|
"target": "3.1.50",
|
||||||
"targetId": "microbit"
|
"targetId": "microbit"
|
||||||
},
|
},
|
||||||
"supportedTargets": [
|
"supportedTargets": [
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
namespace jacdac {
|
namespace jacdac {
|
||||||
// Service: Role Manager
|
// Service: Role Manager
|
||||||
export const SRV_ROLE_MANAGER = 0x119c3ad1
|
export const SRV_ROLE_MANAGER = 0x1e4b7e66
|
||||||
export const enum RoleManagerReg {
|
export const enum RoleManagerReg {
|
||||||
|
/**
|
||||||
|
* Read-write bool (uint8_t). Normally, if some roles are unfilled, and there are idle services that can fulfill them,
|
||||||
|
* the brain device will assign roles (bind) automatically.
|
||||||
|
* Such automatic assignment happens every second or so, and is trying to be smart about
|
||||||
|
* co-locating roles that share "host" (part before first slash),
|
||||||
|
* as well as reasonably stable assignments.
|
||||||
|
* Once user start assigning roles manually using this service, auto-binding should be disabled to avoid confusion.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* const [autoBind] = jdunpack<[number]>(buf, "u8")
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
AutoBind = 0x80,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read-only bool (uint8_t). Indicates if all required roles have been allocated to devices.
|
* Read-only bool (uint8_t). Indicates if all required roles have been allocated to devices.
|
||||||
*
|
*
|
||||||
|
@ -14,10 +28,10 @@ namespace jacdac {
|
||||||
|
|
||||||
export const enum RoleManagerCmd {
|
export const enum RoleManagerCmd {
|
||||||
/**
|
/**
|
||||||
* Argument: device_id devid (uint64_t). Get the role corresponding to given device identifer. Returns empty string if unset.
|
* Get the role corresponding to given device identifer. Returns empty string if unset.
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* const [deviceId] = jdunpack<[Buffer]>(buf, "b[8]")
|
* const [deviceId, serviceIdx] = jdunpack<[Buffer, number]>(buf, "b[8] u8")
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
GetRole = 0x80,
|
GetRole = 0x80,
|
||||||
|
@ -25,7 +39,7 @@ namespace jacdac {
|
||||||
/**
|
/**
|
||||||
* report GetRole
|
* report GetRole
|
||||||
* ```
|
* ```
|
||||||
* const [deviceId, role] = jdunpack<[Buffer, string]>(buf, "b[8] s")
|
* const [deviceId, serviceIdx, role] = jdunpack<[Buffer, number, string]>(buf, "b[8] u8 s")
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
@ -33,7 +47,7 @@ namespace jacdac {
|
||||||
* Set role. Can set to empty to remove role binding.
|
* Set role. Can set to empty to remove role binding.
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* const [deviceId, role] = jdunpack<[Buffer, string]>(buf, "b[8] s")
|
* const [deviceId, serviceIdx, role] = jdunpack<[Buffer, number, string]>(buf, "b[8] u8 s")
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
SetRole = 0x81,
|
SetRole = 0x81,
|
||||||
|
@ -53,7 +67,7 @@ namespace jacdac {
|
||||||
ListStoredRoles = 0x82,
|
ListStoredRoles = 0x82,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Argument: required_roles pipe (bytes). List all roles required by the current program. `device_id` is `0` if role is unbound.
|
* Argument: required_roles pipe (bytes). List all roles required by the current program. `device_id` and `service_idx` are `0` if role is unbound.
|
||||||
*
|
*
|
||||||
* ```
|
* ```
|
||||||
* const [requiredRoles] = jdunpack<[Buffer]>(buf, "b[12]")
|
* const [requiredRoles] = jdunpack<[Buffer]>(buf, "b[12]")
|
||||||
|
@ -66,14 +80,14 @@ namespace jacdac {
|
||||||
/**
|
/**
|
||||||
* pipe_report StoredRoles
|
* pipe_report StoredRoles
|
||||||
* ```
|
* ```
|
||||||
* const [deviceId, role] = jdunpack<[Buffer, string]>(buf, "b[8] s")
|
* const [deviceId, serviceIdx, role] = jdunpack<[Buffer, number, string]>(buf, "b[8] u8 s")
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* pipe_report RequiredRoles
|
* pipe_report RequiredRoles
|
||||||
* ```
|
* ```
|
||||||
* const [deviceId, serviceClass, roles] = jdunpack<[Buffer, number, string]>(buf, "b[8] u32 s")
|
* const [deviceId, serviceClass, serviceIdx, role] = jdunpack<[Buffer, number, number, string]>(buf, "b[8] u32 u8 s")
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,330 @@
|
||||||
|
namespace jacdac._rolemgr {
|
||||||
|
const roleSettingPrefix = "#jdr:"
|
||||||
|
|
||||||
|
export function clearRoles() {
|
||||||
|
settings.list(roleSettingPrefix).forEach(settings.remove)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRole(devid: string, servIdx: number) {
|
||||||
|
return settings.readString(roleSettingPrefix + devid + ":" + servIdx)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRole(devid: string, servIdx: number, role: string) {
|
||||||
|
const key = roleSettingPrefix + devid + ":" + servIdx
|
||||||
|
if (role)
|
||||||
|
settings.writeString(key, role)
|
||||||
|
else
|
||||||
|
settings.remove(key)
|
||||||
|
Device.clearNameCache()
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeviceWrapper {
|
||||||
|
bindings: RoleBinding[] = []
|
||||||
|
score = -1
|
||||||
|
constructor(
|
||||||
|
public device: Device
|
||||||
|
) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoleBinding {
|
||||||
|
boundToDev: Device
|
||||||
|
boundToServiceIdx: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public role: string,
|
||||||
|
public serviceClass: number
|
||||||
|
) { }
|
||||||
|
|
||||||
|
host() {
|
||||||
|
const slashIdx = this.role.indexOf("/")
|
||||||
|
if (slashIdx < 0) return this.role
|
||||||
|
else return this.role.slice(0, slashIdx - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
select(devwrap: DeviceWrapper, serviceIdx: number) {
|
||||||
|
const dev = devwrap.device
|
||||||
|
if (dev == this.boundToDev && serviceIdx == this.boundToServiceIdx)
|
||||||
|
return
|
||||||
|
if (this.boundToDev)
|
||||||
|
setRole(this.boundToDev.deviceId, this.boundToServiceIdx, null)
|
||||||
|
devwrap.bindings[serviceIdx] = this
|
||||||
|
setRole(dev.deviceId, serviceIdx, this.role)
|
||||||
|
this.boundToDev = dev
|
||||||
|
this.boundToServiceIdx = serviceIdx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HostBindings {
|
||||||
|
bindings: RoleBinding[] = []
|
||||||
|
constructor(
|
||||||
|
public host: string
|
||||||
|
) { }
|
||||||
|
|
||||||
|
get fullyBound() {
|
||||||
|
return this.bindings.every(b => b.boundToDev != null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// candidate devices are ordered by [numBound, numPossible, device_id]
|
||||||
|
// where numBound is number of clients already bound to this device
|
||||||
|
// and numPossible is number of clients that can possibly be additionally bound
|
||||||
|
scoreFor(devwrap: DeviceWrapper, select = false) {
|
||||||
|
let numBound = 0
|
||||||
|
let numPossible = 0
|
||||||
|
const dev = devwrap.device
|
||||||
|
const missing: RoleBinding[] = []
|
||||||
|
for (const b of this.bindings) {
|
||||||
|
if (b.boundToDev) {
|
||||||
|
if (b.boundToDev == dev)
|
||||||
|
numBound++
|
||||||
|
} else {
|
||||||
|
missing.push(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sbuf = dev.services
|
||||||
|
for (let idx = 4; idx < sbuf.length; idx += 4) {
|
||||||
|
const serviceIndex = idx >> 2
|
||||||
|
// if service is already bound to some client, move on
|
||||||
|
if (devwrap.bindings[serviceIndex])
|
||||||
|
continue
|
||||||
|
|
||||||
|
const serviceClass = sbuf.getNumber(NumberFormat.UInt32LE, idx)
|
||||||
|
for (let i = 0; i < missing.length; ++i) {
|
||||||
|
if (missing[i].serviceClass == serviceClass) {
|
||||||
|
// we've got a match!
|
||||||
|
numPossible++ // this can be assigned
|
||||||
|
// in fact, assign if requested
|
||||||
|
if (select) {
|
||||||
|
control.dmesg("autobind: " + missing[i].role + " -> " + dev.shortId + ":" + serviceIndex)
|
||||||
|
missing[i].select(devwrap, serviceIndex)
|
||||||
|
}
|
||||||
|
// this one is no longer missing
|
||||||
|
missing.splice(i, 1)
|
||||||
|
// move on to the next service in announce
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if nothing can be assigned, the score is zero
|
||||||
|
if (numPossible == 0)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
// otherwise the score is [numBound, numPossible], lexicographic
|
||||||
|
// numPossible can't be larger than ~64, leave it a few more bits
|
||||||
|
return (numBound << 8) | numPossible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maxIn<T>(arr: T[], cmp: (a: T, b: T) => number) {
|
||||||
|
let maxElt = arr[0]
|
||||||
|
for (let i = 1; i < arr.length; ++i) {
|
||||||
|
if (cmp(maxElt, arr[i]) < 0)
|
||||||
|
maxElt = arr[i]
|
||||||
|
}
|
||||||
|
return maxElt
|
||||||
|
}
|
||||||
|
|
||||||
|
export function autoBind() {
|
||||||
|
// console.log(`autobind: devs=${_devices.length} cl=${_unattachedClients.length}`)
|
||||||
|
if (_devices.length == 0 || _unattachedClients.length == 0)
|
||||||
|
return
|
||||||
|
|
||||||
|
const bindings: RoleBinding[] = []
|
||||||
|
const wraps = _devices.map(d => new DeviceWrapper(d))
|
||||||
|
|
||||||
|
for (const cl of _allClients) {
|
||||||
|
if (!cl.broadcast && cl.requiredDeviceName) {
|
||||||
|
const b = new RoleBinding(cl.requiredDeviceName, cl.serviceClass)
|
||||||
|
if (cl.device) {
|
||||||
|
b.boundToDev = cl.device
|
||||||
|
b.boundToServiceIdx = cl.serviceIndex
|
||||||
|
for (const w of wraps)
|
||||||
|
if (w.device == cl.device) {
|
||||||
|
w.bindings[cl.serviceIndex] = b
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bindings.push(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hosts: HostBindings[] = []
|
||||||
|
|
||||||
|
// Group all clients by host
|
||||||
|
for (const b of bindings) {
|
||||||
|
const hn = b.host()
|
||||||
|
let h = hosts.find(h => h.host == hn)
|
||||||
|
if (!h) {
|
||||||
|
h = new HostBindings(hn)
|
||||||
|
hosts.push(h)
|
||||||
|
}
|
||||||
|
h.bindings.push(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// exclude hosts that have already everything bound
|
||||||
|
hosts = hosts.filter(h => !h.fullyBound)
|
||||||
|
|
||||||
|
while (hosts.length > 0) {
|
||||||
|
// Get host with maximum number of clients (resolve ties by name)
|
||||||
|
// This gives priority to assignment of "more complicated" hosts, which are generally more difficult to assign
|
||||||
|
const h = maxIn(hosts, (a, b) => a.bindings.length - b.bindings.length || b.host.compare(a.host))
|
||||||
|
|
||||||
|
for (const d of wraps)
|
||||||
|
d.score = h.scoreFor(d)
|
||||||
|
|
||||||
|
const dev = maxIn(wraps, (a, b) => a.score - b.score || b.device.deviceId.compare(a.device.deviceId))
|
||||||
|
|
||||||
|
if (dev.score == 0) {
|
||||||
|
// nothing can be assigned, on any device
|
||||||
|
hosts.removeElement(h)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign services in order of names - this way foo/servo1 will be assigned before foo/servo2
|
||||||
|
// in list of advertised services
|
||||||
|
h.bindings.sort((a, b) => a.role.compare(b.role))
|
||||||
|
|
||||||
|
// "recompute" score, assigning names in process
|
||||||
|
h.scoreFor(dev, true)
|
||||||
|
|
||||||
|
// if everything bound on this host, remove it from further consideration
|
||||||
|
if (h.fullyBound)
|
||||||
|
hosts.removeElement(h)
|
||||||
|
else {
|
||||||
|
// otherwise, remove bindings on the current device, to update sort order
|
||||||
|
// it's unclear we need this
|
||||||
|
h.bindings = h.bindings.filter(b => b.boundToDev != dev.device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoleManagerHost extends Host {
|
||||||
|
constructor() {
|
||||||
|
super("rolemgr", SRV_ROLE_MANAGER)
|
||||||
|
}
|
||||||
|
|
||||||
|
public handlePacket(packet: JDPacket) {
|
||||||
|
jacdac.autoBind = this.handleRegBool(packet, RoleManagerReg.AutoBind, jacdac.autoBind)
|
||||||
|
|
||||||
|
switch (packet.serviceCommand) {
|
||||||
|
case RoleManagerReg.AllRolesAllocated | CMD_GET_REG:
|
||||||
|
this.sendReport(JDPacket.jdpacked(RoleManagerReg.AllRolesAllocated | CMD_GET_REG,
|
||||||
|
"u8", [_allClients.every(c => c.broadcast || !!c.device) ? 1 : 0]))
|
||||||
|
break
|
||||||
|
case RoleManagerCmd.GetRole:
|
||||||
|
if (packet.data.length == 9) {
|
||||||
|
let name = getRole(packet.data.slice(0, 8).toHex(), packet.data[8]) || ""
|
||||||
|
this.sendReport(JDPacket.from(RoleManagerCmd.GetRole, packet.data.concat(Buffer.fromUTF8(name))))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case RoleManagerCmd.SetRole:
|
||||||
|
if (packet.data.length >= 9) {
|
||||||
|
setRole(packet.data.slice(0, 8).toHex(), packet.data[8], packet.data.slice(9).toString())
|
||||||
|
this.sendChangeEvent();
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case RoleManagerCmd.ListStoredRoles:
|
||||||
|
OutPipe.respondForEach(packet, settings.list(roleSettingPrefix), k => {
|
||||||
|
const name = settings.readString(k)
|
||||||
|
const len = roleSettingPrefix.length
|
||||||
|
return jdpack("b[8] u8 s", [
|
||||||
|
Buffer.fromHex(k.slice(len, len + 16)),
|
||||||
|
parseInt(k.slice(len + 16)),
|
||||||
|
name
|
||||||
|
])
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case RoleManagerCmd.ListRequiredRoles:
|
||||||
|
OutPipe.respondForEach(packet, _allClients, packName)
|
||||||
|
break
|
||||||
|
case RoleManagerCmd.ClearAllRoles:
|
||||||
|
clearRoles()
|
||||||
|
this.sendChangeEvent();
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
function packName(c: Client) {
|
||||||
|
const devid = c.device ? Buffer.fromHex(c.device.deviceId) : Buffer.create(8)
|
||||||
|
const servidx = c.device ? c.serviceIndex : 0
|
||||||
|
return jdpack("b[8] u32 u8 s", [devid, c.serviceClass, servidx, c.requiredDeviceName || ""])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace jacdac {
|
||||||
|
|
||||||
|
//% fixedInstance whenUsed block="role manager"
|
||||||
|
export const roleManagerHost = new _rolemgr.RoleManagerHost()
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
function addRequested(devs: RoleBinding[], name: string, service_class: number,
|
||||||
|
parent: RoleManagerClient) {
|
||||||
|
let r = devs.find(d => d.role == name)
|
||||||
|
if (!r)
|
||||||
|
devs.push(r = new RoleBinding(parent, name))
|
||||||
|
r.serviceClasses.push(service_class)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RoleManagerClient extends Client {
|
||||||
|
public remoteRequestedDevices: RoleBinding[] = []
|
||||||
|
|
||||||
|
constructor(requiredDevice: string = null) {
|
||||||
|
super("rolemgrc", SRV_ROLE_MANAGER, requiredDevice)
|
||||||
|
|
||||||
|
onNewDevice(() => {
|
||||||
|
recomputeCandidates(this.remoteRequestedDevices)
|
||||||
|
})
|
||||||
|
|
||||||
|
onAnnounce(() => {
|
||||||
|
if (this.isConnected())
|
||||||
|
control.runInParallel(() => this.scanCore())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private scanCore() {
|
||||||
|
const inp = new InPipe()
|
||||||
|
this.sendCommand(inp.openCommand(RoleManagerCmd.ListRequiredRoles))
|
||||||
|
|
||||||
|
const localDevs = devices()
|
||||||
|
const devs: RoleBinding[] = []
|
||||||
|
|
||||||
|
inp.readList(buf => {
|
||||||
|
const [devidbuf, service_class, name] = jdunpack<[Buffer, number, string]>(buf, "b[8] u32 s")
|
||||||
|
if (!name)
|
||||||
|
return
|
||||||
|
const devid = devidbuf.toHex();
|
||||||
|
const r = addRequested(devs, name, service_class, this)
|
||||||
|
const dev = localDevs.find(d => d.deviceId == devid)
|
||||||
|
if (dev)
|
||||||
|
r.boundTo = dev
|
||||||
|
})
|
||||||
|
|
||||||
|
devs.sort((a, b) => a.role.compare(b.role))
|
||||||
|
|
||||||
|
this.remoteRequestedDevices = devs
|
||||||
|
recomputeCandidates(this.remoteRequestedDevices)
|
||||||
|
}
|
||||||
|
|
||||||
|
scan() {
|
||||||
|
pauseUntil(() => this.isConnected())
|
||||||
|
this.scanCore()
|
||||||
|
}
|
||||||
|
|
||||||
|
clearNames() {
|
||||||
|
this.sendCommandWithAck(JDPacket.onlyHeader(RoleManagerCmd.ClearAllRoles))
|
||||||
|
}
|
||||||
|
|
||||||
|
setName(sd: ServiceDescriptor, name: string) {
|
||||||
|
this.sendCommandWithAck(JDPacket.from(RoleManagerCmd.SetRole,
|
||||||
|
Buffer.fromHex(dev.deviceId).concat(Buffer.fromUTF8(name))))
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePacket(pkt: JDPacket) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
288
routing.ts
288
routing.ts
|
@ -1,28 +1,20 @@
|
||||||
/*
|
|
||||||
services from jacdac-v0
|
|
||||||
|
|
||||||
debugging services?
|
|
||||||
name service - need to re-implement
|
|
||||||
identification service - led blinking
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace jacdac {
|
namespace jacdac {
|
||||||
const devNameSettingPrefix = "#jddev:"
|
|
||||||
// common logging level for jacdac services
|
// common logging level for jacdac services
|
||||||
export let consolePriority = ConsolePriority.Debug;
|
export let consolePriority = ConsolePriority.Debug;
|
||||||
|
|
||||||
let _hostServices: Host[]
|
let _hostServices: Host[]
|
||||||
let _unattachedClients: Client[]
|
export let _unattachedClients: Client[]
|
||||||
let _allClients: Client[]
|
export let _allClients: Client[]
|
||||||
let _myDevice: Device
|
let _myDevice: Device
|
||||||
//% whenUsed
|
//% whenUsed
|
||||||
let _devices: Device[] = []
|
export let _devices: Device[] = []
|
||||||
//% whenUsed
|
//% whenUsed
|
||||||
let _announceCallbacks: (() => void)[] = [];
|
let _announceCallbacks: (() => void)[] = [];
|
||||||
let _newDeviceCallbacks: (() => void)[];
|
let _newDeviceCallbacks: (() => void)[];
|
||||||
let _pktCallbacks: ((p: JDPacket) => void)[];
|
let _pktCallbacks: ((p: JDPacket) => void)[];
|
||||||
let restartCounter = 0
|
let restartCounter = 0
|
||||||
|
let autoBindCnt = 0
|
||||||
|
export let autoBind = true
|
||||||
|
|
||||||
function log(msg: string) {
|
function log(msg: string) {
|
||||||
console.add(consolePriority, msg);
|
console.add(consolePriority, msg);
|
||||||
|
@ -302,6 +294,10 @@ namespace jacdac {
|
||||||
) {
|
) {
|
||||||
this.eventId = control.allocateNotifyEvent();
|
this.eventId = control.allocateNotifyEvent();
|
||||||
this.config = new ClientPacketQueue(this)
|
this.config = new ClientPacketQueue(this)
|
||||||
|
if (!this.name)
|
||||||
|
throw "no name"
|
||||||
|
if (!this.requiredDeviceName)
|
||||||
|
this.requiredDeviceName = this.name
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcastDevices() {
|
broadcastDevices() {
|
||||||
|
@ -335,7 +331,7 @@ namespace jacdac {
|
||||||
_attach(dev: Device, serviceNum: number) {
|
_attach(dev: Device, serviceNum: number) {
|
||||||
if (this.device) throw "Invalid attach"
|
if (this.device) throw "Invalid attach"
|
||||||
if (!this.broadcast) {
|
if (!this.broadcast) {
|
||||||
if (this.requiredDeviceName && this.requiredDeviceName != dev.name && this.requiredDeviceName != dev.deviceId)
|
if (!dev.matchesRoleAt(this.requiredDeviceName, serviceNum))
|
||||||
return false // don't attach
|
return false // don't attach
|
||||||
this.device = dev
|
this.device = dev
|
||||||
this.serviceIndex = serviceNum
|
this.serviceIndex = serviceNum
|
||||||
|
@ -461,9 +457,9 @@ namespace jacdac {
|
||||||
lastSeen: number
|
lastSeen: number
|
||||||
clients: Client[] = []
|
clients: Client[] = []
|
||||||
_eventCounter: number
|
_eventCounter: number
|
||||||
private _name: string
|
|
||||||
private _shortId: string
|
private _shortId: string
|
||||||
private queries: RegQuery[]
|
private queries: RegQuery[]
|
||||||
|
_score: number
|
||||||
|
|
||||||
constructor(public deviceId: string) {
|
constructor(public deviceId: string) {
|
||||||
_devices.push(this)
|
_devices.push(this)
|
||||||
|
@ -473,13 +469,6 @@ namespace jacdac {
|
||||||
return this.clients != null
|
return this.clients != null
|
||||||
}
|
}
|
||||||
|
|
||||||
get name() {
|
|
||||||
// TODO measure if caching is worth it
|
|
||||||
if (this._name === undefined)
|
|
||||||
this._name = settings.readString(devNameSettingPrefix + this.deviceId) || null
|
|
||||||
return this._name
|
|
||||||
}
|
|
||||||
|
|
||||||
get shortId() {
|
get shortId() {
|
||||||
// TODO measure if caching is worth it
|
// TODO measure if caching is worth it
|
||||||
if (!this._shortId)
|
if (!this._shortId)
|
||||||
|
@ -488,7 +477,19 @@ namespace jacdac {
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return this.shortId + (this.name ? ` (${this.name})` : ``)
|
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) {
|
private lookupQuery(reg: number) {
|
||||||
|
@ -555,6 +556,14 @@ namespace jacdac {
|
||||||
return false
|
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) {
|
sendCtrlCommand(cmd: number, payload: Buffer = null) {
|
||||||
const pkt = !payload ? JDPacket.onlyHeader(cmd) : JDPacket.from(cmd, payload)
|
const pkt = !payload ? JDPacket.onlyHeader(cmd) : JDPacket.from(cmd, payload)
|
||||||
pkt.serviceIndex = JD_SERVICE_INDEX_CTRL
|
pkt.serviceIndex = JD_SERVICE_INDEX_CTRL
|
||||||
|
@ -562,8 +571,6 @@ namespace jacdac {
|
||||||
}
|
}
|
||||||
|
|
||||||
static clearNameCache() {
|
static clearNameCache() {
|
||||||
for (let d of _devices)
|
|
||||||
d._name = undefined
|
|
||||||
clearAttachCache()
|
clearAttachCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -665,6 +672,16 @@ namespace jacdac {
|
||||||
for (const cl of _allClients)
|
for (const cl of _allClients)
|
||||||
cl.announceCallback()
|
cl.announceCallback()
|
||||||
gcDevices()
|
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
|
||||||
|
_rolemgr.autoBind()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearAttachCache() {
|
function clearAttachCache() {
|
||||||
|
@ -691,7 +708,7 @@ namespace jacdac {
|
||||||
continue // will re-attach
|
continue // will re-attach
|
||||||
}
|
}
|
||||||
const newClass = dev.services.getNumber(NumberFormat.UInt32LE, c.serviceIndex << 2)
|
const newClass = dev.services.getNumber(NumberFormat.UInt32LE, c.serviceIndex << 2)
|
||||||
if (newClass == c.serviceClass && (!c.requiredDeviceName || c.requiredDeviceName == dev.name)) {
|
if (newClass == c.serviceClass && dev.matchesRoleAt(c.requiredDeviceName, c.serviceIndex)) {
|
||||||
newClients.push(c)
|
newClients.push(c)
|
||||||
occupied[c.serviceIndex] = 1
|
occupied[c.serviceIndex] = 1
|
||||||
} else {
|
} else {
|
||||||
|
@ -760,8 +777,10 @@ namespace jacdac {
|
||||||
h.handlePacketOuter(pkt)
|
h.handlePacketOuter(pkt)
|
||||||
}
|
}
|
||||||
} else if (devId == selfDevice().deviceId) {
|
} else if (devId == selfDevice().deviceId) {
|
||||||
if (!pkt.isCommand)
|
if (!pkt.isCommand) {
|
||||||
|
// control.dmesg(`invalid echo ${pkt}`)
|
||||||
return // huh? someone's pretending to be us?
|
return // huh? someone's pretending to be us?
|
||||||
|
}
|
||||||
const h = _hostServices[pkt.serviceIndex]
|
const h = _hostServices[pkt.serviceIndex]
|
||||||
if (h && h.running) {
|
if (h && h.running) {
|
||||||
// log(`handle pkt at ${h.name} cmd=${pkt.service_command}`)
|
// log(`handle pkt at ${h.name} cmd=${pkt.service_command}`)
|
||||||
|
@ -842,7 +861,10 @@ namespace jacdac {
|
||||||
}
|
}
|
||||||
|
|
||||||
function gcDevices() {
|
function gcDevices() {
|
||||||
const cutoff = control.millis() - 2000
|
const now = control.millis()
|
||||||
|
const cutoff = now - 2000
|
||||||
|
selfDevice().lastSeen = now // make sure not to gc self
|
||||||
|
|
||||||
let numdel = 0
|
let numdel = 0
|
||||||
for (let i = 0; i < _devices.length; ++i) {
|
for (let i = 0; i < _devices.length; ++i) {
|
||||||
const dev = _devices[i]
|
const dev = _devices[i]
|
||||||
|
@ -938,214 +960,4 @@ namespace jacdac {
|
||||||
// and we're done
|
// and we're done
|
||||||
log("jacdac started");
|
log("jacdac started");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function autoBind() {
|
|
||||||
function log(msg: string) {
|
|
||||||
control.dmesg("autobind: " + msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
function pending() {
|
|
||||||
return _allClients.filter(c => !!c.requiredDeviceName && !c.isConnected())
|
|
||||||
}
|
|
||||||
|
|
||||||
pauseUntil(() => pending().length == 0, 1000)
|
|
||||||
|
|
||||||
const plen = pending().length
|
|
||||||
log(`pending: ${plen}`)
|
|
||||||
if (plen == 0) return
|
|
||||||
|
|
||||||
pause(1000) // wait for everyone to enumerate
|
|
||||||
|
|
||||||
const requested: RemoteRequestedDevice[] = []
|
|
||||||
|
|
||||||
for (const client of _allClients) {
|
|
||||||
if (client.requiredDeviceName) {
|
|
||||||
const r = addRequested(requested, client.requiredDeviceName, client.serviceClass, null)
|
|
||||||
r.boundTo = client.device
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!requested.length)
|
|
||||||
return
|
|
||||||
|
|
||||||
function nameFree(d: Device) {
|
|
||||||
return !d.name || requested.every(r => r.boundTo != d)
|
|
||||||
}
|
|
||||||
|
|
||||||
requested.sort((a, b) => a.name.compare(b.name))
|
|
||||||
|
|
||||||
let numSel = 0
|
|
||||||
recomputeCandidates(requested)
|
|
||||||
for (const r of requested) {
|
|
||||||
if (r.boundTo)
|
|
||||||
continue
|
|
||||||
const cand = r.candidates.filter(nameFree)
|
|
||||||
log(`name: ${r.name}, ${cand.length} candidate(s)`)
|
|
||||||
if (cand.length > 0) {
|
|
||||||
// take ones without existing names first
|
|
||||||
cand.sort((a, b) => (a.name || "").compare(b.name || "") || a.deviceId.compare(b.deviceId))
|
|
||||||
log(`setting to ${cand[0].toString()}`)
|
|
||||||
r.select(cand[0])
|
|
||||||
numSel++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAllNames() {
|
|
||||||
settings.list(devNameSettingPrefix).forEach(settings.remove)
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDevName(id: string, name: string) {
|
|
||||||
const devid = devNameSettingPrefix + id
|
|
||||||
if (name.length == 0)
|
|
||||||
settings.remove(devid)
|
|
||||||
else
|
|
||||||
settings.writeString(devid, name)
|
|
||||||
Device.clearNameCache()
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RoleManagerHost extends Host {
|
|
||||||
constructor() {
|
|
||||||
super("rolemgr", SRV_ROLE_MANAGER)
|
|
||||||
}
|
|
||||||
|
|
||||||
public handlePacket(packet: JDPacket) {
|
|
||||||
switch (packet.serviceCommand) {
|
|
||||||
case RoleManagerCmd.GetRole:
|
|
||||||
if (packet.data.length == 8) {
|
|
||||||
let name = settings.readBuffer(devNameSettingPrefix + packet.data.toHex())
|
|
||||||
if (!name) name = Buffer.create(0)
|
|
||||||
this.sendReport(JDPacket.from(RoleManagerCmd.GetRole, packet.data.concat(name)))
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case RoleManagerCmd.SetRole:
|
|
||||||
if (packet.data.length >= 8) {
|
|
||||||
setDevName(packet.data.slice(0, 8).toHex(), packet.data.slice(8).toString())
|
|
||||||
this.sendChangeEvent();
|
|
||||||
}
|
|
||||||
break
|
|
||||||
case RoleManagerCmd.ListStoredRoles:
|
|
||||||
OutPipe.respondForEach(packet, settings.list(devNameSettingPrefix), k =>
|
|
||||||
Buffer.fromHex(k.slice(devNameSettingPrefix.length))
|
|
||||||
.concat(settings.readBuffer(k)))
|
|
||||||
break
|
|
||||||
case RoleManagerCmd.ListRequiredRoles:
|
|
||||||
const namedClients = _allClients.filter(c => !!c.requiredDeviceName)
|
|
||||||
OutPipe.respondForEach(packet, namedClients, packName)
|
|
||||||
break
|
|
||||||
case RoleManagerCmd.ClearAllRoles:
|
|
||||||
clearAllNames()
|
|
||||||
this.sendChangeEvent();
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
function packName(c: Client) {
|
|
||||||
const devid = c.device ? Buffer.fromHex(c.device.deviceId) : Buffer.create(8)
|
|
||||||
return jdpack("b[8] u32 s", [devid, c.serviceClass, c.requiredDeviceName])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//% fixedInstance whenUsed block="role manager"
|
|
||||||
export const roleManagerHost = new RoleManagerHost()
|
|
||||||
|
|
||||||
export class RemoteRequestedDevice {
|
|
||||||
services: number[] = [];
|
|
||||||
boundTo: Device;
|
|
||||||
candidates: Device[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public parent: RoleManagerClient,
|
|
||||||
public name: string
|
|
||||||
) { }
|
|
||||||
|
|
||||||
isCandidate(ldev: Device) {
|
|
||||||
return this.services.every(s => ldev.hasService(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
select(dev: Device) {
|
|
||||||
if (dev == this.boundTo)
|
|
||||||
return
|
|
||||||
if (this.parent == null) {
|
|
||||||
setDevName(dev.deviceId, this.name)
|
|
||||||
} else {
|
|
||||||
if (this.boundTo)
|
|
||||||
this.parent.setName(this.boundTo, "")
|
|
||||||
this.parent.setName(dev, this.name)
|
|
||||||
}
|
|
||||||
this.boundTo = dev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function recomputeCandidates(remotes: RemoteRequestedDevice[]) {
|
|
||||||
const localDevs = devices()
|
|
||||||
for (let dev of remotes)
|
|
||||||
dev.candidates = localDevs.filter(ldev => dev.isCandidate(ldev))
|
|
||||||
}
|
|
||||||
|
|
||||||
function addRequested(devs: RemoteRequestedDevice[], name: string, serviceClass: number,
|
|
||||||
parent: RoleManagerClient) {
|
|
||||||
let r = devs.find(d => d.name == name)
|
|
||||||
if (!r)
|
|
||||||
devs.push(r = new RemoteRequestedDevice(parent, name))
|
|
||||||
r.services.push(serviceClass)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export class RoleManagerClient extends Client {
|
|
||||||
public remoteRequestedDevices: RemoteRequestedDevice[] = []
|
|
||||||
|
|
||||||
constructor(requiredDevice: string = null) {
|
|
||||||
super("rolemgrc", SRV_ROLE_MANAGER, requiredDevice)
|
|
||||||
|
|
||||||
onNewDevice(() => {
|
|
||||||
recomputeCandidates(this.remoteRequestedDevices)
|
|
||||||
})
|
|
||||||
|
|
||||||
onAnnounce(() => {
|
|
||||||
if (this.isConnected())
|
|
||||||
control.runInParallel(() => this.scanCore())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private scanCore() {
|
|
||||||
const inp = new InPipe()
|
|
||||||
this.sendCommand(inp.openCommand(RoleManagerCmd.ListRequiredRoles))
|
|
||||||
|
|
||||||
const localDevs = devices()
|
|
||||||
const devs: RemoteRequestedDevice[] = []
|
|
||||||
|
|
||||||
inp.readList(buf => {
|
|
||||||
const [devidbuf, serviceClass, name] = jdunpack<[Buffer, number, string]>(buf, "b[8] u32 s")
|
|
||||||
const devid = devidbuf.toHex();
|
|
||||||
const r = addRequested(devs, name, serviceClass, this)
|
|
||||||
const dev = localDevs.find(d => d.deviceId == devid)
|
|
||||||
if (dev)
|
|
||||||
r.boundTo = dev
|
|
||||||
})
|
|
||||||
|
|
||||||
devs.sort((a, b) => a.name.compare(b.name))
|
|
||||||
|
|
||||||
this.remoteRequestedDevices = devs
|
|
||||||
recomputeCandidates(this.remoteRequestedDevices)
|
|
||||||
}
|
|
||||||
|
|
||||||
scan() {
|
|
||||||
pauseUntil(() => this.isConnected())
|
|
||||||
this.scanCore()
|
|
||||||
}
|
|
||||||
|
|
||||||
clearNames() {
|
|
||||||
this.sendCommandWithAck(JDPacket.onlyHeader(RoleManagerCmd.ClearAllRoles))
|
|
||||||
}
|
|
||||||
|
|
||||||
setName(dev: Device, name: string) {
|
|
||||||
this.sendCommandWithAck(JDPacket.from(RoleManagerCmd.SetRole,
|
|
||||||
Buffer.fromHex(dev.deviceId).concat(Buffer.fromUTF8(name))))
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePacket(pkt: JDPacket) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
20
test.ts
20
test.ts
|
@ -35,9 +35,27 @@ function jdpackTest() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// pins.A9.digitalWrite(false)
|
// pins.A9.digitalWrite(false)
|
||||||
|
|
||||||
jacdac.consolePriority = ConsolePriority.Log;
|
jacdac.consolePriority = ConsolePriority.Log;
|
||||||
jacdac.roleManagerHost.start()
|
jacdac.roleManagerHost.start()
|
||||||
jacdac.protoTestHost.start()
|
jacdac.protoTestHost.start()
|
||||||
jacdac.start()
|
jacdac.start()
|
||||||
jacdac.loggerHost.log("test started")
|
jacdac.loggerHost.log("test started")
|
||||||
jdpackTest()
|
//jdpackTest()
|
||||||
|
|
||||||
|
function addClient(cls:number,name:string) {
|
||||||
|
console.log(`client: ${name} (${cls})`)
|
||||||
|
new jacdac.Client(name,cls,name).start()
|
||||||
|
}
|
||||||
|
addClient(0x1f140409, "left_leg/acc1" )
|
||||||
|
addClient(0x1473a263, "btn1" )
|
||||||
|
addClient(0x16c810b8, "small/hum" )
|
||||||
|
addClient(0x1421bac7, "small/temp" )
|
||||||
|
addClient(0x169c9dc6, "big/eco2" )
|
||||||
|
addClient(0x16c810b8, "big/hum" )
|
||||||
|
addClient(0x1421bac7, "big/temp" )
|
||||||
|
addClient(0x16c810b8, "xsmall/hum" )
|
||||||
|
addClient(0x1421bac7, "xsmall/temp" )
|
||||||
|
|
||||||
|
|
||||||
|
jacdac._rolemgr.clearRoles()
|
||||||
|
|
Загрузка…
Ссылка в новой задаче