Example: Foglight

../../../_images/foglight.png

This is an example for combining a script module implementing some functional extension with a web plugin to provide a user interface.

The plugin realizes a fog light with automatic speed adaption, i.e. the light will be turned off above a configurable speed and on again when the vehicle slows down again. It also switches off automatically when the vehicle is parked.

The plugin shows how to…

  • read custom config variables
  • use module state variables
  • react to events
  • send custom events
  • read metrics
  • execute system commands
  • provide new commands
  • provide a web UI

Installation

  • Save foglight.js as /store/scripts/lib/foglight.js (add the lib directory as necessary)
  • Add foglight.htm as a web hook type plugin at page /dashboard, hook body.pre
  • Execute script eval 'foglight = require("lib/foglight")'

Optionally:

  • To test the module, execute script eval foglight.info() – it should print config and state
  • To automatically load the module on boot, add the line foglight = require("lib/foglight"); to /store/scripts/ovmsmain.js

Commands / Functions

The module provides two public API functions:

Function Description
foglight.set(onoff) …switch fog light on (1) / off (0)
foglight.info() …output config & state in JSON format

To call these from a shell, use script eval. Example:

  • script eval foglight.set(1)

Configuration

We’ll add the configuration for this to the vehicle section:

Config Default Description
foglight.port 1 …EGPIO output port number
foglight.auto no …yes = speed automation
foglight.speed.on 45 …auto turn on below this speed
foglight.speed.off 55 …auto turn off above this speed

Note

You can add arbitrary config instances to defined sections simply by setting them: config set vehicle foglight.auto yes

Update: Beginning with firmware release 3.2.009, a general configuration section usr is provided for plugins. We recommend using this for all custom parameters now. Keep in mind to prefix all instances introduced by the plugin name, so your plugin can nicely coexist with others.

To store the config for simple & quick script access and implement the defaults, we introduce an internal module member object cfg:

Module Plugin
28
29
30
31
32
33
var cfg = {
  "foglight.port":      "1",
  "foglight.auto":      "no",
  "foglight.speed.on":  "45",
  "foglight.speed.off": "55",
};

By foglight = require(…), the module is added to the global name space as a javascript object. This object can contain any internal standard javascript variables and functions. Internal members are hidden by default, if you would like to expose the cfg object, you would simply add a reference to it to the exports object as is done below for the API methods.

Reading OVMS config variables from a script currently needs to be done by executing config list and parsing the output. This is done by the readconfig() function:

