pxt-common-packages
Version:
Microsoft MakeCode (PXT) common packages
293 lines (256 loc) • 9.59 kB
text/typescript
namespace control {
/**
* Run code when a registered event happens.
* @param id the event compoent id
* @param value the event value to match
*/
//% weight=20 blockGap=8 blockId="control_on_event" block="on event|from %src|with value %value"
//% blockExternalInputs=1
//% help="control/on-event"
export function onEvent(src: number, value: number, handler: () => void, flags = 16) { // EVENT_LISTENER_DEFAULT_FLAGS
const ctx = control.eventContext();
if (!ctx)
control.internalOnEvent(src, value, handler, flags);
else
ctx.registerHandler(src, value, handler, flags);
}
export class FrameCallback {
order: number
handler: () => void
}
class EventHandler {
constructor(
public src: number,
public value: number,
public handler: () => void,
public flags: number
) { }
register() {
control.internalOnEvent(this.src, this.value, () => {
if (this.handler) this.handler();
}, this.flags)
}
unregister() {
control.internalOnEvent(this.src, this.value, doNothing, this.flags);
}
}
function doNothing() { }
export class EventContext {
private handlers: EventHandler[];
private frameCallbacks: FrameCallback[];
private frameWorker: number;
private framesInSample: number;
private timeInSample: number;
private lastPerfDump: number;
public deltaTimeMillis: number;
private prevTimeMillis: number;
private idleCallbacks: (() => void)[];
static lastStats: string;
static onStats: (stats: string) => void;
constructor() {
this.handlers = [];
this.framesInSample = 0;
this.timeInSample = 0;
this.deltaTimeMillis = 0;
this.frameWorker = 0;
this.idleCallbacks = undefined;
if (!EventContext.lastStats) {
EventContext.lastStats = "";
}
}
get deltaTime() {
return this.deltaTimeMillis / 1000;
}
private runCallbacks() {
control.enablePerfCounter("all frame callbacks")
let loopStart = control.millis()
this.deltaTimeMillis = loopStart - this.prevTimeMillis;
this.prevTimeMillis = loopStart;
for (let f of this.frameCallbacks) {
f.handler()
}
const now = control.millis()
let runtime = now - loopStart
this.timeInSample += runtime
this.framesInSample++
if (this.timeInSample > 1000 || this.framesInSample > 30) {
const realTimeInSample = now - this.lastPerfDump
this.lastPerfDump = now
const fps = this.framesInSample / (this.timeInSample / 1000);
EventContext.lastStats = `fps:${Math.round(fps)}`;
if (fps < 99)
EventContext.lastStats += "." + (Math.round(fps * 10) % 10)
if (control.profilingEnabled()) {
control.dmesg(`${(fps * 100) | 0}/100 fps - ${this.framesInSample} frames (${this.timeInSample}ms/${realTimeInSample}ms)`)
control.gc()
control.dmesgPerfCounters()
}
this.timeInSample = 0
this.framesInSample = 0
}
let delay = Math.max(1, 20 - runtime)
return delay
}
private runningCallbacks: boolean;
private registerFrameCallbacks() {
if (!this.frameCallbacks) return;
const worker = this.frameWorker;
control.runInParallel(() => {
if (this.runningCallbacks) {
// this context is still running in a different fiber;
// delay until the other fiber doing so has ceased.
pauseUntil(() => !this.runningCallbacks);
}
this.runningCallbacks = true;
this.framesInSample = 0;
this.timeInSample = 0;
this.deltaTimeMillis = 0;
this.prevTimeMillis = control.millis();
while (worker == this.frameWorker) {
let delay = this.runCallbacks()
pause(delay)
}
this.runningCallbacks = false;
})
}
register() {
for (const h of this.handlers)
h.register();
this.registerFrameCallbacks();
}
unregister() {
for (const h of this.handlers)
h.unregister();
this.frameWorker++;
}
registerFrameHandler(order: number, handler: () => void): FrameCallback {
if (!this.frameCallbacks) {
this.frameCallbacks = [];
this.registerFrameCallbacks();
}
const fn = new FrameCallback()
fn.order = order
fn.handler = handler
for (let i = 0; i < this.frameCallbacks.length; ++i) {
if (this.frameCallbacks[i].order > order) {
this.frameCallbacks.insertAt(i, fn)
return fn;
}
}
this.frameCallbacks.push(fn);
return fn;
}
unregisterFrameHandler(fn: FrameCallback) {
if (!fn || !this.frameCallbacks) return;
const i = this.frameCallbacks.indexOf(fn);
if (i > -1)
this.frameCallbacks.splice(i, 1);
}
registerHandler(src: number, value: number, handler: () => void, flags: number) {
// already there?
for (const h of this.handlers) {
if (h.src == src && h.value == value) {
h.flags = flags;
h.handler = handler;
return;
}
}
// register and push
const hn = new EventHandler(src, value, handler, flags);
this.handlers.push(hn);
hn.register();
}
addIdleHandler(handler: () => void) {
if (!this.idleCallbacks) {
this.idleCallbacks = [];
this.registerHandler(15/*DAL.DEVICE_ID_SCHEDULER*/, 2/*DAL.DEVICE_SCHEDULER_EVT_IDLE*/, () => this.runIdleHandler(), 16);
}
this.idleCallbacks.push(handler);
}
removeIdleHandler(handler: () => void) {
if (handler && this.idleCallbacks)
this.idleCallbacks.removeElement(handler);
}
private runIdleHandler() {
if (this.idleCallbacks) {
const ics = this.idleCallbacks.slice(0);
ics.forEach(ic => ic());
}
}
}
let eventContexts: EventContext[];
/**
* Gets the current event context if any
*/
export function eventContext(): EventContext {
return eventContexts ? eventContexts[eventContexts.length - 1] : undefined;
}
/**
* Pushes a new event context and clears all handlers
*/
export function pushEventContext(): EventContext {
if (!eventContexts)
eventContexts = [];
// unregister previous context
const ctx = eventContext();
if (ctx) ctx.unregister();
// register again
const n = new EventContext();
eventContexts.push(n);
return n;
}
/**
* Pops the current event context and restore handlers if any previous context
*/
export function popEventContext() {
if (!eventContexts) return;
// clear current context
const ctx = eventContexts.pop();
if (!ctx) return;
ctx.unregister();
// register old context again
const context = eventContexts[eventContexts.length - 1];
if (context)
context.register();
else
eventContexts = undefined;
}
let _idleCallbacks: (() => void)[];
/**
* Registers a function to run when the device is idling
* @param handler
*/
export function onIdle(handler: () => void) {
if (!handler) return;
const ctx = eventContext();
if (ctx) ctx.addIdleHandler(handler);
else {
if (!_idleCallbacks) {
_idleCallbacks = [];
control.runInBackground(function () {
while (_idleCallbacks) {
_idleCallbacks.slice(0).forEach(cb => cb());
pause(20);
}
})
/*
control.internalOnEvent(
15. // DAL.DEVICE_ID_SCHEDULER
2, // DAL.DEVICE_SCHEDULER_EVT_IDLE
function() {
pins.LED.digitalWrite(on = !on);
if (_idleCallbacks)
_idleCallbacks.slice(0).forEach(cb => cb());
}, 192); // MESSAGE_BUS_LISTENER_IMMEDIATE
*/
}
_idleCallbacks.push(handler);
}
}
export function removeIdleHandler(handler: () => void) {
if (!handler) return;
const ctx = eventContext();
if (ctx) ctx.removeIdleHandler(handler);
else if (_idleCallbacks) _idleCallbacks.removeElement(handler);
}
}