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:
Michał Moskal 2021-02-06 01:53:55 +01:00 коммит произвёл GitHub
Родитель b3c0b3f610
Коммит 70d9cea37a
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 453 добавлений и 256 удалений

Просмотреть файл

@ -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)
} }

Просмотреть файл

@ -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")
* ``` * ```
*/ */

330
rolemgr.ts Normal file
Просмотреть файл

@ -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) {
}
}
*/
}

Просмотреть файл

@ -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
Просмотреть файл

@ -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()