42
43
44
45
46
47
48
49
50
51
function readconfig() {
  var cmdres, lines, cols, i;
  cmdres = OvmsCommand.Exec("config list vehicle");
  lines = cmdres.split("\n");
  for (i=0; i<lines.length; i++) {
    if (lines[i].indexOf("foglight") >= 0) {
      cols = lines[i].substr(2).split(": ");
      cfg[cols[0]] = cols[1];
    }
  }

Update: OVMS release 3.2.009 adds the OvmsConfig.GetValues() API. To use this, we would now omit the “foglight.” prefix from our cfg properties. Reading the foglight configuration can then be reduced to a single line:

Object.assign(cfg, OvmsConfig.GetValues("vehicle", "foglight."));

Listen to Events

The module needs to listen to three events:

  • config.changed triggers reloading the configuration
  • ticker.1 is used to check the speed once per second
  • vehicle.off automatically also turns off the fog light

The per second ticker is only necessary when the speed adaption is enabled, so we can use this to show how to dynamically add and remove event handlers through the PubSub API:

Module Plugin
52
53
54
55
56
57
58
// update ticker subscription:
if (cfg["foglight.auto"] == "yes" && !state.ticker) {
  state.ticker = PubSub.subscribe("ticker.1", checkspeed);
} else if (cfg["foglight.auto"] != "yes" && state.ticker) {
  PubSub.unsubscribe(state.ticker);
  state.ticker = false;
}

state is another internal object for our state variables.

Send Events

Sending custom events is a lightweight method to inform the web UI (or other plugins) about simple state changes. In this case we’d like to inform listeners when the fog light actually physically is switched on or off, so the web UI can give visual feedback to the driver on this.

Beginning with firmware release 3.2.006 there is a native API OvmsEvents.Raise() available to send events.

Before 3.2.006 we simply use the standard command event raise:

Module Plugin
61
62
63
64
65
66
67
68
// EGPIO port control:
function toggle(onoff) {
  if (state.port != onoff) {
    OvmsCommand.Exec("egpio output " + cfg["foglight.port"] + " " + onoff);
    state.port = onoff;
    OvmsCommand.Exec("event raise usr.foglight." + (onoff ? "on" : "off"));
  }
}

The web plugin subscribes to the foglight events just as to any system event:

Web Plugin
64
65
66
67
68
69
70
71
72
73
74
// Listen to foglight events:
$('#foglight').on('msg:event', function(e, event) {
  if (event == "usr.foglight.on")
    update({ state: { port: 1 } });
  else if (event == "usr.foglight.off")
    update({ state: { port: 0 } });
  else if (event == "vehicle.off") {
    update({ state: { on: 0 } });
    $('#action-foglight-output').empty();
  }
});

Note

You can raise any event you like, but you shouldn’t raise system events without good knowledge of their effects. Event codes are simply strings, so you’re free to extend them. Use the prefix usr. for custom events to avoid potential conflicts with future system event additions.

Read Metrics

Reading metrics is straight forward through the OvmsMetrics API:

Module Plugin
74
var speed = OvmsMetrics.AsFloat("v.p.speed");

Use Value() instead of AsFloat() for non-numerical metrics.

Note

You cannot subscribe to metrics changes directly (yet). Metrics can change thousands of times per second, which would overload the scripting capabilities. To periodically check a metric, register for a ticker event (as shown here).

Provide Commands

To add commands, simply expose their handler functions through the exports object. By this, users will be able to call these functions using the script eval command from any shell, or from any script by referencing them via the global module variable, foglight in our case.

Module Plugin
 99
100
101
102
// API method foglight.info():
exports.info = function() {
  JSON.print({ "cfg": cfg, "state": state });
}

JSON.print() is a convenient way to communicate with a web plugin, as that won’t need to parse some potentially ambigous textual output but can simply use JSON.parse() to read it into a variable:

Web Plugin
82
83
84
85
// Init & install:
$('#main').one('load', function(ev) {
  loadcmd('script eval foglight.info()').then(function(output) {
    update(JSON.parse(output));

Note

Keep in mind commands should always output some textual response indicating their action and result. If a command does nothing, it should tell the user so. If a command is not intended for shell use, it should still provide some clue about this when called from the shell.

Module Plugin

foglight.js (hint: right click, save as)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
/**
 * /store/scripts/lib/foglight.js
 * 
 * Module plugin:
 *  Foglight control with speed adaption and auto off on vehicle off.
 * 
 * Version 1.0   Michael Balzer <dexter@dexters-web.de>
 * 
 * Enable:
 *  - install at above path
 *  - add to /store/scripts/ovmsmain.js:
 *        foglight = require("lib/foglight");
 *  - script reload
 * 
 * Config:
 *  - vehicle foglight.port           …EGPIO output port number
 *  - vehicle foglight.auto           …yes = speed automation
 *  - vehicle foglight.speed.on       …auto turn on below this speed
 *  - vehicle foglight.speed.off      …auto turn off above this speed
 * 
 * Usage:
 *  - script eval foglight.set(1)     …toggle foglight on
 *  - script eval foglight.set(0)     …toggle foglight off
 *  - script eval foglight.info()     …show config & state (JSON)
 * 
 */

var cfg = {
  "foglight.port":      "1",
  "foglight.auto":      "no",
  "foglight.speed.on":  "45",
  "foglight.speed.off": "55",
};

var state = {
  on: 0,            // foglight on/off
  port: 0,          // current port output state
  ticker: false,    // ticker subscription
};

// Read config:
function readconfig() {
  var cmdres, lines, cols, i;
  cmdres = OvmsCommand.Exec("config list vehicle");
  lines = cmdres.split("\n");
  for (i=0; i<lines.length; i++) {
    if (lines[i].indexOf("foglight") >= 0) {
      cols = lines[i].substr(2).split(": ");
      cfg[cols[0]] = cols[1];
    }
  }
  // update ticker subscription:
  if (cfg["foglight.auto"] == "yes" && !state.ticker) {
    state.ticker = PubSub.subscribe("ticker.1", checkspeed);
  } else if (cfg["foglight.auto"] != "yes" && state.ticker) {
    PubSub.unsubscribe(state.ticker);
    state.ticker = false;
  }
}

// EGPIO port control:
function toggle(onoff) {
  if (state.port != onoff) {
    OvmsCommand.Exec("egpio output " + cfg["foglight.port"] + " " + onoff);
    state.port = onoff;
    OvmsCommand.Exec("event raise usr.foglight." + (onoff ? "on" : "off"));
  }
}

// Check speed:
function checkspeed() {
  if (!state.on)
    return;
  var speed = OvmsMetrics.AsFloat("v.p.speed");
  if (speed <= cfg["foglight.speed.on"])
    toggle(1);
  else if (speed >= cfg["foglight.speed.off"])
    toggle(0);
}

// API method foglight.set(onoff):
exports.set = function(onoff) {
  if (onoff) {
    state.on = 1;
    if (cfg["foglight.auto"] == "yes") {
      checkspeed();
      print("Foglight AUTO mode\n");
    } else {
      toggle(1);
      print("Foglight ON\n");
    }
  } else {
    state.on = 0;
    toggle(0);
    print("Foglight OFF\n");
  }
}

// API method foglight.info():
exports.info = function() {
  JSON.print({ "cfg": cfg, "state": state });
}

// Init:
readconfig();
PubSub.subscribe("config.changed", readconfig);
PubSub.subscribe("vehicle.off", function(){ exports.set(0); });

Web Plugin

foglight.htm (hint: right click, save as)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<!--
  foglight.htm: Web plugin for hook /dashboard:body.pre
    - add button to activate/deactivate foglight
    - add indicator to show current foglight state
  
  Requires module plugin: foglight.js
  
  Version 1.0  Michael Balzer <dexter@dexters-web.de>
-->

<style>
#foglight {
  margin: 10px 8px 0;
}
#foglight .indicator > .label {
  font-size: 130%;
  line-height: 160%;
  margin: 0px;
  padding: 10px;
  display: block;
  border-radius: 50px;
}
</style>

<div class="receiver" id="foglight" style="display:none">
  <form>
    <div class="form-group">
      <div class="col-xs-6">
        <div class="indicator indicator-foglight">
          <span class="label label-default">FOGLIGHT</span>
        </div>
      </div>
      <div class="col-xs-6">
        <div class="btn-group btn-group-justified action-foglight-set" data-toggle="buttons">
          <label class="btn btn-default action-foglight-0"><input type="radio" name="foglight" value="0">OFF</label>
          <label class="btn btn-default action-foglight-1"><input type="radio" name="foglight" value="1">ON/AUTO</label>
        </div>
        <samp id="action-foglight-output" class="text-center"></samp>
      </div>
    </div>
  </form>
</div>

<script>
(function(){

  var foglight = { cfg: {}, state: { on: 0, port: 0 } };
  var $indicator = $('#foglight .indicator-foglight > .label');
  var $actionset = $('#foglight .action-foglight-set > label');

  // State & UI update:
  function update(data) {
    $.extend(true, foglight, data);
    // update indicator:
    if (foglight.state.port)
      $indicator.removeClass('label-default').addClass('label-danger');
    else
      $indicator.removeClass('label-danger').addClass('label-default');
    // update buttons:
    $actionset.removeClass('active');
    $actionset.find('input[value='+foglight.state.on+']').prop('checked', true).parent().addClass('active');
  }

  // Listen to foglight events:
  $('#foglight').on('msg:event', function(e, event) {
    if (event == "usr.foglight.on")
      update({ state: { port: 1 } });
    else if (event == "usr.foglight.off")
      update({ state: { port: 0 } });
    else if (event == "vehicle.off") {
      update({ state: { on: 0 } });
      $('#action-foglight-output').empty();
    }
  });

  // Button action:
  $('#foglight .action-foglight-set input').on('change', function(e) {
    foglight.state.on = $(this).val();
    loadcmd('script eval foglight.set('+foglight.state.on+')', '#action-foglight-output');
  });

  // Init & install:
  $('#main').one('load', function(ev) {
    loadcmd('script eval foglight.info()').then(function(output) {
      update(JSON.parse(output));
      $('#foglight').appendTo('#panel-dashboard .panel-body').show();
    });
  });

})();
</script>