Skip to content

Commit

Permalink
Merge pull request #19 from joelkoz/config-UI
Browse files Browse the repository at this point in the history
Config ui
  • Loading branch information
joelkoz authored Jun 7, 2019
2 parents 1344c70 + 2ef34c8 commit f0c2ea3
Show file tree
Hide file tree
Showing 42 changed files with 768 additions and 12 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ TODO!

In the examples below, `sensesp` is used as the device name.

SensESP implements a RESTful configuration API. A list of
You can configure your device with any web browser by going to

http://sensesp.local


SensESP also implements a RESTful configuration API. A list of
possible configuration keys can be retrieved from:

http://sensesp.local/config
Expand All @@ -78,7 +83,7 @@ Configuration can be updated with HTTP PUT requests:
- [x] Improved device configuration system
- [x] Authentication token support
- [x] Make the project a library
- [ ] Web configuration UI
- [x] Web configuration UI
- [ ] Control device support. For now, all devices are read-only, and control devices such as leds, relays, or
PWM output are not supported.

Expand Down
31 changes: 31 additions & 0 deletions extra_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Import("env")

def make_c_header(inName, outName):

print 'Writing ',inName,' to src/net/web/',outName,'.h'

infile = open('web/docroot/' + inName, "r")
outfile = open("src/net/web/" + outName + ".h","w")

outfile.write("#include <pgmspace.h>\n")
outfile.write("const char PAGE_")
outfile.write(outName)
outfile.write("[] PROGMEM = R\"=====(\n")

for line in infile:
outfile.write(line)

outfile.write("\n)=====\";\n")

infile.close()
outfile.close()


def build_webUI(*args, **kwargs):
env.Execute("terser --compress --output web/docroot/js/sensesp.min.js -- web/docroot/js/sensesp.js")
make_c_header("js/sensesp.min.js", "js_sensesp")
make_c_header("js/jsoneditor.min.js", "js_jsoneditor")
make_c_header("index.html", "index")
make_c_header("setup/index.html", "setup")

env.AlwaysBuild(env.Alias("webUI", None, build_webUI))
4 changes: 4 additions & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ lib_deps =
DallasTemperature
ESP8266TrueRandom
https://github.com/JoaoLopesF/RemoteDebug.git#0b5a9c1a49fd2ade0e3cadc3a3707781e819359a


extra_scripts = extra_script.py

13 changes: 13 additions & 0 deletions src/devices/onewire_temperature.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ JsonObject& OneWireTemperature::get_configuration(JsonBuffer& buf) {
return root;
}


String OneWireTemperature::get_config_schema() {
return R"({
"type": "object",
"properties": {
"address": { "title": "OneWire address", "type": "string" },
"value": { "title": "Last value", "type" : "number", "readOnly": true }
}
})";
}



bool OneWireTemperature::set_configuration(const JsonObject& config) {
if (!config.containsKey("address")) {
return false;
Expand Down
3 changes: 2 additions & 1 deletion src/devices/onewire_temperature.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class OneWireTemperature : public NumericDevice {
void enable() override final;
virtual JsonObject& get_configuration(JsonBuffer& buf) override final;
virtual bool set_configuration(const JsonObject& config) override final;

virtual String get_config_schema() override;

private:
OneWire* onewire;
DallasTemperatureSensors* dts;
Expand Down
43 changes: 39 additions & 4 deletions src/net/http.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@
#include "sensesp_app.h"
#include "system/configurable.h"

// Include the web UI stored in PROGMEM space
#include "web/index.h"
#include "web/setup.h"
#include "web/js_jsoneditor.h"
#include "web/js_sensesp.h"

// HTTP port for the configuration interface
#ifndef HTTP_SERVER_PORT
#define HTTP_SERVER_PORT 80
Expand All @@ -24,11 +30,10 @@ HTTPServer::HTTPServer(std::function<void()> reset_device) {
this->reset_device = reset_device;
server = new AsyncWebServer(HTTP_SERVER_PORT);
using std::placeholders::_1;

server->onNotFound(std::bind(&HTTPServer::handle_not_found, this, _1));
server->on("/",[](AsyncWebServerRequest *request ) {
request->send_P(200, "text/html", "SensESP");
});

// Handle setting configuration values of a Configurable via a Json PUT to /config
AsyncCallbackJsonWebHandler* config_put_handler
= new AsyncCallbackJsonWebHandler(
"/config",
Expand Down Expand Up @@ -68,6 +73,10 @@ HTTPServer::HTTPServer(std::function<void()> reset_device) {
});
config_put_handler->setMethod(HTTP_PUT);
server->addHandler(config_put_handler);


// Handle requests to retrieve the current Json configuration of a Configurable
// via HTTP GET on /config
server->on("/config", HTTP_GET, [this] (AsyncWebServerRequest *request) {
// omit the "/config" part of the url
String url_tail = request->url().substring(7);
Expand Down Expand Up @@ -95,13 +104,37 @@ HTTPServer::HTTPServer(std::function<void()> reset_device) {
request->send(response);
});


server->on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
debugD("Serving index.html");
request->send_P(200, "text/html", PAGE_index);
});

server->on("/setup", HTTP_GET, [](AsyncWebServerRequest *request) {
debugD("Serving setup.html");
request->send_P(200, "text/html", PAGE_setup);
});

server->on("/js/jsoneditor.min.js", HTTP_GET, [](AsyncWebServerRequest *request) {
debugD("Serving jsoneditor.min.js");
request->send_P(200, "text/javascript", PAGE_js_jsoneditor);
});


server->on("/js/sensesp.js", HTTP_GET, [](AsyncWebServerRequest *request) {
debugD("Serving sensesp.js");
request->send_P(200, "text/javascript", PAGE_js_sensesp);
});


server->on("/device/reset", HTTP_GET,
std::bind(&HTTPServer::handle_device_reset, this, _1));
server->on("/device/restart", HTTP_GET,
std::bind(&HTTPServer::handle_device_restart, this, _1));
server->on("/info", HTTP_GET,
std::bind(&HTTPServer::handle_info, this, _1));
}
}


void HTTPServer::handle_not_found(AsyncWebServerRequest* request) {
debugD("NOT_FOUND: ");
Expand Down Expand Up @@ -178,6 +211,8 @@ void HTTPServer::handle_device_restart(AsyncWebServerRequest* request) {
app.onDelay(50, [](){ ESP.restart(); });
}


void HTTPServer::handle_info(AsyncWebServerRequest* request) {
request->send(200, "text/plain", "/info");
}

11 changes: 11 additions & 0 deletions src/net/networking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ void Networking::set_hostname(String hostname) {
this->hostname->set(hostname);
}


String Networking::get_config_schema() {
return R"({
"type": "object",
"properties": {
"hostname": { "title": "Network SSID", "type": "string" }
}
})";
}


