63 Commits

Author SHA1 Message Date
8a597162ae GitHub Action Build 2022-03-05 14:27:39 +00:00
f49f386569 Updated html for calibration 2022-03-05 15:25:48 +01:00
008ad490a7 GitHub Action Build 2022-03-05 14:17:23 +00:00
3533ee8dac Show tdevice movement in ui 2022-03-05 15:14:46 +01:00
2b9abda873 Bump html version 2022-03-05 14:56:46 +01:00
4bbb558c8b GitHub Action Build 2022-03-05 13:38:39 +00:00
8ebbc6559f Bump version 2022-03-05 14:36:15 +01:00
0af872e743 Updated docs for 0.8 2022-03-05 14:36:04 +01:00
c20f9a534a Remove sled 2022-02-14 21:49:07 +01:00
10ce1fc245 Adding moving info on html 2022-02-14 21:48:34 +01:00
b43874d151 Sorting graph data 2022-02-14 21:47:59 +01:00
1428bec3da GitHub Action Build 2022-02-08 18:07:39 +00:00
f9791dd349 Bump vuild version 2022-02-08 19:05:29 +01:00
1a7f28413c Revert gyro change 2022-02-08 19:05:13 +01:00
d22309bb2e GitHub Action Build 2022-02-06 20:32:15 +00:00
b901a12699 Removed trailing LF from error log 2022-02-06 21:29:58 +01:00
914b4125d8 Fixed tilt calulation error #29 2022-02-06 21:22:46 +01:00
95216ecc54 Fixed ESP32 build 2022-02-06 21:21:55 +01:00
4d83bf8fce GitHub Action Build 2022-02-06 10:21:33 +00:00
e125ca4a10 Added data to upload api 2022-02-06 10:04:53 +01:00
5880d3a6ba GitHub Action Build 2022-02-04 15:35:08 +00:00
cda3a87dd9 Docs updated 2022-02-04 16:22:27 +01:00
4bcacea9d7 Fixed hostname for min ssl buffer 2022-02-04 16:22:14 +01:00
838d062eea validation on header form 2022-02-03 21:48:11 +01:00
77cdbf7649 Updated bootstrap to 4.6.1 2022-02-03 20:33:20 +01:00
f33a58cffe GitHub Action Build 2022-02-03 15:05:54 +00:00
7ca536b216 Changed from s to ms in post timeout 2022-02-03 16:03:39 +01:00
f0ec352538 GitHub Action Build 2022-02-03 08:35:02 +00:00
e336633c38 Mem debug, Variable http timeout and min heapfrag 2022-02-03 09:32:09 +01:00
a130ebd67d GitHub Action Build 2022-02-02 12:32:48 +00:00
4c789a8b37 Minor update 2022-02-02 13:30:48 +01:00
7ab5f451f5 GitHub Action Build 2022-02-02 12:26:05 +00:00
545f274a47 Refactored to free up heap for SSL 2022-02-02 13:23:16 +01:00
9bea54b703 GitHub Action Build 2022-02-01 15:46:41 +00:00
35333469c7 Added thingsspeak setup docs 2022-02-01 16:44:07 +01:00
a9e0d9290a Update release notes 2022-02-01 11:28:28 +01:00
e459ceb2fa Updated docs 2022-02-01 11:27:46 +01:00
2615debe35 Added prel calculation for estimated total runtime 2022-02-01 10:37:17 +01:00
6113a436b0 Added battery percentage to index page 2022-01-31 22:42:41 +01:00
2d5158465f Removed batt from device page 2022-01-31 22:32:52 +01:00
3af52b5464 GitHub Action Build 2022-01-31 17:57:57 +00:00
c116d672d1 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-01-31 18:51:06 +01:00
e1cc54d188 Added token as option in UI #32 2022-01-31 18:50:47 +01:00
761d570d39 Added average runtime + voltage to device page 2022-01-31 18:29:27 +01:00
22ade61af8 Added runtime time logger 2022-01-30 22:54:48 +01:00
928054458a GitHub Action Build 2022-01-30 18:23:44 +00:00
2e67bd1d57 Checking missing params in API #16 2022-01-30 19:21:32 +01:00
83d7aee944 Merge branch 'master' into dev 2022-01-30 17:29:36 +01:00
8bcd27a076 Fix bad link in docs 2022-01-30 17:29:12 +01:00
0912d672fe Merge branch 'master' into dev 2022-01-30 17:27:36 +01:00
01d7c8adb5 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-01-30 17:27:29 +01:00
69076d5878 Fixed bad doc_ref 2022-01-30 17:26:13 +01:00
3d939ad733 Merge branch 'master' into dev 2022-01-30 17:23:59 +01:00
f2112cb344 Added intro section to docs 2022-01-30 17:22:38 +01:00
f88fb8241b GitHub Action Build 2022-01-30 13:57:05 +00:00
6eed5f143b Merged patch 0.7.1 into dev 2022-01-30 14:55:00 +01:00
64e582d0e5 GitHub Action Build 2022-01-30 10:52:11 +00:00
5d0f02eb18 Update docs 2022-01-30 11:50:09 +01:00
fbc1eb4e31 Updated docs 2022-01-28 17:15:12 +01:00
700f00f48d GitHub Action Build 2022-01-27 20:33:34 +00:00
9a2f86fed7 Updated docs for 0.8 2022-01-27 21:31:35 +01:00
63fd80e750 Added errlog, custom http headers 2022-01-27 14:00:12 +01:00
b106ebfa20 Extended length of http url 2022-01-25 21:57:22 +01:00
62 changed files with 1356 additions and 523 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/calibration.htm">Calibration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><hr class="my-4"></div><script type="text/javascript">function getConfig(){var n="/api/device";$("#spinner").show(),$.getJSON(n,function(n){console.log(n),$("#app-ver").text(n["app-ver"]+" (html 0.7.1)"),$("#mdns").text(n.mdns),$("#id").text(n.id)}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/calibration.htm">Calibration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Average runtime:</div><div class="col-md-4 themed-grid-col bg-light" id="runtime">Loading...</div></div><div class="row mb-3"><a class="badge badge-primary" data-toggle="collapse" href="#collapseLog" role="button" aria-expanded="false" aria-controls="collapseLog" id="log-btn">View error log</a></div><script>function loadLog(){$("#logContent").load("/log")}$("#log-btn").click(function(o){loadLog()}),setInterval(function(){loadLog()},3e3)</script><div class="collapse" id="collapseLog"><div class="card card-body"><pre><code id="logContent"></code></pre></div></div><hr class="my-4"></div><script type="text/javascript">function getConfig(){var e="/api/device";$("#spinner").show(),$.getJSON(e,function(e){console.log(e),$("#app-ver").text(e["app-ver"]+" (html 0.8.0)"),$("#mdns").text(e.mdns),$("#id").text(e.id),$("#runtime").text(e["runtime-average"]+" seconds")}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html>

Binary file not shown.

Binary file not shown.

View File

@ -1,2 +1,2 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/config.htm">Back to configuration</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-2"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$("#alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$("#alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$("#alert").addClass("d-none").removeClass("show")})</script><div class="accordion" id="accordion"><div class="card"><div class="card-header" id="headingOne"><h2 class="mb-0"><button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">Push Format Templates</button></h2></div><div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion"><div class="card-body"><input type="text" name="id" id="id" hidden> <input type="text" name="http-1" id="http-1" hidden> <input type="text" name="http-2" id="http-2" hidden><!--<input type="text" name="brewfather" id="brewfather" hidden>--> <input type="text" name="influxdb" id="influxdb" hidden> <input type="text" name="mqtt" id="mqtt" hidden><div class="form-group row"><label for="push-target" class="col-sm-2 col-form-label">Push target:</label> <select class="custom-select col-sm-4" required name="push-target" id="push-target"><option value="http-1">HTTP option 1</option><option value="http-2">HTTP option 2</option><!--<option value="brewfather">Brewfather</option>--><option value="influxdb">Influx DB</option><option value="mqtt">MQTT</option></select></div><div class="form-group row"><div class="col-sm-12"><textarea rows="5" class="form-control" name="format" id="format">
</textarea></div></div><div class="form-group row"><div class="col-sm-8 offset-sm-2"><button class="btn btn-primary" id="format-btn">Save</button> <button class="btn btn-secondary" id="test-btn">Test</button></div></div><pre class="card-preview" id="preview" name="preview"></pre></div></div></div><hr class="my-4"></div><script type="text/javascript">function setButtonDisabled(l){$("#format-btn").prop("disabled",l),$("#test-btn").prop("disabled",l)}function selectFormat(){var l="#"+$("#push-target").val();console.log(l),l=decodeURIComponent($(l).val()),console.log(l),l=l.replaceAll("|","|\n"),console.log(l),$("#format").val(l),$("#preview").text("")}function getConfig(){setButtonDisabled(!0);var l="/api/config/format";$("#spinner").show(),$.getJSON(l,function(l){console.log(l),$("#id").val(l.id),$("#http-1").val(l["http-1"]),$("#http-2").val(l["http-2"]),$("#influxdb").val(l.influxdb),$("#mqtt").val(l.mqtt),selectFormat()}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide(),setButtonDisabled(!1)})}window.onload=getConfig,setButtonDisabled(!0),$(document).ready(function(){null!=location.hash&&""!=location.hash&&($(".collapse").removeClass("in"),$(location.hash+".collapse").collapse("show"))}),$("#push-target").change(function(l){console.log(l),selectFormat()}),$("#format-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("\n","");var t="id="+$("#id").val()+"&"+$("#push-target").val()+"="+encodeURIComponent(e);console.log(t),$.ajax({type:"POST",url:"/api/config/format",data:t,success:function(l){showSuccess("Format stored successfully."),getConfig()},error:function(l){showError("Unable to store format.")}})}),$("#test-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("${mdns}","gravmon2"),e=e.replaceAll("${id}","e4a344"),e=e.replaceAll("${sleep-interval}","300"),e=e.replaceAll("${temp}","21.1"),e=e.replaceAll("${temp-c}","21.1"),e=e.replaceAll("${temp-f}","51.3"),e=e.replaceAll("${temp-unit}","C"),e=e.replaceAll("${battery}","3.86"),e=e.replaceAll("${rssi}","-76"),e=e.replaceAll("${run-time}","4.32"),e=e.replaceAll("${gravity}","1.044"),e=e.replaceAll("${gravity-sg}","1.044"),e=e.replaceAll("${gravity-plato}","9.5"),e=e.replaceAll("${gravity-unit}","G"),e=e.replaceAll("${corr-gravity}","1.044"),e=e.replaceAll("${corr-gravity-sg}","1.044"),e=e.replaceAll("${corr-gravity-plato}","9.5"),e=e.replaceAll("${angle}","54.5"),e=e.replaceAll("${tilt}","54.5");try{var t=JSON.parse(e);e=JSON.stringify(t,null,2)}catch(l){console.log("Not a javascript object!")}$("#preview").text(e)})</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/config.htm">Back to configuration</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-2"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$("#alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$("#alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$("#alert").addClass("d-none").removeClass("show")})</script><div class="accordion" id="accordion"><div class="card"><div class="card-header" id="headingOne"><h2 class="mb-0"><button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">Push Format Templates</button></h2></div><div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion"><div class="card-body"><input type="text" name="id" id="id" hidden> <input type="text" name="http-1" id="http-1" hidden> <input type="text" name="http-2" id="http-2" hidden><!--<input type="text" name="brewfather" id="brewfather" hidden>--> <input type="text" name="influxdb" id="influxdb" hidden> <input type="text" name="mqtt" id="mqtt" hidden><div class="form-group row"><label for="push-target" class="col-sm-2 col-form-label">Push target:</label> <select class="custom-select col-sm-4" required name="push-target" id="push-target"><option value="http-1">HTTP option 1</option><option value="http-2">HTTP option 2</option><!--<option value="brewfather">Brewfather</option>--><option value="influxdb">Influx DB</option><option value="mqtt">MQTT</option></select></div><div class="form-group row"><div class="col-sm-12"><textarea rows="5" class="form-control" name="format" id="format">
</textarea></div></div><div class="form-group row"><div class="col-sm-8 offset-sm-2"><button class="btn btn-primary" id="format-btn">Save</button> <button class="btn btn-secondary" id="test-btn">Test</button></div></div><pre class="card-preview" id="preview" name="preview"></pre></div></div></div><hr class="my-4"></div><script type="text/javascript">function setButtonDisabled(l){$("#format-btn").prop("disabled",l),$("#test-btn").prop("disabled",l)}function selectFormat(){var l="#"+$("#push-target").val();console.log(l),l=decodeURIComponent($(l).val()),console.log(l),l=l.replaceAll("|","|\n"),console.log(l),$("#format").val(l),$("#preview").text("")}function getConfig(){setButtonDisabled(!0);var l="/api/config/format";$("#spinner").show(),$.getJSON(l,function(l){console.log(l),$("#id").val(l.id),$("#http-1").val(l["http-1"]),$("#http-2").val(l["http-2"]),$("#influxdb").val(l.influxdb),$("#mqtt").val(l.mqtt),selectFormat()}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide(),setButtonDisabled(!1)})}window.onload=getConfig,setButtonDisabled(!0),$(document).ready(function(){null!=location.hash&&""!=location.hash&&($(".collapse").removeClass("in"),$(location.hash+".collapse").collapse("show"))}),$("#push-target").change(function(l){console.log(l),selectFormat()}),$("#format-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("\n","");var t="id="+$("#id").val()+"&"+$("#push-target").val()+"="+encodeURIComponent(e);console.log(t),$.ajax({type:"POST",url:"/api/config/format",data:t,success:function(l){showSuccess("Format stored successfully."),getConfig()},error:function(l){showError("Unable to store format.")}})}),$("#test-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("${mdns}","testing"),e=e.replaceAll("${id}","e4a344"),e=e.replaceAll("${sleep-interval}","300"),e=e.replaceAll("${temp}","21.1"),e=e.replaceAll("${token}","a-token"),e=e.replaceAll("${temp-c}","21.1"),e=e.replaceAll("${temp-f}","51.3"),e=e.replaceAll("${temp-unit}","C"),e=e.replaceAll("${battery}","3.86"),e=e.replaceAll("${rssi}","-76"),e=e.replaceAll("${run-time}","4.32"),e=e.replaceAll("${gravity}","1.044"),e=e.replaceAll("${gravity-sg}","1.044"),e=e.replaceAll("${gravity-plato}","9.5"),e=e.replaceAll("${gravity-unit}","G"),e=e.replaceAll("${corr-gravity}","1.044"),e=e.replaceAll("${corr-gravity-sg}","1.044"),e=e.replaceAll("${corr-gravity-plato}","9.5"),e=e.replaceAll("${angle}","54.5"),e=e.replaceAll("${tilt}","54.5");try{var t=JSON.parse(e);e=JSON.stringify(t,null,2)}catch(l){console.log("Not a javascript object!")}$("#preview").text(e)})</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></div></body></html>

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
{ "project":"gravmon", "version":"0.7.1", "html": [ "index.min.htm", "device.min.htm", "config.min.htm", "calibration.min.htm", "format.min.htm", "about.min.htm" ] }
{ "project":"gravmon", "version":"0.8.0", "html": [ "index.min.htm", "device.min.htm", "config.min.htm", "calibration.min.htm", "format.min.htm", "about.min.htm" ] }

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body class="py-4">
@ -274,6 +274,10 @@
function populateChartForm(a, g) {
if( a != 0)
chartDataForm.push( { x: parseFloat(a), y: parseFloat(g) });
chartDataForm.sort(function (a, b) {
return a.x - b.x;
});
}
function populateChartCalc(a, g) {

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
</head>
<body class="py-4">
@ -93,6 +93,7 @@
</script>
<div class="accordion" id="accordion">
<input type="text" name="runtime-average" id="runtime-average" hidden>
<div class="card">
<div class="card-header" id="headingOne">
@ -134,7 +135,7 @@
<div class="col-sm-2">
<input type="number" min="10" max="3600" class="form-control" name="sleep-interval" id="sleep-interval">
</div>
<label for="sleep-interval" class="col-sm-3 col-form-label" id="sleep-interval-info"></label>
<label for="sleep-interval" class="col-sm-7 col-form-label" id="sleep-interval-info"></label>
</div>
<div class="form-group row">
<div class="col-sm-8 offset-sm-3">
@ -170,17 +171,37 @@
<div class="card-body">
<form action="/api/config/push" method="post">
<input type="text" name="id" id="id2" hidden>
<input type="text" name="http-push-h1" id="http-push-h1" hidden>
<input type="text" name="http-push-h2" id="http-push-h2" hidden>
<input type="text" name="http-push2-h1" id="http-push2-h1" hidden>
<input type="text" name="http-push2-h2" id="http-push2-h2" hidden>
<div class="form-group row">
<label for="http-push" class="col-sm-2 col-form-label">Http URL 1:</label>
<div class="col-sm-10">
<div class="col-sm-8">
<input type="url" maxlength="120" class="form-control" name="http-push" id="http-push">
</div>
<div class="col-sm-2">
<button type="button" class="btn btn-info" data-field1="#http-push-h1" data-field2="#http-push-h2" data-toggle="modal" data-target="#modal-http">Headers</button>
</div>
</div>
<div class="form-group row">
<label for="http-push2" class="col-sm-2 col-form-label">Http URL 2:</label>
<div class="col-sm-10">
<div class="col-sm-8">
<input type="url" maxlength="120" class="form-control" name="http-push2" id="http-push2">
</div>
<div class="col-sm-2">
<button type="button" class="btn btn-info" data-field1="#http-push2-h1" data-field2="#http-push2-h2" data-toggle="modal" data-target="#modal-http">Headers</button>
</div>
</div>
<hr class="my-2">
<div class="form-group row">
<label for="token" class="col-sm-2 col-form-label">Token:</label>
<div class="col-sm-4">
<input type="text" maxlength="50" class="form-control" name="token" id="token">
</div>
</div>
<hr class="my-2">
@ -212,8 +233,6 @@
<input type="text" maxlength="50" class="form-control" name="influxdb2-bucket" id="influxdb2-bucket">
</div>
</div>
<div class="form-group row">
</div>
<div class="form-group row">
<label for="influxdb2-auth" class="col-sm-2 col-form-label">InfluxDB v2 Auth:</label>
<div class="col-sm-4">
@ -257,7 +276,7 @@
<div class="form-group row">
<div class="col-sm-8 offset-sm-2">
<button class="btn btn-secondary" id="format-btn">Format editor</button>
<button class="btn btn-info" id="format-btn">Format editor</button>
</div>
</div>
@ -375,6 +394,57 @@
<hr class="my-4">
</div>
<div class="modal fade" id="modal-http" tabindex="-1" role="dialog" aria-labelledby="modal-header" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modal-header">Define HTTP headers</h5>
</div>
<div class="modal-body">
<label for="http-header" class="col-form-label">Header 1 (Header: value)</label>
<input type="text" maxlength="100" class="form-control" id="header1" oninput="checkHeader(this)">
<label for="http-header" class="col-form-label">Header 2 (Header: value)</label>
<input type="text" maxlength="100" class="form-control" id="header2" oninput="checkHeader(this)">
<input type="text" id="field1" hidden>
<input type="text" id="field2" hidden>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="btn-close" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
$('#modal-http').on('show.bs.modal', function (event) {
var button = $(event.relatedTarget)
var field1 = button.data('field1')
var field2 = button.data('field2')
var modal = $(this)
modal.find('.modal-body #header1').val($(field1).val())
modal.find('.modal-body #header2').val($(field2).val())
modal.find('.modal-body #field1').val(field1)
modal.find('.modal-body #field2').val(field2)
})
$('#modal-http').on('hide.bs.modal', function (event) {
var modal = $(this)
field1 = modal.find('.modal-body #field1').val()
field2 = modal.find('.modal-body #field2').val()
$(field1).val(modal.find('.modal-body #header1').val())
$(field2).val(modal.find('.modal-body #header2').val())
})
function checkHeader(input) {
console.log( input.value );
if (input.value != "" && input.value.indexOf(":") == -1) {
$("#btn-close").prop("disabled", true);
$(input).removeClass("is-valid").addClass("is-invalid");
} else {
$("#btn-close").prop("disabled", false);
$(input).removeClass("is-invalid").addClass("is-valid");
}
}
</script>
<script type="text/javascript">
window.onload = getConfig;
@ -405,9 +475,35 @@
window.location.href = "/format.htm";
});
function estimateBatteryLife(interval) {
// ESP8266 consumes between 140-170mA when WIFI is on. Deep sleep is 20uA.
// MPU-6050 consumes 4mA
// DS18B20 consumes 1mA
// For this estimation we use an average of 160mA
var pwrActive = 160; // mA per hour
var pwrSleep = 5; // mA per day
var batt = 2200; // mA
var rt = parseInt($("#runtime-average").val());
if(rt<1) rt = 2;
// The deep sleep will consume approx 1mA per day.
var powerPerDay = (24*3600)/(interval+rt)*(rt/3600)*pwrActive + pwrSleep;
return batt/powerPerDay;
}
function updateSleepInfo() {
var i = $("#sleep-interval").val()
$("#sleep-interval-info").text( Math.floor(i/60) + " m " + (i%60) + " s" )
var i = parseInt($("#sleep-interval").val());
var j = estimateBatteryLife(i);
var t1 = Math.floor(i/60) + " m " + (i%60) + " s";
var t2 = Math.floor(j/7) + " weeks " + (i%7) + " days";
$("#sleep-interval-info").text(t1);
//$("#sleep-interval-info").text( t1 + " - Estimated life: " + t2);
console.log( "Estimated life: " + t2);
hideWarningGyro();
if(i>0 && i<300) {
@ -451,8 +547,13 @@
if( cfg["gravity-format"] == "G" ) $("#gravity-format-g").click();
else $("#gravity-format-p").click();
$("#ota-url").val(cfg["ota-url"]);
$("#token").val(cfg["token"]);
$("#http-push").val(cfg["http-push"]);
$("#http-push-h1").val(cfg["http-push-h1"]);
$("#http-push-h2").val(cfg["http-push-h2"]);
$("#http-push2").val(cfg["http-push2"]);
$("#http-push2-h1").val(cfg["http-push2-h1"]);
$("#http-push2-h2").val(cfg["http-push2-h2"]);
$("#brewfather-push").val(cfg["brewfather-push"]);
$("#influxdb2-push").val(cfg["influxdb2-push"]);
$("#influxdb2-org").val(cfg["influxdb2-org"]);
@ -471,6 +572,7 @@
$("#gyro-calibration-data").text( cfg["gyro-calibration-data"]["ax"] + "," + cfg["gyro-calibration-data"]["ay"] + "," + cfg["gyro-calibration-data"]["az"] + "," + cfg["gyro-calibration-data"]["gx"] + "," + cfg["gyro-calibration-data"]["gy"] + "," + cfg["gyro-calibration-data"]["gz"] );
$("#battery").text(cfg["battery"] + " V");
$("#angle").text(cfg["angle"]);
$("#runtime-average").val(cfg["runtime-average"]);
//$("#gravity").text(cfg["gravity"] + " SG");
})
.fail(function () {

File diff suppressed because one or more lines are too long

View File

@ -6,9 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
</head>
<body class="py-4">
@ -88,7 +88,39 @@
<div class="col-md-8 themed-grid-col bg-light">Device ID:</div>
<div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div>
</div>
<hr class="my-4">
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Average runtime:</div>
<div class="col-md-4 themed-grid-col bg-light" id="runtime">Loading...</div>
</div>
<div class="row mb-3">
<a class="badge badge-primary" data-toggle="collapse" href="#collapseLog" role="button" aria-expanded="false" aria-controls="collapseLog" id="log-btn">
View error log
</a>
</div>
<script>
$("#log-btn").click(function(e){
loadLog();
});
setInterval(function() {
loadLog();
}, 3000); //5 seconds
function loadLog() {
$("#logContent").load("/log");
//$("#logContent").load("/test/log");
};
</script>
<div class="collapse" id="collapseLog">
<div class="card card-body">
<pre><code id="logContent"></code></pre>
</div>
</div>
<hr class="my-4">
</div>
<script type="text/javascript">
@ -100,9 +132,10 @@
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#app-ver").text(cfg["app-ver"] + " (html 0.7.1)");
$("#app-ver").text(cfg["app-ver"] + " (html 0.8.0)");
$("#mdns").text(cfg["mdns"]);
$("#id").text(cfg["id"]);
$("#runtime").text(cfg["runtime-average"] + " seconds");
})
.fail(function () {
showError('Unable to get data from the device.');

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/calibration.htm">Calibration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><hr class="my-4"></div><script type="text/javascript">function getConfig(){var n="/api/device";$("#spinner").show(),$.getJSON(n,function(n){console.log(n),$("#app-ver").text(n["app-ver"]+" (html 0.7.1)"),$("#mdns").text(n.mdns),$("#id").text(n.id)}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/calibration.htm">Calibration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Average runtime:</div><div class="col-md-4 themed-grid-col bg-light" id="runtime">Loading...</div></div><div class="row mb-3"><a class="badge badge-primary" data-toggle="collapse" href="#collapseLog" role="button" aria-expanded="false" aria-controls="collapseLog" id="log-btn">View error log</a></div><script>function loadLog(){$("#logContent").load("/log")}$("#log-btn").click(function(o){loadLog()}),setInterval(function(){loadLog()},3e3)</script><div class="collapse" id="collapseLog"><div class="card card-body"><pre><code id="logContent"></code></pre></div></div><hr class="my-4"></div><script type="text/javascript">function getConfig(){var e="/api/device";$("#spinner").show(),$.getJSON(e,function(e){console.log(e),$("#app-ver").text(e["app-ver"]+" (html 0.8.0)"),$("#mdns").text(e.mdns),$("#id").text(e.id),$("#runtime").text(e["runtime-average"]+" seconds")}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html>

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
</head>
<body class="py-4">
@ -148,10 +148,11 @@
// Test the calibration
$("#test-btn").click(function(e) {
var doc = $("#format").val();
doc = doc.replaceAll("${mdns}", "gravmon2");
doc = doc.replaceAll("${mdns}", "testing");
doc = doc.replaceAll("${id}", "e4a344");
doc = doc.replaceAll("${sleep-interval}", "300");
doc = doc.replaceAll("${temp}", "21.1");
doc = doc.replaceAll("${token}", "a-token");
doc = doc.replaceAll("${temp-c}", "21.1");
doc = doc.replaceAll("${temp-f}", "51.3");
doc = doc.replaceAll("${temp-unit}", "C");

View File

@ -1,2 +1,2 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/config.htm">Back to configuration</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-2"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$("#alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$("#alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$("#alert").addClass("d-none").removeClass("show")})</script><div class="accordion" id="accordion"><div class="card"><div class="card-header" id="headingOne"><h2 class="mb-0"><button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">Push Format Templates</button></h2></div><div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion"><div class="card-body"><input type="text" name="id" id="id" hidden> <input type="text" name="http-1" id="http-1" hidden> <input type="text" name="http-2" id="http-2" hidden><!--<input type="text" name="brewfather" id="brewfather" hidden>--> <input type="text" name="influxdb" id="influxdb" hidden> <input type="text" name="mqtt" id="mqtt" hidden><div class="form-group row"><label for="push-target" class="col-sm-2 col-form-label">Push target:</label> <select class="custom-select col-sm-4" required name="push-target" id="push-target"><option value="http-1">HTTP option 1</option><option value="http-2">HTTP option 2</option><!--<option value="brewfather">Brewfather</option>--><option value="influxdb">Influx DB</option><option value="mqtt">MQTT</option></select></div><div class="form-group row"><div class="col-sm-12"><textarea rows="5" class="form-control" name="format" id="format">
</textarea></div></div><div class="form-group row"><div class="col-sm-8 offset-sm-2"><button class="btn btn-primary" id="format-btn">Save</button> <button class="btn btn-secondary" id="test-btn">Test</button></div></div><pre class="card-preview" id="preview" name="preview"></pre></div></div></div><hr class="my-4"></div><script type="text/javascript">function setButtonDisabled(l){$("#format-btn").prop("disabled",l),$("#test-btn").prop("disabled",l)}function selectFormat(){var l="#"+$("#push-target").val();console.log(l),l=decodeURIComponent($(l).val()),console.log(l),l=l.replaceAll("|","|\n"),console.log(l),$("#format").val(l),$("#preview").text("")}function getConfig(){setButtonDisabled(!0);var l="/api/config/format";$("#spinner").show(),$.getJSON(l,function(l){console.log(l),$("#id").val(l.id),$("#http-1").val(l["http-1"]),$("#http-2").val(l["http-2"]),$("#influxdb").val(l.influxdb),$("#mqtt").val(l.mqtt),selectFormat()}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide(),setButtonDisabled(!1)})}window.onload=getConfig,setButtonDisabled(!0),$(document).ready(function(){null!=location.hash&&""!=location.hash&&($(".collapse").removeClass("in"),$(location.hash+".collapse").collapse("show"))}),$("#push-target").change(function(l){console.log(l),selectFormat()}),$("#format-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("\n","");var t="id="+$("#id").val()+"&"+$("#push-target").val()+"="+encodeURIComponent(e);console.log(t),$.ajax({type:"POST",url:"/api/config/format",data:t,success:function(l){showSuccess("Format stored successfully."),getConfig()},error:function(l){showError("Unable to store format.")}})}),$("#test-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("${mdns}","gravmon2"),e=e.replaceAll("${id}","e4a344"),e=e.replaceAll("${sleep-interval}","300"),e=e.replaceAll("${temp}","21.1"),e=e.replaceAll("${temp-c}","21.1"),e=e.replaceAll("${temp-f}","51.3"),e=e.replaceAll("${temp-unit}","C"),e=e.replaceAll("${battery}","3.86"),e=e.replaceAll("${rssi}","-76"),e=e.replaceAll("${run-time}","4.32"),e=e.replaceAll("${gravity}","1.044"),e=e.replaceAll("${gravity-sg}","1.044"),e=e.replaceAll("${gravity-plato}","9.5"),e=e.replaceAll("${gravity-unit}","G"),e=e.replaceAll("${corr-gravity}","1.044"),e=e.replaceAll("${corr-gravity-sg}","1.044"),e=e.replaceAll("${corr-gravity-plato}","9.5"),e=e.replaceAll("${angle}","54.5"),e=e.replaceAll("${tilt}","54.5");try{var t=JSON.parse(e);e=JSON.stringify(t,null,2)}catch(l){console.log("Not a javascript object!")}$("#preview").text(e)})</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/config.htm">Back to configuration</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-2"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$("#alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$("#alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$("#alert").addClass("d-none").removeClass("show")})</script><div class="accordion" id="accordion"><div class="card"><div class="card-header" id="headingOne"><h2 class="mb-0"><button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">Push Format Templates</button></h2></div><div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion"><div class="card-body"><input type="text" name="id" id="id" hidden> <input type="text" name="http-1" id="http-1" hidden> <input type="text" name="http-2" id="http-2" hidden><!--<input type="text" name="brewfather" id="brewfather" hidden>--> <input type="text" name="influxdb" id="influxdb" hidden> <input type="text" name="mqtt" id="mqtt" hidden><div class="form-group row"><label for="push-target" class="col-sm-2 col-form-label">Push target:</label> <select class="custom-select col-sm-4" required name="push-target" id="push-target"><option value="http-1">HTTP option 1</option><option value="http-2">HTTP option 2</option><!--<option value="brewfather">Brewfather</option>--><option value="influxdb">Influx DB</option><option value="mqtt">MQTT</option></select></div><div class="form-group row"><div class="col-sm-12"><textarea rows="5" class="form-control" name="format" id="format">
</textarea></div></div><div class="form-group row"><div class="col-sm-8 offset-sm-2"><button class="btn btn-primary" id="format-btn">Save</button> <button class="btn btn-secondary" id="test-btn">Test</button></div></div><pre class="card-preview" id="preview" name="preview"></pre></div></div></div><hr class="my-4"></div><script type="text/javascript">function setButtonDisabled(l){$("#format-btn").prop("disabled",l),$("#test-btn").prop("disabled",l)}function selectFormat(){var l="#"+$("#push-target").val();console.log(l),l=decodeURIComponent($(l).val()),console.log(l),l=l.replaceAll("|","|\n"),console.log(l),$("#format").val(l),$("#preview").text("")}function getConfig(){setButtonDisabled(!0);var l="/api/config/format";$("#spinner").show(),$.getJSON(l,function(l){console.log(l),$("#id").val(l.id),$("#http-1").val(l["http-1"]),$("#http-2").val(l["http-2"]),$("#influxdb").val(l.influxdb),$("#mqtt").val(l.mqtt),selectFormat()}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide(),setButtonDisabled(!1)})}window.onload=getConfig,setButtonDisabled(!0),$(document).ready(function(){null!=location.hash&&""!=location.hash&&($(".collapse").removeClass("in"),$(location.hash+".collapse").collapse("show"))}),$("#push-target").change(function(l){console.log(l),selectFormat()}),$("#format-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("\n","");var t="id="+$("#id").val()+"&"+$("#push-target").val()+"="+encodeURIComponent(e);console.log(t),$.ajax({type:"POST",url:"/api/config/format",data:t,success:function(l){showSuccess("Format stored successfully."),getConfig()},error:function(l){showError("Unable to store format.")}})}),$("#test-btn").click(function(l){var e=$("#format").val();e=e.replaceAll("${mdns}","testing"),e=e.replaceAll("${id}","e4a344"),e=e.replaceAll("${sleep-interval}","300"),e=e.replaceAll("${temp}","21.1"),e=e.replaceAll("${token}","a-token"),e=e.replaceAll("${temp-c}","21.1"),e=e.replaceAll("${temp-f}","51.3"),e=e.replaceAll("${temp-unit}","C"),e=e.replaceAll("${battery}","3.86"),e=e.replaceAll("${rssi}","-76"),e=e.replaceAll("${run-time}","4.32"),e=e.replaceAll("${gravity}","1.044"),e=e.replaceAll("${gravity-sg}","1.044"),e=e.replaceAll("${gravity-plato}","9.5"),e=e.replaceAll("${gravity-unit}","G"),e=e.replaceAll("${corr-gravity}","1.044"),e=e.replaceAll("${corr-gravity-sg}","1.044"),e=e.replaceAll("${corr-gravity-plato}","9.5"),e=e.replaceAll("${angle}","54.5"),e=e.replaceAll("${tilt}","54.5");try{var t=JSON.parse(e);e=JSON.stringify(t,null,2)}catch(l){console.log("Not a javascript object!")}$("#preview").text(e)})</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></div></body></html>

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
</head>
<body class="py-4">
@ -122,14 +122,37 @@
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#id").text(cfg["id"]);
$("#angle").text(cfg["angle"]);
var angle = cfg["angle"];
if( cfg["gravity-format"] == "G")
$("#gravity").text(cfg["gravity"] + " SG");
else
$("#gravity").text(cfg["gravity"] + " °P");
if(angle==0) {
$("#angle").text("Gyro moving");
$("#gravity").text("Gyro moving");
} else {
$("#angle").text(cfg["angle"]);
if( cfg["gravity-format"] == "G")
$("#gravity").text(cfg["gravity"] + " SG");
else
$("#gravity").text(cfg["gravity"] + " °P");
}
$("#battery").text(cfg["battery"] + " V");
var batt = cfg["battery"];
var charge = 0;
if(batt>4.15) charge = 100;
else if(batt>4.05) charge = 90;
else if(batt>3.97) charge = 80;
else if(batt>3.91) charge = 70;
else if(batt>3.86) charge = 60;
else if(batt>3.81) charge = 50;
else if(batt>3.78) charge = 40;
else if(batt>3.76) charge = 30;
else if(batt>3.73) charge = 20;
else if(batt>3.67) charge = 10;
else if(batt>3.44) charge = 5;
$("#battery").text(batt + " V (" + charge + "%)" );
if( cfg["temp-format"] == "C")
$("#temp").text(cfg["temp-c"] + " C");

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/css/bootstrap.min.css" integrity="sha384-zCbKRCUGaJDkqS1kPbPd7TveP5iyJE0EjAuZQTgFLD2ylzuqKfdKlfG/eSrtxUkn" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.6.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-fQybjgWLrvvRgtW6bFlB7jaZrFsaBXjsOMm/tB9LTS58ONXgqbR9W8oWht/amnpF" crossorigin="anonymous"></script>
</head>
<body class="py-4">

File diff suppressed because one or more lines are too long

View File

@ -34,7 +34,7 @@ build_flags =
-D EMBED_HTML # If this is not used the html files needs to be on the file system (can be uploaded)
-D USER_SSID=\""\"" # =\""myssid\""
-D USER_SSID_PWD=\""\"" # =\""mypwd\""
-D CFG_APPVER="\"0.7.1\""
-D CFG_APPVER="\"0.8.0\""
lib_deps = # Switched to forks for better version control.
# Using local copy of these libraries
#https://github.com/jrowberg/i2cdevlib.git#<document>

View File

@ -49,7 +49,8 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
#endif
if (!noAngles) {
Log.error(F("CALC: Not enough values for deriving formula" CR));
ErrorFileLog errLog;
errLog.addEntry(F("CALC: Not enough values for deriving formula"));
return ERR_FORMULA_NOTENOUGHVALUES;
} else {
double coeffs[order + 1];
@ -103,7 +104,10 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
}
if (!valid) {
Log.error(F("CALC: Deviation to large, formula rejected." CR));
ErrorFileLog errLog;
errLog.addEntry(
F("CALC: Error validating created formula. Deviation to large, "
"formula rejected."));
return ERR_FORMULA_UNABLETOFFIND;
}
@ -112,7 +116,8 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
}
}
Log.error(F("CALC: Internal error finding formula." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CALC: Internal error finding formula."));
return ERR_FORMULA_INTERNAL;
}
@ -157,7 +162,8 @@ double calculateGravity(double angle, double temp, const char *tempFormula) {
return g;
}
Log.error(F("CALC: Failed to parse expression %d." CR), err);
ErrorFileLog errLog;
errLog.addEntry("CALC: Failed to parse gravity expression " + String(err));
return 0;
}
@ -203,9 +209,10 @@ double gravityTemperatureCorrectionC(double gravity, double tempC,
return g;
}
Log.error(
F("CALC: Failed to parse expression %d, no correction has been made." CR),
err);
ErrorFileLog errLog;
errLog.addEntry(
"CALC: Failed to parse expression for gravity temperature correction " +
String(err));
return gravity;
}

View File

@ -34,11 +34,11 @@ HardwareConfig myHardwareConfig;
Config::Config() {
// Assiging default values
char buf[30];
#if defined (ESP8266)
#if defined(ESP8266)
snprintf(&buf[0], sizeof(buf), "%6x", (unsigned int)ESP.getChipId());
#else // defined (ESP32)
#else // defined (ESP32)
uint32_t chipId = 0;
for (int i = 0; i < 17; i = i+8) {
for (int i = 0; i < 17; i = i + 8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
snprintf(&buf[0], sizeof(buf), "%6x", chipId);
@ -52,21 +52,11 @@ Config::Config() {
_mDNS.c_str());
#endif
setTempFormat('C');
setGravityFormat('G');
setSleepInterval(900); // 15 minutes
#if defined (ESP8266)
#if defined(ESP8266)
setVoltageFactor(1.59); // Conversion factor for battery on ESP8266
#else // defined (ESP32)
#else // defined (ESP32)
setVoltageFactor(1.43); // Conversion factor for battery on ESP32
#endif
setTempSensorAdjC(0.0);
setGravityTempAdj(false);
_gyroCalibration = {0, 0, 0, 0, 0, 0};
_formulaData = {{0, 0, 0, 0, 0}, {1, 1, 1, 1, 1}};
_gyroTemp = false;
_saveNeeded = false;
_mqttPort = 1883;
}
//
@ -75,14 +65,20 @@ Config::Config() {
//
void Config::createJson(DynamicJsonDocument& doc) {
doc[PARAM_MDNS] = getMDNS();
//doc[PARAM_CONFIG_VER] = getConfigVersion();
doc[PARAM_ID] = getID();
doc[PARAM_OTA] = getOtaURL();
doc[PARAM_SSID] = getWifiSSID();
doc[PARAM_PASS] = getWifiPass();
doc[PARAM_TEMPFORMAT] = String(getTempFormat());
doc[PARAM_PUSH_BREWFATHER] = getBrewfatherPushUrl();
doc[PARAM_PUSH_HTTP] = getHttpPushUrl();
doc[PARAM_PUSH_HTTP2] = getHttpPushUrl2();
doc[PARAM_TOKEN] = getToken();
doc[PARAM_PUSH_HTTP] = getHttpUrl();
doc[PARAM_PUSH_HTTP_H1] = getHttpHeader(0);
doc[PARAM_PUSH_HTTP_H2] = getHttpHeader(1);
doc[PARAM_PUSH_HTTP2] = getHttp2Url();
doc[PARAM_PUSH_HTTP2_H1] = getHttp2Header(0);
doc[PARAM_PUSH_HTTP2_H2] = getHttp2Header(1);
doc[PARAM_PUSH_INFLUXDB2] = getInfluxDb2PushUrl();
doc[PARAM_PUSH_INFLUXDB2_ORG] = getInfluxDb2PushOrg();
doc[PARAM_PUSH_INFLUXDB2_BUCKET] = getInfluxDb2PushBucket();
@ -139,7 +135,8 @@ bool Config::saveFile() {
File configFile = LittleFS.open(CFG_FILENAME, "w");
if (!configFile) {
Log.error(F("CFG : Failed to open file " CFG_FILENAME " for save." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to save configuration."));
return false;
}
@ -156,7 +153,6 @@ bool Config::saveFile() {
configFile.close();
_saveNeeded = false;
myConfig.debug();
Log.notice(F("CFG : Configuration saved to " CFG_FILENAME "." CR));
return true;
}
@ -170,15 +166,16 @@ bool Config::loadFile() {
#endif
if (!LittleFS.exists(CFG_FILENAME)) {
Log.error(
F("CFG : Configuration file does not exist " CFG_FILENAME "." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Configuration file does not exist."));
return false;
}
File configFile = LittleFS.open(CFG_FILENAME, "r");
if (!configFile) {
Log.error(F("CFG : Failed to open " CFG_FILENAME "." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to load configuration."));
return false;
}
@ -194,8 +191,8 @@ bool Config::loadFile() {
configFile.close();
if (err) {
Log.error(F("CFG : Failed to parse " CFG_FILENAME " file, Err: %s, %d." CR),
err.c_str(), doc.capacity());
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to parse configuration (json)"));
return false;
}
@ -215,8 +212,17 @@ bool Config::loadFile() {
if (!doc[PARAM_PUSH_BREWFATHER].isNull())
setBrewfatherPushUrl(doc[PARAM_PUSH_BREWFATHER]);
if (!doc[PARAM_PUSH_HTTP].isNull()) setHttpPushUrl(doc[PARAM_PUSH_HTTP]);
if (!doc[PARAM_PUSH_HTTP2].isNull()) setHttpPushUrl2(doc[PARAM_PUSH_HTTP2]);
if (!doc[PARAM_TOKEN].isNull()) setToken(doc[PARAM_TOKEN]);
if (!doc[PARAM_PUSH_HTTP].isNull()) setHttpUrl(doc[PARAM_PUSH_HTTP]);
if (!doc[PARAM_PUSH_HTTP_H1].isNull())
setHttpHeader(doc[PARAM_PUSH_HTTP_H1], 0);
if (!doc[PARAM_PUSH_HTTP_H2].isNull())
setHttpHeader(doc[PARAM_PUSH_HTTP_H2], 1);
if (!doc[PARAM_PUSH_HTTP2].isNull()) setHttp2Url(doc[PARAM_PUSH_HTTP2]);
if (!doc[PARAM_PUSH_HTTP2_H1].isNull())
setHttp2Header(doc[PARAM_PUSH_HTTP2_H1], 0);
if (!doc[PARAM_PUSH_HTTP2_H2].isNull())
setHttp2Header(doc[PARAM_PUSH_HTTP2_H2], 1);
if (!doc[PARAM_PUSH_INFLUXDB2].isNull())
setInfluxDb2PushUrl(doc[PARAM_PUSH_INFLUXDB2]);
@ -287,7 +293,13 @@ bool Config::loadFile() {
if (!doc[PARAM_FORMULA_DATA]["g5"].isNull())
_formulaData.g[4] = doc[PARAM_FORMULA_DATA]["g5"].as<double>();
myConfig.debug();
/*if( doc[PARAM_CONFIG_VER].isNull() ) {
// If this parameter is missing we need to reset the gyrocalibaration due to bug #29
_gyroCalibration.ax = _gyroCalibration.ay = _gyroCalibration.az = 0;
_gyroCalibration.gx = _gyroCalibration.gy = _gyroCalibration.gz = 0;
Log.warning(F("CFG : Old configuration format, clearing gyro calibration." CR));
}*/
_saveNeeded = false; // Reset save flag
Log.notice(F("CFG : Configuration file " CFG_FILENAME " loaded." CR));
return true;
@ -317,34 +329,6 @@ void Config::checkFileSystem() {
}
}
//
// Dump the configuration to the serial port
//
void Config::debug() {
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
Log.verbose(F("CFG : Dumping configration " CFG_FILENAME "." CR));
Log.verbose(F("CFG : ID; '%s'." CR), getID());
Log.verbose(F("CFG : WIFI; '%s', '%s'." CR), getWifiSSID(), getWifiPass());
Log.verbose(F("CFG : mDNS; '%s'." CR), getMDNS());
Log.verbose(F("CFG : Sleep interval; %d." CR), getSleepInterval());
Log.verbose(F("CFG : OTA; '%s'." CR), getOtaURL());
Log.verbose(F("CFG : Temp Format; %c." CR), getTempFormat());
Log.verbose(F("CFG : Temp Adj; %F." CR), getTempSensorAdjC());
Log.verbose(F("CFG : VoltageFactor; %F." CR), getVoltageFactor());
Log.verbose(F("CFG : Gravity formula; '%s'." CR), getGravityFormula());
Log.verbose(F("CFG : Gravity format; '%c'." CR), getGravityFormat());
Log.verbose(F("CFG : Gravity temp adj; %s." CR),
isGravityTempAdj() ? "true" : "false");
Log.verbose(F("CFG : Gyro temp; %s." CR), isGyroTemp() ? "true" : "false");
Log.verbose(F("CFG : Push brewfather; '%s'." CR), getBrewfatherPushUrl());
Log.verbose(F("CFG : Push http; '%s'." CR), getHttpPushUrl());
Log.verbose(F("CFG : Push http2; '%s'." CR), getHttpPushUrl2());
Log.verbose(F("CFG : InfluxDb2; '%s', '%s', '%s', '%s'." CR),
getInfluxDb2PushUrl(), getInfluxDb2PushOrg(),
getInfluxDb2PushBucket(), getInfluxDb2PushToken());
#endif
}
//
// Save json document to file
//
@ -356,7 +340,8 @@ bool HardwareConfig::saveFile() {
File configFile = LittleFS.open(CFG_HW_FILENAME, "w");
if (!configFile) {
Log.error(F("CFG : Failed to open file " CFG_HW_FILENAME " for save." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to write hardware configuration "));
return false;
}
@ -399,7 +384,8 @@ bool HardwareConfig::loadFile() {
File configFile = LittleFS.open(CFG_HW_FILENAME, "r");
if (!configFile) {
Log.error(F("CFG : Failed to open " CFG_HW_FILENAME "." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to read hardware configuration "));
return false;
}
@ -415,9 +401,8 @@ bool HardwareConfig::loadFile() {
configFile.close();
if (err) {
Log.error(
F("CFG : Failed to parse " CFG_HW_FILENAME " file, Err: %s, %d." CR),
err.c_str(), doc.capacity());
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to parse hardware configuration (json)"));
return false;
}
@ -440,6 +425,8 @@ bool HardwareConfig::loadFile() {
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].as<float>());
if (!doc[PARAM_HW_WIFI_PORTALTIMEOUT].isNull())
this->setWifiPortalTimeout(doc[PARAM_HW_WIFI_PORTALTIMEOUT].as<int>());
if (!doc[PARAM_HW_PUSH_TIMEOUT].isNull())
this->setPushTimeout(doc[PARAM_HW_PUSH_TIMEOUT].as<int>());
Log.notice(F("CFG : Configuration file " CFG_HW_FILENAME " loaded." CR));
return true;

View File

@ -29,7 +29,7 @@ SOFTWARE.
#define CFG_JSON_BUFSIZE 3192
#define CFG_APPNAME "GravityMon " // Name of firmware
#define CFG_APPNAME "GravityMon" // Name of firmware
#define CFG_FILENAME "/gravitymon.json" // Name of config file
#define CFG_HW_FILENAME "/hardware.json" // Name of config file for hw
@ -60,6 +60,7 @@ class HardwareConfig {
int _gyroSensorMovingThreashold = 500;
int _gyroReadCount = 50;
int _gyroReadDelay = 3150; // us, empirical, to hold sampling to 200 Hz
int _pushTimeout = 10; // seconds
public:
int getWifiPortalTimeout() { return _wifiPortalTimeout; }
@ -78,6 +79,8 @@ class HardwareConfig {
void setGyroReadCount(int c) { _gyroReadCount = c; }
int getGyroReadDelay() { return _gyroReadDelay; }
void setGyroReadDelay(int d) { _gyroReadDelay = d; }
int getPushTimeout() { return _pushTimeout; }
void setPushTimeout(int t) { _pushTimeout = t; }
bool saveFile();
bool loadFile();
@ -85,48 +88,52 @@ class HardwareConfig {
class Config {
private:
bool _saveNeeded;
bool _saveNeeded = false;
int _configVersion = 2;
// Device configuration
String _id;
String _mDNS;
String _otaURL;
char _tempFormat;
float _voltageFactor;
float _tempSensorAdjC;
int _sleepInterval;
bool _gyroTemp;
String _id = "";
String _mDNS = "";
String _otaURL = "";
char _tempFormat = 'C';
float _voltageFactor = 0;
float _tempSensorAdjC = 0;
int _sleepInterval = 900;
bool _gyroTemp = false;
// Wifi Config
String _wifiSSID;
String _wifiPASS;
String _wifiSSID = "";
String _wifiPASS = "";
// Push target settings
String _brewfatherPushUrl;
String _brewfatherPushUrl = "";
String _httpPushUrl;
String _httpPushUrl2;
String _token = "";
String _influxDb2Url;
String _influxDb2Org;
String _influxDb2Bucket;
String _influxDb2Token;
String _httpUrl = "";
String _httpHeader[2] = {"Content-Type: application/json", ""};
String _http2Url = "";
String _http2Header[2] = {"Content-Type: application/json", ""};
String _mqttUrl;
int _mqttPort;
String _mqttUser;
String _mqttPass;
String _influxDb2Url = "";
String _influxDb2Org = "";
String _influxDb2Bucket = "";
String _influxDb2Token = "";
String _mqttUrl = "";
int _mqttPort = 1883;
String _mqttUser = "";
String _mqttPass = "";
// Gravity and temperature calculations
String _gravityFormula;
bool _gravityTempAdj;
char _gravityFormat;
String _gravityFormula = "";
bool _gravityTempAdj = false;
char _gravityFormat = 'G';
// Gyro calibration and formula calculation data
RawGyroData _gyroCalibration;
RawFormulaData _formulaData;
RawGyroData _gyroCalibration = {0, 0, 0, 0, 0, 0};
RawFormulaData _formulaData = {{0, 0, 0, 0, 0}, {1, 1, 1, 1, 1}};
void debug();
void formatFileSystem();
public:
@ -139,6 +146,8 @@ class Config {
_saveNeeded = true;
}
int getConfigVersion() { return _configVersion; }
const bool isGyroTemp() { return _gyroTemp; }
void setGyroTemp(bool b) {
_gyroTemp = b;
@ -151,6 +160,7 @@ class Config {
_saveNeeded = true;
}
bool isOtaActive() { return _otaURL.length() ? true : false; }
bool isOtaSSL() { return _otaURL.startsWith("https://"); }
const char* getWifiSSID() { return _wifiSSID.c_str(); }
void setWifiSSID(String s) {
@ -173,19 +183,39 @@ class Config {
return _brewfatherPushUrl.length() ? true : false;
}
// Token parameter
const char* getToken() { return _token.c_str(); }
void setToken(String s) {
_token = s;
_saveNeeded = true;
}
// Standard HTTP
const char* getHttpPushUrl() { return _httpPushUrl.c_str(); }
void setHttpPushUrl(String s) {
_httpPushUrl = s;
const char* getHttpUrl() { return _httpUrl.c_str(); }
void setHttpUrl(String s) {
_httpUrl = s;
_saveNeeded = true;
}
bool isHttpActive() { return _httpPushUrl.length() ? true : false; }
const char* getHttpPushUrl2() { return _httpPushUrl2.c_str(); }
void setHttpPushUrl2(String s) {
_httpPushUrl2 = s;
const char* getHttpHeader(int idx) { return _httpHeader[idx].c_str(); }
void setHttpHeader(String s, int idx) {
_httpHeader[idx] = s;
_saveNeeded = true;
}
bool isHttpActive2() { return _httpPushUrl2.length() ? true : false; }
bool isHttpActive() { return _httpUrl.length() ? true : false; }
bool isHttpSSL() { return _httpUrl.startsWith("https://"); }
const char* getHttp2Url() { return _http2Url.c_str(); }
void setHttp2Url(String s) {
_http2Url = s;
_saveNeeded = true;
}
const char* getHttp2Header(int idx) { return _http2Header[idx].c_str(); }
void setHttp2Header(String s, int idx) {
_http2Header[idx] = s;
_saveNeeded = true;
}
bool isHttp2Active() { return _http2Url.length() ? true : false; }
bool isHttp2SSL() { return _http2Url.startsWith("https://"); }
// InfluxDB2
const char* getInfluxDb2PushUrl() { return _influxDb2Url.c_str(); }
@ -211,12 +241,14 @@ class Config {
}
// MQTT
bool isMqttActive() { return _mqttUrl.length() ? true : false; }
const char* getMqttUrl() { return _mqttUrl.c_str(); }
void setMqttUrl(String s) {
_mqttUrl = s;
_saveNeeded = true;
}
bool isMqttActive() { return _mqttUrl.length() ? true : false; }
bool isMqttSSL() { return _mqttPort > 8000 ? true : false; }
int getMqttPort() { return _mqttPort; }
void setMqttPort(String s) {
_mqttPort = s.toInt();

View File

@ -44,7 +44,8 @@ bool GyroSensor::setup() {
// compilation difficulties
if (!accelgyro.testConnection()) {
Log.error(F("GYRO: Failed to connect to MPU6050 (gyro)." CR));
ErrorFileLog errLog;
errLog.addEntry(F("GYRO: Failed to connect to gyro, is it connected?"));
_sensorConnected = false;
} else {
#if !defined(GYRO_DISABLE_LOGGING)
@ -195,11 +196,15 @@ float GyroSensor::calculateAngle(RawGyroData &raw) {
az = (static_cast<float>(raw.az)) / 16384;
// Source: https://www.nxp.com/docs/en/application-note/AN3461.pdf
float v = (acos(ay / sqrt(ax * ax + ay * ay + az * az)) * 180.0 / PI);
float vY = (acos(abs(ay) / sqrt(ax * ax + ay * ay + az * az)) * 180.0 / PI);
//float vZ = (acos(abs(az) / sqrt(ax * ax + ay * ay + az * az)) * 180.0 / PI);
//float vX = (acos(abs(ax) / sqrt(ax * ax + ay * ay + az * az)) * 180.0 / PI);
#if LOG_LEVEL == 6 && !defined(GYRO_DISABLE_LOGGING)
Log.verbose(F("GYRO: angle = %F." CR), v);
//Log.notice(F("GYRO: angleX= %F." CR), vX);
Log.notice(F("GYRO: angleY= %F." CR), vY);
//Log.notice(F("GYRO: angleZ= %F." CR), vZ);
#endif
return v;
return vY;
}
//
@ -240,16 +245,16 @@ bool GyroSensor::read() {
// If the sensor is unstable we return false to signal we dont have valid
// value
if (isSensorMoving(_lastGyroData)) {
#if !defined(GYRO_DISABLE_LOGGING)
#if LOG_LEVEL == 6 && !defined(GYRO_DISABLE_LOGGING)
Log.notice(F("GYRO: Sensor is moving." CR));
#endif
_validValue = false;
} else {
_validValue = true;
_angle = calculateAngle(_lastGyroData);
#if !defined(GYRO_DISABLE_LOGGING)
Log.notice(F("GYRO: Sensor values %d,%d,%d\t%F" CR), _lastGyroData.ax,
_lastGyroData.ay, _lastGyroData.az, _angle);
#if LOG_LEVEL == 6 && !defined(GYRO_DISABLE_LOGGING)
Log.verbose(F("GYRO: Sensor values %d,%d,%d\t%F" CR), _lastGyroData.ax,
_lastGyroData.ay, _lastGyroData.az, _angle);
#endif
}
@ -286,7 +291,9 @@ void GyroSensor::applyCalibration() {
if ((_calibrationOffset.ax + _calibrationOffset.ay + _calibrationOffset.az +
_calibrationOffset.gx + _calibrationOffset.gy + _calibrationOffset.gz) ==
0) {
Log.error(F("GYRO: No valid calibraion values exist, aborting." CR));
ErrorFileLog errLog;
errLog.addEntry(
F("GYRO: No valid calibration values, please calibrate the device."));
return;
}

View File

@ -21,12 +21,12 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#if defined (ESP8266)
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#else // defined (ESP32)
#include <WiFi.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#include <WiFi.h>
#endif
#include <config.hpp>
@ -39,6 +39,14 @@ SOFTWARE.
SerialDebug mySerial;
BatteryVoltage myBatteryVoltage;
// tcp cleanup, to avoid memory crash.
struct tcp_pcb;
extern struct tcp_pcb* tcp_tw_pcbs;
extern "C" void tcp_abort(struct tcp_pcb* pcb);
void tcp_cleanup() {
while (tcp_tw_pcbs) tcp_abort(tcp_tw_pcbs);
}
//
// Convert sg to plato
//
@ -59,20 +67,108 @@ float convertCtoF(float c) { return (c * 1.8) + 32.0; }
//
float convertFtoC(float f) { return (f - 32.0) / 1.8; }
//
// Load error log from disk
//
ErrorFileLog::ErrorFileLog() {
File errFile = LittleFS.open(ERR_FILENAME, "r");
int i = 0;
if (errFile) {
do {
_errors[i] = errFile.readStringUntil('\n');
_errors[i].replace("\r", "");
_errors[i].replace("\n", "");
} while (_errors[i++].length());
errFile.close();
}
}
//
// Add new entry to top of error log
//
void ErrorFileLog::addEntry(String err) {
for (int i = (ERR_COUNT - 1); i > 0; i--) {
_errors[i] = _errors[i - 1];
}
_errors[0] = err;
Log.errorln(err.c_str());
save();
}
//
// Save error log
//
void ErrorFileLog::save() {
File errFile = LittleFS.open(ERR_FILENAME, "w");
if (errFile) {
for (int i = 0; i < ERR_COUNT; i++) {
errFile.println(_errors[i]);
}
errFile.close();
}
}
//
// Load history log of floats
//
FloatHistoryLog::FloatHistoryLog(String fName) {
_fName = fName;
File runFile = LittleFS.open(_fName, "r");
if (runFile) {
for (int i = 0; i < 10; i++) {
_runTime[i] = runFile.readStringUntil('\n').toFloat();
if (_runTime[i]) {
_average += _runTime[i];
_count++;
}
}
runFile.close();
_average = _average / _count;
}
}
//
// Add entry to top of log
//
void FloatHistoryLog::addEntry(float time) {
for (int i = (10 - 1); i > 0; i--) {
_runTime[i] = _runTime[i - 1];
}
_runTime[0] = time;
save();
}
//
// Save log
//
void FloatHistoryLog::save() {
File runFile = LittleFS.open(_fName, "w");
if (runFile) {
for (int i = 0; i < 10; i++) {
runFile.println(_runTime[i], 2);
}
runFile.close();
}
}
//
// Print the heap information.
//
void printHeap() {
#if LOG_LEVEL == 6 && !defined(HELPER_DISABLE_LOGGING)
#if defined (ESP8266)
Log.verbose(F("HELP: Heap %d kb, HeapFrag %d %%, FreeSketch %d kb." CR),
ESP.getFreeHeap() / 1024, ESP.getHeapFragmentation(),
ESP.getFreeSketchSpace() / 1024);
#else // defined (ESP32)
void printHeap(String prefix) {
#if defined(ESP8266)
Log.notice(
F("%s: Free-heap %d kb, Heap-rag %d %%, Max-block %d kb Stack=%d b." CR),
prefix.c_str(), ESP.getFreeHeap() / 1024, ESP.getHeapFragmentation(),
ESP.getMaxFreeBlockSize() / 1024, ESP.getFreeContStack());
// Log.notice(F("%s: Heap %d kb, HeapFrag %d %%, FreeSketch %d kb." CR),
// prefix.c_str(), ESP.getFreeHeap() / 1024,
// ESP.getHeapFragmentation(), ESP.getFreeSketchSpace() / 1024);
#else // defined (ESP32)
Log.verbose(F("HELP: Heap %d kb, FreeSketch %d kb." CR),
ESP.getFreeHeap() / 1024, ESP.getFreeSketchSpace() / 1024);
#endif
#endif
}
//
@ -141,11 +237,11 @@ void BatteryVoltage::read() {
// An ESP8266 has a ADC range of 0-1023 and a maximum voltage of 3.3V
// An ESP32 has an ADC range of 0-4095 and a maximum voltage of 3.3V
#if defined (ESP8266)
#if defined(ESP8266)
_batteryLevel = ((3.3 / 1023) * v) * factor;
#else // defined (ESP32)
#else // defined (ESP32)
_batteryLevel = ((3.3 / 4095) * v) * factor;
#endif
#endif
#if LOG_LEVEL == 6 && !defined(HELPER_DISABLE_LOGGING)
Log.verbose(
F("BATT: Reading voltage level. Factor=%F Value=%d, Voltage=%F." CR),
@ -217,22 +313,24 @@ void PerfLogging::print() {
void PerfLogging::pushInflux() {
if (!myConfig.isInfluxDb2Active()) return;
WiFiClient wifi;
HTTPClient http;
String serverPath =
String(myConfig.getInfluxDb2PushUrl()) +
"/api/v2/write?org=" + String(myConfig.getInfluxDb2PushOrg()) +
"&bucket=" + String(myConfig.getInfluxDb2PushBucket());
http.begin(myWifi.getWifiClient(), serverPath);
http.begin(wifi, serverPath);
// Create body for influxdb2, format used
// key,host=mdns value=0.0
String body;
body.reserve(500);
// Create the payload with performance data.
// ------------------------------------------------------------------------------------------
PerfEntry* pe = first;
char buf[100];
char buf[150];
snprintf(&buf[0], sizeof(buf), "perf,host=%s,device=%s ", myConfig.getMDNS(),
myConfig.getID());
body += &buf[0];
@ -254,15 +352,23 @@ void PerfLogging::pushInflux() {
snprintf(&buf[0], sizeof(buf), "\ndebug,host=%s,device=%s ",
myConfig.getMDNS(), myConfig.getID());
body += &buf[0];
#if defined (ESP8266)
snprintf(
&buf[0], sizeof(buf),
"angle=%.4f,gyro-ax=%d,gyro-ay=%d,gyro-az=%d,gyro-temp=%.2f,ds-temp=%.2f",
"angle=%.4f,gyro-ax=%d,gyro-ay=%d,gyro-az=%d,gyro-temp=%.2f,ds-temp=%.2f,heap=%d,heap-frag=%d,heap-max=%d,stack=%d",
myGyro.getAngle(), myGyro.getLastGyroData().ax,
myGyro.getLastGyroData().ay, myGyro.getLastGyroData().az,
myGyro.getSensorTempC(), myTempSensor.getTempC(myConfig.isGyroTemp()));
body += &buf[0];
myGyro.getSensorTempC(), myTempSensor.getTempC(myConfig.isGyroTemp()), ESP.getFreeHeap(), ESP.getHeapFragmentation(), ESP.getMaxFreeBlockSize(), ESP.getFreeContStack());
#else // defined (ESP32)
snprintf(
&buf[0], sizeof(buf),
"angle=%.4f,gyro-ax=%d,gyro-ay=%d,gyro-az=%d,gyro-temp=%.2f,ds-temp=%.2f,heap=%d,heap-frag=%d,heap-max=%d",
myGyro.getAngle(), myGyro.getLastGyroData().ax,
myGyro.getLastGyroData().ay, myGyro.getLastGyroData().az,
myGyro.getSensorTempC(), myTempSensor.getTempC(myConfig.isGyroTemp()), ESP.getFreeHeap(), 0, ESP.getMaxAllocHeap());
#endif
// Log.notice(F("PERF: data %s." CR), body.c_str() );
body += &buf[0];
#if LOG_LEVEL == 6 && !defined(HELPER_DISABLE_LOGGING)
Log.verbose(F("PERF: url %s." CR), serverPath.c_str());
@ -272,6 +378,7 @@ void PerfLogging::pushInflux() {
// Send HTTP POST request
String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
http.addHeader(F("Authorization"), auth.c_str());
http.setTimeout(myHardwareConfig.getPushTimeout());
int httpResponseCode = http.POST(body);
if (httpResponseCode == 204) {
@ -286,7 +393,8 @@ void PerfLogging::pushInflux() {
}
http.end();
myWifi.closeWifiClient();
wifi.stop();
tcp_cleanup();
}
#endif // COLLECT_PERFDATA
@ -315,52 +423,59 @@ float reduceFloatPrecision(float f, int dec) {
// https://circuits4you.com/2019/03/21/esp8266-url-encode-decode-example/
//
String urlencode(String str) {
String encodedString = "";
String encodedString;
encodedString.reserve(str.length()*2);
encodedString = "";
char c;
char code0;
char code1;
for (int i =0; i < static_cast<int>(str.length()); i++) {
for (int i = 0; i < static_cast<int>(str.length()); i++) {
c = str.charAt(i);
if (isalnum(c)){
if (isalnum(c)) {
encodedString += c;
} else {
code1 = (c & 0xf) + '0';
if ((c & 0xf) >9) {
code1 = (c & 0xf) - 10 + 'A';
if ((c & 0xf) > 9) {
code1 = (c & 0xf) - 10 + 'A';
}
c = (c>>4) & 0xf;
c = (c >> 4) & 0xf;
code0 = c + '0';
if (c > 9) {
code0 = c - 10 + 'A';
code0 = c - 10 + 'A';
}
encodedString += '%';
encodedString += code0;
encodedString += code1;
}
}
//Log.verbose(F("HELP: encode=%s" CR), encodedString.c_str());
return encodedString;
// Log.verbose(F("HELP: encode=%s" CR), encodedString.c_str());
return encodedString;
}
unsigned char h2int(char c) {
if (c >= '0' && c <='9') {
return((unsigned char)c - '0');
if (c >= '0' && c <= '9') {
return ((unsigned char)c - '0');
}
if (c >= 'a' && c <='f') {
return((unsigned char)c - 'a' + 10);
if (c >= 'a' && c <= 'f') {
return ((unsigned char)c - 'a' + 10);
}
if (c >= 'A' && c <='F') {
return((unsigned char)c - 'A' + 10);
if (c >= 'A' && c <= 'F') {
return ((unsigned char)c - 'A' + 10);
}
return(0);
return (0);
}
//
// urlencode string
//
String urldecode(String str) {
String encodedString = "";
String encodedString;
encodedString.reserve(str.length());
encodedString = "";
char c;
char code0;
char code1;
for (int i = 0; i < static_cast<int>(str.length()); i++){
for (int i = 0; i < static_cast<int>(str.length()); i++) {
c = str.charAt(i);
if (c == '%') {
i++;
@ -372,9 +487,9 @@ String urldecode(String str) {
} else {
encodedString += c;
}
}
}
//Log.verbose(F("HELP: decode=%s" CR), encodedString.c_str());
// Log.verbose(F("HELP: decode=%s" CR), encodedString.c_str());
return encodedString;
}

View File

@ -27,6 +27,14 @@ SOFTWARE.
// Includes
#include <main.hpp>
#define ERR_FILENAME "/error.log"
#define ERR_COUNT 15
#define RUNTIME_FILENAME "/runtime.log"
// tcp cleanup
void tcp_cleanup();
// Sleep mode
void deepSleep(int t);
@ -50,7 +58,7 @@ float reduceFloatPrecision(float f, int dec = 2);
// Logging via serial
void printTimestamp(Print* _logOutput, int _logLevel);
void printNewline(Print* _logOutput);
void printHeap();
void printHeap(String prefix = "HELP");
// Classes
class SerialDebug {
@ -59,6 +67,30 @@ class SerialDebug {
static Logging* getLog() { return &Log; }
};
class ErrorFileLog {
private:
String _errors[ERR_COUNT];
public:
ErrorFileLog();
void addEntry(String error);
void save();
};
class FloatHistoryLog {
private:
String _fName;
float _average = 0;
float _runTime[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
int _count = 0;
void save();
public:
explicit FloatHistoryLog(String fName);
void addEntry(float time);
float getAverage() { return _average; }
};
class BatteryVoltage {
private:
float _batteryLevel;
@ -138,7 +170,7 @@ extern PerfLogging myPerfLogging;
// Use these to collect performance data from various parts of the code
#define LOG_PERF_START(s) myPerfLogging.start(s)
#define LOG_PERF_STOP(s) myPerfLogging.stop(s)
// #define LOG_PERF_PRINT() myPerfLogging.print()
// #define LOG_PERF_PRINT() myPerfLogging.print()
#define LOG_PERF_PRINT()
#define LOG_PERF_CLEAR() myPerfLogging.clear()
#define LOG_PERF_PUSH() myPerfLogging.pushInflux()

View File

@ -40,6 +40,7 @@ int interval = 200; // ms, time to wait between changes to output
bool sleepModeAlwaysSkip =
false; // Flag set in web interface to override normal behaviour
uint32_t loopMillis = 0; // Used for main loop to run the code every _interval_
uint32_t pushMillis = 0; // Used to control how often we will send push data
uint32_t runtimeMillis; // Used to calculate the total time since start/wakeup
uint32_t stableGyroMillis; // Used to calculate the total time since last
// stable gyro reading
@ -110,19 +111,19 @@ void setup() {
#if LOG_LEVEL == 6 && !defined(MAIN_DISABLE_LOGGING)
// Add a delay so that serial is started.
// delay(3000);
#if defined (ESP8266)
#if defined(ESP8266)
Log.verbose(F("Main: Reset reason %s." CR), ESP.getResetInfo().c_str());
#else // defined (ESP32)
#else // defined (ESP32)
#endif
#endif
// Main startup
#if defined (ESP8266)
#if defined(ESP8266)
Log.notice(F("Main: Started setup for %s." CR),
String(ESP.getChipId(), HEX).c_str());
#else // defined (ESP32)
#else // defined (ESP32)
char buf[20];
uint32_t chipId = 0;
for (int i = 0; i < 17; i = i+8) {
for (int i = 0; i < 17; i = i + 8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
snprintf(&buf[0], sizeof(buf), "%6x", chipId);
@ -138,10 +139,10 @@ void setup() {
LOG_PERF_STOP("main-config-load");
// Setup watchdog
#if defined (ESP8266)
#if defined(ESP8266)
ESP.wdtDisable();
ESP.wdtEnable(5000); // 5 seconds
#else // defined (ESP32)
#else // defined (ESP32)
#endif
// No stored config, move to portal
@ -173,7 +174,9 @@ void setup() {
LOG_PERF_STOP("main-temp-setup");
if (!myGyro.setup()) {
Log.error(F("Main: Failed to initialize the gyro." CR));
ErrorFileLog errLog;
errLog.addEntry(
F("MAIN: Failed to initialize the gyro, is it connected?"));
} else {
LOG_PERF_START("main-gyro-read");
myGyro.read();
@ -207,7 +210,8 @@ void setup() {
LOG_PERF_STOP("main-setup");
Log.notice(F("Main: Setup completed." CR));
stableGyroMillis = millis(); // Dont include time for wifi connection
pushMillis = stableGyroMillis =
millis(); // Dont include time for wifi connection
}
//
@ -244,15 +248,24 @@ bool loopReadGravity() {
angle, tempC, gravity, corrGravity);
#endif
LOG_PERF_START("loop-push");
// Force the transmission if we are going to sleep
myPushTarget.send(angle, gravitySG, corrGravitySG, tempC,
(millis() - runtimeMillis) / 1000,
runMode == RunMode::gravityMode ? true : false);
LOG_PERF_STOP("loop-push");
bool pushExpired = (abs((int32_t)(millis() - pushMillis)) >
(myConfig.getSleepInterval() * 1000));
if (pushExpired || runMode == RunMode::gravityMode) {
pushMillis = millis();
LOG_PERF_START("loop-push");
PushTarget push;
push.send(angle, gravitySG, corrGravitySG, tempC,
(millis() - runtimeMillis) / 1000);
LOG_PERF_STOP("loop-push");
// Send stats to influx after each push run.
if (runMode == RunMode::configurationMode) {
LOG_PERF_PUSH();
}
}
return true;
} else {
Log.error(F("Main: No gyro value." CR));
Log.error(F("MAIN: No gyro value found, the device might be moving."));
}
return false;
}
@ -265,7 +278,7 @@ void loopGravityOnInterval() {
if (abs((int32_t)(millis() - loopMillis)) > interval) {
loopReadGravity();
loopMillis = millis();
printHeap();
// printHeap("MAIN");
LOG_PERF_START("loop-gyro-read");
myGyro.read();
LOG_PERF_STOP("loop-gyro-read");
@ -274,6 +287,8 @@ void loopGravityOnInterval() {
}
}
bool skipRunTimeLog = false;
//
// Main loop that determines if device should go to sleep
//
@ -281,6 +296,11 @@ void goToSleep(int sleepInterval) {
float volt = myBatteryVoltage.getVoltage();
float runtime = (millis() - runtimeMillis);
if (!skipRunTimeLog) {
FloatHistoryLog runLog(RUNTIME_FILENAME);
runLog.addEntry(runtime);
}
Log.notice(F("MAIN: Entering deep sleep for %ds, run time %Fs, "
"battery=%FV." CR),
sleepInterval, reduceFloatPrecision(runtime / 1000, 2), volt);
@ -301,6 +321,9 @@ void loop() {
myWebServerHandler.loop();
myWifi.loop();
loopGravityOnInterval();
// If we switched mode, dont include this in the log.
if (runMode != RunMode::configurationMode) skipRunTimeLog = true;
break;
case RunMode::gravityMode:

View File

@ -22,10 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#include <ESP8266mDNS.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#endif
#include <MQTT.h>
@ -34,35 +32,14 @@ SOFTWARE.
#include <pushtarget.hpp>
#include <wifi.hpp>
PushTarget myPushTarget;
//
// Send the data to targets
//
void PushTarget::send(float angle, float gravitySG, float corrGravitySG,
float tempC, float runTime, bool force) {
uint32_t timePassed = abs((int32_t)(millis() - _ms));
uint32_t interval = myConfig.getSleepInterval() * 1000;
if ((timePassed < interval) && !force) {
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: Timer has not expired %l vs %l." CR), timePassed,
interval);
#endif
return;
}
_ms = millis();
#if defined(ESP8266)
if (ESP.getFreeContStack() < 1500) {
Log.error(F("PUSH: Low on memory, skipping push since it will crasch. "
"(stack=%d, heap=%d)." CR),
ESP.getFreeContStack(), ESP.getFreeHeap());
myWifi.closeWifiClient();
return;
}
#endif
float tempC, float runTime) {
printHeap("PUSH");
http.setReuse(false);
httpSecure.setReuse(false);
TemplatingEngine engine;
engine.initialize(angle, gravitySG, corrGravitySG, tempC, runTime);
@ -75,13 +52,13 @@ void PushTarget::send(float angle, float gravitySG, float corrGravitySG,
if (myConfig.isHttpActive()) {
LOG_PERF_START("push-http");
sendHttp(engine, 0);
sendHttp(engine, myConfig.isHttpSSL(), 0);
LOG_PERF_STOP("push-http");
}
if (myConfig.isHttpActive2()) {
if (myConfig.isHttp2Active()) {
LOG_PERF_START("push-http2");
sendHttp(engine, 1);
sendHttp(engine, myConfig.isHttp2SSL(), 1);
LOG_PERF_STOP("push-http2");
}
@ -93,11 +70,9 @@ void PushTarget::send(float angle, float gravitySG, float corrGravitySG,
if (myConfig.isMqttActive()) {
LOG_PERF_START("push-mqtt");
sendMqtt(engine);
sendMqtt(engine, myConfig.isMqttSSL());
LOG_PERF_STOP("push-mqtt");
}
LOG_PERF_PUSH();
}
//
@ -114,15 +89,14 @@ void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
"&bucket=" + String(myConfig.getInfluxDb2PushBucket());
String doc = engine.create(TemplatingEngine::TEMPLATE_INFLUX);
HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath);
http.begin(wifi, serverPath);
http.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: data %s." CR), doc.c_str());
#endif
// Send HTTP POST request
String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
http.addHeader(F("Authorization"), auth.c_str());
int httpResponseCode = http.POST(doc);
@ -131,12 +105,14 @@ void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
Log.notice(F("PUSH: InfluxDB2 push successful, response=%d" CR),
httpResponseCode);
} else {
Log.error(F("PUSH: InfluxDB2 push failed, response=%d" CR),
httpResponseCode);
ErrorFileLog errLog;
errLog.addEntry("PUSH: Influxdb push failed response=" +
String(httpResponseCode));
}
http.end();
myWifi.closeWifiClient();
wifi.stop();
tcp_cleanup();
}
//
@ -150,16 +126,14 @@ void PushTarget::sendBrewfather(TemplatingEngine& engine) {
String serverPath = myConfig.getBrewfatherPushUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_BREWFATHER);
// Your Domain name with URL path or IP address with path
HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath);
http.begin(wifi, serverPath);
http.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json"));
int httpResponseCode = http.POST(doc);
@ -167,83 +141,151 @@ void PushTarget::sendBrewfather(TemplatingEngine& engine) {
Log.notice(F("PUSH: Brewfather push successful, response=%d" CR),
httpResponseCode);
} else {
Log.error(F("PUSH: Brewfather push failed, response=%d" CR),
httpResponseCode);
ErrorFileLog errLog;
errLog.addEntry("PUSH: Brewfather push failed response=" +
String(httpResponseCode));
}
http.end();
myWifi.closeWifiClient();
wifi.stop();
tcp_cleanup();
}
//
//
//
void PushTarget::addHttpHeader(HTTPClient& http, String header) {
if (!header.length()) return;
int i = header.indexOf(":");
if (i) {
String name = header.substring(0, i);
String value = header.substring(i + 1);
value.trim();
Log.notice(F("PUSH: Adding header '%s': '%s'" CR), name.c_str(),
value.c_str());
http.addHeader(name, value);
} else {
ErrorFileLog errLog;
errLog.addEntry("PUSH: Unable to set header, invalid value " + header);
}
}
//
// Send data to http target
//
void PushTarget::sendHttp(TemplatingEngine& engine, int index) {
void PushTarget::sendHttp(TemplatingEngine& engine, bool isSecure, int index) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to http (%s)" CR),
index ? "http2" : "http1");
index ? "http2" : "http");
#endif
String serverPath, doc;
if (index == 0) {
serverPath = myConfig.getHttpPushUrl();
serverPath = myConfig.getHttpUrl();
doc = engine.create(TemplatingEngine::TEMPLATE_HTTP1);
} else {
serverPath = myConfig.getHttpPushUrl2();
serverPath = myConfig.getHttp2Url();
doc = engine.create(TemplatingEngine::TEMPLATE_HTTP2);
}
HTTPClient http;
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
http.begin(myWifi.getWifiClientSecure(), serverPath);
} else {
http.begin(myWifi.getWifiClient(), serverPath);
}
int httpResponseCode;
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json"));
int httpResponseCode = http.POST(doc);
if (isSecure) {
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
wifiSecure.setInsecure();
#if defined (ESP8266)
String host = serverPath.substring(8); // remove the prefix or the probe will fail, it needs a pure host name.
int idx = host.indexOf("/");
if (idx!=-1)
host = host.substring(0, idx);
if (wifiSecure.probeMaxFragmentLength(host, 443, 512)) {
Log.notice(F("PUSH: HTTP server supports smaller SSL buffer." CR));
wifiSecure.setBufferSizes(512, 512);
}
#endif
httpSecure.begin(wifiSecure, serverPath);
httpSecure.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
if (index == 0) {
addHttpHeader(httpSecure, myConfig.getHttpHeader(0));
addHttpHeader(httpSecure, myConfig.getHttpHeader(1));
} else {
addHttpHeader(httpSecure, myConfig.getHttp2Header(0));
addHttpHeader(httpSecure, myConfig.getHttp2Header(1));
}
httpResponseCode = httpSecure.POST(doc);
} else {
http.begin(wifi, serverPath);
http.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
if (index == 0) {
addHttpHeader(http, myConfig.getHttpHeader(0));
addHttpHeader(http, myConfig.getHttpHeader(1));
} else {
addHttpHeader(http, myConfig.getHttp2Header(0));
addHttpHeader(http, myConfig.getHttp2Header(1));
}
httpResponseCode = http.POST(doc);
}
if (httpResponseCode == 200) {
Log.notice(F("PUSH: HTTP push successful, response=%d" CR),
httpResponseCode);
} else {
Log.error(F("PUSH: HTTP push failed, response=%d" CR), httpResponseCode);
ErrorFileLog errLog;
errLog.addEntry(
"PUSH: HTTP push failed response=" + String(httpResponseCode) +
String(index == 0 ? " (http)" : " (http2)"));
}
http.end();
myWifi.closeWifiClient();
if (isSecure) {
httpSecure.end();
wifiSecure.stop();
} else {
http.end();
wifi.stop();
}
tcp_cleanup();
}
//
// Send data to http target
//
void PushTarget::sendMqtt(TemplatingEngine& engine) {
void PushTarget::sendMqtt(TemplatingEngine& engine, bool isSecure) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to mqtt." CR));
#endif
MQTTClient mqtt(512);
String url = myConfig.getMqttUrl();
String host = myConfig.getMqttUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_MQTT);
int port = myConfig.getMqttPort();
// if (url.endsWith(":8883")) {
if (port > 8000) {
// Allow secure channel, but without certificate validation
myWifi.getWifiClientSecure().setInsecure();
if (myConfig.isMqttSSL()) {
Log.notice(F("PUSH: MQTT, SSL enabled without validation." CR));
mqtt.begin(url.c_str(), port, myWifi.getWifiClientSecure());
wifiSecure.setInsecure();
#if defined (ESP8266)
if (wifiSecure.probeMaxFragmentLength(host, port, 512)) {
Log.notice(F("PUSH: MQTT server supports smaller SSL buffer." CR));
wifiSecure.setBufferSizes(512, 512);
}
#endif
mqtt.begin(host.c_str(), port, wifiSecure);
} else {
mqtt.begin(myConfig.getMqttUrl(), port, myWifi.getWifiClient());
mqtt.begin(host.c_str(), port, wifi);
}
mqtt.connect(myConfig.getMDNS(), myConfig.getMqttUser(),
@ -255,7 +297,7 @@ void PushTarget::sendMqtt(TemplatingEngine& engine) {
#endif
// Send MQQT message(s)
mqtt.setTimeout(10); // 10 seconds timeout
mqtt.setTimeout(myHardwareConfig.getPushTimeout()); // 10 seconds timeout
int lines = 1;
// Find out how many lines are in the document. Each line is one
@ -279,8 +321,9 @@ void PushTarget::sendMqtt(TemplatingEngine& engine) {
if (mqtt.publish(topic, value)) {
Log.notice(F("PUSH: MQTT publish successful on %s" CR), topic.c_str());
} else {
Log.error(F("PUSH: MQTT publish failed err=%d, ret=%d" CR),
mqtt.lastError(), mqtt.returnCode());
ErrorFileLog errLog;
errLog.addEntry("PUSH: MQTT push on " + topic +
" failed error=" + String(mqtt.lastError()));
}
index = next + 1;
@ -288,7 +331,12 @@ void PushTarget::sendMqtt(TemplatingEngine& engine) {
}
mqtt.disconnect();
myWifi.closeWifiClient();
if (isSecure) {
wifiSecure.stop();
} else {
wifi.stop();
}
tcp_cleanup();
}
// EOF

View File

@ -26,23 +26,31 @@ SOFTWARE.
#include <templating.hpp>
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#endif
class PushTarget {
private:
uint32_t _ms; // Used to check that we do not post to often
WiFiClient wifi;
WiFiClientSecure wifiSecure;
HTTPClient http;
HTTPClient httpSecure;
void sendBrewfather(TemplatingEngine& engine);
void sendHttp(TemplatingEngine& engine, int index);
void sendHttp(TemplatingEngine& engine, bool isSecure, int index);
void sendInfluxDb2(TemplatingEngine& engine);
void sendMqtt(TemplatingEngine& engine);
void sendMqtt(TemplatingEngine& engine, bool isSecure);
void addHttpHeader(HTTPClient& http, String header);
public:
PushTarget() { _ms = millis(); }
void send(float angle, float gravitySG, float corrGravitySG, float tempC,
float runTime, bool force = false);
float runTime);
};
extern PushTarget myPushTarget;
#endif // SRC_PUSHTARGET_HPP_
// EOF

View File

@ -27,12 +27,19 @@ SOFTWARE.
// Common strings used in json formats.
#define PARAM_ID "id"
#define PARAM_MDNS "mdns"
#define PARAM_CONFIG_VER "config-version"
#define PARAM_OTA "ota-url"
#define PARAM_SSID "wifi-ssid"
#define PARAM_PASS "wifi-pass"
#define PARAM_RUNTIME_AVERAGE "runtime-average"
#define PARAM_PUSH_BREWFATHER "brewfather-push"
#define PARAM_TOKEN "token"
#define PARAM_PUSH_HTTP "http-push"
#define PARAM_PUSH_HTTP_H1 "http-push-h1"
#define PARAM_PUSH_HTTP_H2 "http-push-h2"
#define PARAM_PUSH_HTTP2 "http-push2"
#define PARAM_PUSH_HTTP2_H1 "http-push2-h1"
#define PARAM_PUSH_HTTP2_H2 "http-push2-h2"
#define PARAM_PUSH_INFLUXDB2 "influxdb2-push"
#define PARAM_PUSH_INFLUXDB2_ORG "influxdb2-org"
#define PARAM_PUSH_INFLUXDB2_BUCKET "influxdb2-bucket"
@ -51,6 +58,9 @@ SOFTWARE.
#define PARAM_GYRO_CALIBRATION "gyro-calibration-data"
#define PARAM_GYRO_TEMP "gyro-temp"
#define PARAM_FORMULA_DATA "formula-calculation-data"
#define PARAM_FILES "files"
#define PARAM_FILE_NAME "file-name"
#define PARAM_FILE_SIZE "file-size"
#define PARAM_APP_NAME "app-name"
#define PARAM_APP_VER "app-ver"
#define PARAM_ANGLE "angle"
@ -67,6 +77,7 @@ SOFTWARE.
#define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation"
#define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp"
#define PARAM_HW_WIFI_PORTALTIMEOUT "wifi-portaltimeout"
#define PARAM_HW_PUSH_TIMEOUT "push-timeout"
#define PARAM_FORMAT_HTTP1 "http-1"
#define PARAM_FORMAT_HTTP2 "http-2"
#define PARAM_FORMAT_BREWFATHER "brewfather"

View File

@ -35,7 +35,7 @@ const char iSpindleFormat[] PROGMEM =
"{"
"\"name\" : \"${mdns}\", "
"\"ID\": \"${id}\", "
"\"token\" : \"gravmon\", "
"\"token\" : \"${token}\", "
"\"interval\": ${sleep-interval}, "
"\"temperature\": ${temp}, "
"\"temp-units\": \"${temp-unit}\", "
@ -88,6 +88,7 @@ void TemplatingEngine::initialize(float angle, float gravitySG, float corrGravit
// Names
setVal(TPL_MDNS, myConfig.getMDNS());
setVal(TPL_ID, myConfig.getID());
setVal(TPL_TOKEN, myConfig.getToken());
// Temperature
if (myConfig.isTempC()) {
@ -138,6 +139,7 @@ void TemplatingEngine::initialize(float angle, float gravitySG, float corrGravit
//
const String& TemplatingEngine::create(TemplatingEngine::Templates idx) {
String fname;
baseTemplate.reserve(600);
// Load templates from memory
switch (idx) {

View File

@ -26,30 +26,32 @@ SOFTWARE.
// Includes
#include <Arduino.h>
#include <main.hpp>
#include <helper.hpp>
#include <algorithm>
#include <helper.hpp>
#include <main.hpp>
// Templating variables
#define TPL_MDNS "${mdns}"
#define TPL_ID "${id}"
#define TPL_TOKEN "${token}"
#define TPL_SLEEP_INTERVAL "${sleep-interval}"
#define TPL_TEMP "${temp}"
#define TPL_TEMP_C "${temp-c}"
#define TPL_TEMP_F "${temp-f}"
#define TPL_TEMP_UNITS "${temp-unit}" // C or F
#define TPL_TEMP_UNITS "${temp-unit}" // C or F
#define TPL_BATTERY "${battery}"
#define TPL_RSSI "${rssi}"
#define TPL_RUN_TIME "${run-time}"
#define TPL_ANGLE "${angle}"
#define TPL_TILT "${tilt}" // same as angle
#define TPL_TILT "${tilt}" // same as angle
#define TPL_GRAVITY "${gravity}"
#define TPL_GRAVITY_G "${gravity-sg}"
#define TPL_GRAVITY_P "${gravity-plato}"
#define TPL_GRAVITY_CORR "${corr-gravity}"
#define TPL_GRAVITY_CORR_G "${corr-gravity-sg}"
#define TPL_GRAVITY_CORR_P "${corr-gravity-plato}"
#define TPL_GRAVITY_UNIT "${gravity-unit}" // G or P
#define TPL_GRAVITY_UNIT "${gravity-unit}" // G or P
#define TPL_FNAME_HTTP1 "/http-1.tpl"
#define TPL_FNAME_HTTP2 "/http-2.tpl"
@ -70,36 +72,29 @@ class TemplatingEngine {
String val;
};
KeyVal items[19] = {
{ TPL_MDNS, "" },
{ TPL_ID, "" },
{ TPL_SLEEP_INTERVAL, "" },
{ TPL_TEMP, "" },
{ TPL_TEMP_C, "" },
{ TPL_TEMP_F, "" },
{ TPL_TEMP_UNITS, "" },
{ TPL_BATTERY, "" },
{ TPL_RSSI, "" },
{ TPL_RUN_TIME, "" },
{ TPL_ANGLE, "" },
{ TPL_TILT, "" },
{ TPL_GRAVITY, "" },
{ TPL_GRAVITY_G, "" },
{ TPL_GRAVITY_P, "" },
{ TPL_GRAVITY_CORR, "" },
{ TPL_GRAVITY_CORR_G, "" },
{ TPL_GRAVITY_CORR_P, "" },
{ TPL_GRAVITY_UNIT, "" }
};
KeyVal items[20] = {{TPL_MDNS, ""}, {TPL_ID, ""},
{TPL_SLEEP_INTERVAL, ""}, {TPL_TEMP, ""},
{TPL_TEMP_C, ""}, {TPL_TEMP_F, ""},
{TPL_TEMP_UNITS, ""}, {TPL_BATTERY, ""},
{TPL_RSSI, ""}, {TPL_RUN_TIME, ""},
{TPL_ANGLE, ""}, {TPL_TILT, ""},
{TPL_GRAVITY, ""}, {TPL_GRAVITY_G, ""},
{TPL_GRAVITY_P, ""}, {TPL_GRAVITY_CORR, ""},
{TPL_GRAVITY_CORR_G, ""}, {TPL_GRAVITY_CORR_P, ""},
{TPL_GRAVITY_UNIT, ""}, {TPL_TOKEN, ""}};
char buffer[20];
String baseTemplate;
void setVal(String key, float val, int dec = 2) { String s = convertFloatToString(val, &buffer[0], dec); s.trim(); setVal(key, s); }
void setVal(String key, float val, int dec = 2) {
String s = convertFloatToString(val, &buffer[0], dec);
s.trim();
setVal(key, s);
}
void setVal(String key, int val) { setVal(key, String(val)); }
void setVal(String key, char val) { setVal(key, String(val)); }
void setVal(String key, String val) {
int max = sizeof(items)/sizeof(KeyVal);
int max = sizeof(items) / sizeof(KeyVal);
for (int i = 0; i < max; i++) {
if (items[i].key.equals(key)) {
items[i].val = val;
@ -107,11 +102,11 @@ class TemplatingEngine {
}
}
Log.error(F("TPL : Key not found %s." CR), key.c_str());
Log.warning(F("TPL : Key not found %s." CR), key.c_str());
}
void transform(String& s) {
int max = sizeof(items)/sizeof(KeyVal);
int max = sizeof(items) / sizeof(KeyVal);
for (int i = 0; i < max; i++) {
while (s.indexOf(items[i].key) != -1)
s.replace(items[i].key, items[i].val);
@ -119,13 +114,13 @@ class TemplatingEngine {
}
void dumpAll() {
int max = sizeof(items)/sizeof(KeyVal);
int max = sizeof(items) / sizeof(KeyVal);
for (int i = 0; i < max; i++) {
Serial.print( "Key=\'" );
Serial.print( items[i].key.c_str() );
Serial.print( "\', Val=\'" );
Serial.print( items[i].val.c_str() );
Serial.println( "\'" );
Serial.print("Key=\'");
Serial.print(items[i].key.c_str());
Serial.print("\', Val=\'");
Serial.print(items[i].val.c_str());
Serial.println("\'");
}
}
@ -138,7 +133,8 @@ class TemplatingEngine {
TEMPLATE_MQTT = 4
};
void initialize(float angle, float gravitySG, float corrGravitySG, float tempC, float runTime);
void initialize(float angle, float gravitySG, float corrGravitySG,
float tempC, float runTime);
const String& create(TemplatingEngine::Templates idx);
};

View File

@ -42,7 +42,7 @@ extern bool sleepModeAlwaysSkip;
void WebServerHandler::webHandleDevice() {
LOG_PERF_START("webserver-api-device");
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
Log.verbose(F("WEB : webServer callback for /api/device." CR));
Log.verbose(F("WEB : webServer callback for /api/device(get)." CR));
#endif
DynamicJsonDocument doc(100);
@ -50,11 +50,17 @@ void WebServerHandler::webHandleDevice() {
doc[PARAM_APP_NAME] = CFG_APPNAME;
doc[PARAM_APP_VER] = CFG_APPVER;
doc[PARAM_MDNS] = myConfig.getMDNS();
FloatHistoryLog runLog(RUNTIME_FILENAME);
doc[PARAM_RUNTIME_AVERAGE] = reduceFloatPrecision(
runLog.getAverage() ? runLog.getAverage() / 1000 : 0, 1);
#if LOG_LEVEL == 6
serializeJson(doc, Serial);
Serial.print(CR);
#endif
String out;
out.reserve(100);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-device");
@ -65,14 +71,18 @@ void WebServerHandler::webHandleDevice() {
//
void WebServerHandler::webHandleConfig() {
LOG_PERF_START("webserver-api-config");
Log.notice(F("WEB : webServer callback for /api/config." CR));
Log.notice(F("WEB : webServer callback for /api/config(get)." CR));
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
myConfig.createJson(doc);
doc[PARAM_PASS] = ""; // dont show the wifi password
double angle = myGyro.getAngle();
double angle = 0;
if (myGyro.hasValue())
angle = myGyro.getAngle();
double tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
double gravity = calculateGravity(angle, tempC);
@ -92,12 +102,17 @@ void WebServerHandler::webHandleConfig() {
doc[PARAM_BATTERY] = reduceFloatPrecision(myBatteryVoltage.getVoltage());
FloatHistoryLog runLog(RUNTIME_FILENAME);
doc[PARAM_RUNTIME_AVERAGE] = reduceFloatPrecision(
runLog.getAverage() ? runLog.getAverage() / 1000 : 0, 1);
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
Serial.print(CR);
#endif
String out;
out.reserve(CFG_JSON_BUFSIZE);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-config");
@ -109,7 +124,7 @@ void WebServerHandler::webHandleConfig() {
void WebServerHandler::webHandleUpload() {
LOG_PERF_START("webserver-api-upload");
Log.notice(F("WEB : webServer callback for /api/upload." CR));
DynamicJsonDocument doc(100);
DynamicJsonDocument doc(300);
doc["index"] = checkHtmlFile(WebServerHandler::HTML_INDEX);
doc["device"] = checkHtmlFile(WebServerHandler::HTML_DEVICE);
@ -118,12 +133,29 @@ void WebServerHandler::webHandleUpload() {
doc["format"] = checkHtmlFile(WebServerHandler::HTML_FORMAT);
doc["about"] = checkHtmlFile(WebServerHandler::HTML_ABOUT);
#if defined(ESP8266)
JsonArray files = doc.createNestedArray(PARAM_FILES);
// Show files in the filessytem at startup
FSInfo fs;
LittleFS.info(fs);
Dir dir = LittleFS.openDir("/");
while (dir.next()) {
JsonObject obj = files.createNestedObject();
obj[PARAM_FILE_NAME] = dir.fileName();
obj[PARAM_FILE_SIZE] = dir.fileSize();
}
#else // defined(ESP32)
#warning "TODO: Implement file listing for ESP32"
#endif
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
Serial.print(CR);
#endif
String out;
out.reserve(300);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-upload");
@ -199,13 +231,21 @@ void WebServerHandler::webHandleCalibrate() {
//
// Callback from webServer when / has been accessed.
//
void WebServerHandler::webHandleFactoryReset() {
void WebServerHandler::webHandleFactoryDefaults() {
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/factory." CR));
if (!id.compareTo(myConfig.getID())) {
_server->send(200, "text/plain", "Doing reset...");
_server->send(200, "text/plain",
"Removing configuration and restarting...");
LittleFS.remove(CFG_FILENAME);
LittleFS.remove(CFG_HW_FILENAME);
LittleFS.remove(ERR_FILENAME);
LittleFS.remove(RUNTIME_FILENAME);
LittleFS.remove(TPL_FNAME_HTTP1);
LittleFS.remove(TPL_FNAME_HTTP2);
LittleFS.remove(TPL_FNAME_INFLUXDB);
LittleFS.remove(TPL_FNAME_MQTT);
LittleFS.end();
delay(500);
ESP_RESET();
@ -219,11 +259,15 @@ void WebServerHandler::webHandleFactoryReset() {
//
void WebServerHandler::webHandleStatus() {
LOG_PERF_START("webserver-api-status");
Log.notice(F("WEB : webServer callback for /api/status." CR));
Log.notice(F("WEB : webServer callback for /api/status(get)." CR));
DynamicJsonDocument doc(256);
double angle = myGyro.getAngle();
double angle = 0;
if (myGyro.hasValue())
angle = myGyro.getAngle();
double tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
double gravity = calculateGravity(angle, tempC);
@ -251,6 +295,7 @@ void WebServerHandler::webHandleStatus() {
#endif
String out;
out.reserve(300);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-status");
@ -280,7 +325,7 @@ void WebServerHandler::webHandleClearWIFI() {
void WebServerHandler::webHandleStatusSleepmode() {
LOG_PERF_START("webserver-api-sleepmode");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/status/sleepmode." CR));
Log.notice(F("WEB : webServer callback for /api/status/sleepmode(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -308,7 +353,7 @@ void WebServerHandler::webHandleStatusSleepmode() {
void WebServerHandler::webHandleConfigDevice() {
LOG_PERF_START("webserver-api-config-device");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/device." CR));
Log.notice(F("WEB : webServer callback for /api/config/device(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -322,9 +367,12 @@ void WebServerHandler::webHandleConfigDevice() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setMDNS(_server->arg(PARAM_MDNS).c_str());
myConfig.setTempFormat(_server->arg(PARAM_TEMPFORMAT).charAt(0));
myConfig.setSleepInterval(_server->arg(PARAM_SLEEP_INTERVAL).c_str());
if (_server->hasArg(PARAM_MDNS))
myConfig.setMDNS(_server->arg(PARAM_MDNS).c_str());
if (_server->hasArg(PARAM_TEMPFORMAT))
myConfig.setTempFormat(_server->arg(PARAM_TEMPFORMAT).charAt(0));
if (_server->hasArg(PARAM_SLEEP_INTERVAL))
myConfig.setSleepInterval(_server->arg(PARAM_SLEEP_INTERVAL).c_str());
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseOne", true);
_server->send(302, "text/plain", "Device config updated");
@ -337,7 +385,7 @@ void WebServerHandler::webHandleConfigDevice() {
void WebServerHandler::webHandleConfigPush() {
LOG_PERF_START("webserver-api-config-push");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/push." CR));
Log.notice(F("WEB : webServer callback for /api/config/push(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -350,19 +398,41 @@ void WebServerHandler::webHandleConfigPush() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setHttpPushUrl(_server->arg(PARAM_PUSH_HTTP).c_str());
myConfig.setHttpPushUrl2(_server->arg(PARAM_PUSH_HTTP2).c_str());
myConfig.setBrewfatherPushUrl(_server->arg(PARAM_PUSH_BREWFATHER).c_str());
myConfig.setInfluxDb2PushUrl(_server->arg(PARAM_PUSH_INFLUXDB2).c_str());
myConfig.setInfluxDb2PushOrg(_server->arg(PARAM_PUSH_INFLUXDB2_ORG).c_str());
myConfig.setInfluxDb2PushBucket(
_server->arg(PARAM_PUSH_INFLUXDB2_BUCKET).c_str());
myConfig.setInfluxDb2PushToken(
_server->arg(PARAM_PUSH_INFLUXDB2_AUTH).c_str());
myConfig.setMqttUrl(_server->arg(PARAM_PUSH_MQTT).c_str());
myConfig.setMqttPort(_server->arg(PARAM_PUSH_MQTT_PORT).c_str());
myConfig.setMqttUser(_server->arg(PARAM_PUSH_MQTT_USER).c_str());
myConfig.setMqttPass(_server->arg(PARAM_PUSH_MQTT_PASS).c_str());
if (_server->hasArg(PARAM_TOKEN))
myConfig.setToken(_server->arg(PARAM_TOKEN).c_str());
if (_server->hasArg(PARAM_PUSH_HTTP))
myConfig.setHttpUrl(_server->arg(PARAM_PUSH_HTTP).c_str());
if (_server->hasArg(PARAM_PUSH_HTTP_H1))
myConfig.setHttpHeader(_server->arg(PARAM_PUSH_HTTP_H1).c_str(), 0);
if (_server->hasArg(PARAM_PUSH_HTTP_H2))
myConfig.setHttpHeader(_server->arg(PARAM_PUSH_HTTP_H2).c_str(), 1);
if (_server->hasArg(PARAM_PUSH_HTTP2))
myConfig.setHttp2Url(_server->arg(PARAM_PUSH_HTTP2).c_str());
if (_server->hasArg(PARAM_PUSH_HTTP2_H1))
myConfig.setHttp2Header(_server->arg(PARAM_PUSH_HTTP2_H1).c_str(), 0);
if (_server->hasArg(PARAM_PUSH_HTTP2_H2))
myConfig.setHttp2Header(_server->arg(PARAM_PUSH_HTTP2_H2).c_str(), 1);
if (_server->hasArg(PARAM_PUSH_BREWFATHER))
myConfig.setBrewfatherPushUrl(_server->arg(PARAM_PUSH_BREWFATHER).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2))
myConfig.setInfluxDb2PushUrl(_server->arg(PARAM_PUSH_INFLUXDB2).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_ORG))
myConfig.setInfluxDb2PushOrg(
_server->arg(PARAM_PUSH_INFLUXDB2_ORG).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_BUCKET))
myConfig.setInfluxDb2PushBucket(
_server->arg(PARAM_PUSH_INFLUXDB2_BUCKET).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_AUTH))
myConfig.setInfluxDb2PushToken(
_server->arg(PARAM_PUSH_INFLUXDB2_AUTH).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT))
myConfig.setMqttUrl(_server->arg(PARAM_PUSH_MQTT).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT_PORT))
myConfig.setMqttPort(_server->arg(PARAM_PUSH_MQTT_PORT).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT_USER))
myConfig.setMqttUser(_server->arg(PARAM_PUSH_MQTT_USER).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT_PASS))
myConfig.setMqttPass(_server->arg(PARAM_PUSH_MQTT_PASS).c_str());
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseTwo", true);
_server->send(302, "text/plain", "Push config updated");
@ -394,7 +464,7 @@ String WebServerHandler::getRequestArguments() {
void WebServerHandler::webHandleConfigGravity() {
LOG_PERF_START("webserver-api-config-gravity");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/gravity." CR));
Log.notice(F("WEB : webServer callback for /api/config/gravity(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -408,11 +478,14 @@ void WebServerHandler::webHandleConfigGravity() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setGravityFormat(_server->arg(PARAM_GRAVITY_FORMAT).charAt(0));
myConfig.setGravityFormula(_server->arg(PARAM_GRAVITY_FORMULA).c_str());
myConfig.setGravityTempAdj(
_server->arg(PARAM_GRAVITY_TEMP_ADJ).equalsIgnoreCase("on") ? true
: false);
if (_server->hasArg(PARAM_GRAVITY_FORMAT))
myConfig.setGravityFormat(_server->arg(PARAM_GRAVITY_FORMAT).charAt(0));
if (_server->hasArg(PARAM_GRAVITY_FORMULA))
myConfig.setGravityFormula(_server->arg(PARAM_GRAVITY_FORMULA).c_str());
if (_server->hasArg(PARAM_GRAVITY_TEMP_ADJ))
myConfig.setGravityTempAdj(
_server->arg(PARAM_GRAVITY_TEMP_ADJ).equalsIgnoreCase("on") ? true
: false);
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseThree", true);
_server->send(302, "text/plain", "Gravity config updated");
@ -425,7 +498,7 @@ void WebServerHandler::webHandleConfigGravity() {
void WebServerHandler::webHandleConfigHardware() {
LOG_PERF_START("webserver-api-config-hardware");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/hardware." CR));
Log.notice(F("WEB : webServer callback for /api/config/hardware(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -439,15 +512,20 @@ void WebServerHandler::webHandleConfigHardware() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setVoltageFactor(_server->arg(PARAM_VOLTAGEFACTOR).toFloat());
if (myConfig.isTempC()) {
myConfig.setTempSensorAdjC(_server->arg(PARAM_TEMP_ADJ));
} else {
myConfig.setTempSensorAdjF(_server->arg(PARAM_TEMP_ADJ));
if (_server->hasArg(PARAM_VOLTAGEFACTOR))
myConfig.setVoltageFactor(_server->arg(PARAM_VOLTAGEFACTOR).toFloat());
if (_server->hasArg(PARAM_TEMP_ADJ)) {
if (myConfig.isTempC()) {
myConfig.setTempSensorAdjC(_server->arg(PARAM_TEMP_ADJ));
} else {
myConfig.setTempSensorAdjF(_server->arg(PARAM_TEMP_ADJ));
}
}
myConfig.setOtaURL(_server->arg(PARAM_OTA).c_str());
myConfig.setGyroTemp(
_server->arg(PARAM_GYRO_TEMP).equalsIgnoreCase("on") ? true : false);
if (_server->hasArg(PARAM_OTA))
myConfig.setOtaURL(_server->arg(PARAM_OTA).c_str());
if (_server->hasArg(PARAM_GYRO_TEMP))
myConfig.setGyroTemp(
_server->arg(PARAM_GYRO_TEMP).equalsIgnoreCase("on") ? true : false);
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseFour", true);
_server->send(302, "text/plain", "Hardware config updated");
@ -460,7 +538,7 @@ void WebServerHandler::webHandleConfigHardware() {
void WebServerHandler::webHandleDeviceParam() {
LOG_PERF_START("webserver-api-device-param");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/device/param." CR));
Log.notice(F("WEB : webServer callback for /api/device/param(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -491,6 +569,8 @@ void WebServerHandler::webHandleDeviceParam() {
myHardwareConfig.SetDefaultCalibrationTemp(s.toFloat());
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_WIFI_PORTALTIMEOUT))
myHardwareConfig.setWifiPortalTimeout(s.toInt());
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_PUSH_TIMEOUT))
myHardwareConfig.setPushTimeout(s.toInt());
}
myHardwareConfig.saveFile();
@ -514,6 +594,7 @@ void WebServerHandler::webHandleDeviceParam() {
#endif
String out;
out.reserve(512);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-device-param");
@ -580,6 +661,7 @@ void WebServerHandler::webHandleFormulaRead() {
#endif
String out;
out.reserve(256);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-formula-read");
@ -626,7 +708,8 @@ void WebServerHandler::webHandleConfigFormatWrite() {
_server->sendHeader("Location", "/format.htm", true);
_server->send(302, "text/plain", "Format updated");
} else {
Log.error(F("WEB : Unable to store format file" CR));
ErrorFileLog errLog;
errLog.addEntry(F("WEB : Unable to store format file"));
_server->send(400, "text/plain", "Unable to store format in file.");
}
@ -725,6 +808,7 @@ void WebServerHandler::webHandleConfigFormatRead() {
#endif
String out;
out.reserve(2048);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-config-format-read");
@ -920,6 +1004,8 @@ bool WebServerHandler::setupWebServer() {
_server->on("/", std::bind(&WebServerHandler::webReturnUploadHtm, this));
}
#endif
_server->serveStatic("/log", LittleFS, ERR_FILENAME);
_server->serveStatic("/runtime", LittleFS, RUNTIME_FILENAME);
// Dynamic content
_server->on(
@ -938,7 +1024,7 @@ bool WebServerHandler::setupWebServer() {
std::bind(&WebServerHandler::webHandleCalibrate,
this)); // Run calibration routine (param id)
_server->on("/api/factory", HTTP_GET,
std::bind(&WebServerHandler::webHandleFactoryReset,
std::bind(&WebServerHandler::webHandleFactoryDefaults,
this)); // Reset the device
_server->on("/api/status", HTTP_GET,
std::bind(&WebServerHandler::webHandleStatus,

View File

@ -64,7 +64,7 @@ class WebServerHandler {
void webHandleStatusSleepmode();
void webHandleClearWIFI();
void webHandleStatus();
void webHandleFactoryReset();
void webHandleFactoryDefaults();
void webHandleCalibrate();
void webHandleUploadFile();
void webHandleUpload();

View File

@ -21,24 +21,28 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#if defined (ESP8266)
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#include <ESP8266WiFi.h>
#include <ESP8266httpUpdate.h>
#else // defined (ESP32)
#include <WiFi.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#include <HTTPUpdate.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#endif
#include <incbin.h>
#include <config.hpp>
#include <main.hpp>
#include <wifi.hpp>
// Settings for DRD
#if defined (ESP8266)
#if defined(ESP8266)
#define ESP_DRD_USE_LITTLEFS true
#define ESP_DRD_USE_SPIFFS false
#else // defined (ESP32)
#else // defined (ESP32)
#define ESP_DRD_USE_LITTLEFS false
#define ESP_DRD_USE_SPIFFS true
#endif
@ -191,8 +195,9 @@ bool WifiConnection::waitForConnection(int maxTime) {
if (i++ >
(maxTime * 10)) { // Try for maxTime seconds. Since delay is 100ms.
Log.error(F("WIFI: Failed to connect to wifi %d, aborting %s." CR),
WiFi.status(), getIPAddress().c_str());
ErrorFileLog errLog;
errLog.addEntry("WIFI: Failed to connect to wifi " +
String(WiFi.status()));
WiFi.disconnect();
Serial.print(CR);
return false; // Return to main that we have failed to connect.
@ -236,32 +241,36 @@ bool WifiConnection::updateFirmware() {
Log.verbose(F("WIFI: Updating firmware." CR));
#endif
WiFiClient wifi;
WiFiClientSecure wifiSecure;
HTTPUpdateResult ret;
String serverPath = myConfig.getOtaURL();
serverPath += "firmware.bin";
HTTPUpdateResult ret;
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
wifiSecure.setInsecure();
Log.notice(F("WIFI: OTA, SSL enabled without validation." CR));
ret = ESPhttpUpdate.update(myWifi.getWifiClientSecure(), serverPath);
ret = ESPhttpUpdate.update(wifiSecure, serverPath);
} else {
ret = ESPhttpUpdate.update(myWifi.getWifiClient(), serverPath);
ret = ESPhttpUpdate.update(wifi, serverPath);
}
switch (ret) {
case HTTP_UPDATE_FAILED:
Log.error(F("WIFI: OTA update failed %d, %s." CR),
ESPhttpUpdate.getLastError(),
ESPhttpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_FAILED: {
ErrorFileLog errLog;
errLog.addEntry("WIFI: OTA update failed " +
String(ESPhttpUpdate.getLastError()));
} break;
case HTTP_UPDATE_NO_UPDATES:
break;
case HTTP_UPDATE_OK:
case HTTP_UPDATE_OK: {
Log.notice("WIFI: OTA Update sucesfull, rebooting.");
delay(100);
ESP_RESET();
break;
}
}
return false;
}
@ -272,16 +281,18 @@ void WifiConnection::downloadFile(const char *fname) {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Download file %s." CR), fname);
#endif
WiFiClient wifi;
WiFiClientSecure wifiSecure;
HTTPClient http;
String serverPath = myConfig.getOtaURL();
serverPath += fname;
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
if (myConfig.isOtaSSL()) {
wifiSecure.setInsecure();
Log.notice(F("WIFI: OTA, SSL enabled without validation." CR));
http.begin(myWifi.getWifiClientSecure(), serverPath);
http.begin(wifiSecure, serverPath);
} else {
http.begin(myWifi.getWifiClient(), serverPath);
http.begin(wifi, serverPath);
}
int httpResponseCode = http.GET();
@ -292,11 +303,11 @@ void WifiConnection::downloadFile(const char *fname) {
f.close();
Log.notice(F("WIFI: Downloaded file %s." CR), fname);
} else {
Log.error(F("WIFI: Failed to download file, respone=%d" CR),
httpResponseCode);
ErrorFileLog errLog;
errLog.addEntry("WIFI: Failed to download html-file " +
String(httpResponseCode));
}
http.end();
myWifi.closeWifiClient();
}
//
@ -306,17 +317,19 @@ bool WifiConnection::checkFirmwareVersion() {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Checking if new version exist." CR));
#endif
WiFiClient wifi;
WiFiClientSecure wifiSecure;
HTTPClient http;
String serverPath = myConfig.getOtaURL();
serverPath += "version.json";
// Your Domain name with URL path or IP address with path
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
if (myConfig.isOtaSSL()) {
wifiSecure.setInsecure();
Log.notice(F("WIFI: OTA, SSL enabled without validation." CR));
http.begin(myWifi.getWifiClientSecure(), serverPath);
http.begin(wifiSecure, serverPath);
} else {
http.begin(myWifi.getWifiClient(), serverPath);
http.begin(wifi, serverPath);
}
// Send HTTP GET request
@ -332,7 +345,8 @@ bool WifiConnection::checkFirmwareVersion() {
DynamicJsonDocument ver(300);
DeserializationError err = deserializeJson(ver, payload);
if (err) {
Log.error(F("WIFI: Failed to parse version.json, %s" CR), err);
ErrorFileLog errLog;
errLog.addEntry(F("WIFI: Failed to parse version.json"));
} else {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Project %s version %s." CR),
@ -378,11 +392,12 @@ bool WifiConnection::checkFirmwareVersion() {
httpResponseCode);
}
http.end();
myWifi.closeWifiClient();
#if LOG_LEVEL == 6
Log.verbose(F("WIFI: OTA found new version %s." CR),
_newFirmware ? "true" : "false");
#endif
return _newFirmware;
}

View File

@ -24,28 +24,14 @@ SOFTWARE.
#ifndef SRC_WIFI_HPP_
#define SRC_WIFI_HPP_
#if defined (ESP8266)
#include <ESP8266WiFi.h>
#else // defined (ESP32)
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#endif
#define WIFI_DEFAULT_SSID "GravityMon" // Name of created SSID
#define WIFI_DEFAULT_PWD "password" // Password for created SSID
#define WIFI_MDNS "gravitymon" // Prefix for MDNS name
// tcp cleanup, to avoid memory crash.
struct tcp_pcb;
extern struct tcp_pcb* tcp_tw_pcbs;
extern "C" void tcp_abort(struct tcp_pcb* pcb);
class WifiConnection {
private:
// WIFI
WiFiClient _client;
WiFiClientSecure _secureClient;
// OTA
bool _newFirmware = false;
bool parseFirmwareVersionString(int (&num)[3], const char* version);
@ -67,16 +53,6 @@ class WifiConnection {
void startPortal();
void loop();
WiFiClient& getWifiClient() { return _client; }
WiFiClientSecure& getWifiClientSecure() { return _secureClient; }
void closeWifiClient() {
_client.stop();
_secureClient.stop();
// Cleanup memory allocated by open tcp connetions.
while (tcp_tw_pcbs) tcp_abort(tcp_tw_pcbs);
}
// OTA
bool updateFirmware();
bool checkFirmwareVersion();

View File

@ -23,6 +23,7 @@ Other parameters are the same as in the configuration guide.
"ota-url": "http://192.168.1.50:80/firmware/gravmon/",
"temp-format": "C",
"brewfather-push": "http://log.brewfather.net/stream?id=Qwerty",
"token": "token",
"http-push": "http://192.168.1.50:9090/api/v1/Qwerty/telemetry",
"http-push-h1": "header: value",
"http-push-h2": "header: value",
@ -54,7 +55,8 @@ Other parameters are the same as in the configuration guide.
},
"angle": 90.93,
"gravity": 1.105,
"battery": 0.04
"battery": 0.04,
"runtime-average": 3.12
}
@ -69,7 +71,8 @@ Retrive the current device settings via an HTTP GET command. Payload is in JSON
"app-name": "GravityMon",
"app-ver": "0.0.0",
"id": "ee1bfc",
"mdns": "gravmon"
"mdns": "gravmon",
"runtime-average": 3.12
}
@ -125,6 +128,18 @@ Retrive the data used for formula calculation data via an HTTP GET command. Payl
}
GET: /api/factory
================
Will do a reset to factory defaults and delete all data except wifi settings.
For this to work you will need to supply the device id as a parameter in the request:
::
http://mygravity.local/api/factory?id=<mydeviceid>
POST: /api/config/device
========================
@ -271,6 +286,7 @@ The requests package converts the json to standard form post format.
url = "http://" + host + "/api/config/push"
json = { "id": id,
"token": "",
"http-push": "http://192.168.1.1/ispindel",
"http-push2": "",
"http-push-h1": "",

View File

@ -22,7 +22,7 @@ copyright = '2021-2022, Magnus Persson'
author = 'Magnus Persson'
# The full version, including alpha/beta/rc tags
release = '0.7.1'
release = '0.8.0'
# -- General configuration ---------------------------------------------------

View File

@ -41,6 +41,10 @@ URL: (http://gravmon.local/device)
:width: 800
:alt: Device Settings
.. tip::
The button `view error log` will show the last 15 errors on the device. This can be useful for checking errors without
the need to connect to the serial port or to check what errors has occured while in `gravity mode`.
* **Version:**
@ -55,6 +59,12 @@ URL: (http://gravmon.local/device)
This is unique identifier for the device (ESP8266 id), this is required when using the API as an API Key to safeguard
against faulty requests. This is the ESP8266 chip ID, so it should be unique.
* **Average runtime:**
This shows the average time a gravity measurement takes. Under optimal setting this should be
around 1.5 - 2.0 seconds. If this is higher than 2 seconds this is most likley connected to slow wifi
connection. It will show 0 if data has not been collected yet.
Configuration
=============
@ -121,6 +131,11 @@ Push Settings
If you add the prefix `https://` then the device will use SSL when sending data.
* **Token:**
The token is included in the iSpindle JSON format and will be used for both HTTP targets. If you
need to have 2 different tokens please use the :ref:`format-editor` to customize the data format.
* **Brewfather URL:**
Endpoint to send data via http to brewfather. Format used :ref:`data-formats-brewfather`
@ -161,6 +176,28 @@ Push Settings
Password or blank if anonymous is accepted
* **HTTP Headers**
.. image:: images/config-popup1.png
:width: 300
:alt: HTTP Headers
You can define 2 http headers per push target. This is available via a pop-up window but dont forget
to press the save buttons on the post section to save the values. One common header is content type which is the
default setting for http targets.
The input must have the format **'<header>: <value>'** for it to work. The UI will accept any value so errors
will not show until the device tries to push data.
::
Content-Type: application/json
X-Auth-Token: <api-token>
Mozilla has a good guide on what headers are valid; `HTTP Headers <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers>`_
Gravity Settings
++++++++++++++++
@ -234,5 +271,6 @@ Hardware Settings
.. code-block::
http://192.168.1.1/firmware/gravmon/
https://192.168.1.1/firmware/gravmon/

View File

@ -12,8 +12,12 @@ The main features
angle/tile, temperature, calculates gravity and pushes the data to defined endpoints.
In ``configuration mode`` the device is always active and the webserver is active. Here you can view the
angle/tilt values, change configuration options and more. When in this mode you can also interact with the device
via an REST API so data can be pushed to the device via scripts (see API section for more information).
angle/tilt values, change configuration, update the gravity formula. When in this mode you can also interact
with the device via an REST API so data can be pushed to the device via scripts (see API section for more information).
.. image:: images/index.png
:width: 700
:alt: UI example
You can force the device into ``configuration mode`` while measuring gravity. This is useful when calibrating
the device so you don't needs to wait for the device to wake up and push the data. The entire calibration
@ -21,61 +25,83 @@ The main features
See the :ref:`setting-up-device` section for more information on how to trigger the configuration mode.
* **Can send data to multiple endpoints at once**
* **Can send data to multiple endpoints**
The original iSpindle can only have one destination, this software will push data to all defined endpoints so
in theory you can use them all. However this will consume more battery power so use only as many as needed.
in theory you can use them all. However this will consume more battery power so use only as many as needed. Its much
more efficient to have the endpoints on your local network than on the internet.
Currently the device supports the following endpoints: http (2 different), influxdb2, Brewfather and MQTT.
Currently the device supports the following endpoints.
If you want additional targets please raise a feature request in the github repo.
* http or https
* influxdb v2
* Brewfather
* MQTT
* Home Assistant
* Brew Spy
* Brewers Friend
* Fermentrack
* Ubidots
* Thingsspeak
Under the :ref:`services` section you can find guides for how to connect GravityMon to these services. For a
description of what data is transmitted you can see :ref:`data-formats`.
The software support SSL endpoints but currently without CA validation, this means that the data is encrypted
but it does not validate if the remote endpoint is who it claims to be.
if you require CA validation please leave a comment on GitHub and I will make that a priority. Adding this function
will dramatically reduce the battery life of the device.
.. note::
Using SSL on a small device such as the esp8266 can be unstable since it requires a lot of RAM to work. And running out
of RAM will cause the device to crash. So enable SSL with caution and only when you really need it. GravityMon will try
to minimize the needed RAM but the remote service might not support that feature.
* **Create gravity formulas on the device**
Another big difference is that this software can create the gravity formula in the device, just enter the
angle/gravity data that you have collected. You will also see a graph simulating how the formula would work.
.. note::
Currently the device can handle 5 data points which should be enough to get a accurate formula. At least 3 data points
is needed to get an accurate formula.
This feature needs more testing to be validated.
If there is a need for more data points, raise a comment on github.
* **Customize the data format beeing sent to push targets**
In order to make it easier to support more targets there is a built in format editor that can be used to
customize the data that is to be sent. This way you can easily adapt the software to new targets without coding.
If you have a good template please share it on the girhub repository and I will add it to the documentation
for other users to enjoy. See the :ref:`format-editor` for more information.
.. note::
This feature needs more testing to be validated.
If you have a good template please share it on the github repository and I will add it to the documentation
for other users to enjoy. See the :ref:`format-editor` for more information. See :ref:`services` for a list of
services currently validated.
* **Automatic temperature adjustment of gravity reading**
If you want to correct gravity based on beer temperature you can do this in the formula but here is a nice
feature that can correct the gravity as a second step making this independant of the formula.
.. note::
* **OTA support from webserver**
This feature needs more testing to be validated.
* **OTA support from local webserver**
When starting up in configuration mode the device will check for a software update from a local webserver.
When starting up in configuration mode the device will check for a software update from a webserver. This is an easily
way to keep the software up to date. In the future I might add a hosted endpoint for providing updates.
* **DS18B20 temperature adjustments**
You can adjust the temperature reading of the temperature sensor.
You can adjust the temperature reading of the temperature sensor. In normal cases this should not be needed since
the sensors should be calibrated.
* **Gyro Movement**
The software will detect if the gyro is moving and if this is the case it will go back to sleep for 60seconds.
This way we should avoid faulty measurements.
This way we should avoid faulty measurements and peaks in the graphs.
* **WIFI connection issues**
The software will not wait indefiently for a wifi connection. If it takes longer than 20 seconds to connect then
the device will go into deep sleep for 60 seoncds and then retry.
the device will go into deep sleep for 60 seoncds and then retry later. This to conserve batter as much as possible.
* **Use gyro temperature sensor**
@ -93,21 +119,36 @@ The main features
:width: 800
:alt: Gyro temp vs DS18B20
Other features
--------------
* **Celsius or Farenheigt**
* Support for Celcius and Farenheigt as temperature formats.
You can switch between different temperature formats. GravityMon will always use C for it's internal calculations and
convert to F when displayed.
* Support SG (Plato is not yet supported)
* **SG or Plato**
* Gyro data is read 50 times to ensure good accuracy
You can switch between different gravity formats. GravityMon will always use SG for it's internal calculations and
convert to Plato when displayed.
Experimental features
---------------------
* **Stable gyro data**
The device will read the gyro 50 times to get an accurate reading. If the standad deviation is to high it will not
use the data since this is inacurate and the device is probably moving, probably do to active fermentation or movement of
fermentation vessel. This sequence takes 900 ms seconds to execute and besides wifi connection this is what consumes the most
battery. With more testing this might be changes to either speed up or provide more stable readings.
* **Performance measurements**
I've also create a small library to measure execution code in some areas of the code that i know is time consuming. This way I can find a good balance between performace and quality.
I've also create a small library to measure execution code in some areas of the code that i know is time consuming. This
way I can find a good balance between performace and quality. This is a lot of help trying to figure out where bottlenecks
are in the code and where to put optimization efforts. Examples of real measurements:
* Reading the gyro: 885 ms
* Reading DS18B20 temperature sensor: 546 ms
* Connect to WIFI: 408 ms
* Send data to local influxdb v2: 25 ms
* Send data to local mqtt server: 35 ms
* Send data to local http server: 40 ms
* Send data to http server on internet: 0.2 - 5 seconds
See the :ref:`compiling-the-software` for more information.
@ -119,15 +160,12 @@ Experimental features
Battery life
------------
I'm currently measuring battery life of v0.5 but previous versions have been able to measure gravity for
a 2-3 weeks without issues (Using 900 seconds as interval).
The long term battery test has now been completed. Using a 2200 mA battery and sending data every 5 minutes to a local server on my network. The battery lasted 47 days which is excellet battery life.
I had a device running with an sleep interval of only 30s with ok wifi connection. The device lasted
12 days which i think is excellent considering the short sleep interval. From what I have discovered
it's the WIFI connection that has the most impact on the battery life. In a perfect scenario it
can take around 400ms but can in some cases take up to 8 seconds.
In another test I had a device running with an sleep interval of only 30s with ok wifi connection. The device lasted 12 days which i think is excellent considering the short sleep interval.
From what I have discovered it's the WIFI connection or latency to internet hosted that has the most impact on the battery life. The typical runtime in the tests above was around 2 seconds.
*More on this topics once my tests are done*
Performance
-----------

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -7,46 +7,55 @@ Welcome to GravityMon's documentation!
######################################
.. note::
This documentation reflects **v0.7.1**. Last updated 2022-01-30
This documentation reflects **v0.8**. Last updated 2022-03-05
GravityMon is used to measure gravity and temperature during fermentation of beer and report the progress. The graph below is
an example on how the fermentation process can be tracked. This is from my last brew that was over on a few days. The graph is rendered using
Fermentrack.
GravityMon is a replacement firmare for the iSpindle firmware, it uses the same hardware configuration so
you can easily switch between them.
.. image:: images/fermentation.png
:width: 500
:alt: Example fermentation
It's used to measure gravity in beer and show the progress of fermentation.
For more information on this topic and function please visit `iSpindel Homepage <https://www.ispindel.de>`_ .
GravityMon is a replacement firmare for the iSpindle and uses the same hardware configuration and is 100% compatible. It
implements a lot of the features that has been requested in the orginal iSpindle project but has been rejected for
various reasons. Here is a list of :ref:`main_features`.
I started GravityMon because i like to create software and wanted to do some low level programming. I had done a few
projects based on esp8266 and also started to brew beer so this combination was quite natural.
The hardware design comes from the fantastic iSpindle project so that is not covered in this documentation.
The hardware design comes from the fantastic iSpindle project so that is not covered in this documentation. For more
information on this topic and function please visit `iSpindel Homepage <https://www.ispindel.de>`_ .
My approach to this software is a little different from that the original ispindle firmware. The github repository can
be found here; `GravityMon on Github <https://github.com/mp-se/gravitymon>`_
My approach to this software is a little different from that the original ispindle firmware. The github repository
can be found here; `GravityMon on Github <https://github.com/mp-se/gravitymon>`_
.. note::
This software is in the early stages even though its more than one year old so if you find issues, please
open a ticket on github.
I dont take responsibility for any errors that can cause problems with the use. I have tested v0.4 on 5+ brews
over the last 6 months without any issues.
.. note::
I dont take responsibility for any errors or issues caused by the software. The software is provided as-is. I will however
try my best to fix issues that might occur.
The main differences:
---------------------
I have tested this software over the last year on 20+ brews with good results.
* Operates in two modes gravity monitoring and configuration mode (simplify calibration)
* Modern web based UI for configuration (in config mode)
* REST API
* Send data to multiple endpoints when pushing data (2xhttp, brewfather, influxdb v2, mqtt supported)
.. _main_features:
Main features:
--------------
* Operates in two modes gravity monitoring and configuration mode (simplify calibration). Gravity mode
is comparable to how the iSpindle works.
* Modern web based UI when in configuration mode. No need to start the access point changing settings.
* REST API to enable scripted configuration
* Send data to multiple endpoints and services at once
* Setup guides for how to send data to many popular services. Currently 8+ are documented.
* Automatic temperature adjustment of gravity reading
* OTA support from local webserver
* Built in function to create gravity formulas, no need for additional software, just enter tilt/gravity.
* Visual graph showing how formula will be interpreted
* OTA support from webserver
* Built in function to create gravity formulas, no need for additional software, just enter tilt/gravity and
let GravityMon create the formula.
* Visual graph showing how formula will be interpreted based on entered values
* Using the temperature sensor in gyro instead of DS18B20 (faster)
* Built in performance measurements (used to optimise code)
* SSL support in standard HTTP and MQTT connections.
* Option to customize data posted to endpoints using template from the UI.
* Built in performance measurements (used to optimise code)
For a complete breakdown see the :ref:`functionallity`
@ -121,9 +130,10 @@ the following libraries and without these this would have been much more difficu
:caption: Contents:
license
releases
functionallity
intro
installation
releases
configuration
formula
services
@ -132,6 +142,7 @@ the following libraries and without these this would have been much more difficu
data
compiling
contributing
troubleshooting
q_and_a
Indices and tables

View File

@ -1,3 +1,5 @@
.. _installation:
Installation
------------
@ -57,6 +59,9 @@ Just select a baud rate of 115200, 8N1.
:width: 800
:alt: Serial output
.. _setup_wifi:
Configuring WIFI
================

73
src_docs/source/intro.rst Normal file
View File

@ -0,0 +1,73 @@
.. _getting_started:
Getting started
===============
First you need a completed iSpindle hardware, there are several resouces around that topic so it
will not be covered in this documentation. Please visit `iSpindel Homepage <https://www.ispindel.de>`_ for
more information.
Step 1 - Flash the device
-------------------------
The first step is to flash the firmware, I recommend using Brewflasher as the easy option. Detailed
instructions can be found here :ref:`installation`
Step 2 - Setup WIFI
-------------------
When the device starts up the first time it will first start an WIFI access point so that the WIFI Settings
can be configured. The instructions for that can be found here :ref:`setup_wifi`
Step 3 - Configuration
----------------------
Once the device can connect to WIFI it will go into `configuration mode` and start a web server for
doing the initial configuration. In order to access the device you will need to find its name or ip adress.
It will broadcast a name like gravitymonXXXXXX.local over mDNS. Where the XXXXXX is the unique device id. You can
find the name via an mDNS browser, check your router or connect the device to a serial monitor. On windows mDNS
might not work so then use the IP address instead. Once connected you will meet a web interface that looks like this.
.. image:: images/index.png
:width: 800
:alt: Index page
The next step is then to configure the device, most settings should work but there are a few that should be changed.
Configuration - Device Settings - Device Name
+++++++++++++++++++++++++++++++++++++++++++++
Give your device a good name.
Configuration - Device Settings - Gyro Calibration
++++++++++++++++++++++++++++++++++++++++++++++++++
You need to place the device on a flat surface and then press the
calibrate button. It will take a few seconds for this to complete and the angle should be close to 90 degress. Without
calibration the device will not go into gravity mode.
Configuration - Push Settings
+++++++++++++++++++++++++++++
Add the endpoints where you want data to be transmitted. All URLs that contain a valid endpoint will receive the data.
Calibration
+++++++++++
I recommend to use the calibration feature to create a gravity formula. If you have values from a
previous calibration then you can add them here, if not follow the calibration guidelines on the iSpindle site.
There are several guides for how to calibrate the device (`iSpindle Calibration <https://www.ispindel.de/docs/Calibration_en.html>`_)
This will get the data points needed to create the formula, and the datapoints will be stored on the device so you can
adjust them when needed.
Step 4 - Completed
------------------
You are now done and can enjoy the GravityMon software. Check out the :ref:`setting-up-device` section for other configuration options.
If you want to enter the configuration mode place the device flat on a surface and do a reset (or wait until it wakes up).
Its recommended to attach the device to power when you have it in `configuration mode` so the battery is not drained.
**If you have suggestions for more awesome features, head over to the github repository and make a request.**

View File

@ -3,6 +3,27 @@
Releases
########
v0.8.0
------
* Added option to set http headers (2 per http endpoint), these can be used for
other http formats than json (default) and for adding authentication headers.
* Added possibility to view last 10 errors on device page.
* Added possibility to define token parameter used in iSpindle format.
* Added instructions for how to configure integration with Brewspy
* Added instructions for how to configure integration with Thingspeak
* Added option to do a factory reset via API.
* Logging the runtime, how long a measurement take (last 10 are stored). This can be
used to check how good the wifi connection is and estimate the lifetime when on battery.
Check the device page in the UI for this information.
* Refactored code to free up more RAM to make SSL more stable.
* Before connecting to an SSL endpoint the device will try to use a new SSL feature
called MFLN (Maximum Fragment Length Negotiation) that allow us to reduce the buffers
from 16k to 2k. This can make a huge difference on a device with only 40k RAM. Not all
servers might support this feature.
* Updated documentation pages.
* Tested batterylife, 47 days using an update frequency of 5 min
v0.7.1
------

View File

@ -48,6 +48,27 @@ Enter the following as URL:
http://industrial.api.ubidots.com/api/v1.6/devices/<devicename>/?token=<api-token>
This is the less secure option.
**Option 2** - token as the http header
Enter the following as URL, use either standard or ssl.
.. code-block::
http://industrial.api.ubidots.com/api/v1.6/devices/<devicename>
https://industrial.api.ubidots.com/api/v1.6/devices/<devicename>
Under `Headers` (button after the http url in the UI) enter the following string:
.. code-block::
X-Auth-Token: <api-token>
This is the more secure option.
Even though ubidots can handle the default ispindle format it probably better to just post the data you want. This is an example of a
format template that can be used. For information on customizing the format see :ref:`format-editor`.
@ -105,7 +126,7 @@ Brewer's friend is an all in one service that allows you to manage you recepies
.. warning::
I dont have an account for brewers friend so I have not been able to verfy this completely. Its based on
the available documentation.
the available documentation. If this works please let
You can find you API key when logged in to the service. Follow these `instructions <https://docs.brewersfriend.com/devices/ispindel>`_
@ -134,3 +155,43 @@ format for the endpoint. Just add you API key after token.
"battery": ${battery},
"rssi": ${rssi}
}
Brewspy
+++++++
BrewSpy is a service that can show the history and manage the brew.
You need to enter the Token found in brewspy. The field is found under the field for http configuration.
.. code-block::
http://brew-spy.com/api/ispindel
Thingspeak
++++++++++
Thingspeak is an IoT platform for receiving data which can be visualized.
In order to use this platform you need to create a channel (channel = device) and get the APIKEY for
writing to the channel. Each channel can handle up to 8 measurements. In the http field enter the following URL.
.. code-block::
http://api.thingspeak.com/update.json
You also need to create a custom format for the selected endpoint where the field1-field8 contains the data
you want to include. The example below sends 5 different values to the channel identified by the API key.
.. code-block::
{
"api_key": "<your write api key for channel>",
"field1": ${gravity},
"field2": ${temp},
"field3": ${angle},
"field4": ${battery},
"field5": ${rssi}
}

View File

@ -0,0 +1,48 @@
.. _troubleshooting:
Troubleshooting
###############
Log errors
++++++++++
* Not enough values for deriving formula
To create a formula its required to have at least 3 measurements.
* Error validating created formula. Deviation to large, formula rejected
The device will try to create formulas with different complexities. It will try to
validate the formula using the supplied values. If the differnce is more than 1.6 SG on any point
the formula will be rejected. Check the entered values if they seams to be resonable.
* No valid calibration values, please calibrate the device.
The gyro needs to be calibrated at 90 degress (flat). This is done on the configration page.
* Low on memory, skipping push
The arduino libraries sometimes leak memory, this only occurs when running in configuration mode. To avoid
crashes the device will skip pushing data if the memory drops to much. Network connections seams to be connected
to memory leaks.
* Unable to set header, invalid value
Check the format for your custom header. This means it has not a correct format.
* Influxdb push failed response
* Brewfather push failed response
* HTTP push failed response
All these errors are standard http error codes. This are the commone ones;
* 400 - Bad request. Probably an issue with the post format. Check format in the format editor.
* 401 - Unathorized. The service needs an token or other means to authenticate the device.
* 403 - Forbidden. Could be an issue with token or URL.
* 404 - Not found. Probably a wrong URL.
* MQTT push on <topic> failed error
* -3 - Network failed connected
* -10 - Connection denied

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

View File

@ -5,7 +5,12 @@
"temp-format": "C",
"brewfather-push": "http://log.brewfather.net/stream?id=KfkJU43jUFfj",
"http-push": "http://192.168.1.10:9090/api/v1/ZYfjlUNeiuyu9N/telemetry",
"http-push-h1": "Auth: Basic T7IF9DD9fF3RDddE=",
"http-push-h2": "Auth: Advanced T7IF9DD9fF3RDddE=",
"http-push2": "http://192.168.1.10/ispindel",
"http-push2-h1": "Second",
"http-push2-h2": "First",
"token": "mytoken",
"influxdb2-push": "http://192.168.1.10:8086",
"influxdb2-org": "hello",
"influxdb2-bucket": "spann",
@ -31,5 +36,6 @@
},
"angle": 90.93,
"gravity": 1.105,
"battery": 0.04
"battery": 0.04,
"runtime-average": 2.0
}

View File

@ -30,8 +30,13 @@ set_config( url, json )
#
url = "http://" + host + "/api/config/push"
json = { "id": id,
"token": "",
"http-push": "http://192.168.1.1/ispindel", # HTTP endpoint
"http-push2": "", # HTTP endpoint2
"http-push-h1": "Content-Type: application/json",
"http-push-h2": "",
"http-push2-h1": "Content-Type: application/json",
"http-push2-h2": "",
"brewfather-push": "", # Brewfather URL
"influxdb2-push": "", # InfluxDB2 settings
"influxdb2-org": "",
@ -51,6 +56,7 @@ set_config( url, json )
url = "http://" + host + "/api/config/gravity"
json = { "id": id,
"gravity-formula": "", # If you want to set the gravity formula
"gravity-format": "G",
"gravity-temp-adjustment": "off" # on or off
}
set_config( url, json )

View File

@ -2,5 +2,6 @@
"app-name": "GravityMon ",
"app-ver": "0.0.0",
"id": "7376ef",
"mdns": "gravmon"
"mdns": "gravmon",
"runtime-average": 3.12
}

View File

@ -2,13 +2,13 @@
"gravity-formula": "0.00000166*tilt^3+-0.00024799*tilt^2+0.01344400*tilt+0.79358248",
"angle": 45,
"a1": 25,
"a2": 35,
"a3": 45,
"a3": 35,
"a2": 45,
"a4": 55,
"a5": 65,
"a5": 30,
"g1": 1.000,
"g2": 1.010,
"g3": 1.025,
"g3": 1.010,
"g2": 1.025,
"g4": 1.040,
"g5": 1.060
"g5": 1.005
}

View File

@ -1,11 +1,12 @@
{
"id": "7376ef",
"angle": 89.86,
"gravity": 1.1052,
"gravity-tempcorr": 1.1031,
"temp-c": 0,
"angle": 22.4,
"gravity": 1.044,
"gravity-tempcorr": 1.031,
"gravity-format": "G",
"temp-c": 12,
"temp-f": 32,
"battery": 0,
"battery": 3.81,
"temp-format": "C",
"sleep-mode": false,
"rssi": -56