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
|
||||
recvQ = []
|
||||
control.simmessages.onReceived("jacdac", buf => {
|
||||
recvQ.push(buf)
|
||||
control.raiseEvent(__physId(), 1)
|
||||
if (buf[2] + 12 != buf.length) {
|
||||
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
|
||||
control.runInParallel(function() {
|
||||
while(true) {
|
||||
control.runInParallel(function () {
|
||||
while (true) {
|
||||
control.raiseEvent(__physId(), 100);
|
||||
pause(500)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ namespace jacdac {
|
|||
static fromBinary(buf: Buffer) {
|
||||
const p = new JDPacket()
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -183,7 +183,7 @@ namespace jacdac {
|
|||
data.write(sz, s)
|
||||
sz += s.length
|
||||
}
|
||||
this._data = data
|
||||
this.data = data
|
||||
}
|
||||
|
||||
withFrameStripped() {
|
||||
|
@ -195,7 +195,7 @@ namespace jacdac {
|
|||
}
|
||||
|
||||
jdpack(fmt: string, nums: any[]) {
|
||||
this._data = jdpack(fmt, nums)
|
||||
this.data = jdpack(fmt, nums)
|
||||
}
|
||||
|
||||
get isCommand() {
|
||||
|
@ -214,6 +214,8 @@ namespace jacdac {
|
|||
}
|
||||
|
||||
_sendCore() {
|
||||
if (this._data.length != this._header[12])
|
||||
throw "jdsize mismatch"
|
||||
jacdac.__physSendPacket(this._header, this._data)
|
||||
}
|
||||
|
||||
|
|
3
pxt.json
3
pxt.json
|
@ -43,6 +43,7 @@
|
|||
"sensor/client.ts",
|
||||
"sensor/host.ts",
|
||||
"role-manager/constants.ts",
|
||||
"rolemgr.ts",
|
||||
"proto-test/constants.ts",
|
||||
"proto-test/host.ts"
|
||||
],
|
||||
|
@ -58,7 +59,7 @@
|
|||
},
|
||||
"public": true,
|
||||
"targetVersions": {
|
||||
"target": "3.1.39",
|
||||
"target": "3.1.50",
|
||||
"targetId": "microbit"
|
||||
},
|
||||
"supportedTargets": [
|
||||
|
|
|
@ -1,7 +1,21 @@
|
|||
namespace jacdac {
|
||||
// Service: Role Manager
|
||||
export const SRV_ROLE_MANAGER = 0x119c3ad1
|
||||
export const SRV_ROLE_MANAGER = 0x1e4b7e66
|
||||
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.
|
||||
*
|
||||
|
@ -14,10 +28,10 @@ namespace jacdac {
|
|||
|
||||
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,
|
||||
|
@ -25,7 +39,7 @@ namespace jacdac {
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* ```
|
||||
* 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,
|
||||
|
@ -53,7 +67,7 @@ namespace jacdac {
|
|||
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]")
|
||||
|
@ -66,14 +80,14 @@ namespace jacdac {
|
|||
/**
|
||||
* 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
|
||||
* ```
|
||||
* 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 {
|
||||
const devNameSettingPrefix = "#jddev:"
|
||||
// common logging level for jacdac services
|
||||
export let consolePriority = ConsolePriority.Debug;
|
||||
|
||||
let _hostServices: Host[]
|
||||
let _unattachedClients: Client[]
|
||||
let _allClients: Client[]
|
||||
export let _unattachedClients: Client[]
|
||||
export let _allClients: Client[]
|
||||
let _myDevice: Device
|
||||
//% whenUsed
|
||||
let _devices: Device[] = []
|
||||
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);
|
||||
|
@ -302,6 +294,10 @@ namespace jacdac {
|
|||
) {
|
||||
this.eventId = control.allocateNotifyEvent();
|
||||
this.config = new ClientPacketQueue(this)
|
||||
if (!this.name)
|
||||
throw "no name"
|
||||
if (!this.requiredDeviceName)
|
||||
this.requiredDeviceName = this.name
|
||||
}
|
||||
|
||||
broadcastDevices() {
|
||||
|
@ -335,7 +331,7 @@ namespace jacdac {
|
|||
_attach(dev: Device, serviceNum: number) {
|
||||
if (this.device) throw "Invalid attach"
|
||||
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
|
||||
this.device = dev
|
||||
this.serviceIndex = serviceNum
|
||||
|
@ -461,9 +457,9 @@ namespace jacdac {
|
|||
lastSeen: number
|
||||
clients: Client[] = []
|
||||
_eventCounter: number
|
||||
private _name: string
|
||||
private _shortId: string
|
||||
private queries: RegQuery[]
|
||||
_score: number
|
||||
|
||||
constructor(public deviceId: string) {
|
||||
_devices.push(this)
|
||||
|
@ -473,13 +469,6 @@ namespace jacdac {
|
|||
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() {
|
||||
// TODO measure if caching is worth it
|
||||
if (!this._shortId)
|
||||
|
@ -488,7 +477,19 @@ namespace jacdac {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
@ -555,6 +556,14 @@ namespace jacdac {
|
|||
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
|
||||
|
@ -562,8 +571,6 @@ namespace jacdac {
|
|||
}
|
||||
|
||||
static clearNameCache() {
|
||||
for (let d of _devices)
|
||||
d._name = undefined
|
||||
clearAttachCache()
|
||||
}
|
||||
|
||||
|
@ -665,6 +672,16 @@ namespace jacdac {
|
|||
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
|
||||
_rolemgr.autoBind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearAttachCache() {
|
||||
|
@ -691,7 +708,7 @@ namespace jacdac {
|
|||
continue // will re-attach
|
||||
}
|
||||
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)
|
||||
occupied[c.serviceIndex] = 1
|
||||
} else {
|
||||
|
@ -760,8 +777,10 @@ namespace jacdac {
|
|||
h.handlePacketOuter(pkt)
|
||||
}
|
||||
} else if (devId == selfDevice().deviceId) {
|
||||
if (!pkt.isCommand)
|
||||
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}`)
|
||||
|
@ -842,7 +861,10 @@ namespace jacdac {
|
|||
}
|
||||
|
||||
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
|
||||
for (let i = 0; i < _devices.length; ++i) {
|
||||
const dev = _devices[i]
|
||||
|
@ -938,214 +960,4 @@ namespace jacdac {
|
|||
// and we're done
|
||||
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)
|
||||
|
||||
jacdac.consolePriority = ConsolePriority.Log;
|
||||
jacdac.roleManagerHost.start()
|
||||
jacdac.protoTestHost.start()
|
||||
jacdac.start()
|
||||
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()
|
||||
|
|
Загрузка…
Ссылка в новой задаче