JsonObject& Networking::get_configuration(JsonBuffer& buf) {
JsonObject& root = buf.createObject();
root["hostname"] = this->hostname->get();
Expand Down
3 changes: 2 additions & 1 deletion src/net/networking.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ class Networking : public Configurable {
ObservableValue<String>* get_hostname();
virtual JsonObject& get_configuration(JsonBuffer& buf) override final;
virtual bool set_configuration(const JsonObject& config) override final;

virtual String get_config_schema() override;

void set_hostname(String hostname);

void reset_settings();
Expand Down
19 changes: 19 additions & 0 deletions src/net/web/index.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include <pgmspace.h>
const char PAGE_index[] PROGMEM = R"=====(
<html>
<head>
<title>SensESP SignalK Sensor</title>
</head>
<body>
<h1>SensESP SignalK Sensor</h1>
Your options:
<ul>
<li><a href="/info">Device information</a></li>
<li><a href="/setup">Configure device</a></li>
<li><a href="/device/restart" onclick="return confirm('Restart the device?')">Restart sensor</a></li>
<li><a href="/device/reset" onclick="return confirm('Are you sure you want to reset device to factory settings?')">Reset sensor</a></li>
</ul>
</body>
</html>

)=====";
22 changes: 22 additions & 0 deletions src/net/web/js_jsoneditor.h

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions src/net/web/js_sensesp.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#include <pgmspace.h>
const char PAGE_js_sensesp[] PROGMEM = R"=====(
function ajax(method,url,data,contentType){return new Promise(function(resolve,reject){var request=new XMLHttpRequest;request.open(method,url,!0),request.onload=function(){200===request.status?resolve(request.response):reject(Error(request.statusText))},request.onerror=function(){reject(Error("Network Error"))},contentType&&request.setRequestHeader("Content-Type",contentType),request.send(data)})}class TreeList{constructor(main,pathList){this.main=main,this.main.appendChild(document.createElement("div")),this.root=document.createElement("ul"),this.root.id="tree",this.main.appendChild(this.root);for(var i=0;i<pathList.length;i++){var entry=pathList[i],fullPath=entry,parts=entry.split("/"),nodeName=parts.pop(),section=this.main;for(parts.shift();parts.length>0;){var sectionName=parts.shift();section=this.findNode(section,sectionName)}this.addEntry(section,nodeName,fullPath)}var toggler=document.getElementsByClassName("caret");for(i=0;i<toggler.length;i++)toggler[i].addEventListener("click",function(){this.parentElement.querySelector(".nested").classList.toggle("active"),this.classList.toggle("caret-down")})}makeSectionNode(name){var section=document.createElement("li"),caret=document.createElement("span");caret.className="caret",caret.innerHTML=name,section.appendChild(caret);var ul=document.createElement("ul");return ul.className="nested",section.appendChild(ul),section}addEntry(section,name,fullPath){var entry=document.createElement("li");entry.innerHTML=name,section.children[1].appendChild(entry),entry.className="selectable",entry.addEventListener("click",function(){editConfig(fullPath)})}findNode(sectionRoot,nodeName){if(""!=nodeName){var i,nextEntry,searchChildren=sectionRoot.children[1].childNodes;for(i=0;i<searchChildren.length;i++)if((nextEntry=searchChildren[i]).childNodes[0].textContent==nodeName)return nextEntry;return nextEntry=this.makeSectionNode(nodeName),sectionRoot.children[1].appendChild(nextEntry),nextEntry}return sectionRoot}}var globalEditor=null;function getEmptyMountDiv(){var main=document.getElementById("mountNode");return main.empty(),globalEditor=null,main}function editConfig(config_path){var main=getEmptyMountDiv();ajax("GET","/config"+config_path).then(response=>{var json=JSON.parse(response),config=json.config,schema=json.schema;if(0==Object.keys(schema).length)return alert(`No schema available for ${config_path}`),void showConfigTree();schema.title||(schema.title=`Configuration for ${config_path}`),main.innerHTML="\n <div class='row'>\n <div id='editor_holder' class='medium-12 columns'></div> \n </div>\n <div class='row'>\n <div class='medium-12-columns'>\n <button id='submit' class='tiny'>Save</button>\n <button id='cancel' class='tiny'>Cancel</button>\n <span id='valid_indicator' class='label'></span>\n </div>\n </div>\n ",globalEditor=new JSONEditor(document.getElementById("editor_holder"),{schema:schema,startval:config,no_additional_properties:!0,disable_collapse:!0,disable_properties:!0,disable_edit_json:!0,show_opt_in:!0}),document.getElementById("submit").addEventListener("click",function(){saveConfig(config_path,globalEditor.getValue())}),document.getElementById("cancel").addEventListener("click",function(){showConfigTree()}),globalEditor.on("change",function(){var errors=globalEditor.validate(),indicator=document.getElementById("valid_indicator");errors.length?(indicator.className="label alert",indicator.textContent="not valid"):(indicator.className="label success",indicator.textContent="valid")})}).catch(err=>{alert(`Error retrieving configuration ${config_path}: ${err.message}`),showConfigTree()})}function saveConfig(config_path,values){ajax("PUT","/config"+config_path,JSON.stringify(values),"application/json").then(response=>{showConfigTree()}).catch(err=>{alert(`Error saving configuration ${config_path}: ${err.message}`),showConfigTree()})}function showConfigTree(){var main=getEmptyMountDiv();ajax("GET","/config").then(response=>{var configList=JSON.parse(response).keys;return configList.sort(),configList}).then(configList=>{new TreeList(main,configList)}).catch(err=>{alert("Error: "+err.statusText)})}Element.prototype.empty=function(){for(var child=this.lastElementChild;child;)this.removeChild(child),child=this.lastElementChild};
)=====";
61 changes: 61 additions & 0 deletions src/net/web/setup.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include <pgmspace.h>
const char PAGE_setup[] PROGMEM = R"=====(
<html>
<head>
<title>Configure SensESP Sensor</title>

<style>
/* Remove default bullets */
ul, #tree {
list-style-type: none;
}

/* Remove margins and padding from the parent ul */
#tree {
margin: 0;
padding: 0;
}

