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
28var cfg = {
29  "foglight.port":      "1",
30  "foglight.auto":      "no",
31  "foglight.speed.on":  "45",
32  "foglight.speed.off": "55",
33};

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:

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

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// update ticker subscription:
53if (cfg["foglight.auto"] == "yes" && !state.ticker) {
54  state.ticker = PubSub.subscribe("ticker.1", checkspeed);
55} else if (cfg["foglight.auto"] != "yes" && state.ticker) {
56  PubSub.unsubscribe(state.ticker);
57  state.ticker = false;
58}

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// EGPIO port control:
62function toggle(onoff) {
63  if (state.port != onoff) {
64    OvmsCommand.Exec("egpio output " + cfg["foglight.port"] + " " + onoff);
65    state.port = onoff;
66    OvmsCommand.Exec("event raise usr.foglight." + (onoff ? "on" : "off"));
67  }
68}

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

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

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
74var 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// API method foglight.info():
100exports.info = function() {
101  JSON.print({ "cfg": cfg, "state": state });
102}

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// Init & install:
83$('#main').one('load', function(ev) {
84  loadcmd('script eval foglight.info()').then(function(output) {
85    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 * /store/scripts/lib/foglight.js
  3 * 
  4 * Module plugin:
  5 *  Foglight control with speed adaption and auto off on vehicle off.
  6 * 
  7 * Version 1.0   Michael Balzer <dexter@dexters-web.de>
  8 * 
  9 * Enable:
 10 *  - install at above path
 11 *  - add to /store/scripts/ovmsmain.js:
 12 *        foglight = require("lib/foglight");
 13 *  - script reload
 14 * 
 15 * Config:
 16 *  - vehicle foglight.port           …EGPIO output port number
 17 *  - vehicle foglight.auto           …yes = speed automation
 18 *  - vehicle foglight.speed.on       …auto turn on below this speed
 19 *  - vehicle foglight.speed.off      …auto turn off above this speed
 20 * 
 21 * Usage:
 22 *  - script eval foglight.set(1)     …toggle foglight on
 23 *  - script eval foglight.set(0)     …toggle foglight off
 24 *  - script eval foglight.info()     …show config & state (JSON)
 25 * 
 26 */
 27
 28var cfg = {
 29  "foglight.port":      "1",
 30  "foglight.auto":      "no",
 31  "foglight.speed.on":  "45",
 32  "foglight.speed.off": "55",
 33};
 34
 35var state = {
 36  on: 0,            // foglight on/off
 37  port: 0,          // current port output state
 38  ticker: false,    // ticker subscription
 39};
 40
 41// Read config:
 42function readconfig() {
 43  var cmdres, lines, cols, i;
 44  cmdres = OvmsCommand.Exec("config list vehicle");
 45  lines = cmdres.split("\n");
 46  for (i=0; i<lines.length; i++) {
 47    if (lines[i].indexOf("foglight") >= 0) {
 48      cols = lines[i].substr(2).split(": ");
 49      cfg[cols[0]] = cols[1];
 50    }
 51  }
 52  // update ticker subscription:
 53  if (cfg["foglight.auto"] == "yes" && !state.ticker) {
 54    state.ticker = PubSub.subscribe("ticker.1", checkspeed);
 55  } else if (cfg["foglight.auto"] != "yes" && state.ticker) {
 56    PubSub.unsubscribe(state.ticker);
 57    state.ticker = false;
 58  }
 59}
 60
 61// EGPIO port control:
 62function toggle(onoff) {
 63  if (state.port != onoff) {
 64    OvmsCommand.Exec("egpio output " + cfg["foglight.port"] + " " + onoff);
 65    state.port = onoff;
 66    OvmsCommand.Exec("event raise usr.foglight." + (onoff ? "on" : "off"));
 67  }
 68}
 69
 70// Check speed:
 71function checkspeed() {
 72  if (!state.on)
 73    return;
 74  var speed = OvmsMetrics.AsFloat("v.p.speed");
 75  if (speed <= cfg["foglight.speed.on"])
 76    toggle(1);
 77  else if (speed >= cfg["foglight.speed.off"])
 78    toggle(0);
 79}
 80
 81// API method foglight.set(onoff):
 82exports.set = function(onoff) {
 83  if (onoff) {
 84    state.on = 1;
 85    if (cfg["foglight.auto"] == "yes") {
 86      checkspeed();
 87      print("Foglight AUTO mode\n");
 88    } else {
 89      toggle(1);
 90      print("Foglight ON\n");
 91    }
 92  } else {
 93    state.on = 0;
 94    toggle(0);
 95    print("Foglight OFF\n");
 96  }
 97}
 98
 99// API method foglight.info():
100exports.info = function() {
101  JSON.print({ "cfg": cfg, "state": state });
102}
103
104// Init:
105readconfig();
106PubSub.subscribe("config.changed", readconfig);
107PubSub.subscribe("vehicle.off", function(){ exports.set(0); });

Web Plugin

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

 1<!--
 2  foglight.htm: Web plugin for hook /dashboard:body.pre
 3    - add button to activate/deactivate foglight
 4    - add indicator to show current foglight state
 5  
 6  Requires module plugin: foglight.js
 7  
 8  Version 1.0  Michael Balzer <dexter@dexters-web.de>
 9-->
10
11<style>
12#foglight {
13  margin: 10px 8px 0;
14}
15#foglight .indicator > .label {
16  font-size: 130%;
17  line-height: 160%;
18  margin: 0px;
19  padding: 10px;
20  display: block;
21  border-radius: 50px;
22}
23</style>
24
25<div class="receiver" id="foglight" style="display:none">
26  <form>
27    <div class="form-group">
28      <div class="col-xs-6">
29        <div class="indicator indicator-foglight">
30          <span class="label label-default">FOGLIGHT</span>
31        </div>
32      </div>
33      <div class="col-xs-6">
34        <div class="btn-group btn-group-justified action-foglight-set" data-toggle="buttons">
35          <label class="btn btn-default action-foglight-0"><input type="radio" name="foglight" value="0">OFF</label>
36          <label class="btn btn-default action-foglight-1"><input type="radio" name="foglight" value="1">ON/AUTO</label>
37        </div>
38        <samp id="action-foglight-output" class="text-center"></samp>
39      </div>
40    </div>
41  </form>
42</div>
43
44<script>
45(function(){
46
47  var foglight = { cfg: {}, state: { on: 0, port: 0 } };
48  var $indicator = $('#foglight .indicator-foglight > .label');
49  var $actionset = $('#foglight .action-foglight-set > label');
50
51  // State & UI update:
52  function update(data) {
53    $.extend(true, foglight, data);
54    // update indicator:
55    if (foglight.state.port)
56      $indicator.removeClass('label-default').addClass('label-danger');
57    else
58      $indicator.removeClass('label-danger').addClass('label-default');
59    // update buttons:
60    $actionset.removeClass('active');
61    $actionset.find('input[value='+foglight.state.on+']').prop('checked', true).parent().addClass('active');
62  }
63
64  // Listen to foglight events:
65  $('#foglight').on('msg:event', function(e, event) {
66    if (event == "usr.foglight.on")
67      update({ state: { port: 1 } });
68    else if (event == "usr.foglight.off")
69      update({ state: { port: 0 } });
70    else if (event == "vehicle.off") {
71      update({ state: { on: 0 } });
72      $('#action-foglight-output').empty();
73    }
74  });
75
76  // Button action:
77  $('#foglight .action-foglight-set input').on('change', function(e) {
78    foglight.state.on = $(this).val();
79    loadcmd('script eval foglight.set('+foglight.state.on+')', '#action-foglight-output');
80  });
81
82  // Init & install:
83  $('#main').one('load', function(ev) {
84    loadcmd('script eval foglight.info()').then(function(output) {
85      update(JSON.parse(output));
86      $('#foglight').appendTo('#panel-dashboard .panel-body').show();
87    });
88  });
89
90})();
91</script>