turtlestitch/libraries/signada.js

163 wiersze
6.0 KiB
JavaScript

/* Signada - a network remote control procotol
* ===========================================
* By Bernat Romagosa, August 2021
*
* Enables Snap! to talk to wifi-enabled
* MicroBlocks devices.
*
* This protocol was designed for the Citilab
* ED1 microcontroller board, but it can be
* used for any ESP32 board that has a display
* and two buttons, like the M5Stack.
*
* First, load the Signada example project
* from the MicroBlocks IDE and follow the
* instructions on the long comment in that
* project.
*
* Get MicroBlocks at https://microblocks.fun
*/
SnapExtensions.primitives.set(
'sgd_connect(ip)',
function (ip) {
if (location.protocol.indexOf('https') > -1) {
throw new Error(
'Signada requires an HTTP only instance of Snap!, like:\n' +
'http://extensions.snap.berkeley.edu'
);
}
var socket = new WebSocket('ws://' + ip + ':81'),
stage = this.parentThatIsA(StageMorph),
cache;
stage.signada = {};
stage.signada.ip = ip;
stage.signada.socket = socket;
stage.signada.responses = {};
stage.signada.responseCache = {};
stage.signada.lastID = 0;
stage.signada.eventListener = function (event) {
response = JSON.parse(event.data);
if (Array.isArray(response[1])) {
response[1] = new List(response[1]);
}
stage.signada.responses[response[0]] = response[1];
// Cache the response for when hat blocks
cache = Object.values(stage.signada.responseCache).find(
each => each.requestID === response[0]
);
if (cache) {
cache.value = response[1];
cache.updating = false;
}
};
socket.addEventListener('message', stage.signada.eventListener);
}
);
SnapExtensions.primitives.set(
'sgd_disconnect()',
function () {
var stage = this.parentThatIsA(StageMorph);
if (SnapExtensions.primitives.get('sgd_connected()').call(this)) {
stage.signada.socket.removeEventListener(
'message',
stage.signada.eventListener
);
stage.signada.socket.close();
}
}
);
SnapExtensions.primitives.set(
'sgd_connected()',
function () {
var signada = this.parentThatIsA(StageMorph).signada;
return (signada !== undefined) && (signada.socket.readyState === 1);
}
);
SnapExtensions.primitives.set(
'sgd_call(blockname, params, defaultresponse)',
function (blockname, params, defaultresponse, proc) {
var signada = this.parentThatIsA(StageMorph).signada,
hatBlock = proc.topBlock.parentThatIsA(HatBlockMorph),
needsCaching = !isNil(hatBlock) &&
(hatBlock.selector === 'receiveCondition'),
updatingCache = (signada.responseCache[blockname]?.updating),
defaultresponse =
(defaultresponse === undefined) ? 0 : defaultresponse;
if (!SnapExtensions.primitives.get('sgd_connected()').call(this)) {
throw new Error(
'You are not connected to any device.\n' +
'Please use the connect block to establish a connection and' +
'try again.'
);
}
if (!proc.requestID) {
if (!needsCaching || !updatingCache) {
proc.startTime = new Date();
proc.requestID = signada.lastID;
// Cache responses for reporters inside when hat blocks
if (signada.responseCache[blockname]) {
// Let's mark the cached response as updating so we don't
// request it again until we get a response from the device
signada.responseCache[blockname].updating = true;
signada.responseCache[blockname].requestID = proc.requestID;
signada.responseCache[blockname].requestTime =
(new Date()).getTime();
} else {
// Never sent a similar request before. Let's give it a
// default value.
signada.responseCache[blockname] = {
requestID: proc.requestID,
updating: true,
value: defaultresponse,
requestTime: (new Date()).getTime()
};
}
signada.socket.send(
JSON.stringify([signada.lastID, blockname, params.contents])
);
// Last ID wraps at 1.000.000 to make sure it doesn't grow too
// much and doesn't wrap too early
signada.lastID = (signada.lastID + 1) % 1000000;
}
} else {
if (signada.responses[proc.requestID] !== undefined) {
var response = signada.responses[proc.requestID];
proc.requestID = null;
return response;
} else if ((new Date() - proc.startTime) > 1000) {
// Timeout after 1 second. Return last cached value
proc.requestID = null;
signada.responseCache[blockname].updating = false;
return signada.responseCache[blockname].value;
}
}
if (needsCaching) {
// This reporter needs caching. Let's return the last value for this
// particular block name.
if (updatingCache &&
(((new Date()).getTime() -
signada.responseCache[blockname].requestTime) > 250)) {
// We've been waiting for the cache to update for a long time.
// Let's invalidate it so the value is requested again.
signada.responseCache[blockname].updating = false;
}
return signada.responseCache[blockname].value;
}
proc.pushContext('doYield');
proc.pushContext();
}
);