/* Style the caret/arrow */
.caret, .selectable {
cursor: pointer;
user-select: none; /* Prevent text selection */
}


/* Create the caret/arrow with a unicode, and style it */
.caret::before {
content: "\25B6";
color: black;
display: inline-block;
margin-right: 6px;
}

/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.caret-down::before {
transform: rotate(90deg);
}

/* Hide the nested list */
.nested {
display: none;
}

/* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
.active {
display: block;
}
</style>

<script src="/js/jsoneditor.min.js"></script>
<script src="/js/sensesp.js"></script>
</head>

<body onload="showConfigTree()">
<h1>SensESP Sensor</h1>
<div id="mountNode">
</div>
</body>
</html>

)=====";
14 changes: 14 additions & 0 deletions src/net/ws_client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,20 @@ JsonObject& WSClient::get_configuration(JsonBuffer& buf) {
return root;
}


String WSClient::get_config_schema() {
return R"({
"type": "object",
"properties": {
"sk_host": { "title": "SignalK Host", "type": "string" },
"sk_port": { "title": "SignalK host port", "type": "integer" },
"client_id": { "title": "Client id", "type": "string" },
"polling_href": { "title": "Server authorization polling href", "type": "string" }
}
})";
}


bool WSClient::set_configuration(const JsonObject& config) {
String expected[] = {"sk_host", "sk_port", "token"};
for (auto str : expected) {
Expand Down
1 change: 1 addition & 0 deletions src/net/ws_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class WSClient : public Configurable {

virtual JsonObject& get_configuration(JsonBuffer& buf) override final;
virtual bool set_configuration(const JsonObject& config) override final;
virtual String get_config_schema() override;

private:
String host = "";
Expand Down
14 changes: 14 additions & 0 deletions src/transforms/difference.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,20 @@ JsonObject& Difference::get_configuration(JsonBuffer& buf) {
return root;
}


String Difference::get_config_schema() {
return R"({
"type": "object",
"properties": {
"sk_path": { "title": "SignalK Path", "type": "string" },
"k1": { "title": "Input #1 multiplier", "type": "number" },
"k2": { "title": "Input #2 multiplier", "type": "number" },
"value": { "title": "Last value", "type" : "number", "readOnly": true }
}
})";
}


bool Difference::set_configuration(const JsonObject& config) {
String expected[] = {"k1", "k2", "sk_path"};
for (auto str : expected) {
Expand Down
1 change: 1 addition & 0 deletions src/transforms/difference.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class Difference : public SymmetricTransform<float> {
virtual String as_signalK() override final;
virtual JsonObject& get_configuration(JsonBuffer& buf) override final;
virtual bool set_configuration(const JsonObject& config) override final;
virtual String get_config_schema() override;

private:
uint8_t received = 0;
Expand Down
12 changes: 12 additions & 0 deletions src/transforms/frequency.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ JsonObject& Frequency::get_configuration(JsonBuffer& buf) {
return root;
}

String Frequency::get_config_schema() {
return R"({
"type": "object",
"properties": {
"sk_path": { "title": "SignalK Path", "type": "string" },
"k": { "title": "Multiplier", "type": "number" },
"value": { "title": "Last value", "type" : "number", "readOnly": true }
}
})";
}


bool Frequency::set_configuration(const JsonObject& config) {
String expected[] = {"k", "c", "sk_path"};
for (auto str : expected) {
Expand Down
2 changes: 2 additions & 0 deletions src/transforms/frequency.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ class Frequency : public IntegerConsumer, public NumericTransform {
virtual void enable() override final;
virtual JsonObject& get_configuration(JsonBuffer& buf) override final;
virtual bool set_configuration(const JsonObject& config) override final;
virtual String get_config_schema() override;

private:
float k;
int ticks = 0;
Expand Down
11 changes: 11 additions & 0 deletions src/transforms/gnss_position.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ JsonObject& GNSSPosition::get_configuration(JsonBuffer& buf) {
return root;
}

String GNSSPosition::get_config_schema() {
return R"({
"type": "object",
"properties": {
"sk_path": { "title": "SignalK Path", "type": "string" }
}
})";
}



bool GNSSPosition::set_configuration(const JsonObject& config) {
if (!config.containsKey("sk_path")) {
return false;
Expand Down
Loading

0 comments on commit f0c2ea3

Please sign in to comment.