The SIGHTS software suite is designed to be very extensible when it comes to adding new sensors to your robot. We achieve this by using special Python classes called sensor wrappers to collect sensor data, and JavaScript classes called Graphs to display it. SIGHTS is shipped with many useful sensor wrappers for a lot of common sensors, but to use some less common sensors, you may need to write your own wrappers and graphs.
In this blog post, I’ll unpack one of the bundled SIGHTS graphs to show you how you can create your own. If you want to learn how to create a sensor wrapper, check out the previous post in this series.
What is a Graph?
A graph is a JavaScript class that determines how to display sensor data on the interface retrieved from sensors by the SIGHTS host service. A graph extends the Graph
class, which implements a few functions standard to all graphs and defines a few more that you need to implement yourself.
Take a look at the Graph
class in graph.js
(view on GitHub).
constructor(config)
The graph constructor. If there are multiple graphs with the same unique ID we warn the user so they can change one of them.
appendTo(target)
Appends the graph to the target.
update(index, data, name)
Updates the graph when new data is sent from the sensor. This needs to be implemented by the subclass since every type of graph updates differently. We’ll go through an example afterwards.
setup(index, data, name)
This function is used to set up the graph using any initialisation data sent by the sensor wrapper. It’s optional – only required if your graph uses initialisation data such as the “limit” message our Disk Usage sensor wrapper sent (see the get_initial()
function definition in the previous blog).
remove()
Removes the graph from the DOM when it is disabled. This is handled completely by the Graph
class unless your graph does anything unusual that will need to be removed by the subclass.
Writing a Graph
Now we can write a graph subclass that extends Graph
. We’ll go through the circle graph since it’s a good match for the disk usage sensor wrapper we implemented in the last tutorial.
Create your new JavaScript file in thesights/interface/js/graphs
directory.
First create the JavaScript class, making sure to extend Graph
so we can use the functions it implements in our own graph.
1 | class CircleGraph extends Graph { |
In this class, the first thing we need to do is create the constructor. You should call the superclass’ constructor first.
Now we create the dom_object
. This is the HTML we want to be appended to the interface for each instance of our new graph.
The dom_object
should begin with a div
class with the id
attribute set to the graph uid and an extra string such as _graph
so that there are no duplicate ID issues if a user gives their graph a uid that is existing. Everything else in the dom_object
is up to you and will be specific to the graph you are trying to create. Any object that needs an ID should also use the uid of the graph.
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 | constructor(config) { super(config); this.dom_object = $("<div/>", { "id": this.config.uid + "_graph", "class": 'col' }); let card = $("<div/>", {"class": "card"}); let header = $("<div/>", {"class": "card-header"}); let body = $("<div/>", {"class": "card-body"}); let title = $("<span/>", { "id": this.config.uid + "_title", "text": this.config.title }); let circle = $("<div/>", { "id": this.config.uid + "_circle", "class": 'c100 p0 med orange center' }).append( $("<span/>", { "id": this.config.uid + "_parent" }).append( $("<span/>", { "id": this.config.uid + "_level" }).append( $("<i/>", { "class": "fa fa-fw fa-ellipsis-h" }) ), $("<span/>", { "id": this.config.uid + "_unit", "style": this.config.unit_style }), ), $("<div/>", { "class": 'slice' }).append( $("<div/>", { "class": 'bar' }), $("<div/>", { "class": 'fill' }) ) ); header.append(title); body.append(circle); card.append(body, header); this.dom_object.append(card); } |
Next up we implement the setup
function. As you’ll recall, this is optional, but we need to use it for the circle graph. If you read the previous post in this series you’ll know that the disk usage sensor wrapper we created sends some data during initialisation – specifically, the maximum value the sensor can report (the sensor’s limit). Some graphs ignore this initialisation data because it is not useful to them, but for representing a percentage of a maximum, it is necessary. In the case of a disk usage sensor, it would be the size of the disk. All we need to do is save this data so we can use it in the future.
1 2 3 | setup(index, data, name) { this.config.limit = data["limit"]; } |
Finally, we implement the update function.
This is where we use the sensor’s limit we received in setup()
so we can calculate the percentage. We prefer the user’s setting if it is provided, otherwise, we use the limit provided by the sensor. If neither of these is provided, we warn the user that they need to set one, since to calculate a percentage we need to know the maximum value. In the meantime, we use 100.
We can use jQuery to select the elements we created in the constructor by ID, and then update it with the new sensor data.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | update(index, data, name) { let max = this.config.maximum || this.config.limit; if (!max) { max = 100; interfaceLog("warning", "sensors", "Sensor " + name + " is not providing" + " a maximum value. You should set this in your " + this.config.uid + " graph config instead. Default " + "of 100 is used.") } let level = $("#" + this.config.uid + "_level"); let unit_text = $("#" + this.config.uid + "_unit"); let circle = $("#" + this.config.uid + "_circle"); let unit = this.config.unit; level.html(data); unit_text.html(unit) let percent = Math.round((data / max) * 100); if (percent > 100) percent = 100; circle.attr('class', "c100 med orange center p" + percent); } |
This is all that we require for the circle graph.
Let’s take a quick look at another graph, the thermal camera graph (view on GitHub), to see how other graph functions can be implemented.
The appendTo()
function is implemented by Graph
, the superclass of all graphs. This means that in most cases, you won’t need to extend this function. The thermal camera graph does this to register interaction events on sliders. You can call the appendTo()
function in the superclass to do the rest of the work.
1 2 3 4 | appendTo(target) { super.appendTo(target); this.registerSliders(); } |
You can do the same thing with the remove()
function. In this case, we need to remove any thermal camera overlay from the visible light cameras.
1 2 3 4 | remove() { super.remove(); $('#thermal_overlay_' + this.overlayCamera).empty(); } |
Once you’ve completed your graph class, you’ll need to add a few lines of code to sights.sensors.js
in sights/interface/js
to create a new instance of your graph when required. Find this forEach method:
1 2 3 | response['interface']['graphs'].forEach(function (graph) { ... } |
And add a new if statement for your new graph:
1 2 3 4 | if (graph.type == "circle") { graphs[graph.uid] = new CircleGraph(graph); graphs[graph.uid].appendTo(graph.location); } |
The browser also needs to know where the file is to load it, so add it to index.html
under the other graphs:
1 | <script src="js/graphs/circlegraph.js"></script> |
Adding a Graph to the Config Schema
Just like sensors, the configuration of each graph can be slightly different, so it’s helpful to users if you create a config schema that defines how your graph can be configured.
Locate the graphs
definition (under interface
) in the config schema at sights/interface/js/sights.config.schema.js
. Again, like sensors, you will notice that a graph can be “any of” a list of graphs. All you need to do is add your graph with its configuration values to the list of options.
Below is an example for the circle graph we created above.
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 | { "type": "object", "options": { "collapsed": true }, "title": "Circle Graph", "properties": { "uid": { "type": "string", "title": "Unique ID", "description": "The UID used to identify the graph. Used in sensor configuration to determine what graph a sensor's data is displayed on." }, "type": { "type": "string", "title": "Type", "description": "The type of graph to use.", "enum": [ "circle" ], "default": "circle", "format": "radio" }, "enabled": { "type": "boolean", "title": "Enable", "description": "Whether the graph is enabled.", "format": "checkbox", "default": true }, "location": { "type": "string", "title": "Location", "description": "Element to append the graph to. Try '#btm_view_sensors', '#left_view_sensors' or '#right_view_sensors'.", "default": "#" }, "title": { "type": "string", "title": "Title", "description": "Title displayed on the graph.", "default": "Graph" }, "unit": { "type": "string", "title": "Units", "description": "The unit of measurement to display on the readout." }, "unit_style": { "type": "string", "title": "Units Styling", "default": "font-size: 24px;", "description": "Inline CSS to style the units text, e.g. to decrease font size if it overflows." }, "maximum": { "type": "number", "title": "Maximum", "required": false, "default": null, "description": "(Optional) The maximum value that the sensor can report, used when calculating how full the circle bar should be. Most sensors, especially the host resource monitors (CPU, disk and RAM usage as well as CPU temperature) can auto-report their correct maximum value, meaning you should leave this blank unless you need to reduce your sensor's maximum value." } } } |
Contributing your Own Graph to SIGHTS
If you’ve created a graph that’s not included with SIGHTS, in the spirit of open source collaboration, we’d love to include it in the official SIGHTS repository! The process is simple:
- Fork the repository at https://github.com/SFXRescue/sights
- Make your changes on your fork (add your new graph and its config schema!)
- Submit a pull request with your additions.
It’s generally easier to focus on one feature per pull request, so if you have other unrelated changes you want to propose or you have multiple graphs to contribute, please consider making separate pull requests for each.
Further Reading
You can read the documentation about extending SIGHTS. Documentation is available offline when you install SIGHTS and is accessible through the interface. If you haven’t installed SIGHTS, you can still access this online at GitHub.
If there are major changes to the function of sensor wrappers and graphs, you may need to refer to the most up-to-date documentation at https://github.com/SFXRescue/sights/blob/master/docs/extending.md.
If the process for creating a sensor wrapper or graph changes significantly enough, we may post a new tutorial as well.