336 строки
12 KiB
TypeScript
336 строки
12 KiB
TypeScript
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
|
|
changed = false;
|
|
|
|
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
|
|
this.changed = true;
|
|
}
|
|
}
|
|
|
|
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 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.role])
|
|
}
|
|
}
|
|
|
|
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.role) {
|
|
const b = new RoleBinding(cl.role, 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)
|
|
}
|
|
}
|
|
|
|
// notify clients that something changed
|
|
if (bindings.some(binding => binding.changed))
|
|
this.sendChangeEvent()
|
|
}
|
|
}
|
|
}
|
|
|
|
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(role: string) {
|
|
super("rolemgrc", SRV_ROLE_MANAGER, role)
|
|
|
|
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) {
|
|
}
|
|
}
|
|
*/
|
|
} |