Init first version
Contains a basic web server, 1W sensor reading, relays actuator, a working control algorithm and recipes loading
This commit is contained in:
commit
c45114f6f7
|
@ -0,0 +1,57 @@
|
|||
import atexit
|
||||
try:
|
||||
import RPi.GPIO as GPIO
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
rpi = True
|
||||
except:
|
||||
rpi = False
|
||||
|
||||
|
||||
class Actuator():
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
|
||||
class GPIOPin(Actuator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.pin = (kwargs.pop('pin'))
|
||||
self.initial = (kwargs.pop('initial'))
|
||||
self.onstate = (kwargs.pop('onstate'))
|
||||
self.offstate = (kwargs.pop('offstate'))
|
||||
GPIO.setup(self.pin, GPIO.OUT, initial=self.initial)
|
||||
atexit.register(self.deinit)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def deinit(self):
|
||||
# Be sure everything is off at exit
|
||||
print('Cleaning before exit')
|
||||
GPIO.output(self.pin, self.initial)
|
||||
GPIO.cleanup()
|
||||
|
||||
def enable(self, enable=True):
|
||||
GPIO.output(self.pin, self.onstate) if enable else self.disable()
|
||||
|
||||
|
||||
def disable(self):
|
||||
GPIO.output(self.pin, self.offstate)
|
||||
|
||||
class MockPIN(Actuator):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.pin = (kwargs.pop('pin'))
|
||||
self.initial = (kwargs.pop('initial'))
|
||||
self.onstate = (kwargs.pop('onstate'))
|
||||
self.offstate = (kwargs.pop('offstate'))
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def enable(self, enable=True):
|
||||
print('FAKE ENABLE') if enable else self.disable()
|
||||
|
||||
|
||||
def disable(self):
|
||||
print('FAKE DISABLE')
|
||||
|
||||
actuators = {
|
||||
'heater': GPIOPin(
|
||||
pin=22, initial=GPIO.HIGH, onstate=GPIO.LOW, offstate=GPIO.HIGH) if (
|
||||
rpi) else MockPIN(pin=22, initial=False, onstate=True, offstate=False),
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
import json
|
||||
|
||||
from flask import Flask, Response
|
||||
from flask import render_template, render_template_string, escape
|
||||
from flask import request, send_file
|
||||
from flask_socketio import SocketIO, join_room
|
||||
|
||||
from random import randint
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
from os.path import exists
|
||||
from phasectrl import State, ManualIntervention
|
||||
from sensors import sensors
|
||||
import phasectrl
|
||||
from time import perf_counter, sleep
|
||||
|
||||
|
||||
def create_app():
|
||||
app = Flask('hakkoso')
|
||||
app.config['DATABASE'] = 'server.db'
|
||||
return app
|
||||
|
||||
app = create_app()
|
||||
|
||||
socketio = SocketIO(app, async_mode='threading')
|
||||
|
||||
def updateState():
|
||||
socketio.emit('state', make_state())
|
||||
print('SENT STATE UPDATE', make_state())
|
||||
|
||||
statemachine = State('recipes.json', updateState)
|
||||
|
||||
def make_state():
|
||||
return {
|
||||
'manual': statemachine.manual.getState(),
|
||||
'recipes': tuple(r.getState() for r in statemachine.recipes),
|
||||
'recipe': (statemachine.recipe.getState(statemachine.phase)
|
||||
if statemachine.recipe else None),
|
||||
'state': statemachine.getState(),
|
||||
'sensors': {
|
||||
'units': {
|
||||
'Temperature': '°C',
|
||||
'Acidity': 'pH',
|
||||
'Humidity': 'RH',
|
||||
'Durezza Acqua': 'BOH',
|
||||
},
|
||||
'found': sensors.list()
|
||||
}
|
||||
}
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
@app.route('/recipe-editor')
|
||||
def recipe_editor():
|
||||
return render_template('recipe-editor.html')
|
||||
|
||||
@app.route('/status')
|
||||
def status():
|
||||
return make_state()
|
||||
|
||||
|
||||
@app.route('/dist/<filename>')
|
||||
def dist(filename):
|
||||
# TODO: check file exists and path is under src
|
||||
filename = 'dist/' + filename
|
||||
if exists(filename):
|
||||
return send_file(filename)
|
||||
print('Missing', filename)
|
||||
return ''
|
||||
|
||||
@app.route('/recipe')
|
||||
def recipe():
|
||||
return render_template('recipe.html', recipe=phasectrl.recipe)
|
||||
|
||||
@socketio.on('new client')
|
||||
def handle_new_client():
|
||||
updateState()
|
||||
socketio.emit('sensor history', sensors.get_history())
|
||||
|
||||
@socketio.on('manual response')
|
||||
def handle_manual_response(response):
|
||||
if statemachine.recipe is not None:
|
||||
statemachine.manual.set(response)
|
||||
updateState()
|
||||
|
||||
@socketio.on('load recipe idx')
|
||||
def load_recipe_idx(idx):
|
||||
statemachine.loadByIdx(idx)
|
||||
|
||||
def run_recipes():
|
||||
while True:
|
||||
while statemachine.recipe is not None:
|
||||
done = statemachine.run_step()
|
||||
if done:
|
||||
statemachine.done()
|
||||
updateState()
|
||||
sleep(1)
|
||||
|
||||
def read_esnsors():
|
||||
while True:
|
||||
sensors.read()
|
||||
socketio.emit('sensors', (sensors.get(),))
|
||||
sleep(0.5)
|
||||
|
||||
import threading
|
||||
if __name__ == '__main__':
|
||||
thread = threading.Thread(target=run_recipes, daemon=True)
|
||||
thread.start()
|
||||
sensors_thread = threading.Thread(target=read_esnsors, daemon=True)
|
||||
sensors_thread.start()
|
||||
print('RUN APP')
|
||||
socketio.run(app)
|
|
@ -0,0 +1,39 @@
|
|||
from nio import AsyncClient # pip3 install matrix-nio
|
||||
import asyncio
|
||||
|
||||
class Chatter():
|
||||
def __init__(self, homeserver, defaultroom):
|
||||
self.homeserver = homeserver
|
||||
self.defaultroom = defaultroom
|
||||
|
||||
def login(self, username, device, token):
|
||||
self.client = AsyncClient(self.homeserver, username)
|
||||
self.client.restore_login(
|
||||
user_id=username,
|
||||
device_id=device,
|
||||
access_token = token)
|
||||
|
||||
async def send(self, message, room=None):
|
||||
print(f"Sending {message}")
|
||||
await self.client.room_send(room_id = room or self.defaultroom,
|
||||
message_type="m.room.message",
|
||||
content = {
|
||||
"msgtype": "m.text",
|
||||
"body": message
|
||||
}
|
||||
)
|
||||
await self.client.close()
|
||||
|
||||
|
||||
def send_sync(self, msg):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
coroutine = chat.send(msg)
|
||||
loop.run_until_complete(coroutine)
|
||||
|
||||
|
||||
|
||||
chat = Chatter('https://chat.nixo.xyz', '!zJiIjYrICcdvRPKuen:nixo.xyz')
|
||||
chat.login(
|
||||
'@rabbit:nixo.xyz', 'ESJZVKGHVF',
|
||||
'MDAxNmxvY2F0aW9uIG5peG8ueHl6CjAwMTNpZGVudGlmaWVyIGtleQowMDEwY2lkIGdlbiA9IDEKMDAyM2NpZCB1c2VyX2lkID0gQHJhYmJpdDpuaXhvLnh5egowMDE2Y2lkIHR5cGUgPSBhY2Nlc3MKMDAyMWNpZCBub25jZSA9IDUjbUAjTHdlYjpxeEd3MlYKMDAyZnNpZ25hdHVyZSBoyKyldyQxYejjrznz_M47h6TQFxCv3PxpcESV0OnGago')
|
|
@ -0,0 +1,3 @@
|
|||
from .controllers import *
|
||||
from .fixed_duty import *
|
||||
from .feedforward_duty import *
|
|
@ -0,0 +1,48 @@
|
|||
from time import perf_counter
|
||||
|
||||
class Controller():
|
||||
def __init__(self,
|
||||
# Actions
|
||||
# Target value we want to reach, in what period ()
|
||||
target=None, reach_period_s=None,
|
||||
# Timeframe we need to wait before changing the
|
||||
# system output (means: how fast we can trigger
|
||||
# on/off states). Used to prevent breaking fridge or
|
||||
# other actuators.
|
||||
refractary_period_s=10,
|
||||
# Send notifications outside this range?
|
||||
warning_range=(None, None)):
|
||||
self.input = None
|
||||
self.target = target
|
||||
self.range = warning_range
|
||||
|
||||
self.reach_period_s = reach_period_s
|
||||
self.refractary_period_s = refractary_period_s
|
||||
|
||||
def set_reach_period(self, reach_period_s):
|
||||
self.reach_period_s = reach_period_s
|
||||
|
||||
def set_input(self, input):
|
||||
self.input = input
|
||||
|
||||
def set_input_label(self, label):
|
||||
self.input_label = label
|
||||
|
||||
def set_target(self, target):
|
||||
print('Setting target', target)
|
||||
self.target = target
|
||||
|
||||
def set_warning_range(self, target):
|
||||
self.target = target
|
||||
|
||||
def one_cycle(self, time=None):
|
||||
print("ERROR: This function must be implemented by the controller")
|
||||
|
||||
"Set the input, run `one_cycle' and return the required actuator action."
|
||||
def apply(self, input, time=perf_counter):
|
||||
# Set the current input
|
||||
self.set_input(input)
|
||||
# Call one cycle
|
||||
action = self.one_cycle(time())
|
||||
# TODO: Check refractary period?
|
||||
return action
|
|
@ -0,0 +1,6 @@
|
|||
from . import controllers
|
||||
|
||||
class FeedforwardDutyCycle(controllers.Controller):
|
||||
def one_cycle(self, time):
|
||||
print("FIXME: implement this!")
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
from . import controllers
|
||||
from scm import stringify
|
||||
from time import perf_counter
|
||||
|
||||
class FixedDutyCycle(controllers.Controller):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.start = kwargs.pop('start_time', perf_counter())
|
||||
self.period = kwargs.pop('period')
|
||||
self.set_duty(kwargs.pop('duty_perc'))
|
||||
super().__init__(*args, **kwargs)
|
||||
self.last_time = None
|
||||
|
||||
def set_duty(self, duty=None):
|
||||
# Updates the duty cycle to a point in time where the switch
|
||||
# must occur
|
||||
if duty is not None:
|
||||
self.duty_perc = duty
|
||||
self.duty = self.duty_perc/100 * self.period
|
||||
|
||||
def set_period(self, period):
|
||||
# Shift start point so that we know where in the new period we are.
|
||||
# Then, move period
|
||||
# "|-----X----------------|" punto % old_period
|
||||
# "|-----X----|-----------|" start = now - (punto % old_period)
|
||||
# "|----|X----|-----|-----|"
|
||||
self.start = self.last_time - (self.timepoint() % self.period)
|
||||
print(period, self.start)
|
||||
self.period = period
|
||||
self.set_duty()
|
||||
|
||||
def timepoint(self):
|
||||
return ((self.last_time - self.start) % self.period)
|
||||
|
||||
def one_cycle(self, time):
|
||||
self.last_time = time
|
||||
return (self.timepoint() < self.duty) if (
|
||||
self.input < self.target) else False
|
||||
|
||||
|
||||
def fixed_duty_wrapper(args):
|
||||
name, duty, period = stringify(args.car, quote=False), args.cdr.car, args.cdr.cdr.car
|
||||
print('name, duty, period', name, duty, period)
|
||||
return (name, FixedDutyCycle(
|
||||
target=None, # reach_period_s=60*60,
|
||||
duty_perc=duty*100, period=period,
|
||||
start_time=perf_counter()))
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,37 @@
|
|||
htmx.defineExtension('client-side-templates', {
|
||||
transformResponse : function(text, xhr, elt) {
|
||||
|
||||
var mustacheTemplate = htmx.closest(elt, "[mustache-template]");
|
||||
if (mustacheTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateId = mustacheTemplate.getAttribute('mustache-template');
|
||||
var template = htmx.find("#" + templateId);
|
||||
if (template) {
|
||||
return Mustache.render(template.innerHTML, data);
|
||||
} else {
|
||||
throw "Unknown mustache template: " + templateId;
|
||||
}
|
||||
}
|
||||
|
||||
var handlebarsTemplate = htmx.closest(elt, "[handlebars-template]");
|
||||
if (handlebarsTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateName = handlebarsTemplate.getAttribute('handlebars-template');
|
||||
return Handlebars.partials[templateName](data);
|
||||
}
|
||||
|
||||
var nunjucksTemplate = htmx.closest(elt, "[nunjucks-template]");
|
||||
if (nunjucksTemplate) {
|
||||
var data = JSON.parse(text);
|
||||
var templateName = nunjucksTemplate.getAttribute('nunjucks-template');
|
||||
var template = htmx.find('#' + templateName);
|
||||
if (template) {
|
||||
return nunjucks.renderString(template.innerHTML, data);
|
||||
} else {
|
||||
return nunjucks.render(templateName, data);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
});
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,772 @@
|
|||
(function (global, factory) {
|
||||
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
||||
typeof define === 'function' && define.amd ? define(factory) :
|
||||
(global = global || self, global.Mustache = factory());
|
||||
}(this, (function () { 'use strict';
|
||||
|
||||
/*!
|
||||
* mustache.js - Logic-less {{mustache}} templates with JavaScript
|
||||
* http://github.com/janl/mustache.js
|
||||
*/
|
||||
|
||||
var objectToString = Object.prototype.toString;
|
||||
var isArray = Array.isArray || function isArrayPolyfill (object) {
|
||||
return objectToString.call(object) === '[object Array]';
|
||||
};
|
||||
|
||||
function isFunction (object) {
|
||||
return typeof object === 'function';
|
||||
}
|
||||
|
||||
/**
|
||||
* More correct typeof string handling array
|
||||
* which normally returns typeof 'object'
|
||||
*/
|
||||
function typeStr (obj) {
|
||||
return isArray(obj) ? 'array' : typeof obj;
|
||||
}
|
||||
|
||||
function escapeRegExp (string) {
|
||||
return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Null safe way of checking whether or not an object,
|
||||
* including its prototype, has a given property
|
||||
*/
|
||||
function hasProperty (obj, propName) {
|
||||
return obj != null && typeof obj === 'object' && (propName in obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe way of detecting whether or not the given thing is a primitive and
|
||||
* whether it has the given property
|
||||
*/
|
||||
function primitiveHasOwnProperty (primitive, propName) {
|
||||
return (
|
||||
primitive != null
|
||||
&& typeof primitive !== 'object'
|
||||
&& primitive.hasOwnProperty
|
||||
&& primitive.hasOwnProperty(propName)
|
||||
);
|
||||
}
|
||||
|
||||
// Workaround for https://issues.apache.org/jira/browse/COUCHDB-577
|
||||
// See https://github.com/janl/mustache.js/issues/189
|
||||
var regExpTest = RegExp.prototype.test;
|
||||
function testRegExp (re, string) {
|
||||
return regExpTest.call(re, string);
|
||||
}
|
||||
|
||||
var nonSpaceRe = /\S/;
|
||||
function isWhitespace (string) {
|
||||
return !testRegExp(nonSpaceRe, string);
|
||||
}
|
||||
|
||||
var entityMap = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/',
|
||||
'`': '`',
|
||||
'=': '='
|
||||
};
|
||||
|
||||
function escapeHtml (string) {
|
||||
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
|
||||
return entityMap[s];
|
||||
});
|
||||
}
|
||||
|
||||
var whiteRe = /\s*/;
|
||||
var spaceRe = /\s+/;
|
||||
var equalsRe = /\s*=/;
|
||||
var curlyRe = /\s*\}/;
|
||||
var tagRe = /#|\^|\/|>|\{|&|=|!/;
|
||||
|
||||
/**
|
||||
* Breaks up the given `template` string into a tree of tokens. If the `tags`
|
||||
* argument is given here it must be an array with two string values: the
|
||||
* opening and closing tags used in the template (e.g. [ "<%", "%>" ]). Of
|
||||
* course, the default is to use mustaches (i.e. mustache.tags).
|
||||
*
|
||||
* A token is an array with at least 4 elements. The first element is the
|
||||
* mustache symbol that was used inside the tag, e.g. "#" or "&". If the tag
|
||||
* did not contain a symbol (i.e. {{myValue}}) this element is "name". For
|
||||
* all text that appears outside a symbol this element is "text".
|
||||
*
|
||||
* The second element of a token is its "value". For mustache tags this is
|
||||
* whatever else was inside the tag besides the opening symbol. For text tokens
|
||||
* this is the text itself.
|
||||
*
|
||||
* The third and fourth elements of the token are the start and end indices,
|
||||
* respectively, of the token in the original template.
|
||||
*
|
||||
* Tokens that are the root node of a subtree contain two more elements: 1) an
|
||||
* array of tokens in the subtree and 2) the index in the original template at
|
||||
* which the closing tag for that section begins.
|
||||
*
|
||||
* Tokens for partials also contain two more elements: 1) a string value of
|
||||
* indendation prior to that tag and 2) the index of that tag on that line -
|
||||
* eg a value of 2 indicates the partial is the third tag on this line.
|
||||
*/
|
||||
function parseTemplate (template, tags) {
|
||||
if (!template)
|
||||
return [];
|
||||
var lineHasNonSpace = false;
|
||||
var sections = []; // Stack to hold section tokens
|
||||
var tokens = []; // Buffer to hold the tokens
|
||||
var spaces = []; // Indices of whitespace tokens on the current line
|
||||
var hasTag = false; // Is there a {{tag}} on the current line?
|
||||
var nonSpace = false; // Is there a non-space char on the current line?
|
||||
var indentation = ''; // Tracks indentation for tags that use it
|
||||
var tagIndex = 0; // Stores a count of number of tags encountered on a line
|
||||
|
||||
// Strips all whitespace tokens array for the current line
|
||||
// if there was a {{#tag}} on it and otherwise only space.
|
||||
function stripSpace () {
|
||||
if (hasTag && !nonSpace) {
|
||||
while (spaces.length)
|
||||
delete tokens[spaces.pop()];
|
||||
} else {
|
||||
spaces = [];
|
||||
}
|
||||
|
||||
hasTag = false;
|
||||
nonSpace = false;
|
||||
}
|
||||
|
||||
var openingTagRe, closingTagRe, closingCurlyRe;
|
||||
function compileTags (tagsToCompile) {
|
||||
if (typeof tagsToCompile === 'string')
|
||||
tagsToCompile = tagsToCompile.split(spaceRe, 2);
|
||||
|
||||
if (!isArray(tagsToCompile) || tagsToCompile.length !== 2)
|
||||
throw new Error('Invalid tags: ' + tagsToCompile);
|
||||
|
||||
openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + '\\s*');
|
||||
closingTagRe = new RegExp('\\s*' + escapeRegExp(tagsToCompile[1]));
|
||||
closingCurlyRe = new RegExp('\\s*' + escapeRegExp('}' + tagsToCompile[1]));
|
||||
}
|
||||
|
||||
compileTags(tags || mustache.tags);
|
||||
|
||||
var scanner = new Scanner(template);
|
||||
|
||||
var start, type, value, chr, token, openSection;
|
||||
while (!scanner.eos()) {
|
||||
start = scanner.pos;
|
||||
|
||||
// Match any text between tags.
|
||||
value = scanner.scanUntil(openingTagRe);
|
||||
|
||||
if (value) {
|
||||
for (var i = 0, valueLength = value.length; i < valueLength; ++i) {
|
||||
chr = value.charAt(i);
|
||||
|
||||
if (isWhitespace(chr)) {
|
||||
spaces.push(tokens.length);
|
||||
indentation += chr;
|
||||
} else {
|
||||
nonSpace = true;
|
||||
lineHasNonSpace = true;
|
||||
indentation += ' ';
|
||||
}
|
||||
|
||||
tokens.push([ 'text', chr, start, start + 1 ]);
|
||||
start += 1;
|
||||
|
||||
// Check for whitespace on the current line.
|
||||
if (chr === '\n') {
|
||||
stripSpace();
|
||||
indentation = '';
|
||||
tagIndex = 0;
|
||||
lineHasNonSpace = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Match the opening tag.
|
||||
if (!scanner.scan(openingTagRe))
|
||||
break;
|
||||
|
||||
hasTag = true;
|
||||
|
||||
// Get the tag type.
|
||||
type = scanner.scan(tagRe) || 'name';
|
||||
scanner.scan(whiteRe);
|
||||
|
||||
// Get the tag value.
|
||||
if (type === '=') {
|
||||
value = scanner.scanUntil(equalsRe);
|
||||
scanner.scan(equalsRe);
|
||||
scanner.scanUntil(closingTagRe);
|
||||
} else if (type === '{') {
|
||||
value = scanner.scanUntil(closingCurlyRe);
|
||||
scanner.scan(curlyRe);
|
||||
scanner.scanUntil(closingTagRe);
|
||||
type = '&';
|
||||
} else {
|
||||
value = scanner.scanUntil(closingTagRe);
|
||||
}
|
||||
|
||||
// Match the closing tag.
|
||||
if (!scanner.scan(closingTagRe))
|
||||
throw new Error('Unclosed tag at ' + scanner.pos);
|
||||
|
||||
if (type == '>') {
|
||||
token = [ type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace ];
|
||||
} else {
|
||||
token = [ type, value, start, scanner.pos ];
|
||||
}
|
||||
tagIndex++;
|
||||
tokens.push(token);
|
||||
|
||||
if (type === '#' || type === '^') {
|
||||
sections.push(token);
|
||||
} else if (type === '/') {
|
||||
// Check section nesting.
|
||||
openSection = sections.pop();
|
||||
|
||||
if (!openSection)
|
||||
throw new Error('Unopened section "' + value + '" at ' + start);
|
||||
|
||||
if (openSection[1] !== value)
|
||||
throw new Error('Unclosed section "' + openSection[1] + '" at ' + start);
|
||||
} else if (type === 'name' || type === '{' || type === '&') {
|
||||
nonSpace = true;
|
||||
} else if (type === '=') {
|
||||
// Set the tags for the next time around.
|
||||
compileTags(value);
|
||||
}
|
||||
}
|
||||
|
||||
stripSpace();
|
||||
|
||||
// Make sure there are no open sections when we're done.
|
||||
openSection = sections.pop();
|
||||
|
||||
if (openSection)
|
||||
throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos);
|
||||
|
||||
return nestTokens(squashTokens(tokens));
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines the values of consecutive text tokens in the given `tokens` array
|
||||
* to a single token.
|
||||
*/
|
||||
function squashTokens (tokens) {
|
||||
var squashedTokens = [];
|
||||
|
||||
var token, lastToken;
|
||||
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
if (token) {
|
||||
if (token[0] === 'text' && lastToken && lastToken[0] === 'text') {
|
||||
lastToken[1] += token[1];
|
||||
lastToken[3] = token[3];
|
||||
} else {
|
||||
squashedTokens.push(token);
|
||||
lastToken = token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return squashedTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forms the given array of `tokens` into a nested tree structure where
|
||||
* tokens that represent a section have two additional items: 1) an array of
|
||||
* all tokens that appear in that section and 2) the index in the original
|
||||
* template that represents the end of that section.
|
||||
*/
|
||||
function nestTokens (tokens) {
|
||||
var nestedTokens = [];
|
||||
var collector = nestedTokens;
|
||||
var sections = [];
|
||||
|
||||
var token, section;
|
||||
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
|
||||
token = tokens[i];
|
||||
|
||||
switch (token[0]) {
|
||||
case '#':
|
||||
case '^':
|
||||
collector.push(token);
|
||||
sections.push(token);
|
||||
collector = token[4] = [];
|
||||
break;
|
||||
case '/':
|
||||
section = sections.pop();
|
||||
section[5] = token[2];
|
||||
collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens;
|
||||
break;
|
||||
default:
|
||||
collector.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
return nestedTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple string scanner that is used by the template parser to find
|
||||
* tokens in template strings.
|
||||
*/
|
||||
function Scanner (string) {
|
||||
this.string = string;
|
||||
this.tail = string;
|
||||
this.pos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the tail is empty (end of string).
|
||||
*/
|
||||
Scanner.prototype.eos = function eos () {
|
||||
return this.tail === '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Tries to match the given regular expression at the current position.
|
||||
* Returns the matched text if it can match, the empty string otherwise.
|
||||
*/
|
||||
Scanner.prototype.scan = function scan (re) {
|
||||
var match = this.tail.match(re);
|
||||
|
||||
if (!match || match.index !== 0)
|
||||
return '';
|
||||
|
||||
var string = match[0];
|
||||
|
||||
this.tail = this.tail.substring(string.length);
|
||||
this.pos += string.length;
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Skips all text until the given regular expression can be matched. Returns
|
||||
* the skipped string, which is the entire tail if no match can be made.
|
||||
*/
|
||||
Scanner.prototype.scanUntil = function scanUntil (re) {
|
||||
var index = this.tail.search(re), match;
|
||||
|
||||
switch (index) {
|
||||
case -1:
|
||||
match = this.tail;
|
||||
this.tail = '';
|
||||
break;
|
||||
case 0:
|
||||
match = '';
|
||||
break;
|
||||
default:
|
||||
match = this.tail.substring(0, index);
|
||||
this.tail = this.tail.substring(index);
|
||||
}
|
||||
|
||||
this.pos += match.length;
|
||||
|
||||
return match;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a rendering context by wrapping a view object and
|
||||
* maintaining a reference to the parent context.
|
||||
*/
|
||||
function Context (view, parentContext) {
|
||||
this.view = view;
|
||||
this.cache = { '.': this.view };
|
||||
this.parent = parentContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new context using the given view with this context
|
||||
* as the parent.
|
||||
*/
|
||||
Context.prototype.push = function push (view) {
|
||||
return new Context(view, this);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the value of the given name in this context, traversing
|
||||
* up the context hierarchy if the value is absent in this context's view.
|
||||
*/
|
||||
Context.prototype.lookup = function lookup (name) {
|
||||
var cache = this.cache;
|
||||
|
||||
var value;
|
||||
if (cache.hasOwnProperty(name)) {
|
||||
value = cache[name];
|
||||
} else {
|
||||
var context = this, intermediateValue, names, index, lookupHit = false;
|
||||
|
||||
while (context) {
|
||||
if (name.indexOf('.') > 0) {
|
||||
intermediateValue = context.view;
|
||||
names = name.split('.');
|
||||
index = 0;
|
||||
|
||||
/**
|
||||
* Using the dot notion path in `name`, we descend through the
|
||||
* nested objects.
|
||||
*
|
||||
* To be certain that the lookup has been successful, we have to
|
||||
* check if the last object in the path actually has the property
|
||||
* we are looking for. We store the result in `lookupHit`.
|
||||
*
|
||||
* This is specially necessary for when the value has been set to
|
||||
* `undefined` and we want to avoid looking up parent contexts.
|
||||
*
|
||||
* In the case where dot notation is used, we consider the lookup
|
||||
* to be successful even if the last "object" in the path is
|
||||
* not actually an object but a primitive (e.g., a string, or an
|
||||
* integer), because it is sometimes useful to access a property
|
||||
* of an autoboxed primitive, such as the length of a string.
|
||||
**/
|
||||
while (intermediateValue != null && index < names.length) {
|
||||
if (index === names.length - 1)
|
||||
lookupHit = (
|
||||
hasProperty(intermediateValue, names[index])
|
||||
|| primitiveHasOwnProperty(intermediateValue, names[index])
|
||||
);
|
||||
|
||||
intermediateValue = intermediateValue[names[index++]];
|
||||
}
|
||||
} else {
|
||||
intermediateValue = context.view[name];
|
||||
|
||||
/**
|
||||
* Only checking against `hasProperty`, which always returns `false` if
|
||||
* `context.view` is not an object. Deliberately omitting the check
|
||||
* against `primitiveHasOwnProperty` if dot notation is not used.
|
||||
*
|
||||
* Consider this example:
|
||||
* ```
|
||||
* Mustache.render("The length of a football field is {{#length}}{{length}}{{/length}}.", {length: "100 yards"})
|
||||
* ```
|
||||
*
|
||||
* If we were to check also against `primitiveHasOwnProperty`, as we do
|
||||
* in the dot notation case, then render call would return:
|
||||
*
|
||||
* "The length of a football field is 9."
|
||||
*
|
||||
* rather than the expected:
|
||||
*
|
||||
* "The length of a football field is 100 yards."
|
||||
**/
|
||||
lookupHit = hasProperty(context.view, name);
|
||||
}
|
||||
|
||||
if (lookupHit) {
|
||||
value = intermediateValue;
|
||||
break;
|
||||
}
|
||||
|
||||
context = context.parent;
|
||||
}
|
||||
|
||||
cache[name] = value;
|
||||
}
|
||||
|
||||
if (isFunction(value))
|
||||
value = value.call(this.view);
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
/**
|
||||
* A Writer knows how to take a stream of tokens and render them to a
|
||||
* string, given a context. It also maintains a cache of templates to
|
||||
* avoid the need to parse the same template twice.
|
||||
*/
|
||||
function Writer () {
|
||||
this.templateCache = {
|
||||
_cache: {},
|
||||
set: function set (key, value) {
|
||||
this._cache[key] = value;
|
||||
},
|
||||
get: function get (key) {
|
||||
return this._cache[key];
|
||||
},
|
||||
clear: function clear () {
|
||||
this._cache = {};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all cached templates in this writer.
|
||||
*/
|
||||
Writer.prototype.clearCache = function clearCache () {
|
||||
if (typeof this.templateCache !== 'undefined') {
|
||||
this.templateCache.clear();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and caches the given `template` according to the given `tags` or
|
||||
* `mustache.tags` if `tags` is omitted, and returns the array of tokens
|
||||
* that is generated from the parse.
|
||||
*/
|
||||
Writer.prototype.parse = function parse (template, tags) {
|
||||
var cache = this.templateCache;
|
||||
var cacheKey = template + ':' + (tags || mustache.tags).join(':');
|
||||
var isCacheEnabled = typeof cache !== 'undefined';
|
||||
var tokens = isCacheEnabled ? cache.get(cacheKey) : undefined;
|
||||
|
||||
if (tokens == undefined) {
|
||||
tokens = parseTemplate(template, tags);
|
||||
isCacheEnabled && cache.set(cacheKey, tokens);
|
||||
}
|
||||
return tokens;
|
||||
};
|
||||
|
||||
/**
|
||||
* High-level method that is used to render the given `template` with
|
||||
* the given `view`.
|
||||
*
|
||||
* The optional `partials` argument may be an object that contains the
|
||||
* names and templates of partials that are used in the template. It may
|
||||
* also be a function that is used to load partial templates on the fly
|
||||
* that takes a single argument: the name of the partial.
|
||||
*
|
||||
* If the optional `config` argument is given here, then it should be an
|
||||
* object with a `tags` attribute or an `escape` attribute or both.
|
||||
* If an array is passed, then it will be interpreted the same way as
|
||||
* a `tags` attribute on a `config` object.
|
||||
*
|
||||
* The `tags` attribute of a `config` object must be an array with two
|
||||
* string values: the opening and closing tags used in the template (e.g.
|
||||
* [ "<%", "%>" ]). The default is to mustache.tags.
|
||||
*
|
||||
* The `escape` attribute of a `config` object must be a function which
|
||||
* accepts a string as input and outputs a safely escaped string.
|
||||
* If an `escape` function is not provided, then an HTML-safe string
|
||||
* escaping function is used as the default.
|
||||
*/
|
||||
Writer.prototype.render = function render (template, view, partials, config) {
|
||||
var tags = this.getConfigTags(config);
|
||||
var tokens = this.parse(template, tags);
|
||||
var context = (view instanceof Context) ? view : new Context(view, undefined);
|
||||
return this.renderTokens(tokens, context, partials, template, config);
|
||||
};
|
||||
|
||||
/**
|
||||
* Low-level method that renders the given array of `tokens` using
|
||||
* the given `context` and `partials`.
|
||||
*
|
||||
* Note: The `originalTemplate` is only ever used to extract the portion
|
||||
* of the original template that was contained in a higher-order section.
|
||||
* If the template doesn't use higher-order sections, this argument may
|
||||
* be omitted.
|
||||
*/
|
||||
Writer.prototype.renderTokens = function renderTokens (tokens, context, partials, originalTemplate, config) {
|
||||
var buffer = '';
|
||||
|
||||
var token, symbol, value;
|
||||
for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) {
|
||||
value = undefined;
|
||||
token = tokens[i];
|
||||
symbol = token[0];
|
||||
|
||||
if (symbol === '#') value = this.renderSection(token, context, partials, originalTemplate, config);
|
||||
else if (symbol === '^') value = this.renderInverted(token, context, partials, originalTemplate, config);
|
||||
else if (symbol === '>') value = this.renderPartial(token, context, partials, config);
|
||||
else if (symbol === '&') value = this.unescapedValue(token, context);
|
||||
else if (symbol === 'name') value = this.escapedValue(token, context, config);
|
||||
else if (symbol === 'text') value = this.rawValue(token);
|
||||
|
||||
if (value !== undefined)
|
||||
buffer += value;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
};
|
||||
|
||||
Writer.prototype.renderSection = function renderSection (token, context, partials, originalTemplate, config) {
|
||||
var self = this;
|
||||
var buffer = '';
|
||||
var value = context.lookup(token[1]);
|
||||
|
||||
// This function is used to render an arbitrary template
|
||||
// in the current context by higher-order sections.
|
||||
function subRender (template) {
|
||||
return self.render(template, context, partials, config);
|
||||
}
|
||||
|
||||
if (!value) return;
|
||||
|
||||
if (isArray(value)) {
|
||||
for (var j = 0, valueLength = value.length; j < valueLength; ++j) {
|
||||
buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config);
|
||||
}
|
||||
} else if (typeof value === 'object' || typeof value === 'string' || typeof value === 'number') {
|
||||
buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config);
|
||||
} else if (isFunction(value)) {
|
||||
if (typeof originalTemplate !== 'string')
|
||||
throw new Error('Cannot use higher-order sections without the original template');
|
||||
|
||||
// Extract the portion of the original template that the section contains.
|
||||
value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender);
|
||||
|
||||
if (value != null)
|
||||
buffer += value;
|
||||
} else {
|
||||
buffer += this.renderTokens(token[4], context, partials, originalTemplate, config);
|
||||
}
|
||||
return buffer;
|
||||
};
|
||||
|
||||
Writer.prototype.renderInverted = function renderInverted (token, context, partials, originalTemplate, config) {
|
||||
var value = context.lookup(token[1]);
|
||||
|
||||
// Use JavaScript's definition of falsy. Include empty arrays.
|
||||
// See https://github.com/janl/mustache.js/issues/186
|
||||
if (!value || (isArray(value) && value.length === 0))
|
||||
return this.renderTokens(token[4], context, partials, originalTemplate, config);
|
||||
};
|
||||
|
||||
Writer.prototype.indentPartial = function indentPartial (partial, indentation, lineHasNonSpace) {
|
||||
var filteredIndentation = indentation.replace(/[^ \t]/g, '');
|
||||
var partialByNl = partial.split('\n');
|
||||
for (var i = 0; i < partialByNl.length; i++) {
|
||||
if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) {
|
||||
partialByNl[i] = filteredIndentation + partialByNl[i];
|
||||
}
|
||||
}
|
||||
return partialByNl.join('\n');
|
||||
};
|
||||
|
||||
Writer.prototype.renderPartial = function renderPartial (token, context, partials, config) {
|
||||
if (!partials) return;
|
||||
var tags = this.getConfigTags(config);
|
||||
|
||||
var value = isFunction(partials) ? partials(token[1]) : partials[token[1]];
|
||||
if (value != null) {
|
||||
var lineHasNonSpace = token[6];
|
||||
var tagIndex = token[5];
|
||||
var indentation = token[4];
|
||||
var indentedValue = value;
|
||||
if (tagIndex == 0 && indentation) {
|
||||
indentedValue = this.indentPartial(value, indentation, lineHasNonSpace);
|
||||
}
|
||||
var tokens = this.parse(indentedValue, tags);
|
||||
return this.renderTokens(tokens, context, partials, indentedValue, config);
|
||||
}
|
||||
};
|
||||
|
||||
Writer.prototype.unescapedValue = function unescapedValue (token, context) {
|
||||
var value = context.lookup(token[1]);
|
||||
if (value != null)
|
||||
return value;
|
||||
};
|
||||
|
||||
Writer.prototype.escapedValue = function escapedValue (token, context, config) {
|
||||
var escape = this.getConfigEscape(config) || mustache.escape;
|
||||
var value = context.lookup(token[1]);
|
||||
if (value != null)
|
||||
return (typeof value === 'number' && escape === mustache.escape) ? String(value) : escape(value);
|
||||
};
|
||||
|
||||
Writer.prototype.rawValue = function rawValue (token) {
|
||||
return token[1];
|
||||
};
|
||||
|
||||
Writer.prototype.getConfigTags = function getConfigTags (config) {
|
||||
if (isArray(config)) {
|
||||
return config;
|
||||
}
|
||||
else if (config && typeof config === 'object') {
|
||||
return config.tags;
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
Writer.prototype.getConfigEscape = function getConfigEscape (config) {
|
||||
if (config && typeof config === 'object' && !isArray(config)) {
|
||||
return config.escape;
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
var mustache = {
|
||||
name: 'mustache.js',
|
||||
version: '4.2.0',
|
||||
tags: [ '{{', '}}' ],
|
||||
clearCache: undefined,
|
||||
escape: undefined,
|
||||
parse: undefined,
|
||||
render: undefined,
|
||||
Scanner: undefined,
|
||||
Context: undefined,
|
||||
Writer: undefined,
|
||||
/**
|
||||
* Allows a user to override the default caching strategy, by providing an
|
||||
* object with set, get and clear methods. This can also be used to disable
|
||||
* the cache by setting it to the literal `undefined`.
|
||||
*/
|
||||
set templateCache (cache) {
|
||||
defaultWriter.templateCache = cache;
|
||||
},
|
||||
/**
|
||||
* Gets the default or overridden caching object from the default writer.
|
||||
*/
|
||||
get templateCache () {
|
||||
return defaultWriter.templateCache;
|
||||
}
|
||||
};
|
||||
|
||||
// All high-level mustache.* functions use this writer.
|
||||
var defaultWriter = new Writer();
|
||||
|
||||
/**
|
||||
* Clears all cached templates in the default writer.
|
||||
*/
|
||||
mustache.clearCache = function clearCache () {
|
||||
return defaultWriter.clearCache();
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses and caches the given template in the default writer and returns the
|
||||
* array of tokens it contains. Doing this ahead of time avoids the need to
|
||||
* parse templates on the fly as they are rendered.
|
||||
*/
|
||||
mustache.parse = function parse (template, tags) {
|
||||
return defaultWriter.parse(template, tags);
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the `template` with the given `view`, `partials`, and `config`
|
||||
* using the default writer.
|
||||
*/
|
||||
mustache.render = function render (template, view, partials, config) {
|
||||
if (typeof template !== 'string') {
|
||||
throw new TypeError('Invalid template! Template should be a "string" ' +
|
||||
'but "' + typeStr(template) + '" was given as the first ' +
|
||||
'argument for mustache#render(template, view, partials)');
|
||||
}
|
||||
|
||||
return defaultWriter.render(template, view, partials, config);
|
||||
};
|
||||
|
||||
// Export the escaping function so that the user may override it.
|
||||
// See https://github.com/janl/mustache.js/issues/244
|
||||
mustache.escape = escapeHtml;
|
||||
|
||||
// Export these mainly for testing, but also for advanced usage.
|
||||
mustache.Scanner = Scanner;
|
||||
mustache.Context = Context;
|
||||
mustache.Writer = Writer;
|
||||
|
||||
return mustache;
|
||||
|
||||
})));
|
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,302 @@
|
|||
var state = {};
|
||||
var localstate = {
|
||||
manual: {}, recipe: 0, page: ['General', 'Dashboard'],
|
||||
maxpoints: 1000
|
||||
};
|
||||
var menu_items = [
|
||||
{name: "General",
|
||||
subitems:[
|
||||
{name: "Dashboard"},
|
||||
{name: "Sensors & Plots"}
|
||||
]
|
||||
},
|
||||
{name: "Recipes",
|
||||
subitems:[
|
||||
{name: "Recipes Editor", link: 'recipe-editor'},
|
||||
{name: "New Recipe"}
|
||||
]
|
||||
},
|
||||
{name: "Configuration",
|
||||
subitems:[
|
||||
{name: "Settings"}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
function applyState(newstate) {
|
||||
if (newstate !== undefined) {
|
||||
state = JSON.parse(JSON.stringify(newstate));
|
||||
}
|
||||
// preprocess
|
||||
state.manual["dismissed"] = localstate.manual["dismissed"];
|
||||
let selected_recipe = null;
|
||||
for (let i = 0; i < state.recipes.length; i++) {
|
||||
state.recipes[i].idx = i;
|
||||
if (localstate.recipe == i) selected_recipe = state.recipes[i];
|
||||
state.recipes[i].selected = localstate.recipe == i;
|
||||
}
|
||||
state.selected_recipe = JSON.parse(JSON.stringify(selected_recipe));
|
||||
for (let i = 0; i < menu_items.length; i++) {
|
||||
let group_match = menu_items[i].name == localstate.page[0];
|
||||
for (let sub = 0; sub < menu_items[i].subitems.length; sub++) {
|
||||
let sub_match = menu_items[i].subitems[sub].name == localstate.page[1];
|
||||
menu_items[i].subitems[sub].current = group_match && sub_match;
|
||||
menu_items[i].subitems[sub].path = JSON.stringify([
|
||||
menu_items[i].name, menu_items[i].subitems[sub].name]);
|
||||
}
|
||||
}
|
||||
state.menu_items = menu_items;
|
||||
|
||||
console.log('Apply state '+JSON.stringify(state));
|
||||
// apply
|
||||
render_sidebar(state);
|
||||
manual_modal(state.manual);
|
||||
current_recipe(state.recipe);
|
||||
render_load_recipe(state);
|
||||
render_plot();
|
||||
}
|
||||
|
||||
var enable_draw = true;
|
||||
var plot_data = {};
|
||||
|
||||
function add_plot_data(newpoints, render=true) {
|
||||
for (var i = 0; i < newpoints.length; ++i) {
|
||||
let point = newpoints[i]
|
||||
let key = point[0];
|
||||
if (!(key in plot_data)) {
|
||||
plot_data[key] = {
|
||||
x: [],
|
||||
y: [],
|
||||
type: 'scatter',
|
||||
name: key
|
||||
}
|
||||
}
|
||||
plot_data[key].x.push(point[1]);
|
||||
plot_data[key].y.push(point[2]);
|
||||
if (plot_data[key].x.length > localstate.maxpoints) {
|
||||
plot_data[key].x.shift();
|
||||
plot_data[key].y.shift();
|
||||
}
|
||||
}
|
||||
if (render) render_plot();
|
||||
}
|
||||
|
||||
function render_plot() {
|
||||
if (enable_draw && document.getElementById("data-plot") !== null) {
|
||||
Plotly.newPlot('data-plot', Object.values(plot_data));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function render_sensors(sensordata) {
|
||||
let html = document.getElementById("sensor-list");
|
||||
if (state.sensors === undefined || html === null) return;
|
||||
let template = `<div class="content">
|
||||
{{#sensors}}
|
||||
<h4>{{what}}</h4>
|
||||
{{^data}}No sensors found{{/data}}
|
||||
<ul>
|
||||
{{#data}}
|
||||
<li ondblclick="alert('rename')">{{0}} -- {{1}} {{unit}}</li>
|
||||
{{/data}}
|
||||
</ul>
|
||||
{{/sensors}}
|
||||
</div>`;
|
||||
let data = [];
|
||||
for (let i = 0, k = Object.keys(state.sensors.units); i < k.length; ++i) {
|
||||
let what = k[i];
|
||||
let unit = state.sensors.units[k[i]];
|
||||
let sensors = Object.keys(
|
||||
Object.keys(state.sensors.found).reduce(function (filtered, key) {
|
||||
if (state.sensors.found[key] == what) filtered[key] = state.sensors.found[key];
|
||||
return filtered;
|
||||
}, {}));
|
||||
let req_data = [];
|
||||
for (let j = 0; j < sensordata.length; ++j) {
|
||||
if (sensors.includes(sensordata[j][0])) {
|
||||
req_data.push([sensordata[j][0], sensordata[j][2]]);
|
||||
}
|
||||
}
|
||||
data.push({what: what, unit: unit, data: req_data});
|
||||
}
|
||||
console.log({sensors:data});
|
||||
html.innerHTML = Mustache.render(template, {sensors:data});
|
||||
}
|
||||
|
||||
function navigate(path, link) {
|
||||
console.log(link);
|
||||
localstate.page = path;
|
||||
applyState();
|
||||
}
|
||||
|
||||
function render_sidebar(data) {
|
||||
let html = document.getElementById("sidebar");
|
||||
let template = `<aside class="menu">
|
||||
{{#menu_items}}
|
||||
<p class="menu-label">{{name}}</p>
|
||||
<ul class="menu-list">
|
||||
{{#subitems}}
|
||||
<li><a onclick="navigate({{path}}, {{link}})"
|
||||
class="{{#current}}is-active{{/current}}">{{name}}</a></li>
|
||||
{{/subitems}}
|
||||
</ul>
|
||||
{{/menu_items}}
|
||||
</aside>`
|
||||
html.innerHTML = Mustache.render(template, data);
|
||||
}
|
||||
|
||||
function respond_manual(response) {
|
||||
socket.emit('manual response', response);
|
||||
}
|
||||
|
||||
function select_recipe(idx) {
|
||||
localstate.recipe = idx;
|
||||
applyState();
|
||||
}
|
||||
|
||||
function load_recipe() {
|
||||
socket.emit('load recipe idx', localstate.recipe);
|
||||
}
|
||||
|
||||
function dismiss_modal() {
|
||||
applyState(state);
|
||||
}
|
||||
|
||||
function render_load_recipe(data) {
|
||||
let html = document.getElementById("load-recipe");
|
||||
if (html === null) return;
|
||||
let template = `<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Load Recipe
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="tabs is-centered">
|
||||
<ul>
|
||||
{{#recipes}}
|
||||
<li class="{{#selected}}is-active{{/selected}}">
|
||||
<a onclick="select_recipe({{idx}})">{{name}}</a>
|
||||
</li>
|
||||
{{/recipes}}
|
||||
</ul>
|
||||
</div>
|
||||
{{#selected_recipe}}
|
||||
{{description}}
|
||||
{{#phases}}
|
||||
<br><br>
|
||||
<div class="content"><h4>{{name}}</h4></div>
|
||||
{{text}}<br>
|
||||
Ranges: FIXME<br>
|
||||
On Load: {{onload}}<br>
|
||||
Exit When: {{nextcond}}<br>
|
||||
On Exit: {{onexit}}<br>
|
||||
{{/phases}}
|
||||
{{/selected_recipe}}
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a onclick="load_recipe()"
|
||||
class="card-footer-item is-primary">Load</a>
|
||||
<a class="card-footer-item">Edit</a>
|
||||
</footer>
|
||||
</div>`
|
||||
html.innerHTML = Mustache.render(template, data);
|
||||
}
|
||||
|
||||
function current_recipe(data) {
|
||||
let html = document.getElementById("current-recipe");
|
||||
if (html === null) return;
|
||||
if (data === null) {
|
||||
html.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
let template = `
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Active Recipe: {{name}}
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
{{description}}
|
||||
<div class="tabs is-centered">
|
||||
<ul>
|
||||
{{#phases}}
|
||||
<li class="{{#current}}is-active{{/current}}"><a>{{name}}</a></li>
|
||||
{{/phases}}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="card-footer">
|
||||
<a class="card-footer-item is-primary">Pause</a>
|
||||
<a class="card-footer-item">Stop</a>
|
||||
</footer>
|
||||
</div>`;
|
||||
html.innerHTML = Mustache.render(template, data);
|
||||
}
|
||||
|
||||
function manual_modal(data) {
|
||||
let modal = document.getElementById("manual-modal");
|
||||
let template = `<div class="modal {{^dismissed}}{{#required}}is-active{{/required}}{{/dismissed}}">
|
||||
<div class="modal-background"></div>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">Manual Intervention Required</p>
|
||||
<button class="delete" aria-label="close" onclick="dismiss_modal()"></button>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
{{label}}
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
{{#options}}
|
||||
<button class="button" onclick="respond_manual('{{value}}')">
|
||||
{{tag}}
|
||||
</button>
|
||||
{{/options}}
|
||||
</footer>
|
||||
</div>
|
||||
</div>`;
|
||||
modal.innerHTML = Mustache.render(template, data);
|
||||
}
|
||||
|
||||
|
||||
let items = document.querySelectorAll('#items-list > li')
|
||||
|
||||
|
||||
items.forEach(item => {
|
||||
$(item).prop('draggable', true)
|
||||
item.addEventListener('dragstart', dragStart)
|
||||
item.addEventListener('drop', dropped)
|
||||
item.addEventListener('dragenter', cancelDefault)
|
||||
item.addEventListener('dragover', cancelDefault)
|
||||
})
|
||||
|
||||
function dragStart (e) {
|
||||
var index = $(e.target).index()
|
||||
e.dataTransfer.setData('text/plain', index)
|
||||
}
|
||||
|
||||
function dropped (e) {
|
||||
cancelDefault(e)
|
||||
|
||||
// get new and old index
|
||||
let oldIndex = e.dataTransfer.getData('text/plain')
|
||||
let target = $(e.target)
|
||||
let newIndex = target.index()
|
||||
|
||||
// remove dropped items at old place
|
||||
let dropped = $(this).parent().children().eq(oldIndex).remove()
|
||||
|
||||
// insert the dropped items at new place
|
||||
if (newIndex < oldIndex) {
|
||||
target.before(dropped)
|
||||
} else {
|
||||
target.after(dropped)
|
||||
}
|
||||
}
|
||||
|
||||
function cancelDefault (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return false
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,25 @@
|
|||
[draggable="true"] {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
ul.moveable {
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
|
||||
li {
|
||||
list-style-image: none;
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
cursor: move;
|
||||
|
||||
&:hover {
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,35 @@
|
|||
"Supposed to be a singleton"
|
||||
class ManualIntervention():
|
||||
def __init__(self):
|
||||
self.intervention_happened = False
|
||||
self.intervention_required = False
|
||||
self.response = None
|
||||
self.number_of_requests = 0
|
||||
self.options = (('Ok', True),)
|
||||
self.label = 'È richiesto un intervento manuale'
|
||||
|
||||
def set(self, response):
|
||||
self.response = response
|
||||
self.intervention_happened = True
|
||||
return True
|
||||
|
||||
def get(self, label=None, options=None):
|
||||
update = False
|
||||
if not self.intervention_required:
|
||||
update = True
|
||||
self.intervention_required = True
|
||||
self.label = label or self.label
|
||||
self.options = options or self.options
|
||||
self.number_of_requests += 1
|
||||
return update, self.intervention_happened and self.response
|
||||
|
||||
def clear(self):
|
||||
self.__init__()
|
||||
return True
|
||||
|
||||
def getState(self):
|
||||
return {
|
||||
'label': self.label,
|
||||
'required': self.intervention_required,
|
||||
'options': tuple({'tag':o[0], 'value':o[1]} for o in self.options)
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
# Simple Finite State Machine used to control states
|
||||
import json
|
||||
from scm import eval, _, GLOBAL_ENV, stringify, load
|
||||
from time import perf_counter
|
||||
from chat import chat
|
||||
from persona import ManualIntervention
|
||||
from sensors import get_sensor_value
|
||||
from control import fixed_duty_wrapper
|
||||
from actuators import actuators
|
||||
|
||||
MY_GLOBAL_ENV = (
|
||||
_('make-duty-controller', 3, fixed_duty_wrapper,
|
||||
GLOBAL_ENV))
|
||||
|
||||
def safe_eval(data, env):
|
||||
try:
|
||||
return eval(data, env)
|
||||
except Exception as e:
|
||||
err = ' '.join(('evaluating:', data, ':', str(e)))
|
||||
chat.send_sync(err)
|
||||
return True
|
||||
|
||||
def load_recipe(json):
|
||||
def load_phase(json):
|
||||
def fallback(v):
|
||||
return json.get(v, '#t')
|
||||
return Phase(
|
||||
json['name'], json['description'],
|
||||
onexit=fallback('on_exit'),
|
||||
onload=fallback('on_load'),
|
||||
nextcond=fallback('exit_condition'))
|
||||
return Recipe(
|
||||
json['name'], json['variant'], json['description'],
|
||||
json['controllers'],
|
||||
tuple(load_phase(p) for p in json['phases']))
|
||||
|
||||
def load_recipes(file):
|
||||
with open(file, "r") as f:
|
||||
recipes = json.loads(f.read())
|
||||
return tuple(load_recipe(recipe) for recipe in recipes)
|
||||
|
||||
def manual_intervention_wrapper(state):
|
||||
def wrapper(args):
|
||||
label = None
|
||||
options = None
|
||||
if len(args) > 0:
|
||||
label = stringify(args.car, quote=False)
|
||||
if len(args) > 1:
|
||||
options = tuple(
|
||||
(stringify(o.car, quote=False), o.cdr.car)
|
||||
for o in args.cdr.car)
|
||||
resp = state.manual.get(label, options)
|
||||
if resp[0]:
|
||||
print('REQUESTING CLIENT UPDATE')
|
||||
state.onupdate()
|
||||
return resp[1]
|
||||
return wrapper
|
||||
|
||||
|
||||
class State():
|
||||
def __init__(self, recipe_file, onupdate=lambda: None):
|
||||
self.recipes = load_recipes(recipe_file)
|
||||
self.recipe = None
|
||||
self.phase = None
|
||||
self.manual = ManualIntervention()
|
||||
self.baseenv = (
|
||||
_('get-sensor', 1, lambda x: get_sensor_value(stringify(x.car, quote=False)),
|
||||
_('notify', 1, lambda x: chat.send_sync(stringify(x.car, quote=False)),
|
||||
_('hours', 1, lambda x: x.car * 60 * 60,
|
||||
_('minutes', 1, lambda x: x.car * 60,
|
||||
_('seconds', 1, lambda x: x.car,
|
||||
_('manual-intervention', -1, manual_intervention_wrapper(self),
|
||||
_('set-target', 2, self.set_target,
|
||||
_('set-controller', 2, self.set_controller,
|
||||
MY_GLOBAL_ENV)))))))))
|
||||
self.env = self.baseenv
|
||||
self.envdata = {}
|
||||
self.onupdate = onupdate
|
||||
|
||||
def getState(self):
|
||||
return {
|
||||
'phase': self.phase
|
||||
}
|
||||
|
||||
def now(self):
|
||||
return perf_counter()
|
||||
|
||||
def postload(self):
|
||||
if self.recipe is None:
|
||||
return
|
||||
self.phase = 0
|
||||
self.env = (
|
||||
_('recipe-name', 0, lambda x: self.recipe.name,
|
||||
_('recipe-description', 0, lambda x: self.recipe.description,
|
||||
_('time-in-this-phase', 0, lambda x: self.phase_time(),
|
||||
self.baseenv))))
|
||||
self.envdata['controllers'] = {}
|
||||
for controller in self.recipe.controllers:
|
||||
name, ctrl = safe_eval(controller, self.env)
|
||||
self.envdata['controllers'][name] = ctrl
|
||||
self.envdata['sensors'] = {}
|
||||
self.loadphase(self.phase)
|
||||
|
||||
def loadByIdx(self, recipe):
|
||||
self.recipe = self.recipes[recipe]
|
||||
self.postload()
|
||||
|
||||
def loadByName(self, recipe):
|
||||
r = tuple(i for (i, r) in enumerate(self.recipes) if r.name == recipe)
|
||||
if len(r) > 0:
|
||||
self.recipe = self.recipes[r[0]]
|
||||
self.postload()
|
||||
|
||||
def current_phase(self):
|
||||
if self.recipe is None:
|
||||
return None
|
||||
if self.phase >= len(self.recipe.phases) or self.phase < 0:
|
||||
return None
|
||||
return self.recipe.phases[self.phase]
|
||||
|
||||
def check(self):
|
||||
return self.current_phase().satisfied_p(self.env)
|
||||
|
||||
def run_step(self):
|
||||
# Update sensor values
|
||||
for sensor in self.envdata.get('sensors', ()):
|
||||
_, ctrl, target = self.envdata['sensors'][sensor]
|
||||
self.envdata['sensors'][sensor] = (get_sensor_value(sensor), ctrl, target)
|
||||
# Apply actuators
|
||||
for controller in self.envdata.get('controllers', {}).keys():
|
||||
ctrl = self.envdata['controllers'][controller]
|
||||
response = ctrl.apply(self.envdata['sensors'][ctrl.input_label][0])
|
||||
if self.check():
|
||||
if self.next() is None:
|
||||
return True
|
||||
print(f'Waiting for {self.current_phase().nextcond}')
|
||||
return False
|
||||
|
||||
def next(self):
|
||||
self.current_phase().exit(self.env)
|
||||
if self.phase < len(self.recipe.phases) - 1:
|
||||
self.phase += 1
|
||||
phase = self.recipe.phases[self.phase]
|
||||
self.loadphase(self.phase)
|
||||
self.manual.clear()
|
||||
return phase
|
||||
self.onupdate()
|
||||
return None
|
||||
|
||||
def phase_time(self, arg=None):
|
||||
return (self.now() - self.envdata['current-phase-loadtime'])
|
||||
|
||||
def loadphase(self, phaseidx):
|
||||
phase = self.recipe.phases[phaseidx]
|
||||
self.envdata['current-phase-loadtime'] = self.now()
|
||||
resp = safe_eval(phase.onload, self.env)
|
||||
self.onupdate()
|
||||
return resp
|
||||
|
||||
def done(self):
|
||||
self.recipe.done(self.env)
|
||||
self.recipe = None
|
||||
self.env = self.baseenv
|
||||
self.envdata = {}
|
||||
self.onupdate()
|
||||
|
||||
def set_target(self, args):
|
||||
sensor, target = (stringify(args.car, quote=False), args.cdr.car)
|
||||
value, controller, _ = self.envdata['sensors'][sensor]
|
||||
self.envdata['sensors'][sensor] = (value, controller, target)
|
||||
self.envdata['controllers'][controller].set_target(target)
|
||||
print('setting sensor', sensor, controller, 'to target', target)
|
||||
|
||||
def set_controller(self, args):
|
||||
controller, sensor = (stringify(args.car, quote=False),
|
||||
stringify(args.cdr.car, quote=False))
|
||||
self.envdata['controllers'][controller].set_input_label(sensor)
|
||||
# Value/Controller/Target
|
||||
self.envdata['sensors'][sensor] = (get_sensor_value(sensor), controller, None)
|
||||
print('linking sensor', sensor, 'to controller', controller,
|
||||
'(', self.envdata['controllers'][controller], ')')
|
||||
|
||||
class Recipe():
|
||||
def __init__(self, name, variant='default', description='',
|
||||
controllers=(),
|
||||
phases=()):
|
||||
self.name = name
|
||||
self.controllers = controllers
|
||||
self.description = description
|
||||
self.phases = phases
|
||||
|
||||
def getState(self, currentphase=None):
|
||||
def phasetodict(i, p):
|
||||
return {
|
||||
'name': p.name,
|
||||
'text': p.text,
|
||||
'nextcond': p.nextcond,
|
||||
'current': currentphase == i,
|
||||
# 'loaded_time': p.loaded_time,
|
||||
'onload': p.onload,
|
||||
'onexit': p.onexit,
|
||||
# 'envdata': p.envdata
|
||||
}
|
||||
|
||||
return {
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'phases': [
|
||||
phasetodict(i, p)
|
||||
for (i, p) in enumerate(self.phases)
|
||||
],
|
||||
# 'phase': self.phase
|
||||
}
|
||||
|
||||
def done(self, env):
|
||||
safe_eval('(notify (concat "Recipe " (recipe-name) " completed at " (now)))',
|
||||
env)
|
||||
|
||||
class Phase():
|
||||
def __init__(self, name, text='', nextcond='#t', onload='', onexit=''):
|
||||
self.name = name
|
||||
self.text = text
|
||||
self.nextcond = nextcond
|
||||
self.onload = onload
|
||||
self.onexit = onexit
|
||||
# self.loaded_time = None
|
||||
# self.env = env
|
||||
# self.envdata = {}
|
||||
|
||||
def satisfied_p(self, env):
|
||||
return safe_eval(self.nextcond, env)
|
||||
|
||||
def exit(self, env):
|
||||
return safe_eval(self.onexit, env)
|
||||
|
||||
def revert(self, env):
|
||||
# Undo state changes (attuators values)
|
||||
pass
|
|
@ -0,0 +1,78 @@
|
|||
[
|
||||
{
|
||||
"name": "Yogurt",
|
||||
"variant": "default",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "Pastorizza lo yogurt a 82 gradi, lascia raffreddare per fare l'inoculo, mantiene a 42 gradi fino a che è pronto.",
|
||||
"phases": [
|
||||
{
|
||||
"name": "Preheat",
|
||||
"description": "Preheat yogurt to 82°",
|
||||
"on_load": "(begin (set-controller \"heater\" \"T_food\")\n(set-target \"T_food\" 82.0))",
|
||||
"exit_condition": "(and (>= (get-sensor \"T_food\") 81.5)\n(> (time-in-this-phase) (seconds 1)))"
|
||||
},
|
||||
{
|
||||
"name": "Cool",
|
||||
"description": "Cool to 46°",
|
||||
"exit_condition": "(and (< (get-sensor \"T_food\") 46.0)\n(> (get-sensor \"T_food\") 42.0))"
|
||||
},
|
||||
{
|
||||
"name": "Hold",
|
||||
"description": "Keep at 42° for 3h",
|
||||
"exit_condition": "(> (time-in-this-phase) (seconds 5))",
|
||||
"on_exit": "(notify \"You can remove the yogurt from the heater\")"
|
||||
},
|
||||
{
|
||||
"name": "Inoculate",
|
||||
"description": "Add 50ml Yogurt",
|
||||
"on_load": "(notify \"Inoculate 50g of yogurt NOW!\")",
|
||||
"exit_condition": "(manual-intervention \"Inocula 50g di yogurt\" '((\"Fatto\" #t)))",
|
||||
"on_exit": "(notify (concat \"Inoculated at \" (now)))"
|
||||
},
|
||||
{
|
||||
"name": "Hold",
|
||||
"description": "Keep at 42° for 3h",
|
||||
"exit_condition": "(> (time-in-this-phase) (seconds 3))",
|
||||
"on_exit": "(notify \"You can remove the yogurt from the heater\")"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Nattō",
|
||||
"variant": "default",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
},
|
||||
{
|
||||
"name": "Tempeh",
|
||||
"variant": "default",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
},
|
||||
{
|
||||
"name": "Koji Rice",
|
||||
"variant": "default",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
},
|
||||
{
|
||||
"name": "Gorgonzola",
|
||||
"variant": "default",
|
||||
"controllers": [
|
||||
"(make-duty-controller \"heater\" 0.20 120)"
|
||||
],
|
||||
"description": "FIXME",
|
||||
"phases": []
|
||||
}
|
||||
]
|
|
@ -0,0 +1,517 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
A Little Scheme in Python 2.7/3.8, v3.2 H31.01.13/R02.04.09 by SUZUKI Hisao
|
||||
"""
|
||||
from __future__ import print_function
|
||||
from sys import argv, exit
|
||||
try:
|
||||
from sys import intern # for Python 3
|
||||
raw_input = input # for Python 3
|
||||
long = int # for Python 3
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
class List (object):
|
||||
"Empty list"
|
||||
__slots__ = ()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(())
|
||||
|
||||
def __len__(self):
|
||||
n = 0
|
||||
for e in self:
|
||||
n += 1
|
||||
return n
|
||||
|
||||
NIL = List()
|
||||
|
||||
class Cell (List):
|
||||
"Cons cell"
|
||||
__slots__ = ('car', 'cdr')
|
||||
|
||||
def __init__(self, car, cdr):
|
||||
self.car, self.cdr = car, cdr
|
||||
|
||||
def __iter__(self):
|
||||
"Yield car, cadr, caddr and so on."
|
||||
j = self
|
||||
while isinstance(j, Cell):
|
||||
yield j.car
|
||||
j = j.cdr
|
||||
if j is not NIL:
|
||||
raise ImproperListError(j)
|
||||
|
||||
class ImproperListError (Exception):
|
||||
pass
|
||||
|
||||
QUOTE = intern('quote') # Use an interned string as a symbol.
|
||||
IF = intern('if')
|
||||
BEGIN = intern('begin')
|
||||
LAMBDA = intern('lambda')
|
||||
DEFINE = intern('define')
|
||||
DEFINE_RECORD_TYPE = intern('define-record-type')
|
||||
SETQ = intern('set!')
|
||||
APPLY = intern('apply')
|
||||
CALLCC = intern('call/cc')
|
||||
|
||||
NOCONT = () # NOCONT means there is no continuation.
|
||||
# Continuation operators
|
||||
THEN = intern('then')
|
||||
APPLY_FUN = intern('aplly-fun')
|
||||
EVAL_ARG = intern('eval-arg')
|
||||
CONS_ARGS = intern('cons-args')
|
||||
RESTORE_ENV = intern('restore-env')
|
||||
|
||||
class ApplyClass:
|
||||
def __str__(self):
|
||||
return '#<apply>'
|
||||
|
||||
class CallCcClass:
|
||||
def __str__(self):
|
||||
return '#<call/cc>'
|
||||
|
||||
APPLY_OBJ = ApplyClass()
|
||||
CALLCC_OBJ = CallCcClass()
|
||||
|
||||
class SchemeString:
|
||||
"String in Scheme"
|
||||
def __init__(self, string):
|
||||
self.string = string
|
||||
|
||||
def __repr__(self):
|
||||
return '"' + self.string + '"'
|
||||
|
||||
|
||||
class Environment (object):
|
||||
"Linked list of bindings mapping symbols to values"
|
||||
__slots__ = ('sym', 'val', 'next')
|
||||
|
||||
def __init__(self, sym, val, next):
|
||||
"(env.sym is None) means the env is the frame top. "
|
||||
self.sym, self.val, self.next = sym, val, next
|
||||
|
||||
def __iter__(self):
|
||||
"Yield each binding in the linked list."
|
||||
env = self
|
||||
while env is not None:
|
||||
yield env
|
||||
env = env.next
|
||||
|
||||
def look_for(self, symbol):
|
||||
"Search the bindings for a symbol."
|
||||
for env in self:
|
||||
if env.sym is symbol:
|
||||
return env
|
||||
raise NameError(symbol)
|
||||
|
||||
def prepend_defs(self, symbols, data):
|
||||
"Build an environment prepending the bindings of symbols and data."
|
||||
if symbols is NIL:
|
||||
if data is not NIL:
|
||||
raise TypeError('surplus arg: ' + stringify(data))
|
||||
return self
|
||||
else:
|
||||
if data is NIL:
|
||||
raise TypeError('surplus param: ' + stringify(symbols))
|
||||
return Environment(symbols.car, data.car,
|
||||
self.prepend_defs(symbols.cdr, data.cdr))
|
||||
|
||||
class Closure (object):
|
||||
"Lambda expression with its environment"
|
||||
__slots__ = ('params', 'body', 'env')
|
||||
|
||||
def __init__(self, params, body, env):
|
||||
self.params, self.body, self.env = params, body, env
|
||||
|
||||
class Intrinsic (object):
|
||||
"Built-in function"
|
||||
__slots__ = ('name', 'arity', 'fun')
|
||||
|
||||
def __init__(self, name, arity, fun):
|
||||
self.name, self.arity, self.fun = name, arity, fun
|
||||
|
||||
def __repr__(self):
|
||||
return '#<%s:%d>' % (self.name, self.arity)
|
||||
|
||||
def stringify(exp, quote=True):
|
||||
"Convert an expression to a string."
|
||||
if exp is True:
|
||||
return '#t'
|
||||
elif exp is False:
|
||||
return '#f'
|
||||
elif isinstance(exp, List):
|
||||
ss = []
|
||||
try:
|
||||
for element in exp:
|
||||
ss.append(stringify(element, quote))
|
||||
except ImproperListError as ex:
|
||||
ss.append('.')
|
||||
ss.append(stringify(ex.args[0], quote))
|
||||
return '(' + ' '.join(ss) + ')'
|
||||
elif isinstance(exp, Environment):
|
||||
ss = []
|
||||
for env in exp:
|
||||
if env is GLOBAL_ENV:
|
||||
ss.append('GlobalEnv')
|
||||
break
|
||||
elif env.sym is None: # marker of the frame top
|
||||
ss.append('|')
|
||||
else:
|
||||
ss.append(env.sym)
|
||||
return '#<' + ' '.join(ss) + '>'
|
||||
elif isinstance(exp, Closure):
|
||||
p, b, e = [stringify(x) for x in (exp.params, exp.body, exp.env)]
|
||||
return '#<' + p + ':' + b + ':' + e + '>'
|
||||
elif isinstance(exp, tuple) and len(exp) == 3:
|
||||
p, v, k = [stringify(x) for x in exp]
|
||||
return '#<' + p + ':' + v + ':\n ' + k + '>'
|
||||
elif isinstance(exp, SchemeString) and not quote:
|
||||
return exp.string
|
||||
else:
|
||||
return str(exp)
|
||||
|
||||
def _globals(x):
|
||||
"Return a list of keys of the global environment."
|
||||
j, env = NIL, GLOBAL_ENV.next # Take next to skip the marker.
|
||||
for e in env:
|
||||
j = Cell(e.sym, j)
|
||||
return j
|
||||
|
||||
def _error(x):
|
||||
"Based on SRFI-23"
|
||||
raise ErrorException("Error: %s: %s" % (stringify(x.car, False),
|
||||
stringify(x.cdr.car)))
|
||||
|
||||
class ErrorException (Exception):
|
||||
pass
|
||||
|
||||
_ = lambda n, a, f, next: Environment(intern(n), Intrinsic(n, a, f), next)
|
||||
|
||||
GLOBAL_ENV = (
|
||||
_('+', 2, lambda x: x.car + x.cdr.car,
|
||||
_('-', 2, lambda x: x.car - x.cdr.car,
|
||||
_('*', 2, lambda x: x.car * x.cdr.car,
|
||||
_('/', 2, lambda x: x.car / x.cdr.car,
|
||||
_('>', 2, lambda x: x.car > x.cdr.car,
|
||||
_('>=', 2, lambda x: x.car >= x.cdr.car,
|
||||
_('<', 2, lambda x: x.car < x.cdr.car,
|
||||
_('<=', 2, lambda x: x.car <= x.cdr.car,
|
||||
_('=', 2, lambda x: x.car == x.cdr.car,
|
||||
_('and', 2, lambda x: x.car and x.cdr.car,
|
||||
_('number?', 1, lambda x: isinstance(x.car, (int, float, long)),
|
||||
_('error', 2, _error,
|
||||
_('globals', 0, _globals,
|
||||
None))))))))))))))
|
||||
|
||||
# My custom helpers
|
||||
from datetime import datetime
|
||||
GLOBAL_ENV = (
|
||||
_('now', 0, lambda x: datetime.now(),
|
||||
_('concat', -1, lambda x: ''.join((stringify(x, False) for x in x)),
|
||||
_('exit', 0, lambda x: exit(),
|
||||
GLOBAL_ENV))))
|
||||
|
||||
# records = {}
|
||||
# def make_record_type(name, params):
|
||||
# # {n: None for n in x.cdr}
|
||||
# # exp, k = kdr.cdr.car, (SETQ, env.look_for(kdr.car), k)
|
||||
# # def first(cons, res=()):
|
||||
# # print(cons, 'res', res)
|
||||
# # if cons is NIL:
|
||||
# # return res
|
||||
# # return first(cons.cdr, (*res, cons.car))
|
||||
# # print('params:', first(params))
|
||||
# # print('x:', params.car.car)
|
||||
# records[name] = tuple(x for x in params.car)
|
||||
# print(records)
|
||||
# return params
|
||||
|
||||
|
||||
# Records
|
||||
GLOBAL_ENV = (
|
||||
_('record?', 1, lambda x: x.car.string in records.keys(),
|
||||
GLOBAL_ENV))
|
||||
|
||||
GLOBAL_ENV = (
|
||||
_('display', 1, lambda x: print(stringify(x.car, False), end=''),
|
||||
_('newline', 0, lambda x: print(),
|
||||
_('read', 0, lambda x: read_expression('', ''),
|
||||
_('eof-object?', 1, lambda x: isinstance(x.car, EOFError),
|
||||
_('symbol?', 1, lambda x: isinstance(x.car, str),
|
||||
Environment(CALLCC, CALLCC_OBJ,
|
||||
Environment(APPLY, APPLY_OBJ,
|
||||
GLOBAL_ENV))))))))
|
||||
|
||||
GLOBAL_ENV = Environment(
|
||||
None, None, # marker of the frame top
|
||||
_('car', 1, lambda x: x.car.car,
|
||||
_('cdr', 1, lambda x: x.car.cdr,
|
||||
_('cons', 2, lambda x: Cell(x.car, x.cdr.car),
|
||||
_('eq?', 2, lambda x: x.car is x.cdr.car,
|
||||
_('pair?', 1, lambda x: isinstance(x.car, Cell),
|
||||
_('null?', 1, lambda x: x.car is NIL,
|
||||
_('not', 1, lambda x: x.car is False,
|
||||
_('list', -1, lambda x: x,
|
||||
GLOBAL_ENV)))))))))
|
||||
|
||||
|
||||
|
||||
def evaluate(exp, env=GLOBAL_ENV):
|
||||
"Evaluate an expression in an environment."
|
||||
k = NOCONT
|
||||
try:
|
||||
while True:
|
||||
while True:
|
||||
if isinstance(exp, Cell):
|
||||
kar, kdr = exp.car, exp.cdr
|
||||
if kar is QUOTE: # (quote e)
|
||||
exp = kdr.car
|
||||
break
|
||||
elif kar is IF: # (if e1 e2 e3) or (if e1 e2)
|
||||
exp, k = kdr.car, (THEN, kdr.cdr, k)
|
||||
elif kar is BEGIN: # (begin e...)
|
||||
exp = kdr.car
|
||||
if kdr.cdr is not NIL:
|
||||
k = (BEGIN, kdr.cdr, k)
|
||||
elif kar is LAMBDA: # (lambda (v...) e...)
|
||||
exp = Closure(kdr.car, kdr.cdr, env)
|
||||
break
|
||||
elif kar is DEFINE: # (define v e)
|
||||
v = kdr.car
|
||||
assert isinstance(v, str), v
|
||||
exp, k = kdr.cdr.car, (DEFINE, v, k)
|
||||
elif kar is DEFINE_RECORD_TYPE: # x = v
|
||||
print(kar, kdr.car, kdr.cdr.car.car)
|
||||
exp, k = kdr.cdr.car, (DEFINE_RECORD_TYPE, kdr.car, kdr.cdr.car)
|
||||
exp = None
|
||||
elif kar is SETQ: # (set! v e)
|
||||
exp, k = kdr.cdr.car, (SETQ, env.look_for(kdr.car), k)
|
||||
else:
|
||||
exp, k = kar, (APPLY, kdr, k)
|
||||
elif isinstance(exp, str):
|
||||
exp = env.look_for(exp).val
|
||||
break
|
||||
else: # as a number, #t, #f etc.
|
||||
break
|
||||
while True:
|
||||
if k is NOCONT:
|
||||
return exp
|
||||
op, x, k = k
|
||||
if op is THEN: # x = (e2 e3)
|
||||
if exp is False:
|
||||
if x.cdr is NIL:
|
||||
exp = None
|
||||
else:
|
||||
exp = x.cdr.car # e3
|
||||
break
|
||||
else:
|
||||
exp = x.car # e2
|
||||
break
|
||||
elif op is BEGIN: # x = (e...)
|
||||
if x.cdr is not NIL: # unless tail call...
|
||||
k = (BEGIN, x.cdr, k)
|
||||
exp = x.car
|
||||
break
|
||||
elif op is DEFINE: # x = v
|
||||
assert env.sym is None # Check for the marker.
|
||||
env.next = Environment(x, exp, env.next)
|
||||
exp = None
|
||||
elif op is DEFINE_RECORD_TYPE: # x = v
|
||||
assert env.sym is None # Check for the marker.
|
||||
# for x, exp in ():
|
||||
# env.next = Environment(x, exp, env.next)
|
||||
print('OP, x, k', op, x, k, exp)
|
||||
# DEFINE:
|
||||
# 1. x?
|
||||
# 2. x-a, x-b, x-c
|
||||
exit()
|
||||
env.next = Environment(x, exp, env.next)
|
||||
exp = None
|
||||
elif op is SETQ: # x = Environment(v, e, next)
|
||||
x.val = exp
|
||||
exp = None
|
||||
elif op is APPLY: # x = args; exp = fun
|
||||
if x is NIL:
|
||||
exp, k, env = apply_function(exp, NIL, k, env)
|
||||
else:
|
||||
k = (APPLY_FUN, exp, k)
|
||||
while x.cdr is not NIL:
|
||||
k = (EVAL_ARG, x.car, k)
|
||||
x = x.cdr
|
||||
exp = x.car
|
||||
k = (CONS_ARGS, NIL, k)
|
||||
break
|
||||
elif op is CONS_ARGS: # x = evaluated args
|
||||
args = Cell(exp, x)
|
||||
op, exp, k = k
|
||||
if op is EVAL_ARG: # exp = the next arg
|
||||
k = (CONS_ARGS, args, k)
|
||||
break
|
||||
elif op is APPLY_FUN: # exp = evaluated fun
|
||||
exp, k, env = apply_function(exp, args, k, env)
|
||||
else:
|
||||
raise RuntimeError('unexpected op: %s: %s' %
|
||||
(stringify(op), stringify(exp)))
|
||||
elif op is RESTORE_ENV: # x = env
|
||||
env = x
|
||||
else:
|
||||
raise RuntimeError('bad op: %s: %s' %
|
||||
(stringify(op), stringify(x)))
|
||||
except ErrorException:
|
||||
raise
|
||||
except Exception as ex:
|
||||
msg = type(ex).__name__ + ': ' + str(ex)
|
||||
if k is not NOCONT:
|
||||
msg += '\n ' + stringify(k)
|
||||
raise Exception(msg)
|
||||
|
||||
def apply_function(fun, arg, k, env):
|
||||
"""Apply a function to arguments with a continuation.
|
||||
It returns (result, continuation, environment).
|
||||
"""
|
||||
while True:
|
||||
if fun is CALLCC_OBJ:
|
||||
k = _push_RESTORE_ENV(k, env)
|
||||
fun, arg = arg.car, Cell(k, NIL)
|
||||
elif fun is APPLY_OBJ:
|
||||
fun, arg = arg.car, arg.cdr.car
|
||||
else:
|
||||
break
|
||||
if isinstance(fun, Intrinsic):
|
||||
if fun.arity >= 0:
|
||||
if len(arg) != fun.arity:
|
||||
raise TypeError('arity not matched: ' + str(fun) + ' and '
|
||||
+ stringify(arg))
|
||||
return fun.fun(arg), k, env
|
||||
elif isinstance(fun, Closure):
|
||||
k = _push_RESTORE_ENV(k, env)
|
||||
k = (BEGIN, fun.body, k)
|
||||
env = Environment(None, None, # marker of the frame top
|
||||
fun.env.prepend_defs(fun.params, arg))
|
||||
return None, k, env
|
||||
elif isinstance(fun, tuple): # as a continuation
|
||||
return arg.car, fun, env
|
||||
else:
|
||||
raise TypeError('not a function: ' + stringify(fun) + ' with '
|
||||
+ stringify(arg))
|
||||
|
||||
def _push_RESTORE_ENV(k, env):
|
||||
if k is NOCONT or k[0] is not RESTORE_ENV: # unless tail call...
|
||||
k = (RESTORE_ENV, env, k)
|
||||
return k
|
||||
|
||||
|
||||
def split_string_into_tokens(source_string):
|
||||
"split_string_into_tokens('(a 1)') => ['(', 'a', '1', ')']"
|
||||
result = []
|
||||
for line in source_string.split('\n'):
|
||||
ss, x = [], []
|
||||
for i, e in enumerate(line.split('"')):
|
||||
if i % 2 == 0:
|
||||
x.append(e)
|
||||
else:
|
||||
ss.append('"' + e) # Append a string literal.
|
||||
x.append('#s')
|
||||
s = ' '.join(x).split(';')[0] # Ignore ;-commment.
|
||||
s = s.replace("'", " ' ").replace(')', ' ) ').replace('(', ' ( ')
|
||||
x = s.split()
|
||||
result.extend([(ss.pop(0) if e == '#s' else e) for e in x])
|
||||
assert not ss
|
||||
return result
|
||||
|
||||
def read_from_tokens(tokens):
|
||||
"""Read an expression from a list of token strings.
|
||||
The list will be left with the rest of token strings, if any.
|
||||
"""
|
||||
token = tokens.pop(0)
|
||||
if token == '(':
|
||||
y = z = Cell(NIL, NIL)
|
||||
while tokens[0] != ')':
|
||||
if tokens[0] == '.':
|
||||
tokens.pop(0)
|
||||
y.cdr = read_from_tokens(tokens)
|
||||
if tokens[0] != ')':
|
||||
raise SyntaxError(') is expected')
|
||||
break
|
||||
e = read_from_tokens(tokens)
|
||||
y.cdr = Cell(e, NIL)
|
||||
y = y.cdr
|
||||
tokens.pop(0)
|
||||
return z.cdr
|
||||
elif token == ')':
|
||||
raise SyntaxError('unexpected )')
|
||||
elif token == "'":
|
||||
e = read_from_tokens(tokens)
|
||||
return Cell(QUOTE, Cell(e, NIL)) # 'e => (quote e)
|
||||
elif token == '#f':
|
||||
return False
|
||||
elif token == '#t':
|
||||
return True
|
||||
elif token[0] == '"':
|
||||
return SchemeString(token[1:])
|
||||
else:
|
||||
try:
|
||||
return int(token)
|
||||
except ValueError:
|
||||
try:
|
||||
return float(token)
|
||||
except ValueError:
|
||||
return intern(token) # as a symbol
|
||||
|
||||
def eval(code, env=GLOBAL_ENV):
|
||||
tokens = split_string_into_tokens(code)
|
||||
resp = None
|
||||
while tokens:
|
||||
exp = read_from_tokens(tokens)
|
||||
resp = evaluate(exp, env)
|
||||
return resp
|
||||
|
||||
def load(file_name, env=GLOBAL_ENV):
|
||||
"Load a source code from a file."
|
||||
with open(file_name) as rf:
|
||||
source_string = rf.read()
|
||||
return eval(source_string, env)
|
||||
|
||||
TOKENS = []
|
||||
|
||||
def read_expression(prompt1='> ', prompt2='| '):
|
||||
"Read an expression."
|
||||
while True:
|
||||
old = TOKENS[:]
|
||||
try:
|
||||
return read_from_tokens(TOKENS)
|
||||
except IndexError: # tokens have run out unexpectedly.
|
||||
try:
|
||||
source_string = raw_input(prompt2 if old else prompt1)
|
||||
except EOFError as ex:
|
||||
return ex
|
||||
TOKENS[:] = old
|
||||
TOKENS.extend(split_string_into_tokens(source_string))
|
||||
except SyntaxError:
|
||||
del TOKENS[:] # Discard the erroneous tokens.
|
||||
raise
|
||||
|
||||
def read_eval_print_loop():
|
||||
"Repeat read-eval-print until End-of-File."
|
||||
while True:
|
||||
try:
|
||||
exp = read_expression()
|
||||
if isinstance(exp, EOFError):
|
||||
print('Goodbye')
|
||||
return
|
||||
result = evaluate(exp)
|
||||
if result is not None:
|
||||
print(stringify(result, True))
|
||||
except Exception as ex:
|
||||
print(ex)
|
||||
|
||||
# import os
|
||||
# print(load(os.path.join(os.path.dirname(__file__), 'init.scm')))
|
||||
|
||||
if __name__ == '__main__':
|
||||
if argv[1:2]:
|
||||
load(argv[1])
|
||||
if argv[2:3] != ['-']:
|
||||
exit(0)
|
||||
read_eval_print_loop()
|
|
@ -0,0 +1,88 @@
|
|||
from time import perf_counter
|
||||
from datetime import datetime, timedelta
|
||||
from collections import deque
|
||||
import re
|
||||
|
||||
# TODO client: add a way to disable live plotting? server: stop
|
||||
# sending data periodically to everybody!
|
||||
|
||||
from random import random
|
||||
from time import sleep
|
||||
|
||||
class Sensor():
|
||||
def __init__(self, autocorr=0.7):
|
||||
self.prevval = random() * 100
|
||||
self.auto = autocorr
|
||||
self.measure = None
|
||||
|
||||
def read(self):
|
||||
sleep(0.5)
|
||||
return (perf_counter(),
|
||||
int((random() * (1-self.auto) + self.prevval * self.auto) * 100) / 100)
|
||||
|
||||
class Temperature1W(Sensor):
|
||||
def __init__(self, address):
|
||||
super().__init__()
|
||||
self.measure = 'Temperature'
|
||||
self.address = address
|
||||
|
||||
def path(self):
|
||||
return f'/sys/bus/w1/devices/{self.address}/w1_slave'
|
||||
|
||||
def read(self):
|
||||
with open(self.path(), "r") as f:
|
||||
content = f.readlines()
|
||||
if len(content) < 2:
|
||||
return None
|
||||
if content[0].strip()[-3:] != "YES":
|
||||
print("INVALID CHECKSUM")
|
||||
return None
|
||||
return int(re.search("t=([0-9]+)", content[1]).group(1)) / 1000.0
|
||||
|
||||
class Sensors():
|
||||
def __init__(self, history=2621440):
|
||||
self.starttime = (datetime.now(), perf_counter())
|
||||
self.scan()
|
||||
self.values = {}
|
||||
self.history = deque([], maxlen=history)
|
||||
|
||||
def perf_datetime(self, offset):
|
||||
time = offset - self.starttime[1]
|
||||
return self.starttime[0] + timedelta(seconds=time)
|
||||
|
||||
"Scan for new sensors"
|
||||
def scan(self):
|
||||
# FIXME: should scan, apply stored data and return this
|
||||
self.available_sensors = {
|
||||
'T_ext': Temperature1W('28-06214252b671'),
|
||||
'T_food': Temperature1W('28-062142531e5a'),
|
||||
}
|
||||
|
||||
def list(self):
|
||||
return {
|
||||
k: self.available_sensors[k].measure
|
||||
for k in self.available_sensors.keys()
|
||||
}
|
||||
|
||||
def value_tuple(self):
|
||||
return tuple((k, *self.values[k]) for k in self.values.keys())
|
||||
|
||||
def read(self):
|
||||
for sensor in self.available_sensors.keys():
|
||||
time, value = self.available_sensors[sensor].read()
|
||||
self.values[sensor] = (str(self.perf_datetime(time)), value)
|
||||
self.history.append(self.value_tuple())
|
||||
|
||||
def get_sensor_value(self, sensor_name):
|
||||
return self.values.get(sensor_name, None)
|
||||
|
||||
def get(self):
|
||||
return self.value_tuple()
|
||||
|
||||
def get_history(self):
|
||||
return tuple(self.history)
|
||||
|
||||
sensors = Sensors()
|
||||
|
||||
def get_sensor_value(sensor_name):
|
||||
return sensors.get_sensor_value(sensor_name)[1]
|
|
@ -0,0 +1,56 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{% block title %} {% endblock %}</title>
|
||||
<script src="/dist/socket.io.min.js" crossorigin="anonymous"></script>
|
||||
<script src="/dist/server.js" crossorigin="anonymous"></script>
|
||||
<script src="/dist/mustache.js" crossorigin="anonymous"></script>
|
||||
<script src="/dist/plotly-2.16.1.min.js" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
var socket = null;
|
||||
function setup() {
|
||||
socket = io();
|
||||
socket.on('connect', function() {
|
||||
const engine = socket.io.engine;
|
||||
socket.emit('new client');
|
||||
engine.on('packet', ({ type, data }) => {
|
||||
// called for each packet received
|
||||
/* console.log('received packet: '+ type + data); */
|
||||
});
|
||||
// receive a message from the client
|
||||
socket.on("state", (data) => {
|
||||
applyState(data);
|
||||
});
|
||||
socket.on("sensors", (data) => {
|
||||
add_plot_data(data);
|
||||
render_sensors(data);
|
||||
});
|
||||
socket.on("sensor history", (...data) => {
|
||||
enable_draw = false;
|
||||
data.forEach(add_plot_data);
|
||||
enable_draw = true;
|
||||
render_plot();
|
||||
});
|
||||
});
|
||||
}
|
||||
window.onload = setup;
|
||||
</script>
|
||||
<link href="/dist/style.css" rel="stylesheet">
|
||||
<link href="/dist/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="/dist/bulma.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
{% raw %}
|
||||
{% endraw %}
|
||||
<div id="manual-modal"></div>
|
||||
<div class="columns">
|
||||
<div class="column is-one-fifth" id="sidebar">
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="block">
|
||||
{% block content %} {% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,43 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h1>{% block title %}Hakkoso{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<br/>
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Sensors
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div id="sensor-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="card">
|
||||
<header class="card-header">
|
||||
<p class="card-header-title">
|
||||
Sensors Data
|
||||
</p>
|
||||
</header>
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
<div id="data-plot"></div>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<br/>
|
||||
<div id="current-recipe"></div>
|
||||
<br/>
|
||||
<div id="load-recipe"></div>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,91 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h1>{% block title %}Hakkoso{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<!-- https://codepen.io/PJCHENder/pen/PKBVRO/ -->
|
||||
<ul id="items-list" class="moveable">
|
||||
<li>One</li>
|
||||
<li>Two</li>
|
||||
<li>Three</li>
|
||||
<li>Four</li>
|
||||
</ul>
|
||||
<div class="columns">
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label">Recipe Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Text input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Recipe Description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
Phase list:
|
||||
<div class="tile is-ancestor">
|
||||
<div class="tile is-vertical">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child notification is-primary">
|
||||
<p class="title"></p>
|
||||
<p class="subtitle">Top tile</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column is-half">
|
||||
<div class="field">
|
||||
<label class="label">Phase Name</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" placeholder="Phase Name">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">Phase Description</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="Phase Description"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">On Load</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="#t"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Next Condition</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="(> (time-in-this-phase) (minutes 1))"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">On Exit</label>
|
||||
<div class="control">
|
||||
<textarea class="textarea" placeholder="#t"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field is-grouped">
|
||||
<div class="control">
|
||||
<button class="button is-link">Submit</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-link is-light">Cancel</button>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button class="button is-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% block header %}
|
||||
<h1>{% block title %}Recipe{% endblock %}</h1>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>{{recipe.name}} Recipe</h2>
|
||||
<h3>Description</h3>
|
||||
<p>
|
||||
{{recipe.description}}
|
||||
</p>
|
||||
<h3>Phases</h3>
|
||||
{% for p in recipe.sm.phases %}
|
||||
<h4>{{p.name}}</h4>
|
||||
{{p.text}}
|
||||
{% endfor %}
|
||||
|
||||
<h3>Required Sensors</h3>
|
||||
Boh
|
||||
|
||||
</br>
|
||||
</br>
|
||||
Current load (kg)
|
||||
<input value="Automatic"></input>
|
||||
</br>
|
||||
</br>
|
||||
<button>Start!</button>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue