Apps reference

LogicMachine apps reference #

Available libraries/frameworks #

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 { 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; }


(function() {
  var el = $('#resize-demo');

  // simple info
  // extended info to show on click
  $('<div></div>').html('Min: 12%<br>Max: 92%<br>Avg: 56%<small>Daily</small>')

  // toggle size on click {
    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:



Example (daemon.lua) #

Minimal example which logs current timestamp every 5 seconds.

while true do

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 minute
  • hourly – runs at 0 minutes of each hour
  • daily – runs at 0:00

All possible cron job paths for application named myapp:

  • store/cron/myapp/minutely.lua
  • store/cron/myapp/
  • store/cron/myapp/hourly.lua
  • store/cron/myapp/
  • store/cron/myapp/daily.lua
  • store/cron/myapp/

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 found
  • config.getall(app) – return table with all configuration values for given application name or nil if configuration is empty
  • config.set(app, key, value) – adds a new key/value pair or overwrites an existing one
  • config.setall(app, cfg) – overwrites existing config with given cfg table with keys/values
  • config.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

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
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">

(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 });

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 pair
  • storeGet(key) – retrieves key value, returns null when key is not found
  • storeRemove(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);

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 pairs
  • Cookie.get(key) – returns cookie value
  • Cookie.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 closed
  • secure – 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 address
  • event.src – source address, empty for local telegrams
  • event.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 groupresponse
  • event.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.


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 address
  • event.src – source address, empty for local telegrams
  • event.type – event type, either groupread, groupwrite or groupresponse
  • event.datahex – HEX-encoded raw data

lb:sethandler('storage', storagecallback)

Sets a callback function for storage changes.

Callback receives three arguments: action, key, value:

  • action – either set or delete
  • key – storage item key
  • value – new storage item value, nil for delete action

Example (daemon) #

function groupcallback(event)
  if event.dst == '1/1/1' then
    local value = knxdatatype.decode(event.datahex, dt.uint16)

function storagecallback(action, key, value)
  if action == 'set' and key == 'mystoragekey' then

lb = require('localbus').new(0.5) -- timeout is 0.5 seconds
lb:sethandler('groupwrite', groupcallback)
lb:sethandler('storage', storagecallback)

while true do

Translation #

  • $.i18n.lang – current language or undefined 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. Additional vars 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"

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


<div id="myapp-container">
  <span class="tr" data-tr-key="myapp.hello">Hello!</span>


// register french translation
$.i18n.add('myapp', {
  fr: {
    hello: 'Bonjour!'

// apply translation to all elements inside of myapp-container

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 output
  • getvar(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 set
  • getheader(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 miss
  • setcookie(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 CRLF
  • write(...) – similar to print but does not output CRLF at the end
  • escape(val) – escape single/double quotes, less than/greater than characters to HTML entities
  • include(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 file
  • request.path – full path to the directory containing the requested lp file
  • request.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 library
  • vprint(...) and vprinthex(...) - functions to view variable contents in a human-readable form
  • json library
  • getlanguage([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>
    Current date is <?>

Output multiplication table. Size can be a GET/POST variable in 1..20 range (defaults to 10).

<!DOCTYPE html>
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
<table border="1" cellpadding="3">
<? for i = 1, size do ?>
    <? for j = 1, size do ?>
      <td><?=(i * j)?></td>
    <? end ?>
<? end ?>

Include document (row.lp), must be in the same directory as the parent document (index.lp)

  <? for j = 1, size do ?>
    <td><?=(ival * j)?></td>
  <? end ?>

Parent document (index.lp). Note: ival global is used because local counter variable i is not visible in row.lp

<!DOCTYPE html>
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
<table border="1" cellpadding="3">
<? for i = 1, size do ?>
  <? ival = i ?>
  <? include('row.lp') ?>
<? end ?>