LogicMachine apps reference #
Available libraries/frameworks #
- jQuery v2.2.4 (http://jquery.com/):
- Bootstrap v3.3.7 (http://getbootstrap.com):
- Foundation v6.3.1 (http://foundation.zurb.com):
- Font Awesome v4.7 (http://fontawesome.io):
- Foundation Icons v3 (http://zurb.com/playground/foundation-icon-fonts-3):
- Windows 10 Icons by Icons8 (http://icons8.github.io/windows-10-icons/):
Respective file paths:
- /apps/js/jquery.js.gz
- /apps/js/bootstrap.js.gz
- /apps/js/foundation.js.gz
- /apps/css/bootstrap.css.gz
- /apps/css/foundation.css.gz
- /apps/css/font-awesome.css.gz
- /apps/css/foundation-icons.css.gz
- /apps/css/icons8-win10.css.gz
Note: Bootstrap comes without Glyphicons, use Font Awesome instead.
Base directory structure #
- /data – apps and widgets are stored here, accessible at http://IP/apps/data/
- /libs – Lua library storage, loaded via
require('applibs.lib')
where lib is library name - /daemon – app daemon are stored here
- /cron – app cron jobs are stored here
- /user – allows storing user files and LP scripts, accessible at http://IP/user/
- /scripts – optional Lua scripts that are executed when app is installed, updated and removed (respectively: install.lua, update.lua and remove.lua)
Full-screen mode #
In order to hide the top navigation bar icons pass fs
GET variable (variable contents must evaluate to true): http://IP/apps/?fs=1
App / Widget structure #
Application name (ID) must be unique and can only contain alphanumeric characters, hyphens and underscores. Maximum name length is 64 characters.
Directory structure #
- index.lp – required for apps, unless url is specified, clicking app icon will open app directory in the same window. Applications must provide a Back button so user can return to starting page
- icon.svg or icon.png – required for apps, contains application icon, SVG is recommended
- widget.lp or widget.js – required for widgets, can contain JavaScript + Lua code or pure JavaScript source which displays widget contents
- title – optional for apps, text file with title that is shown beneath the icon
- url – optional for apps, text file with URL that should be open when icon is clicked
- style.css – optional for widget, contains custom CSS stylesheet for given widget
- config.lp – optional configuration file, see description below
In widget mode icon element ID is the same as widget name, all other HTML element IDs must be prefixed with a unique application name to minimize collisions between different applications. The same rule applies to CSS selectors.
Note: if your widget has any custom click events, make sure to check for the unlocked class which is set when the widget can be dragged by the end user. Click events must be ignored if this class is set.
Widget sizing #
Default widget size is 100×100px. Width/height can be increased by calling setWidgetSize(cols, rows)
on the widget element. Width formula: cols * 110 – 10
, height formula: rows * 110 – 10
Examples #
Clock widget #
It takes double width/height and places an SVG image which fills all available space inside of the widget container.
(function() {
// get widget element and set double width/height
var el = $('#clock').setWidgetSize(2, 2);
$('<object type="image/svg+xml"></object>') // object allows SVG+JavaScript
.css('width', '100%') // full width
.css('height', '100%') // full height
.attr('data', '/apps/data/clock/clock.svg') // SVG image source
.appendTo(el); // add to container
})();
Change widget size dynamically #
Toggles between square (1, 1) and wide (2, 1) mode after each click.
resize-demo/style.css
#resize-demo { overflow: hidden; background: #fd0; color: #333; cursor: pointer; }
#resize-demo small { position: absolute; bottom: 5px; text-align: center;
font-size: 17px; line-height: 17px; }
#resize-demo div { width: 100px; height: 100px; float: left; text-align: center; }
#resize-demo div:first-child { font-size: 36px; line-height: 90px; }
#resize-demo div:first-child small { width: 100px; left: 0; }
#resize-demo div:last-child { width: 110px; font-size: 15px; padding-top: 8px; }
#resize-demo div:last-child small { width: 110px; left: 100px; }
resize-demo/widget.js
(function() {
var el = $('#resize-demo');
// simple info
$('<div></div>').html('86%<small>Current</small>')
.appendTo(el);
// extended info to show on click
$('<div></div>').html('Min: 12%<br>Max: 92%<br>Avg: 56%<small>Daily</small>')
.appendTo(el);
// toggle size on click
el.click(function() {
var cols = el.hasClass('big') ? 1 : 2;
el.setWidgetSize(cols, 1).toggleClass('big');
});
})();
Daemons #
Create a new directory named as your application in the daemon directory. Place daemon.lua inside your newly created directory. Daemon is started automatically when the device boots up and when the application is installed. Daemon is automatically restarted in case of an error, errors are logged to LogicMachine error log.
Daemon can be forced to (re)start or to stop via HTTP requests:
http://IP/apps/request.lp?password=ADMINPASSWORD&action=restart&name=YOURAPPNAME
http://IP/apps/request.lp?password=ADMINPASSWORD&action=stop&name=YOURAPPNAME
Example (daemon.lua) #
Minimal example which logs current timestamp every 5 seconds.
while true do
alert(os.time())
os.sleep(5)
end
Cron jobs #
Create a new directory named as your application in the cron directory. Each cron job can be a Lua file, shell script or both.
Three types of jobs are supported:
minutely
– runs every minutehourly
– runs at 0 minutes of each hourdaily
– runs at 0:00
All possible cron job paths for application named myapp:
- store/cron/myapp/minutely.lua
- store/cron/myapp/minutely.sh
- store/cron/myapp/hourly.lua
- store/cron/myapp/hourly.sh
- store/cron/myapp/daily.lua
- store/cron/myapp/daily.sh
Each job is executed as a separate process in the background.
Note: execution errors are not logged at this moment.
Configuration #
Application directory must contain either config.lp or config.html file.
This file must contain a form element, id must be set in myapp-config format, where myapp is a unique application name.
Data exchange is done via events triggered on form element:
- config-load – (to app) provides an object with all configuration key/value pairs
- config-check – (to app) triggered when Save button is clicked, app configuration must either show an error message if configuration is invalid or trigger config-save
- config-save – (from app) saves configuration on server side and closed modal window, application must pass configuration parameters as an object
Configuration can be accessed from Lua using these functions:
config.get(app, key, default)
– returns single value for given application name, default value or nil if key is not foundconfig.getall(app)
– return table with all configuration values for given application name or nil if configuration is emptyconfig.set(app, key, value)
– adds a new key/value pair or overwrites an existing oneconfig.setall(app, cfg)
– overwrites existing config with given cfg table with keys/valuesconfig.delete(app, key)
– deletes existing key/value pair
Unpublished apps that have a configuration file present will appear under Dev apps in the admin page.
Examples (config.html) #
Access config value from Lua daemon.
-- get delay value from config, use 15 as default when delay is not set
delay = config.get('myapp', 'delay', 15)
-- main daemon loop
while true do
alert(os.time())
os.sleep(delay)
end
Set value in lp script posted from user.
<?
delay = getvar('delay') -- GET/POST variable
delay = tonumber(delay) or 0 -- convert to number
-- set to default value if empty or invalid
if delay < 5 or 100 < delay then
delay = 15
end
config.set('myapp', 'delay', 15)
Create a simple form element with one numeric input which accepts values in the 5..100 range.
<form id="myapp-config">
<div class="form-group">
<label for="myapp-input">Delay (seconds)</label>
<input type="number" id="myapp-delay" class="form-control" min="5" max="100">
</div>
</form>
<script>
(function() {
var el = $('#myapp-config') // form element
, input = $('#myapp-delay'); // input element
// set element values when config is loaded
el.on('config-load', function(event, data) {
$.each(data, function(key, value) {
$('#myapp-' + key).val(value);
});
});
// runs when Save button is clicked
el.on('config-check', function() {
var val = parseInt(input.val(), 10) // input value
, min = parseInt(input.attr('min'), 10) // minimum value
, max = parseInt(input.attr('max'), 10); // maximum value
// invalid value
if (isNaN(val) || val < min || max < val) {
alert('Please enter a value between ' + min + ' and ' + max);
}
// all good, save configuration
else {
el.triggerHandler('config-save', { delay: val });
}
});
})();
</script>
localStorage wrapper functions #
localStorage allows saving client-side configuration. Several functions are provided to safely execute localStorage functions, as they might fail in some cases like private mode on iOS. It also allows storing any values that can be serialized using JSON.stringify.
storeSet(key, value)
– sets key/value pairstoreGet(key)
– retrieves key value, returns null when key is not foundstoreRemove(key)
– removes key from storage
Storage keys must be prefixed with a unique application name to minimize collisions between different applications.
Examples #
Get the currently selected theme (light/dark).
var theme = storeGet('myapp_theme') || 'light';
Store JavaScript objects.
var user = { name: 'John', surname: 'Doe', age: 42 };
storeSet('myapp_user', user);
Cookie functions #
Use Cookie global object to access cookies from client-side.
Cookie.set(key, value[, attributes])
– sets key/value cookie pair. If the value is a JavaScript array or an object, it will be automatically JSON-encoded for set calls and JSON-decoded for get calls.Cookie.get()
– returns object containing all key/value cookie pairsCookie.get(key)
– returns cookie valueCookie.remove(key[, attributes])
– removes cookie from storage
Cookie attributes (optional):
path
– cookie path, defaults to "/"expires
– number (days) or Date object, containing cookie expiration time. By default cookie expires when browser window is closedsecure
– only allow cookie to be sent via HTTPS, disabled by default
Cookie keys must be prefixed with a unique application name to minimize collisions between different applications.
Examples #
Get the currently selected theme (light/dark).
var theme = Cookie.get('myapp_theme') || 'light';
Set a cookie to expire in 1 year.
var config = { theme: 'dark', language: 'Italian' };
Cookie.set('myapp_config', config, { expires: 365 });
Store JavaScript objects.
var user = { name: 'John', surname: 'Doe', age: 42 };
Cookie.set('myapp_user', user);
Client-side local bus monitoring #
localbus
allows monitoring group address and storage variable changes. If used separately, include /scada/vis/busdecode.js.gz and /apps/js/localbus.js.gz in your app and call localbus.init(). This is not needed for widgets.
localbus.listen('object', groupaddr, callback)
Runs callback function when group address value changes, callback receives two arguments – new object value and the object reference itself (id, datahex, datatype, updatetime, value). Will not work if the given object does not have a known data type.
localbus.listen('storage', key, callback)
Runs callback function when storage key value changes, callback receives one argument – new storage value.
localbus.listen('alerts', callback)
Runs callback function when new alert arrives, callback receives two arguments - source script name and alert text
localbus.listen('groupwrite', callback)
localbus.listen('groupread', callback)
localbus.listen('groupresponse', callback)
Runs callback function when new group telegram is received, callback receives one argument – event object.
Event object:
event.dst
– destination group addressevent.src
– source address, empty for local telegramsevent.tsec
– UNIX timestamp of the telegram (seconds part)event.usec
– UNIX timestamp of the telegram (microseconds part)event.type
– event type, either groupread, groupwrite or groupresponseevent.value
– new value, only valid for groupwrite and groupresponse types and when destination object has known data type
localbus.write(groupaddr, value)
localbus.update(groupaddr, value)
Sends a write/update to the given group address. Will not work if this group address does not have a known data type.
Widget example #
(function() {
// get widget element and add data div
var el = $('#mywidget'), inner = $('<div></div>').addClass('data').appendTo(el);
// create title element and add to widget
$('<div></div>').addClass('title').text('My value').appendTo(el);
// listen for mystoragevar changes
localbus.listen('storage', 'mystoragevar', function(res) {
// check if value is a valid number
res = parseFloat(res);
if (!isNaN(res)) {
inner.text(res.toFixed(2)); // update data div
}
});
})();
Server-side local bus monitoring #
lb = require('localbus').new([timeout])
Loads localbus
library and creates new connection, timeout is an optional value in seconds and is set to 1 second by default.
lb:step()
Waits for a single message or timeout. Returns true on new message, nil on timeout.
lb:sethandler('groupwrite', groupcallback)
lb:sethandler('groupread', groupcallback)
lb:sethandler('groupresponse', groupcallback)
Sets a callback function for each of the group message types.
Callback receives one argument – event table:
event.dst
– destination group addressevent.src
– source address, empty for local telegramsevent.type
– event type, either groupread, groupwrite or groupresponseevent.datahex
– HEX-encoded raw data
lb:sethandler('storage', storagecallback)
Sets a callback function for storage changes.
Callback receives three arguments: action, key, value:
action
– eitherset
ordelete
key
– storage item keyvalue
– new storage item value, nil fordelete
action
Example (daemon) #
function groupcallback(event)
if event.dst == '1/1/1' then
local value = knxdatatype.decode(event.datahex, dt.uint16)
dosomethingwithvalue(value)
end
end
function storagecallback(action, key, value)
if action == 'set' and key == 'mystoragekey' then
dosomethingwithstorage(value)
end
end
lb = require('localbus').new(0.5) -- timeout is 0.5 seconds
lb:sethandler('groupwrite', groupcallback)
lb:sethandler('storage', storagecallback)
while true do
lb:step()
handledaemonstuff()
end
Translation #
$.i18n.lang
– current language orundefined
if the default language is used$.i18n.add(ns, dictionary)
– adds translations to the current dictionary,ns
must be a unique application name$.i18n.translate(key, default, vars)
or$.tr(key, default, vars)
– translates a given key or uses the default value if translation is not found for the current language. Additionalvars
object can be passed to replace variables inside of translation text
Example 1 #
// register translation for application "myapp"
$.i18n.add('myapp', {
// translation for mylang
mylang: {
hello: 'Hello %{username}, current temperature is %{temperature}',
goodbye: 'Goodbye %{username}'
}
});
var text = $.tr('myapp.hello', 'No translation', { username: 'John', temperature: 21 });
// alerts "Hello John, current temperature is 21" if current language is "mylang"
// otherwise alerts "No translation"
alert(text);
Example 2 #
You can apply translation to jQuery selectors by using tr function: all HTML elements that have tr class and data-tr-key attribute will have contents replaced with translated version
HTML
<div id="myapp-container">
<span class="tr" data-tr-key="myapp.hello">Hello!</span>
</div>
JavaScript
// register french translation
$.i18n.add('myapp', {
fr: {
hello: 'Bonjour!'
}
});
// apply translation to all elements inside of myapp-container
$('#myapp-container').tr();
LP scripts #
Allows mixing HTML and Lua inside a single file, Lua chunks must be enclosed in <? ?>
tags, closing tag at the end of the document is not required. You can also use short print tag <?=myvar?>
as an alias of <? write(myvar) ?>
. Note that closing tag is required for short print tag.
Available functions:
header(header_name, header_value)
– adds a header to the outputgetvar(name)
– returns named GET/POST variable or nil when variable is not set- `getvars() - returns all GET/POST variables as Lua table
getcookie(name)
– returns named cookie contents or nil when cookie is not setgetheader(header)
– returns named header contents or nil when header is not set, might return table when header with the same name has been passed several times. Header case does not matter, it is normalized to a pure lowercase form with all underscores converted to dashes in case of a lookup misssetcookie(name, value [, expires [, path ]])
– sets named cookie contents, must be called before any output is sent, expires must be a number (days) or a table, containing cookie expiration time. By default cookie expires when the browser window is closed. Default path value is "/"print(...)
– outputs any number of variables, ending output with CRLFwrite(...)
– similar to print but does not output CRLF at the endescape(val)
– escape single/double quotes, less than/greater than characters to HTML entitiesinclude(file)
– include and run another lp file, note that parent local variables are not visible inside of the included file (see example below for more info)
request
table contains information on current file and path:
request.file
– full path to the requested lp filerequest.path
– full path to the directory containing the requested lp filerequest.username
– current username, can be nil if password access has not been enabled
Library package is loaded via require('apps') and provides access to these functions:
- all built-in LM functions:
alert
,log
,grp.*
,storage.*
etc if core LM package is installed config
libraryvprint(...)
andvprinthex(...)
- functions to view variable contents in a human-readable formjson
librarygetlanguage([default])
– returns selected language for current user or default value (nil when not specified) when language is not set
Examples #
Print current date.
<!DOCTYPE html>
<html>
<body>
Current date is <?=os.date()?>
</body>
</html>
Output multiplication table. Size can be a GET/POST variable in 1..20 range (defaults to 10).
<!DOCTYPE html>
<html><body>
<?
size = getvar('size') -- GET/POST variable
size = tonumber(size) or 0 -- convert to number
if size < 1 or 20 < size then
size = 10 -- set to default value if empty or invalid
end
?>
<table border="1" cellpadding="3">
<? for i = 1, size do ?>
<tr>
<? for j = 1, size do ?>
<td><?=(i * j)?></td>
<? end ?>
</tr>
<? end ?>
</table>
</body></html>
Include document (row.lp), must be in the same directory as the parent document (index.lp)
<tr>
<? for j = 1, size do ?>
<td><?=(ival * j)?></td>
<? end ?>
</tr>
Parent document (index.lp). Note: ival
global is used because local counter variable i is not visible in row.lp
<!DOCTYPE html>
<html>
<body>
<?
size = getvar('size') -- GET/POST variable
size = tonumber(size) or 0 -- convert to number
if size < 1 or 20 < size then
size = 10 -- set to default value if empty or invalid
end
?>
<table border="1" cellpadding="3">
<? for i = 1, size do ?>
<? ival = i ?>
<? include('row.lp') ?>
<? end ?>
</table>
</body>
</html>