Merge branch 'bs5' into dev

This commit is contained in:
Magnus Persson 2022-04-27 22:19:24 +02:00
commit d8cb4fe622
44 changed files with 2728 additions and 2173 deletions

View File

@ -5,74 +5,117 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script> .row-margin-10 { margin-top: 1.0em; }
</style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <!-- START MENU -->
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<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"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<ul class="navbar-nav mr-auto"> <div class="container">
<li class="nav-item"> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
</li> <span class="navbar-toggler-icon"></span>
<li class="nav-item"> </button>
<a class="nav-link" href="/config.htm">Configuration</a> <div class="collapse navbar-collapse" id="navbarNav">
</li> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/calibration.htm">Calibration</a> <a class="nav-link" href="/index.htm">Home</a>
</li> </li>
<li class="nav-item active"> <li class="nav-item">
<a class="nav-link" href="/about.htm">About</a> <a class="nav-link" href="/config.htm">Configuration</a>
</li> </li>
</ul> <li class="nav-item">
</div> <a class="nav-link" href="/calibration.htm">Calibration</a>
</nav> </li>
<li class="nav-item active">
<a class="nav-link" href="#"><b>About</b></a>
</li>
</ul>
</div>
</div>
</nav>
<!-- START MAIN INDEX --> <!-- START BODY -->
<div class="container"> <div class="container row-margin-10">
<hr class="my-4">
<div class="row mb-3">
<h3>Beer Gravity Monitor</h3>
This is a piece of software for the iSpindle hardware and will work in a similar way. No part of this software is copied from the iSpindle project.
</div>
<div class="row mb-3">
<h3>MIT License</h3>
Permission is hereby granted, free of charge, to any person obtaining a copy <div class="accordion row-margin-10" id="accordion">
of this software and associated documentation files (the "Software"), to deal <div class="accordion-item">
in the Software without restriction, including without limitation the rights <h2 class="accordion-header" id="headingAbout">
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAbout" aria-expanded="true" aria-controls="collapseAbout">
copies of the Software, and to permit persons to whom the Software is <b>About</b>
furnished to do so, subject to the following conditions: </button>
</h2>
<div id="collapseAbout" class="accordion-collapse collapse show" aria-labelledby="headingAbout" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row h3 col-sm-8">
Beer Gravity Monitor
</div>
<div class="row col-sm-8 mb-3">
This is a piece of software for the iSpindle hardware and will work in a similar way. No part of this software is copied from the iSpindle project.
</div>
<div class="row h3 col-sm-8 mb-3">
MIT License
</div>
<div class="row col-sm-8 mb-3">
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The copyright notice and this permission notice shall be included in all The copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software. copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
</div>
<div class="row h3 col-sm-8 mb-3">
Credits to
</div>
<div class="row col-sm-8 mb-3">
This software uses
the following libraries and without these this software would have been much more difficult to acheive:<br><br>
<ul>
<li>https://github.com/jrowberg/i2cdevlib</li>
<li>https://github.com/codeplea/tinyexpr</li>
<li>https://github.com/graphitemaster/incbin</li>
<li>https://github.com/khoih-prog/ESP_DoubleResetDetector</li>
<li>https://github.com/khoih-prog/ESP_WiFiManager</li>
<li>https://github.com/thijse/Arduino-Log</li>
<li>https://github.com/bblanchon/ArduinoJson</li>
<li>https://github.com/PaulStoffregen/OneWire</li>
<li>https://github.com/milesburton/Arduino-Temperature-Control-Library</li>
<li>https://github.com/Rotario</li>
<li>https://github.com/256dpi/arduino-mqtt</li>
<li>https://graphjs.com</li>
<li>https://getbootstrap.com</li>
<li>https://github.com/lorol/LITTLEFS</li>
<li>https://github.com/h2zero/NimBLE-Arduino</li>
<li>https://github.com/spouliot/tilt-sim</li>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<hr class="my-4"> <!-- START FOOTER -->
</div>
<!-- START FOOTER --> <div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

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"><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 active"><a class="nav-link" href="/about.htm">About</a></li></ul></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="row mb-3"><h3>Beer Gravity Monitor</h3>This is a piece of software for the iSpindle hardware and will work in a similar way. No part of this software is copied from the iSpindle project.</div><div class="row mb-3"><h3>MIT License</h3>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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.</div><hr class="my-4"></div><!-- 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 href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"><style>.row-margin-10{margin-top:1em}</style></head><body class="py-4"><!-- START MENU --><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script><nav class="navbar navbar-expand-lg navbar-dark bg-primary"><div class="container"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarNav"><ul class="navbar-nav"><li class="nav-item"><a class="nav-link" href="/index.htm">Home</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 active"><a class="nav-link" href="#"><b>About</b></a></li></ul></div></div></nav><!-- START BODY --><div class="container row-margin-10"><div class="accordion row-margin-10" id="accordion"><div class="accordion-item"><h2 class="accordion-header" id="headingAbout"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseAbout" aria-expanded="true" aria-controls="collapseAbout"><b>About</b></button></h2><div id="collapseAbout" class="accordion-collapse collapse show" aria-labelledby="headingAbout" data-bs-parent="#accordion"><div class="accordion-body"><div class="row h3 col-sm-8">Beer Gravity Monitor</div><div class="row col-sm-8 mb-3">This is a piece of software for the iSpindle hardware and will work in a similar way. No part of this software is copied from the iSpindle project.</div><div class="row h3 col-sm-8 mb-3">MIT License</div><div class="row col-sm-8 mb-3">Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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.</div><div class="row h3 col-sm-8 mb-3">Credits to</div><div class="row col-sm-8 mb-3">This software uses the following libraries and without these this software would have been much more difficult to acheive:<br><br><ul><li>https://github.com/jrowberg/i2cdevlib</li><li>https://github.com/codeplea/tinyexpr</li><li>https://github.com/graphitemaster/incbin</li><li>https://github.com/khoih-prog/ESP_DoubleResetDetector</li><li>https://github.com/khoih-prog/ESP_WiFiManager</li><li>https://github.com/thijse/Arduino-Log</li><li>https://github.com/bblanchon/ArduinoJson</li><li>https://github.com/PaulStoffregen/OneWire</li><li>https://github.com/milesburton/Arduino-Temperature-Control-Library</li><li>https://github.com/Rotario</li><li>https://github.com/256dpi/arduino-mqtt</li><li>https://graphjs.com</li><li>https://getbootstrap.com</li><li>https://github.com/lorol/LITTLEFS</li><li>https://github.com/h2zero/NimBLE-Arduino</li><li>https://github.com/spouliot/tilt-sim</li></ul></div></div></div></div></div></div><!-- START FOOTER --><div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div></body></html>

View File

@ -5,379 +5,422 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<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> .row-margin-10 { margin-top: 1.0em; }
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<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/chart.js"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <!-- START MENU -->
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> <div class="container">
<span class="navbar-toggler-icon"></span> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
</button> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="/index.htm">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="#"><b>Calibration</b></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>
</div>
</nav>
<div class="collapse navbar-collapse" id="navbar"> <!-- START BODY -->
<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">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item active">
<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 row-margin-10">
<div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="container"> <script type="text/javascript">
function showError( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
<hr class="my-2"> function showSuccess( msg ) {
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"> $("#alert-btn").click(function(e){
<div id="alert-msg">...</div> $('.alert').addClass('hide').removeClass('show').addClass('d-none');
<button type="button" id="alert-btn" class="close" aria-label="Close"> });
<span aria-hidden="true">&times;</span> </script>
</button>
</div>
<script type="text/javascript"> <div class="accordion" id="accordion">
function showError( msg ) { <div class="accordion-item">
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show') <h2 class="accordion-header" id="headingFormula">
$('#alert-msg').text( msg ); <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFormula" aria-expanded="true" aria-controls="collapseFormula">
} <b>Formula calculation</b>
function showSuccess( msg ) {
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
}
$("#alert-btn").click(function(e){
$('.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="#collapseCalibration" aria-expanded="true" aria-controls="collapseCalibration">
Formula calculation
</button> </button>
</h2> </h2>
<div id="collapseFormula" class="accordion-collapse collapse show" aria-labelledby="headingFormula" data-bs-parent="#accordion">
<div class="accordion-body">
<form action="/api/formula" method="post">
<input type="text" name="gravity-format" id="gravity-format" hidden>
<input type="text" name="id" id="id" hidden>
<div class="row mb-3">
Here you can create your gravity formula by entering angles/tilt and the corresponding gravity. These values
will be saved for future use. Angles with 0 (zero) will be skipped. The values below will be used to check the
formula and if the deviation is more than 1.5SG / 0.38P on any of the provided points then the forumla will be
rejected. On the bottom of the page you can see a graph over the entered values + values calcualated by the formula.
</div>
<div class="row">
<label class="col-sm-1 col-form-label">#</label>
<label class="col-sm-4 col-form-label">Angle/Tilt</label>
<label class="col-sm-4 col-form-label" id="gravity-header">Gravity</label>
</div>
<div class="row mb-3">
<label for="angle1" class="col-sm-1 col-form-label">1.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a1" id="a1" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g1" id="g1" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle2" class="col-sm-1 col-form-label">2.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a2" id="a2" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g2" id="g2" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle3" class="col-sm-1 col-form-label">3.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a3" id="a3" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g3" id="g3" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle4" class="col-sm-1 col-form-label">4.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a4" id="a4" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g4" id="g4" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle5" class="col-sm-1 col-form-label">5.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a5" id="a5" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g5" id="g5" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle6" class="col-sm-1 col-form-label">6.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a6" id="a6" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g6" id="g6" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle7" class="col-sm-1 col-form-label">7.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a7" id="a7" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g7" id="g7" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle8" class="col-sm-1 col-form-label">8.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a8" id="a8" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g8" id="g8" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle9" class="col-sm-1 col-form-label">9.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a9" id="a9" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g9" id="g9" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<label for="angle10" class="col-sm-1 col-form-label">10.</label>
<div class="col-sm-4"><input type="number" min="0" max="90" step="0.001" class="form-control" name="a10" id="a10" data-bs-toggle="tooltip" title="Enter the angle for the gravity. Zero value will be ignored"></div>
<div class="col-sm-4"><input type="number" min="0" max="26" step="0.0001" class="form-control" name="g10" id="g10" data-bs-toggle="tooltip" title="Enter the gravity for the angle."></div>
</div>
<div class="row mb-3">
<div class="col-sm-8 offset-sm-1"><button type="submit" class="btn btn-primary" id="calculate-btn" data-bs-toggle="tooltip" title="Save the values and try to create a formula">Save & Create</button></div>
</div>
<hr class="my-2">
<div class="row">
<label for="calculate-btn" class="col-sm-2 col-form-label">Current angle: </label>
<label for="calculate-btn" class="col-sm-2 col-form-label" id="angle"></label>
</div>
<div class="row">
<label for="calculate-btn" class="col-sm-2 col-form-label">Formula: </label>
<label for="calculate-btn" class="col-sm-8 col-form-label" id="formula">Loading...</label>
</div>
</form>
</div>
</div>
</div> </div>
<div id="collapseCalibration" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion"> <div class="accordion-item">
<div class="card-body"> <h2 class="accordion-header" id="headingGraph">
<form action="/api/formula" method="post"> <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseGraph" aria-expanded="false" aria-controls="collapseGraph">
<input type="text" name="gravity-format" id="gravity-format" hidden> <b>Formula graph</b>
<input type="text" name="id" id="id" hidden> </button>
</h2>
<div class="row mb-3"> <div id="collapseGraph" class="accordion-collapse collapse" aria-labelledby="headingGraph" data-bs-parent="#accordion">
Here you can create your gravity formula by entering angles/tilt and the corresponding gravity. These values <div class="accordion-body">
will be saved for future use. Angles with 0 (zero) will be skipped. The values below will be used to check the <canvas id="gravityChart"></canvas>
formula and if the deviation is more than 1.5SG / 0.38P on any of the provided points then the forumla will be </div>
rejected. On the bottom of the page you can see a graph over the entered values + values calcualated by the formula.
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">#:</label>
<label class="col-sm-4 col-form-label">Angle/Tilt:</label>
<label class="col-sm-4 col-form-label" id="gravity-header">Gravity (SG):</label>
</div>
<div class="form-group row">
<label for="angle1" class="col-sm-2 col-form-label">1.</label>
<div class="col-sm-4">
<input type="number" min="0" max="90" step="0.001" class="form-control" name="a1" id="a1">
</div>
<div class="col-sm-4">
<input type="number" min="0" max="26" step="0.0001" class="form-control" name="g1" id="g1">
</div>
</div>
<div class="form-group row">
<label for="angle2" class="col-sm-2 col-form-label">2.</label>
<div class="col-sm-4">
<input type="number" min="0" max="90" step="0.001" class="form-control" name="a2" id="a2">
</div>
<div class="col-sm-4">
<input type="number" min="0" max="26" step="0.0001" class="form-control" name="g2" id="g2">
</div>
</div>
<div class="form-group row">
<label for="angle3" class="col-sm-2 col-form-label">3.</label>
<div class="col-sm-4">
<input type="number" min="0" max="90" step="0.001" class="form-control" name="a3" id="a3">
</div>
<div class="col-sm-4">
<input type="number" min="0" max="26" step="0.0001" class="form-control" name="g3" id="g3">
</div>
</div>
<div class="form-group row">
<label for="angle4" class="col-sm-2 col-form-label">4.</label>
<div class="col-sm-4">
<input type="number" min="0" max="90" step="0.001" class="form-control" name="a4" id="a4">
</div>
<div class="col-sm-4">
<input type="number" min="0" max="26" step="0.0001" class="form-control" name="g4" id="g4">
</div>
</div>
<div class="form-group row">
<label for="angle5" class="col-sm-2 col-form-label">5.</label>
<div class="col-sm-4">
<input type="number" min="0" max="90" step="0.001" class="form-control" name="a5" id="a5">
</div>
<div class="col-sm-4">
<input type="number" min="0" max="26" step="0.0001" class="form-control" name="g5" id="g5">
</div>
</div>
<div class="form-group row">
<div class="col-sm-8 offset-sm-0">
<button type="submit" class="btn btn-primary" id="calculate-btn">Save & Calculate</button>
</div>
</div>
<div class="form-group row">
<label for="calculate-btn" class="col-sm-2 col-form-label">Current angle: </label>
<label for="calculate-btn" class="col-sm-2 col-form-label" id="angle"></label>
</div>
<div class="form-group row">
<label for="calculate-btn" class="col-sm-2 col-form-label">Formula: </label>
<label for="calculate-btn" class="col-sm-8 col-form-label" id="formula">Loading...</label>
</div>
</form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<hr class="my-4">
<div>
<canvas id="gravityChart"></canvas>
</div>
<hr class="my-4">
</div>
<script type="text/javascript"> <script type="text/javascript">
var chartDataForm = []; var chartDataForm = [];
var chartDataCalc = []; var chartDataCalc = [];
const dataSetChart = { const dataSetChart = {
datasets: [{ datasets: [{
label: 'Raw data', label: 'Raw data',
borderColor: 'blue', borderColor: 'blue',
backgroundColor: 'blue', backgroundColor: 'blue',
data: chartDataForm data: chartDataForm
}, { }, {
label: 'Calculated', label: 'Calculated',
borderColor: 'green', borderColor: 'green',
backgroundColor: 'green', backgroundColor: 'green',
data: chartDataCalc data: chartDataCalc
}] }]
} }
const configChart = { const configChart = {
type: 'line', type: 'line',
data: dataSetChart, data: dataSetChart,
options: { options: {
responsive: true, responsive: true,
interaction: { interaction: {
intersect: false, intersect: false,
},
scales: {
x: {
display: true,
type: 'linear',
grace: '5%',
title: {
display: true,
text: 'Angle/Tilt'
},
ticks: {
crossAlign: 'far'
},
suggestedMin: 25
}, },
y: { scales: {
display: true, x: {
title: {
display: true, display: true,
text: 'Gravity' type: 'linear',
grace: '5%',
title: {
display: true,
text: 'Angle/Tilt'
},
ticks: {
crossAlign: 'far'
},
suggestedMin: 25
}, },
suggestedMin: 1.000 y: {
display: true,
title: {
display: true,
text: 'Gravity'
},
suggestedMin: 1.000
}
} }
} }
} };
};
var myChart = 0; var myChart = 0;
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
g1.onchange = setGravityDecimal g1.onchange = setGravityDecimal
g2.onchange = setGravityDecimal g2.onchange = setGravityDecimal
g3.onchange = setGravityDecimal g3.onchange = setGravityDecimal
g4.onchange = setGravityDecimal g4.onchange = setGravityDecimal
g5.onchange = setGravityDecimal g5.onchange = setGravityDecimal
g6.onchange = setGravityDecimal
g7.onchange = setGravityDecimal
g8.onchange = setGravityDecimal
g9.onchange = setGravityDecimal
g10.onchange = setGravityDecimal
a1.onchange = setAngleDecimal a1.onchange = setAngleDecimal
a2.onchange = setAngleDecimal a2.onchange = setAngleDecimal
a3.onchange = setAngleDecimal a3.onchange = setAngleDecimal
a4.onchange = setAngleDecimal a4.onchange = setAngleDecimal
a5.onchange = setAngleDecimal a5.onchange = setAngleDecimal
a6.onchange = setAngleDecimal
a7.onchange = setAngleDecimal
a8.onchange = setAngleDecimal
a9.onchange = setAngleDecimal
a10.onchange = setAngleDecimal
window.onload = getConfig; window.onload = getConfig;
setButtonDisabled( true );
function convertToPlato(sg) {
return 259-(259/sg);
}
function convertToSG(plato) {
return 259/(259-plato);
}
function setAngleDecimal(event) {
this.value = parseFloat(this.value).toFixed(2);
populateChart();
}
function setGravityDecimal(event) {
if(isPlato())
this.value = parseFloat(this.value).toFixed(1);
else
this.value = parseFloat(this.value).toFixed(4);
populateChart();
}
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) {
chartDataCalc.push( { x: parseFloat(a), y: parseFloat(g) });
}
function isPlato() {
return $("#gravity-format").text() == "P";
}
function populateChart() {
chartDataCalc.length = 0
for( i = 25.0; i<80.0; i+=5.0) {
var formula = $("#formula").text();
var angle = i.toString();
formula=formula.replaceAll( "tilt^3", angle+"*"+angle+"*"+angle );
formula=formula.replaceAll( "tilt^2", angle+"*"+angle );
formula=formula.replaceAll( "tilt", angle );
var g = eval( formula );
if(isPlato())
g = convertToPlato(g);
populateChartCalc( i, g );
}
chartDataForm.length = 0
populateChartForm( $("#a1").val(), $("#g1").val() );
populateChartForm( $("#a2").val(), $("#g2").val() );
populateChartForm( $("#a3").val(), $("#g3").val() );
populateChartForm( $("#a4").val(), $("#g4").val() );
populateChartForm( $("#a5").val(), $("#g5").val() );
if( myChart )
myChart.destroy();
myChart = new Chart(
document.getElementById('gravityChart'),
configChart
);
}
function setButtonDisabled( b ) {
$("#calculate-btn").prop("disabled", b);
}
// Get the configuration values from the API
function getConfig() {
setButtonDisabled( true ); setButtonDisabled( true );
var url = "/api/formula"; function convertToPlato(sg) {
//var url = "/test/formula.json"; return 259-(259/sg);
$('#spinner').show(); }
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#id").val(cfg["id"]); function convertToSG(plato) {
$("#angle").text(cfg["angle"]); return 259/(259-plato);
$("#formula").text(cfg["gravity-formula"]); }
$("#gravity-format").text(cfg["gravity-format"]); // Sets the variable used by isPlato()
if(isPlato()) { function setAngleDecimal(event) {
$("#gravity-header").text("Gravity (Plato):"); this.value = parseFloat(this.value).toFixed(2);
$("#g1").val( parseFloat(cfg["g1"]).toFixed(1) ); populateChart();
$("#g2").val( parseFloat(cfg["g2"]).toFixed(1) ); }
$("#g3").val( parseFloat(cfg["g3"]).toFixed(1) );
$("#g4").val( parseFloat(cfg["g4"]).toFixed(1) );
$("#g5").val( parseFloat(cfg["g5"]).toFixed(1) );
} else {
$("#gravity-header").text("Gravity (SG):");
$("#g1").val( parseFloat(cfg["g1"]).toFixed(4) );
$("#g2").val( parseFloat(cfg["g2"]).toFixed(4) );
$("#g3").val( parseFloat(cfg["g3"]).toFixed(4) );
$("#g4").val( parseFloat(cfg["g4"]).toFixed(4) );
$("#g5").val( parseFloat(cfg["g5"]).toFixed(4) );
}
$("#a1").val( parseFloat(cfg["a1"]).toFixed(2) ); function setGravityDecimal(event) {
$("#a2").val( parseFloat(cfg["a2"]).toFixed(2) ); if(isPlato())
$("#a3").val( parseFloat(cfg["a3"]).toFixed(2) ); this.value = parseFloat(this.value).toFixed(1);
$("#a4").val( parseFloat(cfg["a4"]).toFixed(2) ); else
$("#a5").val( parseFloat(cfg["a5"]).toFixed(2) ); this.value = parseFloat(this.value).toFixed(4);
if( cfg["error"]!="" ) {
showError(cfg["error"]);
}
populateChart(); populateChart();
}) }
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
setButtonDisabled( false );
});
}
</script>
<!-- START FOOTER --> function populateChartForm(a, g) {
if( a != 0)
chartDataForm.push( { x: parseFloat(a), y: parseFloat(g) });
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div> chartDataForm.sort(function (a, b) {
return a.x - b.x;
});
}
function populateChartCalc(a, g) {
chartDataCalc.push( { x: parseFloat(a), y: parseFloat(g) });
}
function isPlato() {
return $("#gravity-format").text() == "P";
}
function populateChart() {
chartDataCalc.length = 0
for( i = 25.0; i<80.0; i+=5.0) {
var formula = $("#formula").text();
var angle = i.toString();
formula=formula.replaceAll( "tilt^3", angle+"*"+angle+"*"+angle );
formula=formula.replaceAll( "tilt^2", angle+"*"+angle );
formula=formula.replaceAll( "tilt", angle );
var g = eval( formula );
if(isPlato())
g = convertToPlato(g);
populateChartCalc( i, g );
}
chartDataForm.length = 0
populateChartForm( $("#a1").val(), $("#g1").val() );
populateChartForm( $("#a2").val(), $("#g2").val() );
populateChartForm( $("#a3").val(), $("#g3").val() );
populateChartForm( $("#a4").val(), $("#g4").val() );
populateChartForm( $("#a5").val(), $("#g5").val() );
populateChartForm( $("#a6").val(), $("#g6").val() );
populateChartForm( $("#a7").val(), $("#g7").val() );
populateChartForm( $("#a8").val(), $("#g8").val() );
populateChartForm( $("#a9").val(), $("#g9").val() );
populateChartForm( $("#a10").val(), $("#g10").val() );
if( myChart )
myChart.destroy();
myChart = new Chart(
document.getElementById('gravityChart'),
configChart
);
}
function setButtonDisabled( b ) {
$("#calculate-btn").prop("disabled", b);
}
// Get the configuration values from the API
function getConfig() {
setButtonDisabled( true );
var url = "/api/formula";
//var url = "/test/formula.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#id").val(cfg["id"]);
$("#angle").text(cfg["angle"]);
$("#formula").text(cfg["gravity-formula"]);
$("#gravity-format").text(cfg["gravity-format"]); // Sets the variable used by isPlato()
if(isPlato()) {
$("#gravity-header").text("Gravity (Plato):");
$("#g1").val( parseFloat(cfg["g1"]).toFixed(1) );
$("#g2").val( parseFloat(cfg["g2"]).toFixed(1) );
$("#g3").val( parseFloat(cfg["g3"]).toFixed(1) );
$("#g4").val( parseFloat(cfg["g4"]).toFixed(1) );
$("#g5").val( parseFloat(cfg["g5"]).toFixed(1) );
$("#g6").val( parseFloat(cfg["g6"]).toFixed(1) );
$("#g7").val( parseFloat(cfg["g7"]).toFixed(1) );
$("#g8").val( parseFloat(cfg["g8"]).toFixed(1) );
$("#g9").val( parseFloat(cfg["g9"]).toFixed(1) );
$("#g10").val( parseFloat(cfg["g10"]).toFixed(1) );
} else {
$("#gravity-header").text("Gravity (SG):");
$("#g1").val( parseFloat(cfg["g1"]).toFixed(4) );
$("#g2").val( parseFloat(cfg["g2"]).toFixed(4) );
$("#g3").val( parseFloat(cfg["g3"]).toFixed(4) );
$("#g4").val( parseFloat(cfg["g4"]).toFixed(4) );
$("#g5").val( parseFloat(cfg["g5"]).toFixed(4) );
$("#g6").val( parseFloat(cfg["g6"]).toFixed(4) );
$("#g7").val( parseFloat(cfg["g7"]).toFixed(4) );
$("#g8").val( parseFloat(cfg["g8"]).toFixed(4) );
$("#g9").val( parseFloat(cfg["g9"]).toFixed(4) );
$("#g10").val( parseFloat(cfg["g10"]).toFixed(4) );
}
$("#a1").val( parseFloat(cfg["a1"]).toFixed(2) );
$("#a2").val( parseFloat(cfg["a2"]).toFixed(2) );
$("#a3").val( parseFloat(cfg["a3"]).toFixed(2) );
$("#a4").val( parseFloat(cfg["a4"]).toFixed(2) );
$("#a5").val( parseFloat(cfg["a5"]).toFixed(2) );
$("#a6").val( parseFloat(cfg["a6"]).toFixed(2) );
$("#a7").val( parseFloat(cfg["a7"]).toFixed(2) );
$("#a8").val( parseFloat(cfg["a8"]).toFixed(2) );
$("#a9").val( parseFloat(cfg["a9"]).toFixed(2) );
$("#a10").val( parseFloat(cfg["a10"]).toFixed(2) );
if( cfg["error"]!="" ) {
showError(cfg["error"]);
}
populateChart();
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
setButtonDisabled( false );
});
}
</script>
<!-- START FOOTER -->
<div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -5,184 +5,191 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<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> .row-margin-10 { margin-top: 1.0em; }
</style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <!-- START MENU -->
<a class="navbar-brand" href="/firmware.htm">Beer Gravity Monitor - Firmware upgrade</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"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<ul class="navbar-nav mr-auto"> <div class="container">
<li class="nav-item"> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<a class="nav-link" href="javascript:history.back()">Back to configuration</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
</li> <span class="navbar-toggler-icon"></span>
</ul> </button>
<div class="spinner-border text-light" id="spinner" role="status"></div> <div class="collapse navbar-collapse" id="navbarNav">
</div> <ul class="navbar-nav mr-auto">
</nav> <li class="nav-item">
<a class="nav-link" href="javascript:history.back()">Back to configuration</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</div>
</nav>
<!-- START MAIN INDEX --> <!-- START MAIN INDEX -->
<div class="container"> <div class="container row-margin-10">
<hr class="my-4"> <div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"> <script type="text/javascript">
<div id="alert-msg">...</div> function showError( msg ) {
<button type="button" id="alert-btn" class="close" aria-label="Close"> $('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
<span aria-hidden="true">&times;</span> $('#alert').text( msg );
</button> }
</div>
<script type="text/javascript"> function showSuccess( msg ) {
function showError( msg ) { $('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show') $('#alert').text( msg );
$('#alert-msg').text( msg ); }
}
function showSuccess( msg ) { $("#alert-btn").click(function(e){
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show') $('.alert').addClass('hide').removeClass('show').addClass('d-none');
$('#alert-msg').text( msg ); });
} </script>
$("#alert-btn").click(function(e){ <div class="accordion" id="accordion">
$('.alert').addClass('d-none').removeClass('show') <div class="accordion-item">
}); <h2 class="accordion-header" id="headingFirmware">
</script> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFirmware" aria-expanded="true" aria-controls="collapseFirmware">
<b>Upload firmware</b>
</button>
</h2>
<div id="collapseFirmware" class="accordion-collapse collapse show" aria-labelledby="headingFirmware" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-12 themed-grid-col bg-light">Here you can upload a new firmware version, it will not check the version number so you can also downgrade the firmware here. <div class="col-md-12 themed-grid-col bg-light">Here you can upload a new firmware version, it will not check the version number so you can also downgrade the firmware here.
</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">Current version:</div>
<div class="col-md-10 themed-grid-col bg-light" id="app-ver">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">Platform:</div>
<div class="col-md-10 themed-grid-col bg-light" id="platform">Loading...</div>
</div>
<form id="uploadForm" enctype="multipart/form-data">
<div class="row mb-3">
<div class="col-md-8 custom-file">
<input type="file" accept=".bin" class="custom-file-input" name="name" id="name" onchange="checkName()" data-bs-toggle="tooltip" title="Select a firmware file to upload">
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<button type="submit" class="btn btn-primary" id="upload-btn" value="upload" data-bs-toggle="tooltip" title="Update the device with the selected firmware">Flash firmware</button>
</div>
</div>
</form>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
<div class="row mb-3"> <script type="text/javascript">
<div class="col-md-8 themed-grid-col bg-light">Current version:</div> window.onload = getStatus;
<div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Platform:</div>
<div class="col-md-4 themed-grid-col bg-light" id="platform">Loading...</div>
</div>
<div class="row mb-3"> $(document).ready(function() {
<!-- $("#uploadForm").on('submit', function(e) {
<form action="/api/upload" method="post" enctype="multipart/form-data"> e.preventDefault();
<div class="col-md-8 custom-file"> $.ajax( {
<input type="file" accept=".bin" class="custom-file-input" name="name" id="name"> xhr: function() {
<label class="custom-file-label" for="name">Choose file</label> var xhr = new window.XMLHttpRequest();
</div> xhr.upload.addEventListener("progress", function(evt) {
<button type="submit" class="btn btn-primary" id="upload-btn" value="upload">Flash firmware</button> if (evt.lengthComputable) {
</form> progressHandler(evt);
--> }
<form id="uploadForm" enctype="multipart/form-data"> }, false);
<div class="col-md-8 custom-file"> return xhr;
<input type="file" accept=".bin" class="custom-file-input" name="name" id="name" onchange="checkName()"> },
<label class="custom-file-label" for="name">Choose file</label> type: 'POST',
</div> url: '/api/upload',
<button type="submit" class="btn btn-primary" id="upload-btn" value="upload">Flash firmware</button> data: new FormData(this),
</form> contentType: false,
</div> cache: false,
processData:false,
<div class="progress"> beforeSend: function() {
<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div> setProgress(0);
</div> },
error:function() {
<hr class="my-4"> showError("Upload failed");
</div> },
success: function(resp) {
<script type="text/javascript"> showSuccess("Upload completed, device is restarting. Waiting 10 seconds to refresh browser.");
window.onload = getStatus; setTimeout(() => {
window.location = "/";
$(document).ready(function() { }, 10000);
$("#uploadForm").on('submit', function(e) { }
e.preventDefault(); });
$.ajax( {
xhr: function() {
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
progressHandler(evt);
}
}, false);
return xhr;
},
type: 'POST',
url: '/api/upload',
data: new FormData(this),
contentType: false,
cache: false,
processData:false,
beforeSend: function() {
setProgress(0);
},
error:function() {
showError("Upload failed");
},
success: function(resp) {
showSuccess("Upload completed, device is restarting. Waiting 10 seconds to refresh browser.");
setTimeout(() => {
window.location = "/";
}, 10000);
}
}); });
}); });
});
function checkName() { function checkName() {
setButtonDisabled( $("#name").val()!="" ? false : true ); setButtonDisabled( $("#name").val()!="" ? false : true );
} }
function progressHandler(event) { function progressHandler(event) {
var percent = (event.loaded / event.total) * 100; var percent = (event.loaded / event.total) * 100;
setProgress(Math.round(percent)); setProgress(Math.round(percent));
} }
function setProgress(val) { function setProgress(val) {
$('.progress-bar').css('width', val+'%').attr('aria-valuenow', val).text(val + "%"); $('.progress-bar').css('width', val+'%').attr('aria-valuenow', val).text(val + "%");
} }
function setButtonDisabled( b ) { function setButtonDisabled( b ) {
$("#upload-btn").prop("disabled", b); $("#upload-btn").prop("disabled", b);
} }
function getStatus() { function getStatus() {
setButtonDisabled( true ); setButtonDisabled( true );
var url = "/api/status"; var url = "/api/status";
//var url = "/test/status.json"; //var url = "/test/status.json";
$('#spinner').show(); $('#spinner').show();
$.getJSON(url, function (cfg) { $.getJSON(url, function (cfg) {
console.log( cfg ); console.log( cfg );
$("#app-ver").text(cfg["app-ver"]); $("#app-ver").text(cfg["app-ver"]);
$("#platform").text(cfg["platform"]); $("#platform").text(cfg["platform"]);
}) })
.fail(function () { .fail(function () {
showError('Unable to get data from the device.'); showError('Unable to get data from the device.');
}) })
.always(function() { .always(function() {
$('#spinner').hide(); $('#spinner').hide();
}); });
} }
function start() { function start() {
setInterval(getStatus, 3000); setInterval(getStatus, 3000);
} }
</script> </script>
<!-- START FOOTER --> <!-- START FOOTER -->
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div> <div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

View File

@ -1,87 +1,79 @@
<!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="/firmware.htm">Beer Gravity Monitor - Firmware upgrade</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="javascript:history.back()">Back to configuration</a></li></ul><div class="spinner-border text-light" id="spinner" role="status"></div></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-12 themed-grid-col bg-light">Here you can upload a new firmware version, it will not check the version number so you can also downgrade the firmware here.</div></div><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"><div class="col-md-8 themed-grid-col bg-light">Platform:</div><div class="col-md-4 themed-grid-col bg-light" id="platform">Loading...</div></div><div class="row mb-3"><!-- <!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://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"><style>.row-margin-10{margin-top:1em}</style></head><body class="py-4"><script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><!-- START MENU --><nav class="navbar navbar-expand-lg navbar-dark bg-primary"><div class="container"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbarNav"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="javascript:history.back()">Back to configuration</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></div></nav><!-- START MAIN INDEX --><div class="container row-margin-10"><div class="alert alert-success alert-dismissible hide fade d-none" role="alert"><div id="alert"></div><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("hide").addClass("show").removeClass("d-none"),$("#alert").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("hide").addClass("show").removeClass("d-none"),$("#alert").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("hide").removeClass("show").addClass("d-none")})</script><div class="accordion" id="accordion"><div class="accordion-item"><h2 class="accordion-header" id="headingFirmware"><button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFirmware" aria-expanded="true" aria-controls="collapseFirmware"><b>Upload firmware</b></button></h2><div id="collapseFirmware" class="accordion-collapse collapse show" aria-labelledby="headingFirmware" data-bs-parent="#accordion"><div class="accordion-body"><div class="row mb-3"><div class="col-md-12 themed-grid-col bg-light">Here you can upload a new firmware version, it will not check the version number so you can also downgrade the firmware here.</div></div><div class="row mb-3"><div class="col-md-2 themed-grid-col bg-light">Current version:</div><div class="col-md-10 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3"><div class="col-md-2 themed-grid-col bg-light">Platform:</div><div class="col-md-10 themed-grid-col bg-light" id="platform">Loading...</div></div><form id="uploadForm" enctype="multipart/form-data"><div class="row mb-3"><div class="col-md-8 custom-file"><input type="file" accept=".bin" class="custom-file-input" name="name" id="name" onchange="checkName()" data-bs-toggle="tooltip" title="Select a firmware file to upload"></div></div><div class="row mb-3"><div class="col-md-4"><button type="submit" class="btn btn-primary" id="upload-btn" value="upload" data-bs-toggle="tooltip" title="Update the device with the selected firmware">Flash firmware</button></div></div></form><div class="progress"><div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div></div></div></div></div><script type="text/javascript">window.onload = getStatus;
<form action="/api/upload" method="post" enctype="multipart/form-data">
<div class="col-md-8 custom-file">
<input type="file" accept=".bin" class="custom-file-input" name="name" id="name">
<label class="custom-file-label" for="name">Choose file</label>
</div>
<button type="submit" class="btn btn-primary" id="upload-btn" value="upload">Flash firmware</button>
</form>
--><form id="uploadForm" enctype="multipart/form-data"><div class="col-md-8 custom-file"><input type="file" accept=".bin" class="custom-file-input" name="name" id="name" onchange="checkName()"> <label class="custom-file-label" for="name">Choose file</label></div><button type="submit" class="btn btn-primary" id="upload-btn" value="upload">Flash firmware</button></form></div><div class="progress"><div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div></div><hr class="my-4"></div><script type="text/javascript">window.onload = getStatus;
$(document).ready(function() { $(document).ready(function() {
$("#uploadForm").on('submit', function(e) { $("#uploadForm").on('submit', function(e) {
e.preventDefault(); e.preventDefault();
$.ajax( { $.ajax( {
xhr: function() { xhr: function() {
var xhr = new window.XMLHttpRequest(); var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) { xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) { if (evt.lengthComputable) {
progressHandler(evt); progressHandler(evt);
} }
}, false); }, false);
return xhr; return xhr;
}, },
type: 'POST', type: 'POST',
url: '/api/upload', url: '/api/upload',
data: new FormData(this), data: new FormData(this),
contentType: false, contentType: false,
cache: false, cache: false,
processData:false, processData:false,
beforeSend: function() { beforeSend: function() {
setProgress(0); setProgress(0);
}, },
error:function() { error:function() {
showError("Upload failed"); showError("Upload failed");
}, },
success: function(resp) { success: function(resp) {
showSuccess("Upload completed, device is restarting. Waiting 10 seconds to refresh browser."); showSuccess("Upload completed, device is restarting. Waiting 10 seconds to refresh browser.");
setTimeout(() => { setTimeout(() => {
window.location = "/"; window.location = "/";
}, 10000); }, 10000);
} }
});
}); });
}); });
});
function checkName() { function checkName() {
setButtonDisabled( $("#name").val()!="" ? false : true ); setButtonDisabled( $("#name").val()!="" ? false : true );
} }
function progressHandler(event) { function progressHandler(event) {
var percent = (event.loaded / event.total) * 100; var percent = (event.loaded / event.total) * 100;
setProgress(Math.round(percent)); setProgress(Math.round(percent));
} }
function setProgress(val) { function setProgress(val) {
$('.progress-bar').css('width', val+'%').attr('aria-valuenow', val).text(val + "%"); $('.progress-bar').css('width', val+'%').attr('aria-valuenow', val).text(val + "%");
} }
function setButtonDisabled( b ) { function setButtonDisabled( b ) {
$("#upload-btn").prop("disabled", b); $("#upload-btn").prop("disabled", b);
} }
function getStatus() { function getStatus() {
setButtonDisabled( true ); setButtonDisabled( true );
var url = "/api/status"; var url = "/api/status";
//var url = "/test/status.json"; //var url = "/test/status.json";
$('#spinner').show(); $('#spinner').show();
$.getJSON(url, function (cfg) { $.getJSON(url, function (cfg) {
console.log( cfg ); console.log( cfg );
$("#app-ver").text(cfg["app-ver"]); $("#app-ver").text(cfg["app-ver"]);
$("#platform").text(cfg["platform"]); $("#platform").text(cfg["platform"]);
}) })
.fail(function () { .fail(function () {
showError('Unable to get data from the device.'); showError('Unable to get data from the device.');
}) })
.always(function() { .always(function() {
$('#spinner').hide(); $('#spinner').hide();
}); });
} }
function start() { function start() {
setInterval(getStatus, 3000); setInterval(getStatus, 3000);
}</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div></body></html> }</script><!-- START FOOTER --><div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div></div></body></html>

View File

@ -5,264 +5,302 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<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> .row-margin-10 { margin-top: 1.0em; }
</style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <!-- START MENU -->
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> <div class="container">
<span class="navbar-toggler-icon"></span> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
</button> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="javascript:history.back()">Back to configuration</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</div>
</nav>
<div class="collapse navbar-collapse" id="navbar"> <!-- START MAIN INDEX -->
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="javascript:history.back()">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 row-margin-10">
<div class="container"> <div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<hr class="my-2"> <script type="text/javascript">
function showError( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"> function showSuccess( msg ) {
<div id="alert-msg">...</div> $('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
<button type="button" id="alert-btn" class="close" aria-label="Close"> $('#alert').text( msg );
<span aria-hidden="true">&times;</span> }
</button>
</div>
<script type="text/javascript"> $("#alert-btn").click(function(e){
function showError( msg ) { $('.alert').addClass('hide').removeClass('show').addClass('d-none');
$('#alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show'); });
$('#alert-msg').text( msg ); </script>
}
function showSuccess( msg ) { <div class="accordion" id="accordion">
$('#alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show'); <div class="accordion-item">
$('#alert-msg').text( msg ); <h2 class="accordion-header" id="headingFormat">
} <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFormat" aria-expanded="true" aria-controls="collapseFormat">
<b>Push Format Templates</b>
$("#alert-btn").click(function(e){
$('#alert').addClass('d-none').removeClass('show');
});
</script>
<div class="accordion" id="accordion">
<div class="card">
<div class="card-header" id="headingFormat">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left" type="button" data-toggle="collapse" data-target="#collapseFormat" aria-expanded="true" aria-controls="collapseFormat">
Push Format Templates
</button> </button>
</h2> </h2>
</div> <div id="collapseFormat" class="accordion-collapse collapse show" aria-labelledby="headingFormat" data-bs-parent="#accordion">
<div id="collapseFormat" class="collapse show" aria-labelledby="headingFormat" data-parent="#accordion"> <div class="accordion-body">
<div class="card-body">
<input type="text" name="id" id="id" hidden> <input type="text" name="id" id="id" hidden>
<input type="text" name="http-1" id="http-1" 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="http-2" id="http-2" hidden>
<input type="text" name="http-3" id="http-3" hidden> <input type="text" name="http-3" id="http-3" hidden>
<!--<input type="text" name="brewfather" id="brewfather" hidden>-->
<input type="text" name="influxdb" id="influxdb" hidden> <input type="text" name="influxdb" id="influxdb" hidden>
<input type="text" name="mqtt" id="mqtt" hidden> <input type="text" name="mqtt" id="mqtt" hidden>
<div class="form-group row"> <div class="row mb-3">
<label for="push-target" class="col-sm-2 col-form-label">Push target:</label> <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"> <select class="custom-select col-sm-4" required name="push-target" id="push-target" data-bs-toggle="tooltip" title="Select the push target to edit format template for">
<option value="http-1">HTTP option 1 (post)</option> <option value="http-1">HTTP option 1 (post)</option>
<option value="http-2">HTTP option 2 (post)</option> <option value="http-2">HTTP option 2 (post)</option>
<option value="http-3">HTTP option 3 (get)</option> <option value="http-3">HTTP option 3 (get)</option>
<!--<option value="brewfather">Brewfather</option>-->
<option value="influxdb">Influx DB</option> <option value="influxdb">Influx DB</option>
<option value="mqtt">MQTT</option> <option value="mqtt">MQTT</option>
</select> </select>
</div> </div>
<div class="form-group row"> <div class="row mb-3">
<div class="col-sm-12"> <div class="col-sm-12">
<textarea rows="5" class="form-control" name="format" id="format"> <textarea rows="10" class="form-control" name="format" id="format">
</textarea> </textarea>
</div> </div>
</div> </div>
<div class="form-group row"> <script>
<div class="col-sm-8 offset-sm-2"> let formatTemplates = [
<button class="btn btn-primary" id="format-btn">Save</button> { "id": "GravityMon-Post", "format": "%7B%0A%20%22name%22%20%3A%20%22%24%7Bmdns%7D%22%2C%0A%20%22ID%22%3A%20%22%24%7Bid%7D%22%2C%0A%20%22token%22%20%3A%20%22gravmon%22%2C%0A%20%22interval%22%3A%20%24%7Bsleep-interval%7D%2C%0A%20%22temperature%22%3A%20%24%7Btemp%7D%2C%0A%20%22temp_units%22%3A%20%22%24%7Btemp-unit%7D%22%2C%0A%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%0A%20%22angle%22%3A%20%24%7Bangle%7D%2C%0A%20%22battery%22%3A%20%24%7Bbattery%7D%2C%0A%20%22RSSI%22%3A%20%24%7Brssi%7D%2C%0A%20%22corr-gravity%22%3A%20%24%7Bcorr-gravity%7D%2C%0A%20%22gravity-unit%22%3A%20%22%24%7Bgravity-unit%7D%22%2C%0A%20%22run-time%22%3A%20%24%7Brun-time%7D%0A%7D" },
<button class="btn btn-secondary" id="test-btn">Test</button> { "id": "GravityMon-Get", "format": "%3Fname%3D%24%7Bmdns%7D%26id%3D%24%7Bid%7D%26token%3D%24%7Btoken2%7D%26interval%3D%24%7Bsleep-interval%7D%26temperature%3D%24%7Btemp%7D%26%0Atemp-units%3D%24%7Btemp-unit%7D%26gravity%3D%24%7Bgravity%7D%26angle%3D%24%7Bangle%7D%26battery%3D%24%7Bbattery%7D%26rssi%3D%24%7Brssi%7D%26%0Acorr-gravity%3D%24%7Bcorr-gravity%7D%26gravity-unit%3D%24%7Bgravity-unit%7D%26run-time%3D%24%7Brun-time%7D" },
{ "id": "iSpindle-Post", "format": "%7B%0A%20%22name%22%20%3A%20%22%24%7Bmdns%7D%22%2C%0A%20%22ID%22%3A%20%22%24%7Bid%7D%22%2C%0A%20%22token%22%20%3A%20%22gravmon%22%2C%0A%20%22interval%22%3A%20%24%7Bsleep-interval%7D%2C%0A%20%22temperature%22%3A%20%24%7Btemp%7D%2C%0A%20%22temp_units%22%3A%20%22%24%7Btemp-unit%7D%22%2C%0A%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%0A%20%22angle%22%3A%20%24%7Bangle%7D%2C%0A%20%22battery%22%3A%20%24%7Bbattery%7D%2C%0A%20%22RSSI%22%3A%20%24%7Brssi%7D%0A%7D" },
{ "id": "BrewFatherCustom-Post", "format": "%7B%0A%20%20%20%22name%22%3A%20%22%24%7Bmdns%7D%22%2C%0A%20%20%20%22temp%22%3A%20%24%7Btemp%7D%2C%0A%20%20%20%22aux_temp%22%3A%200%2C%0A%20%20%20%22ext_temp%22%3A%200%2C%0A%20%20%20%22temp_unit%22%3A%20%22%24%7Btemp-unit%7D%22%2C%0A%20%20%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%0A%20%20%20%22gravity_unit%22%3A%20%22%24%7Bgravity-unit%7D%22%2C%0A%20%20%20%22pressure%22%3A%200%2C%0A%20%20%20%22pressure_unit%22%3A%20%22PSI%22%2C%0A%20%20%20%22ph%22%3A%200%2C%0A%20%20%20%22bpm%22%3A%200%2C%0A%20%20%20%22comment%22%3A%20%22%22%2C%0A%20%20%20%22beer%22%3A%20%22%22%2C%0A%20%20%20%22battery%22%3A%20%24%7Bbattery%7D%0A%7D" },
{ "id": "iSpindle-Mqtt", "format": "ispindel%2F%24%7Bmdns%7D%2Ftilt%3A%24%7Bangle%7D%7C%0Aispindel%2F%24%7Bmdns%7D%2Ftemperature%3A%24%7Btemp%7D%7C%0Aispindel%2F%24%7Bmdns%7D%2Ftemp_units%3A%24%7Btemp-unit%7D%7C%0Aispindel%2F%24%7Bmdns%7D%2Fbattery%3A%24%7Bbattery%7D%7C%0Aispindel%2F%24%7Bmdns%7D%2Fgravity%3A%24%7Bgravity%7D%7C%0Aispindel%2F%24%7Bmdns%7D%2Finterval%3A%24%7Bsleep-interval%7D%7C%0Aispindel%2F%24%7Bmdns%7D%2FRSSI%3A%24%7Brssi%7D%7C" },
{ "id": "HomeAssistant-Mqtt", "format": "gravmon%2F%24%7Bmdns%7D%2Ftilt%3A%24%7Bangle%7D%7C%0Agravmon%2F%24%7Bmdns%7D%2Ftemperature%3A%24%7Btemp%7D%7C%0Agravmon%2F%24%7Bmdns%7D%2Ftemp_units%3A%24%7Btemp-unit%7D%7C" },
{ "id": "UBIDots1-Post", "format": "%7B%0A%20%20%20%22temperature%22%3A%20%24%7Btemp%7D%2C%0A%20%20%20%22gravity%22%3A%20%24%7Bgravity%7D%2C%0A%20%20%20%22angle%22%3A%20%24%7Bangle%7D%2C%0A%20%20%20%22battery%22%3A%20%24%7Bbattery%7D%2C%0A%20%20%20%22rssi%22%3A%20%24%7Brssi%7D%0A%7D" } ];
</script>
<div class="row mb-3">
<div class="col-sm-2">
<button class="btn btn-primary" id="format-btn" data-bs-toggle="tooltip" title="Save the format template, saving an empty form will reset the template to default">Save</button>
<button class="btn btn-secondary" id="test-btn" data-bs-toggle="tooltip" title="Apply device data to template to see how it works">Test</button>
</div>
<select class="custom-select col-sm-4" required name="predefined" id="predefined" data-bs-toggle="tooltip" title="Select a pre-defined format template">
<option value="iSpindle-Post">iSpindle (POST)</option>
<option value="GravityMon-Post">GravityMon (POST)</option>
<option value="iSpindle-Mqtt">iSpindle (MQTT)</option>
<option value="HomeAssistant-Mqtt">Home Assistant (MQTT)</option>
<option value="UBIDots1-Post">UBIdots - Alternative 1 (POST)</option>
<option value="BrewFatherCustom-Post">Brewfather Custom Endpoint (POST)</option>
<option value="GravityMon-Get">GravityMon (GET)</option>
</select>
<div class="col-sm-4">
<button class="btn btn-secondary" id="copy-btn" data-bs-toggle="tooltip" title="Copy the selected format template to the selected push target">Copy format</button>
<button class="btn btn-secondary" id="clear-btn" data-bs-toggle="tooltip" title="Clear the current format template">Clear format</button>
</div> </div>
</div> </div>
<hr class="my-2">
<pre class="card-preview" id="preview" name="preview"></pre> <pre class="card-preview" id="preview" name="preview"></pre>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<hr class="my-4"> <script type="text/javascript">
</div> window.onload = getConfig;
<script type="text/javascript">
window.onload = getConfig;
setButtonDisabled( true );
// Opens the targetet according (if URL has #collapseOne to #collapseFour)
$(document).ready(function () {
if(location.hash != null && location.hash != ""){
$('.collapse').removeClass('in');
$(location.hash + '.collapse').collapse('show');
}
});
$("#push-target").change(function(e){
console.log(e)
selectFormat();
});
// Store the format
$("#format-btn").click(function(e) {
var s = $("#format").val();
s = s.replaceAll("\n", "");
var obj = 'id=' + $("#id").val() + '&' + $("#push-target").val() + '=' + encodeURIComponent(s);
console.log(obj);
$.ajax( {
type: "POST",
url: "/api/config/format",
data: obj,
success: function(result) { showSuccess('Format stored successfully.'); getConfig(); },
error: function(result) { showError('Unable to store format.'); }
} );
});
// Test the calibration
$("#test-btn").click(function(e) {
var url = "/api/status";
//var url = "/test/status.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
var doc = $("#format").val();
if (cfg["temp-format"]=="C")
doc = doc.replaceAll("${temp}", cfg["temp-c"]);
else
doc = doc.replaceAll("${temp}", cfg["temp-f"]);
if (cfg["gravity-format"]=="G") {
var sg = cfg["gravity"];
doc = doc.replaceAll("${gravity-sg}", sg);
doc = doc.replaceAll("${corr-gravity-sg}", sg);
var plato = 259 - (259 - sg);
doc = doc.replaceAll("${gravity-plato}", plato);
doc = doc.replaceAll("${corr-gravity-plato}", plato);
}
else {
var plato = cfg["gravity"];
doc = doc.replaceAll("${gravity-plato}", plato);
doc = doc.replaceAll("${corr-gravity-plato}", plato);
var sg = 259 / (259 - plato);
doc = doc.replaceAll("${gravity-sg}", sg);
doc = doc.replaceAll("${corr-gravity-sg}", sg);
}
doc = doc.replaceAll("${mdns}", cfg["mdns"]);
doc = doc.replaceAll("${id}", cfg["id"]);
doc = doc.replaceAll("${sleep-interval}", cfg["sleep-interval"]);
doc = doc.replaceAll("${token}", cfg["token"]);
doc = doc.replaceAll("${token2}", cfg["token2"]);
doc = doc.replaceAll("${temp-c}", cfg["temp-c"]);
doc = doc.replaceAll("${temp-f}", cfg["temp-f"]);
doc = doc.replaceAll("${temp-unit}", cfg["temp-format"]);
doc = doc.replaceAll("${battery}", cfg["battery"]);
doc = doc.replaceAll("${rssi}", cfg["rssi"]);
doc = doc.replaceAll("${run-time}", cfg["runtime-average"]);
doc = doc.replaceAll("${gravity}", cfg["gravity"]);
doc = doc.replaceAll("${gravity-unit}", cfg["gravity-format"]);
doc = doc.replaceAll("${corr-gravity}", cfg["gravity"]);
doc = doc.replaceAll("${angle}", cfg["angle"]);
doc = doc.replaceAll("${tilt}", cfg["angle"]);
// Format in a readable json string.
try {
var json = JSON.parse(doc);
doc = JSON.stringify(json, null, 2);
} catch(e) {
console.log("Not a javascript object!")
}
$("#preview").text(doc);
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
});
function setButtonDisabled( b ) {
$("#format-btn").prop("disabled", b);
$("#test-btn").prop("disabled", b);
}
function selectFormat() {
var s = "#" + $("#push-target").val()
console.log(s);
s = decodeURIComponent($(s).val());
console.log(s);
s = s.replaceAll("|", "|\n");
console.log(s);
$("#format").val(s);
$("#preview").text("");
}
// Get the configuration values from the API
function getConfig() {
setButtonDisabled( true ); setButtonDisabled( true );
var url = "/api/config/format"; // Opens the targetet according (if URL has #collapseOne to #collapseFour)
//var url = "/test/format.json"; $(document).ready(function () {
$('#spinner').show(); if(location.hash != null && location.hash != ""){
$.getJSON(url, function (cfg) { $('.collapse').removeClass('in');
console.log( cfg ); $(location.hash + '.collapse').collapse('show');
$("#id").val(cfg["id"]); }
$("#http-1").val(cfg["http-1"]);
$("#http-2").val(cfg["http-2"]);
$("#http-3").val(cfg["http-3"]);
//$("#brewfather").val(cfg["brewfather"]);
$("#influxdb").val(cfg["influxdb"]);
$("#mqtt").val(cfg["mqtt"]);
selectFormat();
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
setButtonDisabled( false );
}); });
}
</script>
<!-- START FOOTER --> $("#push-target").change(function(e){
console.log(e)
selectFormat();
});
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div> // Copy the selected template
$("#copy-btn").click(function(e) {
var id = $("#predefined").val();
//console.log( encodeURIComponent( $("#format").val() ) );
formatTemplates.forEach(function (item, index) {
if( item.id == id ) {
$("#format").val( decodeURIComponent(item.format) );
}
});
});
// Clear the selected template
$("#clear-btn").click(function(e) {
$("#format").val( "" );
});
// Store the format
$("#format-btn").click(function(e) {
var s = $("#format").val();
s = s.replaceAll("\n", "");
var obj = 'id=' + $("#id").val() + '&' + $("#push-target").val() + '=' + encodeURIComponent(s);
console.log(obj);
$.ajax( {
type: "POST",
url: "/api/config/format",
data: obj,
success: function(result) { showSuccess('Format stored successfully.'); getConfig(); },
error: function(result) { showError('Unable to store format.'); }
} );
});
// Test the calibration
$("#test-btn").click(function(e) {
var url = "/api/status";
//var url = "/test/status.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
var doc = $("#format").val();
if (cfg["temp-format"]=="C")
doc = doc.replaceAll("${temp}", cfg["temp-c"]);
else
doc = doc.replaceAll("${temp}", cfg["temp-f"]);
if (cfg["gravity-format"]=="G") {
var sg = cfg["gravity"];
doc = doc.replaceAll("${gravity-sg}", sg);
doc = doc.replaceAll("${corr-gravity-sg}", sg);
var plato = 259 - (259 - sg);
doc = doc.replaceAll("${gravity-plato}", plato);
doc = doc.replaceAll("${corr-gravity-plato}", plato);
}
else {
var plato = cfg["gravity"];
doc = doc.replaceAll("${gravity-plato}", plato);
doc = doc.replaceAll("${corr-gravity-plato}", plato);
var sg = 259 / (259 - plato);
doc = doc.replaceAll("${gravity-sg}", sg);
doc = doc.replaceAll("${corr-gravity-sg}", sg);
}
doc = doc.replaceAll("${mdns}", cfg["mdns"]);
doc = doc.replaceAll("${id}", cfg["id"]);
doc = doc.replaceAll("${sleep-interval}", cfg["sleep-interval"]);
doc = doc.replaceAll("${token}", cfg["token"]);
doc = doc.replaceAll("${token2}", cfg["token2"]);
doc = doc.replaceAll("${temp-c}", cfg["temp-c"]);
doc = doc.replaceAll("${temp-f}", cfg["temp-f"]);
doc = doc.replaceAll("${temp-unit}", cfg["temp-format"]);
doc = doc.replaceAll("${battery}", cfg["battery"]);
doc = doc.replaceAll("${rssi}", cfg["rssi"]);
doc = doc.replaceAll("${run-time}", cfg["runtime-average"]);
doc = doc.replaceAll("${gravity}", cfg["gravity"]);
doc = doc.replaceAll("${gravity-unit}", cfg["gravity-format"]);
doc = doc.replaceAll("${corr-gravity}", cfg["gravity"]);
doc = doc.replaceAll("${angle}", cfg["angle"]);
doc = doc.replaceAll("${tilt}", cfg["angle"]);
// Format in a readable json string.
try {
var json = JSON.parse(doc);
doc = JSON.stringify(json, null, 2);
} catch(e) {
console.log("Not a javascript object!")
}
$("#preview").text(doc);
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
});
function setButtonDisabled( b ) {
$("#format-btn").prop("disabled", b);
$("#test-btn").prop("disabled", b);
}
function selectFormat() {
var s = "#" + $("#push-target").val()
console.log(s);
s = decodeURIComponent($(s).val());
console.log(s);
s = s.replaceAll("|", "|\n");
console.log(s);
$("#format").val(s);
$("#preview").text("");
}
// Get the configuration values from the API
function getConfig() {
setButtonDisabled( true );
var url = "/api/config/format";
//var url = "/test/format.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#id").val(cfg["id"]);
$("#http-1").val(cfg["http-1"]);
$("#http-2").val(cfg["http-2"]);
$("#http-3").val(cfg["http-3"]);
$("#influxdb").val(cfg["influxdb"]);
$("#mqtt").val(cfg["mqtt"]);
selectFormat();
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
setButtonDisabled( false );
});
}
</script>
<!-- START FOOTER -->
<div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View File

@ -5,237 +5,258 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<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> .row-margin-10 { margin-top: 1.0em; }
</style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <!-- START MENU -->
<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"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<ul class="navbar-nav mr-auto"> <div class="container">
<li class="nav-item active"> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
</li> <span class="navbar-toggler-icon"></span>
<li class="nav-item"> </button>
<a class="nav-link" href="/config.htm">Configuration</a> <div class="collapse navbar-collapse" id="navbarNav">
</li> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/calibration.htm">Calibration</a> <a class="nav-link active" href="#"><b>Home</b></a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/about.htm">About</a> <a class="nav-link" href="/config.htm">Configuration</a>
</li> </li>
</ul> <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>
</div>
</nav>
<div class="spinner-border text-light" id="spinner" role="status"></div> <!-- START MAIN INDEX -->
</div>
</nav>
<!-- START MAIN INDEX --> <div class="container row-margin-10">
<div class="container"> <div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<hr class="my-4"> <script type="text/javascript">
function showError( msg ) {
console.log("Error:" + msg);
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"> function showSuccess( msg ) {
<div id="alert-msg">...</div> console.log("Success:" + msg);
<button type="button" id="alert-btn" class="close" aria-label="Close"> $('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
<span aria-hidden="true">&times;</span> $('#alert').text( msg );
</button> }
</div>
<script type="text/javascript"> $("#alert-btn").click(function(e){
function showError( msg ) { console.log("Disable");
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show') $('.alert').addClass('hide').removeClass('show').addClass('d-none');
$('#alert-msg').text( msg ); });
} </script>
function showSuccess( msg ) { <div class="accordion" id="accordion">
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show') <div class="accordion-item">
$('#alert-msg').text( msg ); <h2 class="accordion-header" id="headingSoftware">
} <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSoftware" aria-expanded="true" aria-controls="collapseSoftware">
<b>Device</b>
</button>
</h2>
<div id="collapseSoftware" class="accordion-collapse collapse show" aria-labelledby="headingSoftware" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-4 bg-light">Current version:</div>
<div class="col-md-4 bg-light" id="app-ver">Loading...</div>
</div>
<div class="row mb-3" id="h-app-ver-new" hidden>
<div class="col-md-4 bg-light">New version:</div>
<div class="col-md-4 bg-light" id="app-ver-new">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Host name:</div>
<div class="col-md-4 bg-light" id="mdns">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Device ID:</div>
<div class="col-md-4 bg-light" id="id">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Platform:</div>
<div class="col-md-4 bg-light" id="platform">Loading...</div>
</div>
$("#alert-btn").click(function(e){ <script>
$('.alert').addClass('d-none').removeClass('show') $("#log-btn").click(function(e){
}); loadLog();
</script> });
setInterval(function() {
loadLog();
}, 3000); //5 seconds
<div class="row mb-3"> function loadLog() {
<div class="col-md-8 themed-grid-col bg-light">Current version:</div> $("#logContent").load("/log");
<div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div> //$("#logContent").load("/test/status.json");
</div> };
<div class="row mb-3" id="h-app-ver-new" hidden> </script>
<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">Platform:</div>
<div class="col-md-4 themed-grid-col bg-light" id="platform">Loading...</div>
</div>
<script> <button class="btn btn-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLog" aria-expanded="false" aria-controls="collapseLog" data-bs-toggle="tooltip" title="Load and show the last 10 errors that has occured on the device">
$("#log-btn").click(function(e){ View error log
loadLog(); </button>
});
setInterval(function() {
loadLog();
}, 3000); //5 seconds
function loadLog() { <div class="collapse row-margin-10" id="collapseLog">
$("#logContent").load("/log"); <div class="card card-body">
//$("#logContent").load("/test/status.json"); <pre><code class="card-text" id="logContent"></code></pre>
}; </div>
</script> </div>
<div class="row mb-3"> </div>
<a class="badge badge-primary" data-toggle="collapse" href="#collapseLog" role="button" aria-expanded="false" aria-controls="collapseLog" id="log-btn"> </div>
View error log </div>
</a>
</div>
<div class="collapse" id="collapseLog"> <h2 class="accordion-header" id="headingData">
<div class="card card-body"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseData" aria-expanded="true" aria-controls="collapseData">
<pre><code id="logContent"></code></pre> <b>Measurement</b>
</div> </button>
</div> </h2>
<div id="collapseData" class="accordion-collapse collapse show" aria-labelledby="headingData" data-bs-parent="#accordion">
<div class="accordion-body">
<hr class="my-4"> <div class="row mb-3">
<div class="col-md-4 bg-light">Gravity:</div>
<div class="col-md-4 bg-light" id="gravity">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Temperature:</div>
<div class="col-md-4 bg-light" id="temp">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Angle/Tilt:</div>
<div class="col-md-4 bg-light" id="angle">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Battery:</div>
<div class="col-md-4 bg-light" id="battery">Loading...</div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Gravity:</div> <div class="col-md-4 bg-light">Average runtime:</div>
<div class="col-md-4 themed-grid-col bg-light" id="gravity">Loading...</div> <div class="col-md-4 bg-light" id="runtime">Loading...</div>
</div> </div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Temperature:</div>
<div class="col-md-4 themed-grid-col bg-light" id="temp">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Angle/Tilt:</div>
<div class="col-md-4 themed-grid-col bg-light" id="angle">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Battery:</div>
<div class="col-md-4 themed-grid-col bg-light" id="battery">Loading...</div>
</div>
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Average runtime:</div> <div class="col-md-8 bg-light custom-control custom-checkbox">
<div class="col-md-4 themed-grid-col bg-light" id="runtime">Loading...</div> <input type="checkbox" class="custom-control-input" name="sleep-mode" id="sleep-mode" disabled>
</div> <label class="custom-control-label" for="sleep-mode">Do not enter sleep mode when floating (check this if you are collecting angles/tilt for calibration).</label>
</div>
</div>
<div class="row mb-3"> </div>
<div class="col-md-12 px-md-5 themed-grid-col bg-light custom-control custom-checkbox"> </div>
<input type="checkbox" class="custom-control-input" name="sleep-mode" id="sleep-mode" disabled>
<label class="custom-control-label" for="sleep-mode">Do not enter sleep mode when floating (check this if you are collecting angles/tilt for calibration).</label>
</div> </div>
</div> </div>
<hr class="my-4"> <script type="text/javascript">
window.onload = start;
</div> $("#sleep-mode").click(function(e){
console.log( "Blocking sleep mode = " + $("#sleep-mode").is(":checked"));
<script type="text/javascript"> $.ajax( {
window.onload = start; type: "POST",
url: "/api/status/sleepmode",
$("#sleep-mode").click(function(e){ data: { "id": $("#id").text(), "sleep-mode": $("#sleep-mode").is(":checked") },
console.log( "Blocking sleep mode = " + $("#sleep-mode").is(":checked")); success: function(result) { },
$.ajax( { error: function(result) { showError('Could not update sleep mode for device.'); },
type: "POST", } );
url: "/api/status/sleepmode",
data: { "id": $("#id").text(), "sleep-mode": $("#sleep-mode").is(":checked") },
success: function(result) { },
error: function(result) { showError('Could not update sleep mode for device.'); },
} );
});
function getStatus() {
var url = "/api/status";
//var url = "/test/status.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
//$("#app-ver").text(cfg["app-ver"] + " (html 0.8.0)");
$("#app-ver").text(cfg["app-ver"] + " (" + cfg["app-build"] + ")");
$("#mdns").text(cfg["mdns"]);
$("#id").text(cfg["id"]);
$("#platform").text(cfg["platform"]);
$("#runtime").text(cfg["runtime-average"] + " seconds");
var angle = cfg["angle"];
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");
}
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");
else
$("#temp").text(cfg["temp-f"] + " F");
if( cfg["sleep-mode"] )
$("#sleep-mode").attr("checked", true );
else
$("#sleep-mode").attr("checked", false );
$("#sleep-mode").removeAttr("disabled");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
}); });
}
function start() { function getStatus() {
setInterval(getStatus, 3000); var url = "/api/status";
} //var url = "/test/status.json";
</script> $('#spinner').show();
<!-- START FOOTER --> $.getJSON(url, function (cfg) {
console.log( cfg );
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div> $("#app-ver").text(cfg["app-ver"] + " (" + cfg["app-build"] + ")");
$("#mdns").text(cfg["mdns"]);
$("#id").text(cfg["id"]);
$("#platform").text(cfg["platform"]);
$("#runtime").text(cfg["runtime-average"] + " seconds");
var angle = cfg["angle"];
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");
}
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");
else
$("#temp").text(cfg["temp-f"] + " F");
if( cfg["sleep-mode"] )
$("#sleep-mode").attr("checked", true );
else
$("#sleep-mode").attr("checked", false );
$("#sleep-mode").removeAttr("disabled");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
}
function start() {
setInterval(getStatus, 3000);
}
</script>
<!-- START FOOTER -->
<div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View File

@ -5,213 +5,219 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<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> .row-margin-10 { margin-top: 1.0em; }
</style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <!-- START MENU -->
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"> <div class="container">
<span class="navbar-toggler-icon"></span> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
</button> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="javascript:history.back()">Back to configuration</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</div>
</nav>
<div class="collapse navbar-collapse" id="navbar"> <!-- START MAIN INDEX -->
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="javascript:history.back()">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 row-margin-10">
<div class="container"> <div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<hr class="my-2"> <script type="text/javascript">
function showError( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"> function showSuccess( msg ) {
<div id="alert-msg">...</div> $('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
<button type="button" id="alert-btn" class="close" aria-label="Close"> $('#alert').text( msg );
<span aria-hidden="true">&times;</span> }
</button>
$("#alert-btn").click(function(e){
$('.alert').addClass('hide').removeClass('show').addClass('d-none');
});
</script>
<div class="accordion" id="accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingTest">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTest" aria-expanded="true" aria-controls="collapseTest">
<b>Testing push targets</b>
</button>
</h2>
<div id="collapseTest" class="accordion-collapse collapse show" aria-labelledby="headingTest" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row mb-3">
<pre class="card-preview" id="preview" name="preview">Press test button to start testing all defined push targets.</pre>
</div>
<div class="row mb-3">
<div class="col-sm-8">
<button class="btn btn-primary" id="test-btn" data-bs-toggle="tooltip" title="Test all push targets">Test</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
function showError( msg ) { $('#spinner').hide();
$('#alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show');
$('#alert-msg').text( msg ); function clearLog() {
$("#preview").text("");
} }
function showSuccess( msg ) { function appendLog(log) {
$('#alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show'); doc = $("#preview").text();
$('#alert-msg').text( msg ); doc += log + "\n";
$("#preview").text(doc);
} }
$("#alert-btn").click(function(e){ // Get the configuration values from the API
$('#alert').addClass('d-none').removeClass('show'); $("#test-btn").click(function(e) {
clearLog();
appendLog( "Starting test of push targets" );
var url = "/api/status";
//var url = "/test/status.json";
$("#test-btn").prop("disabled", true);
$('#spinner').show();
$.getJSON(url, function (cfg) {
var id = cfg["id"];
console.log( id );
testHttp( id, "http-1" );
testHttp( id, "http-2" );
testHttp( id, "http-3" );
testInfluxdb( id );
testMqtt( id );
$('#spinner').hide();
$("#test-btn").prop("disabled", false);
})
.fail(function () {
showError('Unable to get data from the device.');
$('#spinner').hide();
$("#test-btn").prop("disabled", false);
})
.always(function() {
});
}); });
function testMqtt(id) {
var url = "/api/test/push";
url += "?id=" + id + "&format=mqtt";
//var url = "/test/push.json";
$.getJSON(url, function (cfg) {
var code = cfg["code"];
var success = cfg["success"];
var enabled = cfg["enabled"];
if(!enabled) {
appendLog( "Push target 'mqtt' is not configured/used" );
} else if(success) {
appendLog( "Push target 'mqtt' successful" );
} else{
if(code==-3)
appendLog( "Push target 'mqtt' failed to connect" );
else if(code==-4)
appendLog( "Push target 'mqtt' failed with error timeout" );
else if(code==-10)
appendLog( "Push target 'mqtt' failed with error denied" );
else
appendLog( "Push target 'mqtt' failed with error code " + code );
}
})
.fail(function () {
appendLog( "Failed to test push target 'mqtt'");
})
}
function testInfluxdb(id) {
var url = "/api/test/push";
url += "?id=" + id + "&format=influxdb";
//var url = "/test/push.json";
$.getJSON(url, function (cfg) {
var code = cfg["code"];
var success = cfg["success"];
var enabled = cfg["enabled"];
if(!enabled) {
appendLog( "Push target 'influxdb' is not configured/used" );
} else if(success) {
appendLog( "Push target 'influxdb' successful" );
} else{
if(code==400)
appendLog( "Push target 'influxdb' failed with error code 400, bad request" );
else if(code==401)
appendLog( "Push target 'influxdb' failed with error code 401, unauthorized" );
else if(code==404)
appendLog( "Push target 'influxdb' failed with error code 404, url not found" );
else
appendLog( "Push target 'influxdb' failed with error code " + code );
}
})
.fail(function () {
appendLog( "Failed to test push target 'influxdb'");
})
}
function testHttp(id, target) {
var url = "/api/test/push";
url += "?id=" + id + "&format=" + target;
//var url = "/test/push.json";
$.getJSON(url, function (cfg) {
var code = cfg["code"];
var success = cfg["success"];
var enabled = cfg["enabled"];
if(!enabled) {
appendLog( "Push target '" + target + "' is not configured/used" );
} else if(success) {
appendLog( "Push target '" + target + "' successful" );
} else{
if(code==400)
appendLog( "Push target '" + target + "' failed with error code 400, bad request" );
else if(code==401)
appendLog( "Push target '" + target + "' failed with error code 401, unauthorized" );
else if(code==404)
appendLog( "Push target '" + target + "' failed with error code 404, url not found" );
else
appendLog( "Push target '" + target + "' failed with error code " + code );
}
})
.fail(function () {
appendLog( "Failed to test push target '" + target + "'");
})
}
</script> </script>
<div> <!-- START FOOTER -->
<div class="card">
<div class="card-body">
<pre class="card-preview" id="preview" name="preview"></pre>
</div>
</div>
<div class="form-group row"> <div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
</div>
<div class="form-group row">
<div class="col-sm-8">
<button class="btn btn-primary" id="test-btn">Test</button>
</div>
</div>
<hr class="my-4">
</div>
<script type="text/javascript">
$('#spinner').hide();
function clearLog() {
$("#preview").text("");
}
function appendLog(log) {
doc = $("#preview").text();
doc += log + "\n";
$("#preview").text(doc);
}
// Get the configuration values from the API
$("#test-btn").click(function(e) {
clearLog();
appendLog( "Starting test of push targets" );
var url = "/api/status";
//var url = "/test/status.json";
$("#test-btn").prop("disabled", true);
$('#spinner').show();
$.getJSON(url, function (cfg) {
var id = cfg["id"];
console.log( id );
testHttp( id, "http-1" );
testHttp( id, "http-2" );
testHttp( id, "http-3" );
testHttp( id, "brewfather" );
testInfluxdb( id );
testMqtt( id );
$('#spinner').hide();
$("#test-btn").prop("disabled", false);
})
.fail(function () {
showError('Unable to get data from the device.');
$('#spinner').hide();
$("#test-btn").prop("disabled", false);
})
.always(function() {
});
});
function testMqtt(id) {
var url = "/api/test/push";
url += "?id=" + id + "&format=mqtt";
//var url = "/test/push.json";
$.getJSON(url, function (cfg) {
var code = cfg["code"];
var success = cfg["success"];
var enabled = cfg["enabled"];
if(!enabled) {
appendLog( "Push target 'mqtt' is not configured/used" );
} else if(success) {
appendLog( "Push target 'mqtt' successful" );
} else{
if(code==-3)
appendLog( "Push target 'mqtt' failed to connect" );
else if(code==-4)
appendLog( "Push target 'mqtt' failed with error timeout" );
else if(code==-10)
appendLog( "Push target 'mqtt' failed with error denied" );
else
appendLog( "Push target 'mqtt' failed with error code " + code );
}
})
.fail(function () {
appendLog( "Failed to test push target 'influxdb'");
})
}
function testInfluxdb(id) {
var url = "/api/test/push";
url += "?id=" + id + "&format=influxdb";
//var url = "/test/push.json";
$.getJSON(url, function (cfg) {
var code = cfg["code"];
var success = cfg["success"];
var enabled = cfg["enabled"];
if(!enabled) {
appendLog( "Push target 'influxdb' is not configured/used" );
} else if(success) {
appendLog( "Push target 'influxdb' successful" );
} else{
if(code==400)
appendLog( "Push target 'influxdb' failed with error code 400, bad request" );
else if(code==401)
appendLog( "Push target 'influxdb' failed with error code 401, unauthorized" );
else if(code==404)
appendLog( "Push target 'influxdb' failed with error code 404, url not found" );
else
appendLog( "Push target 'influxdb' failed with error code " + code );
}
})
.fail(function () {
appendLog( "Failed to test push target 'influxdb'");
})
}
function testHttp(id, target) {
var url = "/api/test/push";
url += "?id=" + id + "&format=" + target;
//var url = "/test/push.json";
$.getJSON(url, function (cfg) {
var code = cfg["code"];
var success = cfg["success"];
var enabled = cfg["enabled"];
if(!enabled) {
appendLog( "Push target '" + target + "' is not configured/used" );
} else if(success) {
appendLog( "Push target '" + target + "' successful" );
} else{
if(code==400)
appendLog( "Push target '" + target + "' failed with error code 400, bad request" );
else if(code==401)
appendLog( "Push target '" + target + "' failed with error code 401, unauthorized" );
else if(code==404)
appendLog( "Push target '" + target + "' failed with error code 404, url not found" );
else
appendLog( "Push target '" + target + "' failed with error code " + code );
}
})
.fail(function () {
appendLog( "Failed to test push target '" + target + "'");
})
}
</script>
<!-- START FOOTER -->
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View File

@ -5,160 +5,175 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content=""> <meta name="description" content="">
<title>Beer Gravity Monitor</title> <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"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> <style>
<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> .row-margin-10 { margin-top: 1.0em; }
</style>
</head> </head>
<body class="py-4"> <body class="py-4">
<!-- START MENU --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary"> <!-- START MENU -->
<a class="navbar-brand" href="/upload.htm">Beer Gravity Monitor - Missing html files</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"> <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="spinner-border text-light" id="spinner" role="status"></div> <div class="container">
</div> <a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
</nav> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</div>
</nav>
<!-- START MAIN INDEX --> <!-- START MAIN INDEX -->
<div class="container"> <div class="container row-margin-10">
<hr class="my-4"> <div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"> <script type="text/javascript">
<div id="alert-msg">...</div> function showError( msg ) {
<button type="button" id="alert-btn" class="close" aria-label="Close"> $('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
<span aria-hidden="true">&times;</span> $('#alert').text( msg );
</button> }
function showSuccess( msg ) {
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
$("#alert-btn").click(function(e){
$('.alert').addClass('hide').removeClass('show').addClass('d-none');
});
</script>
<div class="accordion" id="accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingUpload">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseUpload" aria-expanded="true" aria-controls="collapseUpload">
<b>Upload missing html files</b>
</button>
</h2>
<div id="collapseUpload" class="accordion-collapse collapse show" aria-labelledby="headingUpload" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-8 bg-light">The listed files below needs to be uploaded to the FileSystem in order for the GUI to work.
You can also flash the LittleFS filesystem but in that case you will loose your device settings. An OTA upgrade will automatically download
the files if they are found in the same location as the firmware.bin. This page is a fallback option.
</div>
<div class="col-md-8 bg-light"><br><strong>Once all the files are confirmed, please reboot the device for normal operation.</strong></div>
</div>
<div class="row mb-3">
<div class="col-md-2 bg-light">index.min.htm</div>
<div class="col-md-6 bg-light" id="index">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 bg-light">config.min.htm</div>
<div class="col-md-6 bg-light" id="config">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 bg-light">calibration.min.htm</div>
<div class="col-md-6 bg-light" id="calibration">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 bg-light">format.min.htm</div>
<div class="col-md-6 bg-light" id="format">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 bg-light">test.min.htm</div>
<div class="col-md-6 bg-light" id="test">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 bg-light">about.min.htm</div>
<div class="col-md-6 bg-light" id="about">Checking...</div>
</div>
<form action="/api/upload" method="post" enctype="multipart/form-data">
<div class="row mb-3">
<div class="col-md-8 custom-file">
<input type="file" accept=".min.htm" class="custom-file-input" name="name" id="name" data-bs-toggle="tooltip" title="Select a file that matches the name in the list above to upload it to the device">
</div>
</div>
<div class="row mb-3">
<div class="col-md-3">
<button type="submit" class="btn btn-primary" id="upload-btn" value="upload" data-bs-toggle="tooltip" title="Upload the selected file to the device">Upload</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div> </div>
<script type="text/javascript"> <script type="text/javascript">
function showError( msg ) { window.onload = getUpload;
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
}
function showSuccess( msg ) { // Add the following code if you want the name of the file appear on select
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show') $(".custom-file-input").on("change", function() {
$('#alert-msg').text( msg ); var fileName = $(this).val().split("\\").pop();
} $(this).siblings(".custom-file-label").addClass("selected").html(fileName);
});
$("#alert-btn").click(function(e){ function getUpload() {
$('.alert').addClass('d-none').removeClass('show') var url = "/api/upload";
}); //var url = "/test/upload.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
if( cfg["index"] )
$("#index").text("Completed.");
else
$("#index").text("File is missing.");
if( cfg["config"] )
$("#config").text("Completed.");
else
$("#config").text("File is missing.");
if( cfg["calibration"] )
$("#calibration").text("Completed.");
else
$("#calibration").text("File is missing.");
if( cfg["test"] )
$("#test").text("Completed.");
else
$("#test").text("File is missing.");
if( cfg["format"] )
$("#format").text("Completed.");
else
$("#format").text("File is missing.");
if( cfg["about"] )
$("#about").text("Completed.");
else
$("#about").text("File is missing.");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
}
</script> </script>
<div class="row mb-3"> <!-- START FOOTER -->
<div class="col-md-8 themed-grid-col bg-light">The listed files below needs to be uploaded to the FileSystem in order for the GUI to work.
You can also flash the LittleFS filesystem but in that case you will loose your device settings. An OTA upgrade will automatically download
the files if they are found in the same location as the firmware.bin. This page is a fallback option.
</div>
<div class="col-md-8 themed-grid-col bg-light"><br><strong>Once all the files are confirmed, please reboot the device for normal operation.</strong></div>
</div>
<div class="row mb-3"> <div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
<div class="col-md-2 themed-grid-col bg-light">index.min.htm</div>
<div class="col-md-6 themed-grid-col bg-light" id="index">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">config.min.htm</div>
<div class="col-md-6 themed-grid-col bg-light" id="config">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">calibration.min.htm</div>
<div class="col-md-6 themed-grid-col bg-light" id="calibration">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">format.min.htm</div>
<div class="col-md-6 themed-grid-col bg-light" id="format">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">test.min.htm</div>
<div class="col-md-6 themed-grid-col bg-light" id="test">Checking...</div>
</div>
<div class="row mb-3">
<div class="col-md-2 themed-grid-col bg-light">about.min.htm</div>
<div class="col-md-6 themed-grid-col bg-light" id="about">Checking...</div>
</div>
<div class="row mb-3">
<form action="/api/upload" method="post" enctype="multipart/form-data">
<div class="col-md-8 custom-file">
<input type="file" accept=".min.htm" class="custom-file-input" name="name" id="name">
<label class="custom-file-label" for="name">Choose file</label>
</div>
<button type="submit" class="btn btn-primary" id="upload-btn" value="upload">Upload</button>
</form>
</div>
<hr class="my-4">
</div>
<script type="text/javascript">
window.onload = getUpload;
// Add the following code if you want the name of the file appear on select
$(".custom-file-input").on("change", function() {
var fileName = $(this).val().split("\\").pop();
$(this).siblings(".custom-file-label").addClass("selected").html(fileName);
});
function getUpload() {
var url = "/api/upload";
//var url = "/test/upload.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
if( cfg["index"] )
$("#index").text("Completed.");
else
$("#index").text("File is missing.");
if( cfg["config"] )
$("#config").text("Completed.");
else
$("#config").text("File is missing.");
if( cfg["calibration"] )
$("#calibration").text("Completed.");
else
$("#calibration").text("File is missing.");
if( cfg["test"] )
$("#test").text("Completed.");
else
$("#test").text("File is missing.");
if( cfg["format"] )
$("#format").text("Completed.");
else
$("#format").text("File is missing.");
if( cfg["about"] )
$("#about").text("Completed.");
else
$("#about").text("File is missing.");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
}
</script>
<!-- START FOOTER -->
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

View File

@ -33,7 +33,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 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=\""\"" # =\""myssid\""
-D USER_SSID_PWD=\""\"" # =\""mypwd\"" -D USER_SSID_PWD=\""\"" # =\""mypwd\""
-D CFG_APPVER="\"0.9.0\"" -D CFG_APPVER="\"1.0.0\""
!python script/git_rev.py !python script/git_rev.py
lib_deps = # Switched to forks for better version control. lib_deps = # Switched to forks for better version control.
# Using local copy of these libraries # Using local copy of these libraries
@ -66,6 +66,7 @@ build_flags =
#-D PIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS #-D PIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS
#-D SKIP_SLEEPMODE #-D SKIP_SLEEPMODE
#-D DOUBLERESETDETECTOR_DEBUG=true #-D DOUBLERESETDETECTOR_DEBUG=true
#-D FORCE_GRAVITY_MODE # used to debug gravity mode
-D COLLECT_PERFDATA # This option will collect runtime data for a few defined methods to measure time, dumped to serial and/or influxdb -D COLLECT_PERFDATA # This option will collect runtime data for a few defined methods to measure time, dumped to serial and/or influxdb
-D LOG_LEVEL=6 # Maximum log level for the debug build. -D LOG_LEVEL=6 # Maximum log level for the debug build.
-D CFG_DISABLE_LOGGING # Turn off verbose/notice logging to reduce size and dont overload uart. -D CFG_DISABLE_LOGGING # Turn off verbose/notice logging to reduce size and dont overload uart.
@ -126,7 +127,7 @@ board_build.filesystem = littlefs
[env:gravity32-release] [env:gravity32-release]
framework = arduino framework = arduino
platform = espressif32 platform = espressif32 @ 3.5.0
upload_speed = ${common_env_data.upload_speed} upload_speed = ${common_env_data.upload_speed}
monitor_speed = ${common_env_data.monitor_speed} monitor_speed = ${common_env_data.monitor_speed}
extra_scripts = extra_scripts =
@ -161,7 +162,7 @@ monitor_filters = esp32_exception_decoder
[env:gravity32-perf] [env:gravity32-perf]
framework = arduino framework = arduino
platform = espressif32 platform = espressif32 @ 3.5.0
upload_speed = ${common_env_data.upload_speed} upload_speed = ${common_env_data.upload_speed}
monitor_speed = ${common_env_data.monitor_speed} monitor_speed = ${common_env_data.monitor_speed}
extra_scripts = extra_scripts =

View File

@ -33,14 +33,16 @@ SOFTWARE.
int createFormula(RawFormulaData &fd, char *formulaBuffer, int createFormula(RawFormulaData &fd, char *formulaBuffer,
int formulaBufferSize, int order) { int formulaBufferSize, int order) {
int noAngles = 0; int noAngles = 0;
RawFormulaData fd2;
// Check how many valid values we have got // Check how many valid values we have got and make sure we have a full series.
if (fd.a[0] > 0 && fd.a[1] > 0 && fd.a[2] > 0 && fd.a[3] > 0 && fd.a[4] > 0) for (int i = 0; i < FORMULA_DATA_SIZE; i++) {
noAngles = 5; if (fd.a[i]) {
else if (fd.a[0] > 0 && fd.a[1] > 0 && fd.a[2] > 0 && fd.a[3] > 0) fd2.a[noAngles] = fd.a[i];
noAngles = 4; fd2.g[noAngles] = fd.g[i];
else if (fd.a[0] > 0 && fd.a[1] > 0 && fd.a[2] > 0) noAngles++;
noAngles = 3; }
}
#if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING)
Log.verbose( Log.verbose(
@ -48,19 +50,19 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
order, noAngles); order, noAngles);
#endif #endif
if (!noAngles) { if (noAngles <3) {
ErrorFileLog errLog; ErrorFileLog errLog;
errLog.addEntry(F("CALC: Not enough values for deriving formula")); errLog.addEntry(F("CALC: Not enough values for deriving formula"));
return ERR_FORMULA_NOTENOUGHVALUES; return ERR_FORMULA_NOTENOUGHVALUES;
} else { } else {
double coeffs[order + 1]; double coeffs[order + 1];
int ret = fitCurve(order, noAngles, fd.a, fd.g, int ret = fitCurve(order, noAngles, fd2.a, fd2.g,
sizeof(coeffs) / sizeof(double), coeffs); sizeof(coeffs) / sizeof(double), coeffs);
// Returned value is 0 if no error // Returned value is 0 if no error
if (ret == 0) { if (ret == 0) {
#if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING)
Log.verbose(F("CALC: Finshied processing data points." CR)); Log.verbose(F("CALC: Finshied processing data points, order = %d." CR), order);
#endif #endif
// Print the formula based on 'order' // Print the formula based on 'order'
@ -93,12 +95,10 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
double dev = (g - fd.g[i]) < 0 ? (fd.g[i] - g) : (g - fd.g[i]); double dev = (g - fd.g[i]) < 0 ? (fd.g[i] - g) : (g - fd.g[i]);
// If the deviation is more than 2 degress we mark it as failed. // If the deviation is more than 2 degress we mark it as failed.
if (dev * 1000 > myHardwareConfig.getMaxFormulaCreationDeviation()) { if (dev * 1000 > myAdvancedConfig.getMaxFormulaCreationDeviation()) {
#if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING)
char s[20]; char s[20];
snprintf(&s[0], sizeof(s), "%.8f", dev); snprintf(&s[0], sizeof(s), "%.8f", dev);
Log.verbose(F("CALC: Deviation is: %s" CR), &s[0]); Log.error(F("CALC: Deviation to large: %s" CR), &s[0]);
#endif
valid = false; valid = false;
} }
} }

View File

@ -34,7 +34,7 @@ double calculateGravity(double angle, double tempC,
const char *tempFormula = 0); const char *tempFormula = 0);
double gravityTemperatureCorrectionC( double gravityTemperatureCorrectionC(
double gravity, double tempC, double gravity, double tempC,
double calTempC = myHardwareConfig.getDefaultCalibrationTemp()); double calTempC = myAdvancedConfig.getDefaultCalibrationTemp());
int createFormula(RawFormulaData &fd, char *formulaBuffer, int createFormula(RawFormulaData &fd, char *formulaBuffer,
int formulaBufferSize, int order); int formulaBufferSize, int order);

View File

@ -26,7 +26,7 @@ SOFTWARE.
#include <wifi.hpp> #include <wifi.hpp>
Config myConfig; Config myConfig;
HardwareConfig myHardwareConfig; AdvancedConfig myAdvancedConfig;
// //
// Create the config class with default settings. // Create the config class with default settings.
@ -66,7 +66,6 @@ void Config::createJson(DynamicJsonDocument& doc) {
doc[PARAM_PASS] = getWifiPass(); doc[PARAM_PASS] = getWifiPass();
doc[PARAM_BLE] = getColorBLE(); doc[PARAM_BLE] = getColorBLE();
doc[PARAM_TEMPFORMAT] = String(getTempFormat()); doc[PARAM_TEMPFORMAT] = String(getTempFormat());
doc[PARAM_PUSH_BREWFATHER] = getBrewfatherPushUrl();
doc[PARAM_TOKEN] = getToken(); doc[PARAM_TOKEN] = getToken();
doc[PARAM_TOKEN2] = getToken2(); doc[PARAM_TOKEN2] = getToken2();
doc[PARAM_PUSH_HTTP] = getHttpUrl(); doc[PARAM_PUSH_HTTP] = getHttpUrl();
@ -106,12 +105,22 @@ void Config::createJson(DynamicJsonDocument& doc) {
cal2["a3"] = reduceFloatPrecision(_formulaData.a[2], 2); cal2["a3"] = reduceFloatPrecision(_formulaData.a[2], 2);
cal2["a4"] = reduceFloatPrecision(_formulaData.a[3], 2); cal2["a4"] = reduceFloatPrecision(_formulaData.a[3], 2);
cal2["a5"] = reduceFloatPrecision(_formulaData.a[4], 2); cal2["a5"] = reduceFloatPrecision(_formulaData.a[4], 2);
cal2["a6"] = reduceFloatPrecision(_formulaData.a[5], 2);
cal2["a7"] = reduceFloatPrecision(_formulaData.a[6], 2);
cal2["a8"] = reduceFloatPrecision(_formulaData.a[7], 2);
cal2["a9"] = reduceFloatPrecision(_formulaData.a[8], 2);
cal2["a10"] = reduceFloatPrecision(_formulaData.a[9], 2);
cal2["g1"] = reduceFloatPrecision(_formulaData.g[0], 4); cal2["g1"] = reduceFloatPrecision(_formulaData.g[0], 4);
cal2["g2"] = reduceFloatPrecision(_formulaData.g[1], 4); cal2["g2"] = reduceFloatPrecision(_formulaData.g[1], 4);
cal2["g3"] = reduceFloatPrecision(_formulaData.g[2], 4); cal2["g3"] = reduceFloatPrecision(_formulaData.g[2], 4);
cal2["g4"] = reduceFloatPrecision(_formulaData.g[3], 4); cal2["g4"] = reduceFloatPrecision(_formulaData.g[3], 4);
cal2["g5"] = reduceFloatPrecision(_formulaData.g[4], 4); cal2["g5"] = reduceFloatPrecision(_formulaData.g[4], 4);
cal2["g6"] = reduceFloatPrecision(_formulaData.g[5], 4);
cal2["g7"] = reduceFloatPrecision(_formulaData.g[6], 4);
cal2["g8"] = reduceFloatPrecision(_formulaData.g[7], 4);
cal2["g9"] = reduceFloatPrecision(_formulaData.g[8], 4);
cal2["g10"] = reduceFloatPrecision(_formulaData.g[9], 4);
} }
// //
@ -207,9 +216,6 @@ bool Config::loadFile() {
setTempFormat(s.charAt(0)); setTempFormat(s.charAt(0));
} }
if (!doc[PARAM_PUSH_BREWFATHER].isNull())
setBrewfatherPushUrl(doc[PARAM_PUSH_BREWFATHER]);
if (!doc[PARAM_TOKEN].isNull()) setToken(doc[PARAM_TOKEN]); if (!doc[PARAM_TOKEN].isNull()) setToken(doc[PARAM_TOKEN]);
if (!doc[PARAM_TOKEN2].isNull()) setToken2(doc[PARAM_TOKEN2]); if (!doc[PARAM_TOKEN2].isNull()) setToken2(doc[PARAM_TOKEN2]);
if (!doc[PARAM_PUSH_HTTP].isNull()) setHttpUrl(doc[PARAM_PUSH_HTTP]); if (!doc[PARAM_PUSH_HTTP].isNull()) setHttpUrl(doc[PARAM_PUSH_HTTP]);
@ -281,6 +287,16 @@ bool Config::loadFile() {
_formulaData.a[3] = doc[PARAM_FORMULA_DATA]["a4"].as<double>(); _formulaData.a[3] = doc[PARAM_FORMULA_DATA]["a4"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a5"].isNull()) if (!doc[PARAM_FORMULA_DATA]["a5"].isNull())
_formulaData.a[4] = doc[PARAM_FORMULA_DATA]["a5"].as<double>(); _formulaData.a[4] = doc[PARAM_FORMULA_DATA]["a5"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a6"].isNull())
_formulaData.a[5] = doc[PARAM_FORMULA_DATA]["a6"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a7"].isNull())
_formulaData.a[6] = doc[PARAM_FORMULA_DATA]["a7"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a8"].isNull())
_formulaData.a[7] = doc[PARAM_FORMULA_DATA]["a8"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a9"].isNull())
_formulaData.a[8] = doc[PARAM_FORMULA_DATA]["a9"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a10"].isNull())
_formulaData.a[9] = doc[PARAM_FORMULA_DATA]["a10"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g1"].isNull()) if (!doc[PARAM_FORMULA_DATA]["g1"].isNull())
_formulaData.g[0] = doc[PARAM_FORMULA_DATA]["g1"].as<double>(); _formulaData.g[0] = doc[PARAM_FORMULA_DATA]["g1"].as<double>();
@ -292,6 +308,16 @@ bool Config::loadFile() {
_formulaData.g[3] = doc[PARAM_FORMULA_DATA]["g4"].as<double>(); _formulaData.g[3] = doc[PARAM_FORMULA_DATA]["g4"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g5"].isNull()) if (!doc[PARAM_FORMULA_DATA]["g5"].isNull())
_formulaData.g[4] = doc[PARAM_FORMULA_DATA]["g5"].as<double>(); _formulaData.g[4] = doc[PARAM_FORMULA_DATA]["g5"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g6"].isNull())
_formulaData.g[5] = doc[PARAM_FORMULA_DATA]["g6"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g7"].isNull())
_formulaData.g[6] = doc[PARAM_FORMULA_DATA]["g7"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g8"].isNull())
_formulaData.g[7] = doc[PARAM_FORMULA_DATA]["g8"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g9"].isNull())
_formulaData.g[8] = doc[PARAM_FORMULA_DATA]["g9"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g10"].isNull())
_formulaData.g[9] = doc[PARAM_FORMULA_DATA]["g10"].as<double>();
/*if( doc[PARAM_CONFIG_VER].isNull() ) { /*if( doc[PARAM_CONFIG_VER].isNull() ) {
// If this parameter is missing we need to reset the gyrocalibaration due to // If this parameter is missing we need to reset the gyrocalibaration due to
@ -333,7 +359,7 @@ void Config::checkFileSystem() {
// //
// Save json document to file // Save json document to file
// //
bool HardwareConfig::saveFile() { bool AdvancedConfig::saveFile() {
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
Log.verbose(F("CFG : Saving hardware configuration to file." CR)); Log.verbose(F("CFG : Saving hardware configuration to file." CR));
#endif #endif
@ -352,8 +378,14 @@ bool HardwareConfig::saveFile() {
doc[PARAM_HW_GYRO_READ_DELAY] = this->getGyroReadDelay(); doc[PARAM_HW_GYRO_READ_DELAY] = this->getGyroReadDelay();
doc[PARAM_HW_GYRO_MOVING_THREASHOLD] = this->getGyroSensorMovingThreashold(); doc[PARAM_HW_GYRO_MOVING_THREASHOLD] = this->getGyroSensorMovingThreashold();
doc[PARAM_HW_FORMULA_DEVIATION] = this->getMaxFormulaCreationDeviation(); doc[PARAM_HW_FORMULA_DEVIATION] = this->getMaxFormulaCreationDeviation();
doc[PARAM_HW_WIFI_PORTALTIMEOUT] = this->getWifiPortalTimeout(); doc[PARAM_HW_WIFI_PORTAL_TIMEOUT] = this->getWifiPortalTimeout();
doc[PARAM_HW_WIFI_CONNECT_TIMEOUT] = this->getWifiConnectTimeout();
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP] = this->getDefaultCalibrationTemp(); doc[PARAM_HW_FORMULA_CALIBRATION_TEMP] = this->getDefaultCalibrationTemp();
doc[PARAM_HW_PUSH_INTERVAL_HTTP1] = this->getPushIntervalHttp1();
doc[PARAM_HW_PUSH_INTERVAL_HTTP2] = this->getPushIntervalHttp2();
doc[PARAM_HW_PUSH_INTERVAL_HTTP3] = this->getPushIntervalHttp3();
doc[PARAM_HW_PUSH_INTERVAL_INFLUX] = this->getPushIntervalInflux();
doc[PARAM_HW_PUSH_INTERVAL_MQTT] = this->getPushIntervalMqtt();
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
serializeJson(doc, Serial); serializeJson(doc, Serial);
@ -371,7 +403,7 @@ bool HardwareConfig::saveFile() {
// //
// Load config file from disk // Load config file from disk
// //
bool HardwareConfig::loadFile() { bool AdvancedConfig::loadFile() {
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
Log.verbose(F("CFG : Loading hardware configuration from file." CR)); Log.verbose(F("CFG : Loading hardware configuration from file." CR));
#endif #endif
@ -424,10 +456,22 @@ bool HardwareConfig::loadFile() {
if (!doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].isNull()) if (!doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].isNull())
this->SetDefaultCalibrationTemp( this->SetDefaultCalibrationTemp(
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].as<float>()); doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].as<float>());
if (!doc[PARAM_HW_WIFI_PORTALTIMEOUT].isNull()) if (!doc[PARAM_HW_WIFI_PORTAL_TIMEOUT].isNull())
this->setWifiPortalTimeout(doc[PARAM_HW_WIFI_PORTALTIMEOUT].as<int>()); this->setWifiPortalTimeout(doc[PARAM_HW_WIFI_PORTAL_TIMEOUT].as<int>());
if (!doc[PARAM_HW_WIFI_CONNECT_TIMEOUT].isNull())
this->setWifiConnectTimeout(doc[PARAM_HW_WIFI_CONNECT_TIMEOUT].as<int>());
if (!doc[PARAM_HW_PUSH_TIMEOUT].isNull()) if (!doc[PARAM_HW_PUSH_TIMEOUT].isNull())
this->setPushTimeout(doc[PARAM_HW_PUSH_TIMEOUT].as<int>()); this->setPushTimeout(doc[PARAM_HW_PUSH_TIMEOUT].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_HTTP1].isNull())
this->setPushIntervalHttp1(doc[PARAM_HW_PUSH_INTERVAL_HTTP1].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_HTTP2].isNull())
this->setPushIntervalHttp2(doc[PARAM_HW_PUSH_INTERVAL_HTTP2].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_HTTP3].isNull())
this->setPushIntervalHttp3(doc[PARAM_HW_PUSH_INTERVAL_HTTP3].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_INFLUX].isNull())
this->setPushIntervalInflux(doc[PARAM_HW_PUSH_INTERVAL_INFLUX].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_MQTT].isNull())
this->setPushIntervalMqtt(doc[PARAM_HW_PUSH_INTERVAL_MQTT].as<int>());
Log.notice(F("CFG : Configuration file " CFG_HW_FILENAME " loaded." CR)); Log.notice(F("CFG : Configuration file " CFG_HW_FILENAME " loaded." CR));
return true; return true;

View File

@ -47,41 +47,75 @@ struct RawGyroData {
}; };
// Used for holding formulaData (used for calculating formula on device) // Used for holding formulaData (used for calculating formula on device)
#define FORMULA_DATA_SIZE 10
struct RawFormulaData { struct RawFormulaData {
double a[5]; double a[FORMULA_DATA_SIZE];
double g[5]; double g[FORMULA_DATA_SIZE];
}; };
class HardwareConfig { class AdvancedConfig {
private: private:
int _wifiPortalTimeout = 120; int _wifiPortalTimeout = 120;
int _wifiConnectTimeout = 20;
float _maxFormulaCreationDeviation = 1.6; float _maxFormulaCreationDeviation = 1.6;
float _defaultCalibrationTemp = 20.0; float _defaultCalibrationTemp = 20.0;
int _gyroSensorMovingThreashold = 500; int _gyroSensorMovingThreashold = 500;
int _gyroReadCount = 50; int _gyroReadCount = 50;
int _gyroReadDelay = 3150; // us, empirical, to hold sampling to 200 Hz int _gyroReadDelay = 3150; // us, empirical, to hold sampling to 200 Hz
int _pushTimeout = 10; // seconds int _pushTimeout = 10; // seconds
int _pushIntervalHttp1 = 0;
int _pushIntervalHttp2 = 0;
int _pushIntervalHttp3 = 0;
int _pushIntervalInflux = 0;
int _pushIntervalMqtt = 0;
public: public:
int getWifiPortalTimeout() { return _wifiPortalTimeout; } int getWifiPortalTimeout() { return _wifiPortalTimeout; }
void setWifiPortalTimeout(int t) { _wifiPortalTimeout = t; } void setWifiPortalTimeout(int t) { _wifiPortalTimeout = t; }
int getWifiConnectTimeout() { return _wifiConnectTimeout; }
void setWifiConnectTimeout(int t) { _wifiConnectTimeout = t; }
float getMaxFormulaCreationDeviation() { float getMaxFormulaCreationDeviation() {
return _maxFormulaCreationDeviation; return _maxFormulaCreationDeviation;
} }
void setMaxFormulaCreationDeviation(float f) { void setMaxFormulaCreationDeviation(float f) {
_maxFormulaCreationDeviation = f; _maxFormulaCreationDeviation = f;
} }
float getDefaultCalibrationTemp() { return _defaultCalibrationTemp; } float getDefaultCalibrationTemp() { return _defaultCalibrationTemp; }
void SetDefaultCalibrationTemp(float t) { _defaultCalibrationTemp = t; } void SetDefaultCalibrationTemp(float t) { _defaultCalibrationTemp = t; }
int getGyroSensorMovingThreashold() { return _gyroSensorMovingThreashold; } int getGyroSensorMovingThreashold() { return _gyroSensorMovingThreashold; }
void setGyroSensorMovingThreashold(int t) { _gyroSensorMovingThreashold = t; } void setGyroSensorMovingThreashold(int t) { _gyroSensorMovingThreashold = t; }
int getGyroReadCount() { return _gyroReadCount; } int getGyroReadCount() { return _gyroReadCount; }
void setGyroReadCount(int c) { _gyroReadCount = c; } void setGyroReadCount(int c) { _gyroReadCount = c; }
int getGyroReadDelay() { return _gyroReadDelay; } int getGyroReadDelay() { return _gyroReadDelay; }
void setGyroReadDelay(int d) { _gyroReadDelay = d; } void setGyroReadDelay(int d) { _gyroReadDelay = d; }
int getPushTimeout() { return _pushTimeout; } int getPushTimeout() { return _pushTimeout; }
void setPushTimeout(int t) { _pushTimeout = t; } void setPushTimeout(int t) { _pushTimeout = t; }
int getPushIntervalHttp1() { return _pushIntervalHttp1; }
void setPushIntervalHttp1(int t) { _pushIntervalHttp1 = t; }
int getPushIntervalHttp2() { return _pushIntervalHttp2; }
void setPushIntervalHttp2(int t) { _pushIntervalHttp2 = t; }
int getPushIntervalHttp3() { return _pushIntervalHttp3; }
void setPushIntervalHttp3(int t) { _pushIntervalHttp3 = t; }
int getPushIntervalInflux() { return _pushIntervalInflux; }
void setPushIntervalInflux(int t) { _pushIntervalInflux = t; }
int getPushIntervalMqtt() { return _pushIntervalMqtt; }
void setPushIntervalMqtt(int t) { _pushIntervalMqtt = t; }
bool isPushIntervalActive() { return (_pushIntervalHttp1+_pushIntervalHttp2+_pushIntervalHttp3+_pushIntervalInflux+_pushIntervalMqtt) == 0 ? false : true; }
bool saveFile(); bool saveFile();
bool loadFile(); bool loadFile();
}; };
@ -106,8 +140,6 @@ class Config {
String _wifiPASS = ""; String _wifiPASS = "";
// Push target settings // Push target settings
String _brewfatherPushUrl = "";
String _token = ""; String _token = "";
String _token2 = ""; String _token2 = "";
@ -178,16 +210,6 @@ class Config {
_saveNeeded = true; _saveNeeded = true;
} }
// Brewfather
const char* getBrewfatherPushUrl() { return _brewfatherPushUrl.c_str(); }
void setBrewfatherPushUrl(String s) {
_brewfatherPushUrl = s;
_saveNeeded = true;
}
bool isBrewfatherActive() {
return _brewfatherPushUrl.length() ? true : false;
}
// Token parameter // Token parameter
const char* getToken() { return _token.c_str(); } const char* getToken() { return _token.c_str(); }
void setToken(String s) { void setToken(String s) {
@ -242,6 +264,7 @@ class Config {
_saveNeeded = true; _saveNeeded = true;
} }
bool isInfluxDb2Active() { return _influxDb2Url.length() ? true : false; } bool isInfluxDb2Active() { return _influxDb2Url.length() ? true : false; }
bool isInfluxSSL() { return _influxDb2Url.startsWith("https://"); }
const char* getInfluxDb2PushOrg() { return _influxDb2Org.c_str(); } const char* getInfluxDb2PushOrg() { return _influxDb2Org.c_str(); }
void setInfluxDb2PushOrg(String s) { void setInfluxDb2PushOrg(String s) {
_influxDb2Org = s; _influxDb2Org = s;
@ -361,7 +384,7 @@ class Config {
bool isBLEActive() { return _colorBLE.length() ? true : false; } bool isBLEActive() { return _colorBLE.length() ? true : false; }
bool isWifiPushActive() { bool isWifiPushActive() {
return (isHttpActive() || isHttp2Active() || isHttp3Active() || return (isHttpActive() || isHttp2Active() || isHttp3Active() ||
isBrewfatherActive() || isInfluxDb2Active() || isMqttActive()) isInfluxDb2Active() || isMqttActive())
? true ? true
: false; : false;
} }
@ -388,7 +411,7 @@ class Config {
}; };
extern Config myConfig; extern Config myConfig;
extern HardwareConfig myHardwareConfig; extern AdvancedConfig myAdvancedConfig;
#endif // SRC_CONFIG_HPP_ #endif // SRC_CONFIG_HPP_

View File

@ -217,7 +217,7 @@ bool GyroSensor::isSensorMoving(RawGyroData &raw) {
#endif #endif
int x = abs(raw.gx), y = abs(raw.gy), z = abs(raw.gz); int x = abs(raw.gx), y = abs(raw.gy), z = abs(raw.gz);
int threashold = myHardwareConfig.getGyroSensorMovingThreashold(); int threashold = myAdvancedConfig.getGyroSensorMovingThreashold();
if (x > threashold || y > threashold || z > threashold) { if (x > threashold || y > threashold || z > threashold) {
Log.notice(F("GYRO: Movement detected (%d)\t%d\t%d\t%d." CR), threashold, x, Log.notice(F("GYRO: Movement detected (%d)\t%d\t%d\t%d." CR), threashold, x,
@ -239,8 +239,8 @@ bool GyroSensor::read() {
if (!_sensorConnected) return false; if (!_sensorConnected) return false;
readSensor( readSensor(
_lastGyroData, myHardwareConfig.getGyroReadCount(), _lastGyroData, myAdvancedConfig.getGyroReadCount(),
myHardwareConfig.getGyroReadDelay()); // Last param is unused if myAdvancedConfig.getGyroReadDelay()); // Last param is unused if
// GYRO_USE_INTERRUPT is defined. // GYRO_USE_INTERRUPT is defined.
// If the sensor is unstable we return false to signal we dont have valid // If the sensor is unstable we return false to signal we dont have valid

View File

@ -387,7 +387,7 @@ void PerfLogging::pushInflux() {
// Send HTTP POST request // Send HTTP POST request
String auth = "Token " + String(myConfig.getInfluxDb2PushToken()); String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
http.addHeader(F("Authorization"), auth.c_str()); http.addHeader(F("Authorization"), auth.c_str());
http.setTimeout(myHardwareConfig.getPushTimeout()); http.setTimeout(myAdvancedConfig.getPushTimeout());
int httpResponseCode = http.POST(body); int httpResponseCode = http.POST(body);
if (httpResponseCode == 204) { if (httpResponseCode == 204) {

View File

@ -60,6 +60,12 @@ void checkSleepMode(float angle, float volt) {
return; return;
#endif #endif
#if defined( FORCE_GRAVITY_MODE )
Log.notice(
F("MAIN: Forcing device into gravity mode for debugging" CR));
runMode = RunMode::gravityMode;
#endif
const RawGyroData &g = myConfig.getGyroCalibration(); const RawGyroData &g = myConfig.getGyroCalibration();
if (!g.ax && !g.ay && !g.az && !g.gx && !g.gy && !g.gz) { if (!g.ax && !g.ay && !g.az && !g.gx && !g.gy && !g.gz) {
@ -138,7 +144,7 @@ void setup() {
myConfig.checkFileSystem(); myConfig.checkFileSystem();
myConfig.loadFile(); myConfig.loadFile();
myWifi.init(); myWifi.init();
myHardwareConfig.loadFile(); myAdvancedConfig.loadFile();
LOG_PERF_STOP("main-config-load"); LOG_PERF_STOP("main-config-load");
// Setup watchdog // Setup watchdog

View File

@ -32,6 +32,74 @@ SOFTWARE.
#include <pushtarget.hpp> #include <pushtarget.hpp>
#include <wifi.hpp> #include <wifi.hpp>
#define PUSHINT_FILENAME "/push.dat"
//
// Decrease counters
//
void PushIntervalTracker::update(const int index, const int defaultValue) {
if (_counters[index] <= 0)
_counters[index] = defaultValue;
else
_counters[index]--;
}
//
// Load data from file
//
void PushIntervalTracker::load() {
File intFile = LittleFS.open(PUSHINT_FILENAME, "r");
int i = 0;
if (intFile) {
String line = intFile.readStringUntil('\n');
Log.notice(F("PUSH: Read interval tracker %s." CR), line.c_str());
char temp[80];
char *s, *p = &temp[0];
int i = 0;
snprintf(&temp[0], sizeof(temp), "%s", line.c_str());
while ((s = strtok_r(p, ":", &p)) != NULL) {
_counters[i++] = atoi(s);
}
intFile.close();
}
#if !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: Parsed trackers: %d:%d:%d:%d:%d." CR), _counters[0], _counters[1], _counters[2], _counters[3], _counters[4] );
#endif
}
//
// Update and save counters
//
void PushIntervalTracker::save() {
update(0, myAdvancedConfig.getPushIntervalHttp1());
update(1, myAdvancedConfig.getPushIntervalHttp2());
update(2, myAdvancedConfig.getPushIntervalHttp3());
update(3, myAdvancedConfig.getPushIntervalInflux());
update(4, myAdvancedConfig.getPushIntervalMqtt());
// If this feature is disabled we skip saving the file
if (!myAdvancedConfig.isPushIntervalActive()) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Variabled push interval disabled." CR));
#endif
LittleFS.remove(PUSHINT_FILENAME);
} else {
Log.notice(F("PUSH: Variabled push interval enabled, updating counters." CR));
File intFile = LittleFS.open(PUSHINT_FILENAME, "w");
if (intFile) {
// Format=http1:http2:http3:influx:mqtt
intFile.printf("%d:%d:%d:%d:%d\n", _counters[0], _counters[1], _counters[2], _counters[3], _counters[4] );
intFile.close();
}
}
}
// //
// Send the data to targets // Send the data to targets
// //
@ -44,47 +112,76 @@ void PushTarget::sendAll(float angle, float gravitySG, float corrGravitySG,
TemplatingEngine engine; TemplatingEngine engine;
engine.initialize(angle, gravitySG, corrGravitySG, tempC, runTime); engine.initialize(angle, gravitySG, corrGravitySG, tempC, runTime);
if (myConfig.isBrewfatherActive()) { PushIntervalTracker intDelay;
LOG_PERF_START("push-brewfather"); intDelay.load();
sendBrewfather(engine);
LOG_PERF_STOP("push-brewfather");
}
if (myConfig.isHttpActive()) { if (myConfig.isHttpActive() && intDelay.useHttp1()) {
LOG_PERF_START("push-http"); LOG_PERF_START("push-http");
sendHttpPost(engine, myConfig.isHttpSSL(), 0); sendHttpPost(engine, myConfig.isHttpSSL(), 0);
LOG_PERF_STOP("push-http"); LOG_PERF_STOP("push-http");
} }
if (myConfig.isHttp2Active()) { if (myConfig.isHttp2Active() && intDelay.useHttp2()) {
LOG_PERF_START("push-http2"); LOG_PERF_START("push-http2");
sendHttpPost(engine, myConfig.isHttp2SSL(), 1); sendHttpPost(engine, myConfig.isHttp2SSL(), 1);
LOG_PERF_STOP("push-http2"); LOG_PERF_STOP("push-http2");
} }
if (myConfig.isHttp3Active()) { if (myConfig.isHttp3Active() && intDelay.useHttp3()) {
LOG_PERF_START("push-http3"); LOG_PERF_START("push-http3");
sendHttpGet(engine, myConfig.isHttp3SSL()); sendHttpGet(engine, myConfig.isHttp3SSL());
LOG_PERF_STOP("push-http3"); LOG_PERF_STOP("push-http3");
} }
if (myConfig.isInfluxDb2Active()) { if (myConfig.isInfluxDb2Active() && intDelay.useInflux()) {
LOG_PERF_START("push-influxdb2"); LOG_PERF_START("push-influxdb2");
sendInfluxDb2(engine); sendInfluxDb2(engine, myConfig.isInfluxSSL());
LOG_PERF_STOP("push-influxdb2"); LOG_PERF_STOP("push-influxdb2");
} }
if (myConfig.isMqttActive()) { if (myConfig.isMqttActive() && intDelay.useMqtt()) {
LOG_PERF_START("push-mqtt"); LOG_PERF_START("push-mqtt");
sendMqtt(engine, myConfig.isMqttSSL()); sendMqtt(engine, myConfig.isMqttSSL());
LOG_PERF_STOP("push-mqtt"); LOG_PERF_STOP("push-mqtt");
} }
intDelay.save();
}
//
// Check if the server can reduce the buffer size to save memory (ESP8266 only)
//
void PushTarget::probeMaxFragement( String& serverPath ) {
#if defined(ESP8266) // Looks like this is feature is not supported by influxdb
// Format: http:://servername:port/path
int port = 443;
String host =
serverPath.substring(8); // remove the prefix or the probe will fail,
// it needs a pure host name.
// Remove the path if it exist
int idx = host.indexOf("/");
if (idx != -1) host = host.substring(0, idx);
// If a server port is defined, lets extract that part
idx = host.indexOf(":");
if (idx != -1) {
String p = host.substring(idx+1);
port = p.toInt();
host = host.substring(0, idx);
}
Log.notice(F("PUSH: Probing server to max fragment %s:%d" CR), host.c_str(), port);
if (_wifiSecure.probeMaxFragmentLength(host, port, 512)) {
Log.notice(F("PUSH: Server supports smaller SSL buffer." CR));
_wifiSecure.setBufferSizes(512, 512);
}
#endif
} }
// //
// Send to influx db v2 // Send to influx db v2
// //
void PushTarget::sendInfluxDb2(TemplatingEngine& engine) { void PushTarget::sendInfluxDb2(TemplatingEngine& engine, bool isSecure) {
#if !defined(PUSH_DISABLE_LOGGING) #if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to influxdb2." CR)); Log.notice(F("PUSH: Sending values to influxdb2." CR));
#endif #endif
@ -97,17 +194,27 @@ void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
"&bucket=" + String(myConfig.getInfluxDb2PushBucket()); "&bucket=" + String(myConfig.getInfluxDb2PushBucket());
String doc = engine.create(TemplatingEngine::TEMPLATE_INFLUX); String doc = engine.create(TemplatingEngine::TEMPLATE_INFLUX);
_http.begin(_wifi, serverPath);
_http.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str()); Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: data %s." CR), doc.c_str()); Log.verbose(F("PUSH: data %s." CR), doc.c_str());
#endif #endif
String auth = "Token " + String(myConfig.getInfluxDb2PushToken()); String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
_http.addHeader(F("Authorization"), auth.c_str());
_lastCode = _http.POST(doc); if (isSecure) {
Log.notice(F("PUSH: InfluxDB, SSL enabled without validation." CR));
_wifiSecure.setInsecure();
probeMaxFragement( serverPath );
_httpSecure.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_httpSecure.begin(_wifiSecure, serverPath);
_httpSecure.addHeader(F("Authorization"), auth.c_str());
_lastCode = _httpSecure.POST(doc);
} else {
_http.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_http.begin(_wifi, serverPath);
_http.addHeader(F("Authorization"), auth.c_str());
_lastCode = _http.POST(doc);
}
if (_lastCode == 204) { if (_lastCode == 204) {
_lastSuccess = true; _lastSuccess = true;
@ -117,52 +224,18 @@ void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
errLog.addEntry("PUSH: Influxdb push failed response=" + String(_lastCode)); errLog.addEntry("PUSH: Influxdb push failed response=" + String(_lastCode));
} }
_http.end(); if (isSecure) {
_wifi.stop(); _httpSecure.end();
tcp_cleanup(); _wifiSecure.stop();
}
//
// Send data to brewfather
//
void PushTarget::sendBrewfather(TemplatingEngine& engine) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to brewfather" CR));
#endif
_lastCode = 0;
_lastSuccess = false;
String serverPath = myConfig.getBrewfatherPushUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_BREWFATHER);
_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
_http.addHeader(F("Content-Type"), F("application/json"));
_lastCode = _http.POST(doc);
if (_lastCode == 200) {
_lastSuccess = true;
Log.notice(F("PUSH: Brewfather push successful, response=%d" CR),
_lastCode);
} else { } else {
ErrorFileLog errLog; _http.end();
errLog.addEntry("PUSH: Brewfather push failed response=" + _wifi.stop();
String(_lastCode));
} }
_http.end();
_wifi.stop();
tcp_cleanup(); tcp_cleanup();
} }
// //
// // Add HTTP header to request
// //
void PushTarget::addHttpHeader(HTTPClient& http, String header) { void PushTarget::addHttpHeader(HTTPClient& http, String header) {
if (!header.length()) return; if (!header.length()) return;
@ -211,22 +284,9 @@ void PushTarget::sendHttpPost(TemplatingEngine& engine, bool isSecure,
if (isSecure) { if (isSecure) {
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR)); Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
_wifiSecure.setInsecure(); _wifiSecure.setInsecure();
probeMaxFragement( serverPath );
#if defined(ESP8266) _httpSecure.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
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.begin(_wifiSecure, serverPath);
_httpSecure.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
if (index == 0) { if (index == 0) {
addHttpHeader(_httpSecure, myConfig.getHttpHeader(0)); addHttpHeader(_httpSecure, myConfig.getHttpHeader(0));
@ -238,8 +298,8 @@ void PushTarget::sendHttpPost(TemplatingEngine& engine, bool isSecure,
_lastCode = _httpSecure.POST(doc); _lastCode = _httpSecure.POST(doc);
} else { } else {
_http.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_http.begin(_wifi, serverPath); _http.begin(_wifi, serverPath);
_http.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
if (index == 0) { if (index == 0) {
addHttpHeader(_http, myConfig.getHttpHeader(0)); addHttpHeader(_http, myConfig.getHttpHeader(0));
@ -293,26 +353,13 @@ void PushTarget::sendHttpGet(TemplatingEngine& engine, bool isSecure) {
if (isSecure) { if (isSecure) {
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR)); Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
_wifiSecure.setInsecure(); _wifiSecure.setInsecure();
probeMaxFragement( serverPath );
#if defined(ESP8266) _httpSecure.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
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.begin(_wifiSecure, serverPath);
_httpSecure.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
_lastCode = _httpSecure.GET(); _lastCode = _httpSecure.GET();
} else { } else {
_http.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_http.begin(_wifi, serverPath); _http.begin(_wifi, serverPath);
_http.setTimeout(myHardwareConfig.getPushTimeout() * 1000);
_lastCode = _http.GET(); _lastCode = _http.GET();
} }
@ -360,8 +407,10 @@ void PushTarget::sendMqtt(TemplatingEngine& engine, bool isSecure) {
} }
#endif #endif
mqtt.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
mqtt.begin(host.c_str(), port, _wifiSecure); mqtt.begin(host.c_str(), port, _wifiSecure);
} else { } else {
mqtt.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
mqtt.begin(host.c_str(), port, _wifi); mqtt.begin(host.c_str(), port, _wifi);
} }
@ -373,9 +422,6 @@ void PushTarget::sendMqtt(TemplatingEngine& engine, bool isSecure) {
Log.verbose(F("PUSH: data %s." CR), doc.c_str()); Log.verbose(F("PUSH: data %s." CR), doc.c_str());
#endif #endif
// Send MQQT message(s)
mqtt.setTimeout(myHardwareConfig.getPushTimeout()); // 10 seconds timeout
int lines = 1; int lines = 1;
// Find out how many lines are in the document. Each line is one // Find out how many lines are in the document. Each line is one
// topic/message. | is used as new line. // topic/message. | is used as new line.

View File

@ -45,12 +45,12 @@ class PushTarget {
void sendHttpPost(TemplatingEngine& engine, bool isSecure, int index); void sendHttpPost(TemplatingEngine& engine, bool isSecure, int index);
void sendHttpGet(TemplatingEngine& engine, bool isSecure); void sendHttpGet(TemplatingEngine& engine, bool isSecure);
void addHttpHeader(HTTPClient& http, String header); void addHttpHeader(HTTPClient& http, String header);
void probeMaxFragement( String& serverPath );
public: public:
void sendAll(float angle, float gravitySG, float corrGravitySG, float tempC, void sendAll(float angle, float gravitySG, float corrGravitySG, float tempC,
float runTime); float runTime);
void sendBrewfather(TemplatingEngine& engine);
void sendHttp1(TemplatingEngine& engine, bool isSecure) { void sendHttp1(TemplatingEngine& engine, bool isSecure) {
sendHttpPost(engine, isSecure, 0); sendHttpPost(engine, isSecure, 0);
} }
@ -60,12 +60,27 @@ class PushTarget {
void sendHttp3(TemplatingEngine& engine, bool isSecure) { void sendHttp3(TemplatingEngine& engine, bool isSecure) {
sendHttpGet(engine, isSecure); sendHttpGet(engine, isSecure);
} }
void sendInfluxDb2(TemplatingEngine& engine); void sendInfluxDb2(TemplatingEngine& engine, bool isSecure);
void sendMqtt(TemplatingEngine& engine, bool isSecure); void sendMqtt(TemplatingEngine& engine, bool isSecure);
int getLastCode() { return _lastCode; } int getLastCode() { return _lastCode; }
bool getLastSuccess() { return _lastSuccess; } bool getLastSuccess() { return _lastSuccess; }
}; };
class PushIntervalTracker {
private:
int _counters[5] = { 0, 0, 0, 0, 0 };
void update(const int index, const int defaultValue);
public:
bool useHttp1() { return _counters[0] == 0 ? true : false; }
bool useHttp2() { return _counters[1] == 0 ? true : false; }
bool useHttp3() { return _counters[2] == 0 ? true : false; }
bool useInflux() { return _counters[3] == 0 ? true : false; }
bool useMqtt() { return _counters[4] == 0 ? true : false; }
void load();
void save();
};
#endif // SRC_PUSHTARGET_HPP_ #endif // SRC_PUSHTARGET_HPP_
// EOF // EOF

View File

@ -32,7 +32,6 @@ SOFTWARE.
#define PARAM_SSID "wifi-ssid" #define PARAM_SSID "wifi-ssid"
#define PARAM_PASS "wifi-pass" #define PARAM_PASS "wifi-pass"
#define PARAM_RUNTIME_AVERAGE "runtime-average" #define PARAM_RUNTIME_AVERAGE "runtime-average"
#define PARAM_PUSH_BREWFATHER "brewfather-push"
#define PARAM_TOKEN "token" #define PARAM_TOKEN "token"
#define PARAM_TOKEN2 "token2" #define PARAM_TOKEN2 "token2"
#define PARAM_PUSH_HTTP "http-push" #define PARAM_PUSH_HTTP "http-push"
@ -80,12 +79,17 @@ SOFTWARE.
#define PARAM_HW_GYRO_MOVING_THREASHOLD "gyro-moving-threashold" #define PARAM_HW_GYRO_MOVING_THREASHOLD "gyro-moving-threashold"
#define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation" #define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation"
#define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp" #define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp"
#define PARAM_HW_WIFI_PORTALTIMEOUT "wifi-portaltimeout" #define PARAM_HW_WIFI_PORTAL_TIMEOUT "wifi-portal-timeout"
#define PARAM_HW_WIFI_CONNECT_TIMEOUT "wifi-connect-timeout"
#define PARAM_HW_PUSH_TIMEOUT "push-timeout" #define PARAM_HW_PUSH_TIMEOUT "push-timeout"
#define PARAM_HW_PUSH_INTERVAL_HTTP1 "int-http1"
#define PARAM_HW_PUSH_INTERVAL_HTTP2 "int-http2"
#define PARAM_HW_PUSH_INTERVAL_HTTP3 "int-http3"
#define PARAM_HW_PUSH_INTERVAL_INFLUX "int-influx"
#define PARAM_HW_PUSH_INTERVAL_MQTT "int-mqtt"
#define PARAM_FORMAT_HTTP1 "http-1" #define PARAM_FORMAT_HTTP1 "http-1"
#define PARAM_FORMAT_HTTP2 "http-2" #define PARAM_FORMAT_HTTP2 "http-2"
#define PARAM_FORMAT_HTTP3 "http-3" #define PARAM_FORMAT_HTTP3 "http-3"
#define PARAM_FORMAT_BREWFATHER "brewfather"
#define PARAM_FORMAT_INFLUXDB "influxdb" #define PARAM_FORMAT_INFLUXDB "influxdb"
#define PARAM_FORMAT_MQTT "mqtt" #define PARAM_FORMAT_MQTT "mqtt"
#define PARAM_PUSH_FORMAT "format" #define PARAM_PUSH_FORMAT "format"

View File

@ -42,7 +42,7 @@ const char iSpindleFormat[] PROGMEM =
"\"gravity\": ${gravity}, " "\"gravity\": ${gravity}, "
"\"angle\": ${angle}, " "\"angle\": ${angle}, "
"\"battery\": ${battery}, " "\"battery\": ${battery}, "
"\"rssi\": ${rssi}, " "\"RSSI\": ${rssi}, "
"\"corr-gravity\": ${corr-gravity}, " "\"corr-gravity\": ${corr-gravity}, "
"\"gravity-unit\": \"${gravity-unit}\", " "\"gravity-unit\": \"${gravity-unit}\", "
"\"run-time\": ${run-time} " "\"run-time\": ${run-time} "
@ -64,24 +64,6 @@ const char iHttpGetFormat[] PROGMEM =
"&gravity-unit=${gravity-unit}" "&gravity-unit=${gravity-unit}"
"&run-time=${run-time}"; "&run-time=${run-time}";
const char brewfatherFormat[] PROGMEM =
"{"
"\"name\": \"${mdns}\","
"\"temp\": ${temp}, "
"\"aux_temp\": 0, "
"\"ext_temp\": 0, "
"\"temp_unit\": \"${temp-unit}\", "
"\"gravity\": ${gravity}, "
"\"gravity_unit\": \"${gravity-unit}\", "
"\"pressure\": 0, "
"\"pressure_unit\": \"PSI\", "
"\"ph\": 0, "
"\"bpm\": 0, "
"\"comment\": \"\", "
"\"beer\": \"\", "
"\"battery\": ${battery}"
"}";
const char influxDbFormat[] PROGMEM = const char influxDbFormat[] PROGMEM =
"measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-" "measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-"
"format=${gravity-unit} " "format=${gravity-unit} "
@ -174,10 +156,6 @@ const String& TemplatingEngine::create(TemplatingEngine::Templates idx) {
baseTemplate = String(iHttpGetFormat); baseTemplate = String(iHttpGetFormat);
fname = TPL_FNAME_HTTP3; fname = TPL_FNAME_HTTP3;
break; break;
case TEMPLATE_BREWFATHER:
baseTemplate = String(brewfatherFormat);
// fname = TPL_FNAME_BREWFATHER;
break;
case TEMPLATE_INFLUX: case TEMPLATE_INFLUX:
baseTemplate = String(influxDbFormat); baseTemplate = String(influxDbFormat);
fname = TPL_FNAME_INFLUXDB; fname = TPL_FNAME_INFLUXDB;

View File

@ -57,13 +57,11 @@ SOFTWARE.
#define TPL_FNAME_HTTP1 "/http-1.tpl" #define TPL_FNAME_HTTP1 "/http-1.tpl"
#define TPL_FNAME_HTTP2 "/http-2.tpl" #define TPL_FNAME_HTTP2 "/http-2.tpl"
#define TPL_FNAME_HTTP3 "/http-3.tpl" #define TPL_FNAME_HTTP3 "/http-3.tpl"
// #define TPL_FNAME_BREWFATHER "/brewfather.tpl"
#define TPL_FNAME_INFLUXDB "/influxdb.tpl" #define TPL_FNAME_INFLUXDB "/influxdb.tpl"
#define TPL_FNAME_MQTT "/mqtt.tpl" #define TPL_FNAME_MQTT "/mqtt.tpl"
extern const char iSpindleFormat[] PROGMEM; extern const char iSpindleFormat[] PROGMEM;
extern const char iHttpGetFormat[] PROGMEM; extern const char iHttpGetFormat[] PROGMEM;
extern const char brewfatherFormat[] PROGMEM;
extern const char influxDbFormat[] PROGMEM; extern const char influxDbFormat[] PROGMEM;
extern const char mqttFormat[] PROGMEM; extern const char mqttFormat[] PROGMEM;
@ -133,9 +131,8 @@ class TemplatingEngine {
TEMPLATE_HTTP1 = 0, TEMPLATE_HTTP1 = 0,
TEMPLATE_HTTP2 = 1, TEMPLATE_HTTP2 = 1,
TEMPLATE_HTTP3 = 2, TEMPLATE_HTTP3 = 2,
TEMPLATE_BREWFATHER = 3, TEMPLATE_INFLUX = 3,
TEMPLATE_INFLUX = 4, TEMPLATE_MQTT = 4
TEMPLATE_MQTT = 5
}; };
void initialize(float angle, float gravitySG, float corrGravitySG, void initialize(float angle, float gravitySG, float corrGravitySG,

View File

@ -498,8 +498,6 @@ void WebServerHandler::webHandleConfigPush() {
myConfig.setHttp2Header(_server->arg(PARAM_PUSH_HTTP2_H2).c_str(), 1); myConfig.setHttp2Header(_server->arg(PARAM_PUSH_HTTP2_H2).c_str(), 1);
if (_server->hasArg(PARAM_PUSH_HTTP3)) if (_server->hasArg(PARAM_PUSH_HTTP3))
myConfig.setHttp3Url(_server->arg(PARAM_PUSH_HTTP3).c_str()); myConfig.setHttp3Url(_server->arg(PARAM_PUSH_HTTP3).c_str());
if (_server->hasArg(PARAM_PUSH_BREWFATHER))
myConfig.setBrewfatherPushUrl(_server->arg(PARAM_PUSH_BREWFATHER).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2)) if (_server->hasArg(PARAM_PUSH_INFLUXDB2))
myConfig.setInfluxDb2PushUrl(_server->arg(PARAM_PUSH_INFLUXDB2).c_str()); myConfig.setInfluxDb2PushUrl(_server->arg(PARAM_PUSH_INFLUXDB2).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_ORG)) if (_server->hasArg(PARAM_PUSH_INFLUXDB2_ORG))
@ -624,18 +622,18 @@ void WebServerHandler::webHandleConfigHardware() {
} }
// //
// Update device parameters. // Update advanced settings.
// //
void WebServerHandler::webHandleDeviceParam() { void WebServerHandler::webHandleConfigAdvancedWrite() {
LOG_PERF_START("webserver-api-device-param"); LOG_PERF_START("webserver-api-config-advanced");
String id = _server->arg(PARAM_ID); String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/device/param(post)." CR)); Log.notice(F("WEB : webServer callback for /api/config/advaced(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) { if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
myConfig.getID()); myConfig.getID());
_server->send(400, "text/plain", "Invalid ID."); _server->send(400, "text/plain", "Invalid ID.");
LOG_PERF_STOP("webserver-api-device-param"); LOG_PERF_STOP("webserver-api-config-advanced");
return; return;
} }
@ -643,41 +641,65 @@ void WebServerHandler::webHandleDeviceParam() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str()); Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif #endif
for (int i = 0; i < _server->args(); i++) { if (_server->hasArg(PARAM_HW_GYRO_READ_COUNT))
String s = _server->arg(i); myAdvancedConfig.setGyroReadCount(_server->arg(PARAM_HW_GYRO_READ_COUNT).toInt());
if (_server->hasArg(PARAM_HW_GYRO_READ_DELAY))
myAdvancedConfig.setGyroReadDelay(_server->arg(PARAM_HW_GYRO_READ_DELAY).toInt());
if (_server->hasArg(PARAM_HW_GYRO_MOVING_THREASHOLD))
myAdvancedConfig.setGyroSensorMovingThreashold(_server->arg(PARAM_HW_GYRO_MOVING_THREASHOLD).toInt());
if (_server->hasArg(PARAM_HW_FORMULA_DEVIATION))
myAdvancedConfig.setMaxFormulaCreationDeviation(_server->arg(PARAM_HW_FORMULA_DEVIATION).toFloat());
if (_server->hasArg(PARAM_HW_FORMULA_CALIBRATION_TEMP))
myAdvancedConfig.SetDefaultCalibrationTemp(_server->arg(PARAM_HW_FORMULA_CALIBRATION_TEMP).toFloat());
if (_server->hasArg(PARAM_HW_WIFI_PORTAL_TIMEOUT))
myAdvancedConfig.setWifiPortalTimeout(_server->arg(PARAM_HW_WIFI_PORTAL_TIMEOUT).toInt());
if (_server->hasArg(PARAM_HW_WIFI_CONNECT_TIMEOUT))
myAdvancedConfig.setWifiConnectTimeout(_server->arg(PARAM_HW_WIFI_CONNECT_TIMEOUT).toInt());
if (_server->hasArg(PARAM_HW_PUSH_TIMEOUT))
myAdvancedConfig.setPushTimeout(_server->arg(PARAM_HW_PUSH_TIMEOUT).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_HTTP1))
myAdvancedConfig.setPushIntervalHttp1(_server->arg(PARAM_HW_PUSH_INTERVAL_HTTP1).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_HTTP2))
myAdvancedConfig.setPushIntervalHttp2(_server->arg(PARAM_HW_PUSH_INTERVAL_HTTP2).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_HTTP3))
myAdvancedConfig.setPushIntervalHttp3(_server->arg(PARAM_HW_PUSH_INTERVAL_HTTP3).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_INFLUX))
myAdvancedConfig.setPushIntervalInflux(_server->arg(PARAM_HW_PUSH_INTERVAL_INFLUX).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_MQTT))
myAdvancedConfig.setPushIntervalMqtt(_server->arg(PARAM_HW_PUSH_INTERVAL_MQTT).toInt());
if (_server->argName(i).equalsIgnoreCase(PARAM_HW_GYRO_READ_COUNT)) myAdvancedConfig.saveFile();
myHardwareConfig.setGyroReadCount(s.toInt()); _server->sendHeader("Location", "/config.htm#collapseAdvanced", true);
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_GYRO_READ_DELAY)) _server->send(302, "text/plain", "Advanced config updated");
myHardwareConfig.setGyroReadDelay(s.toInt()); LOG_PERF_STOP("webserver-api-config-advanced");
else if (_server->argName(i).equalsIgnoreCase( }
PARAM_HW_GYRO_MOVING_THREASHOLD))
myHardwareConfig.setGyroSensorMovingThreashold(s.toInt());
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_FORMULA_DEVIATION))
myHardwareConfig.setMaxFormulaCreationDeviation(s.toFloat());
else if (_server->argName(i).equalsIgnoreCase(
PARAM_HW_FORMULA_CALIBRATION_TEMP))
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();
// Return the current configuration. //
// Read advanced settings
//
void WebServerHandler::webHandleConfigAdvancedRead() {
LOG_PERF_START("webserver-api-config-advanced");
Log.notice(F("WEB : webServer callback for /api/config/advanced(get)." CR));
DynamicJsonDocument doc(512); DynamicJsonDocument doc(512);
doc[PARAM_HW_GYRO_READ_COUNT] = myHardwareConfig.getGyroReadCount(); doc[PARAM_HW_GYRO_READ_COUNT] = myAdvancedConfig.getGyroReadCount();
doc[PARAM_HW_GYRO_READ_DELAY] = myHardwareConfig.getGyroReadDelay(); doc[PARAM_HW_GYRO_READ_DELAY] = myAdvancedConfig.getGyroReadDelay();
doc[PARAM_HW_GYRO_MOVING_THREASHOLD] = doc[PARAM_HW_GYRO_MOVING_THREASHOLD] =
myHardwareConfig.getGyroSensorMovingThreashold(); myAdvancedConfig.getGyroSensorMovingThreashold();
doc[PARAM_HW_FORMULA_DEVIATION] = doc[PARAM_HW_FORMULA_DEVIATION] =
myHardwareConfig.getMaxFormulaCreationDeviation(); myAdvancedConfig.getMaxFormulaCreationDeviation();
doc[PARAM_HW_WIFI_PORTALTIMEOUT] = myHardwareConfig.getWifiPortalTimeout(); doc[PARAM_HW_WIFI_PORTAL_TIMEOUT] = myAdvancedConfig.getWifiPortalTimeout();
doc[PARAM_HW_WIFI_CONNECT_TIMEOUT] = myAdvancedConfig.getWifiConnectTimeout();
doc[PARAM_HW_PUSH_TIMEOUT] = myAdvancedConfig.getPushTimeout();
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP] = doc[PARAM_HW_FORMULA_CALIBRATION_TEMP] =
myHardwareConfig.getDefaultCalibrationTemp(); myAdvancedConfig.getDefaultCalibrationTemp();
doc[PARAM_HW_PUSH_INTERVAL_HTTP1] = myAdvancedConfig.getPushIntervalHttp1();
doc[PARAM_HW_PUSH_INTERVAL_HTTP2] = myAdvancedConfig.getPushIntervalHttp2();
doc[PARAM_HW_PUSH_INTERVAL_HTTP3] = myAdvancedConfig.getPushIntervalHttp3();
doc[PARAM_HW_PUSH_INTERVAL_INFLUX] = myAdvancedConfig.getPushIntervalInflux();
doc[PARAM_HW_PUSH_INTERVAL_MQTT] = myAdvancedConfig.getPushIntervalMqtt();
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial); serializeJson(doc, Serial);
@ -688,7 +710,7 @@ void WebServerHandler::webHandleDeviceParam() {
out.reserve(512); out.reserve(512);
serializeJson(doc, out); serializeJson(doc, out);
_server->send(200, "application/json", out.c_str()); _server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-device-param"); LOG_PERF_STOP("webserver-api-config-advanced");
} }
// //
@ -698,7 +720,7 @@ void WebServerHandler::webHandleFormulaRead() {
LOG_PERF_START("webserver-api-formula-read"); LOG_PERF_START("webserver-api-formula-read");
Log.notice(F("WEB : webServer callback for /api/formula(get)." CR)); Log.notice(F("WEB : webServer callback for /api/formula(get)." CR));
DynamicJsonDocument doc(250); DynamicJsonDocument doc(512);
const RawFormulaData& fd = myConfig.getFormulaData(); const RawFormulaData& fd = myConfig.getFormulaData();
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
@ -731,6 +753,11 @@ void WebServerHandler::webHandleFormulaRead() {
doc["a3"] = reduceFloatPrecision(fd.a[2], 2); doc["a3"] = reduceFloatPrecision(fd.a[2], 2);
doc["a4"] = reduceFloatPrecision(fd.a[3], 2); doc["a4"] = reduceFloatPrecision(fd.a[3], 2);
doc["a5"] = reduceFloatPrecision(fd.a[4], 2); doc["a5"] = reduceFloatPrecision(fd.a[4], 2);
doc["a6"] = reduceFloatPrecision(fd.a[5], 2);
doc["a7"] = reduceFloatPrecision(fd.a[6], 2);
doc["a8"] = reduceFloatPrecision(fd.a[7], 2);
doc["a9"] = reduceFloatPrecision(fd.a[8], 2);
doc["a10"] = reduceFloatPrecision(fd.a[9], 2);
if (myConfig.isGravityPlato()) { if (myConfig.isGravityPlato()) {
doc["g1"] = reduceFloatPrecision(convertToPlato(fd.g[0]), 1); doc["g1"] = reduceFloatPrecision(convertToPlato(fd.g[0]), 1);
@ -738,12 +765,22 @@ void WebServerHandler::webHandleFormulaRead() {
doc["g3"] = reduceFloatPrecision(convertToPlato(fd.g[2]), 1); doc["g3"] = reduceFloatPrecision(convertToPlato(fd.g[2]), 1);
doc["g4"] = reduceFloatPrecision(convertToPlato(fd.g[3]), 1); doc["g4"] = reduceFloatPrecision(convertToPlato(fd.g[3]), 1);
doc["g5"] = reduceFloatPrecision(convertToPlato(fd.g[4]), 1); doc["g5"] = reduceFloatPrecision(convertToPlato(fd.g[4]), 1);
doc["g6"] = reduceFloatPrecision(convertToPlato(fd.g[5]), 1);
doc["g7"] = reduceFloatPrecision(convertToPlato(fd.g[6]), 1);
doc["g8"] = reduceFloatPrecision(convertToPlato(fd.g[7]), 1);
doc["g9"] = reduceFloatPrecision(convertToPlato(fd.g[8]), 1);
doc["g10"] = reduceFloatPrecision(convertToPlato(fd.g[9]), 1);
} else { } else {
doc["g1"] = reduceFloatPrecision(fd.g[0], 4); doc["g1"] = reduceFloatPrecision(fd.g[0], 4);
doc["g2"] = reduceFloatPrecision(fd.g[1], 4); doc["g2"] = reduceFloatPrecision(fd.g[1], 4);
doc["g3"] = reduceFloatPrecision(fd.g[2], 4); doc["g3"] = reduceFloatPrecision(fd.g[2], 4);
doc["g4"] = reduceFloatPrecision(fd.g[3], 4); doc["g4"] = reduceFloatPrecision(fd.g[3], 4);
doc["g5"] = reduceFloatPrecision(fd.g[4], 4); doc["g5"] = reduceFloatPrecision(fd.g[4], 4);
doc["g6"] = reduceFloatPrecision(fd.g[5], 4);
doc["g7"] = reduceFloatPrecision(fd.g[6], 4);
doc["g8"] = reduceFloatPrecision(fd.g[7], 4);
doc["g9"] = reduceFloatPrecision(fd.g[8], 4);
doc["g10"] = reduceFloatPrecision(fd.g[9], 4);
} }
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING) #if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
@ -792,10 +829,6 @@ void WebServerHandler::webHandleConfigFormatWrite() {
} else if (_server->hasArg(PARAM_FORMAT_MQTT)) { } else if (_server->hasArg(PARAM_FORMAT_MQTT)) {
success = writeFile(TPL_FNAME_MQTT, _server->arg(PARAM_FORMAT_MQTT)); success = writeFile(TPL_FNAME_MQTT, _server->arg(PARAM_FORMAT_MQTT));
} }
/*else if (_server->hasArg(PARAM_FORMAT_BREWFATHER)) {
success = writeFile(TPL_FNAME_BREWFATHER,
_server->arg(PARAM_FORMAT_BREWFATHER));
}*/
if (success) { if (success) {
_server->sendHeader("Location", "/format.htm", true); _server->sendHeader("Location", "/format.htm", true);
@ -841,11 +874,7 @@ void WebServerHandler::webHandleTestPush() {
PushTarget push; PushTarget push;
bool enabled = false; bool enabled = false;
if (!type.compareTo(PARAM_FORMAT_BREWFATHER) && if (!type.compareTo(PARAM_FORMAT_HTTP1) && myConfig.isHttpActive()) {
myConfig.isBrewfatherActive()) {
push.sendBrewfather(engine);
enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_HTTP1) && myConfig.isHttpActive()) {
push.sendHttp1(engine, myConfig.isHttpSSL()); push.sendHttp1(engine, myConfig.isHttpSSL());
enabled = true; enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_HTTP2) && myConfig.isHttp2Active()) { } else if (!type.compareTo(PARAM_FORMAT_HTTP2) && myConfig.isHttp2Active()) {
@ -856,7 +885,7 @@ void WebServerHandler::webHandleTestPush() {
enabled = true; enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_INFLUXDB) && } else if (!type.compareTo(PARAM_FORMAT_INFLUXDB) &&
myConfig.isInfluxDb2Active()) { myConfig.isInfluxDb2Active()) {
push.sendInfluxDb2(engine); push.sendInfluxDb2(engine, myConfig.isInfluxSSL());
enabled = true; enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_MQTT) && myConfig.isMqttActive()) { } else if (!type.compareTo(PARAM_FORMAT_MQTT) && myConfig.isMqttActive()) {
push.sendMqtt(engine, myConfig.isMqttSSL()); push.sendMqtt(engine, myConfig.isMqttSSL());
@ -955,12 +984,6 @@ void WebServerHandler::webHandleConfigFormatRead() {
else else
doc[PARAM_FORMAT_HTTP3] = urlencode(String(&iHttpGetFormat[0])); doc[PARAM_FORMAT_HTTP3] = urlencode(String(&iHttpGetFormat[0]));
/*s = readFile(TPL_FNAME_BREWFATHER);
if (s.length())
doc[PARAM_FORMAT_BREWFATHER] = urlencode(s);
else
doc[PARAM_FORMAT_BREWFATHER] = urlencode(&brewfatherFormat[0]);*/
s = readFile(TPL_FNAME_INFLUXDB); s = readFile(TPL_FNAME_INFLUXDB);
if (s.length()) if (s.length())
doc[PARAM_FORMAT_INFLUXDB] = urlencode(s); doc[PARAM_FORMAT_INFLUXDB] = urlencode(s);
@ -1011,6 +1034,11 @@ void WebServerHandler::webHandleFormulaWrite() {
fd.a[2] = _server->arg("a3").toDouble(); fd.a[2] = _server->arg("a3").toDouble();
fd.a[3] = _server->arg("a4").toDouble(); fd.a[3] = _server->arg("a4").toDouble();
fd.a[4] = _server->arg("a5").toDouble(); fd.a[4] = _server->arg("a5").toDouble();
fd.a[5] = _server->arg("a6").toDouble();
fd.a[6] = _server->arg("a7").toDouble();
fd.a[7] = _server->arg("a8").toDouble();
fd.a[8] = _server->arg("a9").toDouble();
fd.a[9] = _server->arg("a10").toDouble();
if (myConfig.isGravityPlato()) { if (myConfig.isGravityPlato()) {
fd.g[0] = convertToSG(_server->arg("g1").toDouble()); fd.g[0] = convertToSG(_server->arg("g1").toDouble());
@ -1018,12 +1046,22 @@ void WebServerHandler::webHandleFormulaWrite() {
fd.g[2] = convertToSG(_server->arg("g3").toDouble()); fd.g[2] = convertToSG(_server->arg("g3").toDouble());
fd.g[3] = convertToSG(_server->arg("g4").toDouble()); fd.g[3] = convertToSG(_server->arg("g4").toDouble());
fd.g[4] = convertToSG(_server->arg("g5").toDouble()); fd.g[4] = convertToSG(_server->arg("g5").toDouble());
fd.g[5] = convertToSG(_server->arg("g6").toDouble());
fd.g[6] = convertToSG(_server->arg("g7").toDouble());
fd.g[7] = convertToSG(_server->arg("g8").toDouble());
fd.g[8] = convertToSG(_server->arg("g9").toDouble());
fd.g[9] = convertToSG(_server->arg("g10").toDouble());
} else { } else {
fd.g[0] = _server->arg("g1").toDouble(); fd.g[0] = _server->arg("g1").toDouble();
fd.g[1] = _server->arg("g2").toDouble(); fd.g[1] = _server->arg("g2").toDouble();
fd.g[2] = _server->arg("g3").toDouble(); fd.g[2] = _server->arg("g3").toDouble();
fd.g[3] = _server->arg("g4").toDouble(); fd.g[3] = _server->arg("g4").toDouble();
fd.g[4] = _server->arg("g5").toDouble(); fd.g[4] = _server->arg("g5").toDouble();
fd.g[5] = _server->arg("g6").toDouble();
fd.g[6] = _server->arg("g7").toDouble();
fd.g[7] = _server->arg("g8").toDouble();
fd.g[8] = _server->arg("g9").toDouble();
fd.g[9] = _server->arg("g10").toDouble();
} }
myConfig.setFormulaData(fd); myConfig.setFormulaData(fd);
@ -1231,9 +1269,12 @@ bool WebServerHandler::setupWebServer() {
_server->on("/api/config/format", HTTP_POST, _server->on("/api/config/format", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigFormatWrite, std::bind(&WebServerHandler::webHandleConfigFormatWrite,
this)); // Change template formats this)); // Change template formats
_server->on("/api/device/param", HTTP_GET, _server->on("/api/config/advanced", HTTP_GET,
std::bind(&WebServerHandler::webHandleDeviceParam, std::bind(&WebServerHandler::webHandleConfigAdvancedRead,
this)); // Change device params this)); // Read advanced settings
_server->on("/api/config/advanced", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigAdvancedWrite,
this)); // Change advanced params
_server->on("/api/test/push", HTTP_GET, _server->on("/api/test/push", HTTP_GET,
std::bind(&WebServerHandler::webHandleTestPush, std::bind(&WebServerHandler::webHandleTestPush,
this)); // this)); //

View File

@ -60,6 +60,8 @@ class WebServerHandler {
void webHandleConfig(); void webHandleConfig();
void webHandleFormulaWrite(); void webHandleFormulaWrite();
void webHandleFormulaRead(); void webHandleFormulaRead();
void webHandleConfigAdvancedRead();
void webHandleConfigAdvancedWrite();
void webHandleConfigHardware(); void webHandleConfigHardware();
void webHandleConfigGravity(); void webHandleConfigGravity();
void webHandleConfigPush(); void webHandleConfigPush();
@ -74,7 +76,6 @@ class WebServerHandler {
void webHandleCalibrate(); void webHandleCalibrate();
void webHandleUploadFile(); void webHandleUploadFile();
void webHandleUpload(); void webHandleUpload();
void webHandleDeviceParam();
void webHandlePageNotFound(); void webHandlePageNotFound();
String readFile(String fname); String readFile(String fname);

View File

@ -137,7 +137,7 @@ void WifiConnection::startPortal() {
myWifiManager->setMinimumSignalQuality(-1); myWifiManager->setMinimumSignalQuality(-1);
myWifiManager->setConfigPortalChannel(0); myWifiManager->setConfigPortalChannel(0);
myWifiManager->setConfigPortalTimeout( myWifiManager->setConfigPortalTimeout(
myHardwareConfig.getWifiPortalTimeout()); myAdvancedConfig.getWifiPortalTimeout());
String mdns("<p>Default mDNS name is: http://"); String mdns("<p>Default mDNS name is: http://");
mdns += myConfig.getMDNS(); mdns += myConfig.getMDNS();
@ -221,7 +221,7 @@ bool WifiConnection::waitForConnection(int maxTime) {
// //
bool WifiConnection::connect() { bool WifiConnection::connect() {
connectAsync(); connectAsync();
return waitForConnection(20); // 20 seconds. return waitForConnection(myAdvancedConfig.getWifiConnectTimeout());
} }
// //

View File

@ -24,7 +24,6 @@ Other parameters are the same as in the configuration guide.
"ota-url": "http://192.168.1.50:80/firmware/gravmon/", "ota-url": "http://192.168.1.50:80/firmware/gravmon/",
"temp-format": "C", "temp-format": "C",
"ble": "color", "ble": "color",
"brewfather-push": "http://log.brewfather.net/stream?id=Qwerty",
"token": "token", "token": "token",
"token2": "token2", "token2": "token2",
"http-push": "http://192.168.1.50:9090/api/v1/Qwerty/telemetry", "http-push": "http://192.168.1.50:9090/api/v1/Qwerty/telemetry",
@ -63,11 +62,19 @@ Other parameters are the same as in the configuration guide.
"a3":35, "a3":35,
"a4":40, "a4":40,
"a5":45, "a5":45,
"a5":0,
"a6":0,
"a7":0,
"a8":0,
"g1":1, "g1":1,
"g2":1.01, "g2":1.01,
"g3":1.02, "g3":1.02,
"g4":1.03, "g4":1.03,
"g5":1.04 "g4":1.04,
"g5":1,
"g6":1,
"g7":1,
"g8":1
}, },
"angle": 90.93, "angle": 90.93,
"gravity": 1.105, "gravity": 1.105,
@ -120,8 +127,8 @@ GET: /api/config/formula
Retrive the data used for formula calculation data via an HTTP GET command. Payload is in JSON format. Retrive the data used for formula calculation data via an HTTP GET command. Payload is in JSON format.
* ``a1``-``a4`` are the angles/tilt readings (up to 5 are currently supported) * ``a1``-``a8`` are the angles/tilt readings (up to 8 are currently supported)
* ``g1``-``g4`` are the corresponding gravity reaadings in SG or Plato depending on the device-format. * ``g1``-``g8`` are the corresponding gravity reaadings in SG or Plato depending on the device-format.
.. code-block:: json .. code-block:: json
@ -132,16 +139,52 @@ Retrive the data used for formula calculation data via an HTTP GET command. Payl
"a3": 58, "a3": 58,
"a4": 0, "a4": 0,
"a5": 0, "a5": 0,
"a6": 0,
"a7": 0,
"a8": 0,
"g1": 1.000, "g1": 1.000,
"g2": 1.053, "g2": 1.053,
"g3": 1.062, "g3": 1.062,
"g4": 1, "g4": 1,
"g5": 1, "g5": 1,
"g6": 1,
"g7": 1,
"g8": 1,
"error": "Potential error message",
"gravity-format": "G", "gravity-format": "G",
"gravity-formula": "0.0*tilt^3+0.0*tilt^2+0.0017978*tilt+0.9436" "gravity-formula": "0.0*tilt^3+0.0*tilt^2+0.0017978*tilt+0.9436"
} }
GET: /api/config/advanced
=========================
Used for adjusting some internal constants and other advanced settings. Should be used with caution.
.. code-block:: json
{
"gyro-read-count": 50,
"gyro-read-delay": 3150,
"gyro-moving-threashold": 500,
"formula-max-deviation": 1.6,
"wifi-portaltimeout": 120,
"formula-calibration-temp": 20,
"int-http1": 0,
"int-http2": 0,
"int-http3": 0,
"int-influx": 0,
"int-mqtt": 0
}
POST: /api/config/advanced
==========================
Same parameters as above.
Payload should be in standard format used for posting a form
GET: /api/clearwifi GET: /api/clearwifi
=================== ===================
@ -173,7 +216,7 @@ Trigger a push on one of the targets, used to validate the configuration from th
Requires to parameters to function /api/test/push?id=<deviceid>&format=<format> Requires to parameters to function /api/test/push?id=<deviceid>&format=<format>
* ``format`` defines which endpoint to test, valid values are; http-1, http-2, brewfather, influxdb, mqtt * ``format`` defines which endpoint to test, valid values are; http-1, http-2, http-3, influxdb, mqtt
The response is an json message with the following values. The response is an json message with the following values.
@ -223,7 +266,6 @@ Payload should be in standard format used for posting a form. Such as as: `id=va
http-push-h2= http-push-h2=
http-push2-h1= http-push2-h1=
http-push2-h2= http-push2-h2=
brewfather-push=
influxdb2-push=http://192.168.1.50:8086 influxdb2-push=http://192.168.1.50:8086
influxdb2-org= influxdb2-org=
influxdb2-bucket= influxdb2-bucket=
@ -282,8 +324,8 @@ POST: /api/config/formula
Used to update formula calculation data via an HTTP POST command. Payload is in JSON format. Used to update formula calculation data via an HTTP POST command. Payload is in JSON format.
* ``a1``-``a4`` are the angles/tilt readings (up to 5 are currently supported) * ``a1``-``a8`` are the angles/tilt readings (up to 5 are currently supported)
* ``g1``-``g4`` are the corresponding gravity reaadings (in SG) * ``g1``-``g8`` are the corresponding gravity reaadings (in SG)
Payload should be in standard format used for posting a form. Such as as: `id=value&mdns=value` etc. Key value pairs are shown below. Payload should be in standard format used for posting a form. Such as as: `id=value&mdns=value` etc. Key value pairs are shown below.
@ -295,11 +337,17 @@ Payload should be in standard format used for posting a form. Such as as: `id=va
a3=58 a3=58
a4=0 a4=0
a5=0 a5=0
a6=0
a7=0
a8=0
g1=1.000 g1=1.000
g2=1.053 g2=1.053
g3=1.062 g3=1.062
g4=1 g4=1
g5=1 g5=1
g6=1
g7=1
g8=1
Calling the API's from Python Calling the API's from Python
@ -346,7 +394,6 @@ The requests package converts the json to standard form post format.
"http-push-h2": "", "http-push-h2": "",
"http-push2-h1": "" "http-push2-h1": ""
"http-push2-h2": "", "http-push2-h2": "",
"brewfather-push": "",
"influxdb2-push": "", "influxdb2-push": "",
"influxdb2-org": "", "influxdb2-org": "",
"influxdb2-bucket": "", "influxdb2-bucket": "",
@ -383,10 +430,16 @@ The requests package converts the json to standard form post format.
"a3": 58, "a3": 58,
"a4": 0, "a4": 0,
"a5": 0, "a5": 0,
"a6": 0,
"a7": 0,
"a8": 0,
"g1": 1.000, "g1": 1.000,
"g2": 1.053, "g2": 1.053,
"g3": 1.062, "g3": 1.062,
"g4": 1, "g4": 1,
"g5": 1 "g5": 1,
"g6": 1,
"g7": 1,
"g8": 1
} }
set_config( url, json ) set_config( url, json )

View File

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

View File

@ -121,12 +121,6 @@ If you add the prefix `https://` then the device will use SSL when sending data.
The token is included in the default format for the HTTP GET url but can be used for any of the formats. For HTTP GET use can use this for an authorization token with for instance ubidots or blynk http api. The token is included in the default format for the HTTP GET url but can be used for any of the formats. For HTTP GET use can use this for an authorization token with for instance ubidots or blynk http api.
* **Brewfather URL:**
Endpoint to send data via http to brewfather. Format used :ref:`data-formats-brewfather`
SSL is not supported for this target.
* **HTTP Headers** * **HTTP Headers**
.. image:: images/config-popup1.png .. image:: images/config-popup1.png
@ -281,3 +275,14 @@ This option gives you the possibility to install an new version of the firmware
:alt: Update firmware :alt: Update firmware
Advanded Settings
+++++++++++++++++
.. image:: images/config5.png
:width: 800
:alt: Advanced Settings
* **Header:**
To be described

View File

@ -26,7 +26,7 @@ This is the format used for standard http posts.
"gravity": 1.0050, "gravity": 1.0050,
"angle": 45.34, "angle": 45.34,
"battery": 3.67, "battery": 3.67,
"rssi": -12, "RSSI": -12,
"corr-gravity": 1.0050, "corr-gravity": 1.0050,
"gravity-unit": "G", "gravity-unit": "G",
@ -47,32 +47,13 @@ This is the format template used to create the json above.
"gravity": ${gravity}, "gravity": ${gravity},
"angle": ${angle}, "angle": ${angle},
"battery": ${battery}, "battery": ${battery},
"rssi": ${rssi}, "RSSI": ${rssi},
"corr-gravity": ${corr-gravity}, "corr-gravity": ${corr-gravity},
"gravity-unit": "${gravity-unit}", "gravity-unit": "${gravity-unit}",
"run-time": ${run-time} "run-time": ${run-time}
} }
.. _data-formats-brewfather:
Brewfather format
=================
This is the format for Brewfather. See: `Brewfather API docs <https://docs.brewfather.app/integrations/custom-stream>`_
.. code-block:: json
{
"name" : "gravmon",
"temp": 20.5,
"temp_unit": "C",
"battery": 3.67,
"gravity": 1.0050,
"gravity_unit": "G",
}
.. _data-formats-influxdb2: .. _data-formats-influxdb2:
HTTP Get HTTP Get

View File

@ -7,7 +7,7 @@ Welcome to GravityMon's documentation!
###################################### ######################################
.. note:: .. note::
This documentation reflects **v0.9**. Last updated 2022-04-23 This documentation reflects **v1.0**. Last updated 2022-04-26
.. image:: images/gravitymon.gif .. image:: images/gravitymon.gif
:width: 800 :width: 800

View File

@ -3,7 +3,23 @@
Releases Releases
######## ########
v1.0.0
------
* Upgraded to bootstrap v5.1 for web pages.
* Added tooltips to all fields in user interface
* Removed brewfather option (can use standard HTTP options), the old apporach can still be used via changing format template.
* Added 5 more points for formula creation, so a total of 10 angles/gravity values can be stored.
* Added function on format page so that it's easy to copy a format template from the docs (simplify service integration).
* Added https support for Influxdb
* Added possibility to have variable push intervals for different endpoints so that different frequency can be used, for example; 5min mqtt, 15min brewfather.
* Added advanced settings to configuration for adjusting some internal values (gyro reads, accepted formula deviation, timeouts, moving detection etc).
* Added additional http error codes to troubleshooting documentation
* Installation instructions updated on how to find the device after wifi has been configured. * Installation instructions updated on how to find the device after wifi has been configured.
* Documentation on brewfather has been updated to adress SG/Plato conversion
* BUG: Fixed issue in formula calculation in case there were a gap in the data series
* BUG: Field name for wifi strenght changed from "rssi" to "RSSI"
* TODO: Fix documentation for advanced settings.
v0.9.0 v0.9.0
------ ------

View File

@ -10,14 +10,26 @@ Brewfather
Brewfather is an all in one service that allows you to manage you recepies and brews. Brewfather is an all in one service that allows you to manage you recepies and brews.
.. tip:: **Option 1** - iSpindle Endpoint
The integration named Brewfather is uses the custom stream endpoint in brewfather not the standard iSpindle This opion makes use of the standard http (1 or 2) endpoints in the push section. If you are using SG then the device name needs to end with [SG] or brewfather will assume
endpoint. You can use the iSpindle endpoint as well. In that case just use the http-1 or http-2 fields. that the data is in plato. You can also modify the format template using the following options:
**Option 1** - Custom Stream Update the following part `"gravity": ${gravity-plato},` or `"name" : "${mdns}[SG]",``
This option makes use of the push endpoint called Brewfather in the UI. Just enter the http stream adress found This makes use of the standard format template, no changes needed.
.. code-block::
http://log.brewfather.net/ispindel?id=<yourid>
Documentation on this can be found under `Brewfather iSpindle Endpoint <https://docs.brewfather.app/integrations/ispindel>`_
**Option 2** - Custom Stream
This option makes use of the http push endpoint with a custom format template. Just enter the http stream adress found
on brewfather, not other settings are needed. The stream endpoint URL has the following format: on brewfather, not other settings are needed. The stream endpoint URL has the following format:
.. code-block:: .. code-block::
@ -48,18 +60,6 @@ The implementation is basically a http request with the following format templat
} }
**Option 2** - iSpindle Endpoint
This opion makes use of the standard http (1 or 2) endpoints in the push section. If you are using SG then the device name needs to end with [SG] or brewfather will assume
that the data is in plato. The brewfather iSpindle endpoint has the following format:
.. code-block::
http://log.brewfather.net/ispindel?id=<yourid>
Documentation on this can be found under `Brewfather iSpindle Endpoint <https://docs.brewfather.app/integrations/ispindel>`_
Fermentrack Fermentrack
+++++++++++ +++++++++++

View File

@ -31,7 +31,6 @@ Log errors
Check the format for your custom header. This means it has not a correct format. Check the format for your custom header. This means it has not a correct format.
* Influxdb push failed response * Influxdb push failed response
* Brewfather push failed response
* HTTP push failed response * HTTP push failed response
All these errors are standard http error codes. This are the commone ones; All these errors are standard http error codes. This are the commone ones;
@ -41,6 +40,20 @@ Log errors
* 403 - Forbidden. Could be an issue with token or URL. * 403 - Forbidden. Could be an issue with token or URL.
* 404 - Not found. Probably a wrong URL. * 404 - Not found. Probably a wrong URL.
In some cases there can be negative error codes which have the following meaning:
* -1 - Connection refused
* -2 - Send header failed
* -3 - Send payload failed
* -4 - Not connected
* -5 - Connection lost
* -6 - No stream
* -7 - No HTTP server
* -8 - Too little RAM available
* -9 - Error encoding
* -10 - Error writing to stream
* -11 - Read timeout
* MQTT push on <topic> failed error * MQTT push on <topic> failed error
* -3 - Network failed connected * -3 - Network failed connected

15
test/adv.json Normal file
View File

@ -0,0 +1,15 @@
{
"gyro-read-count": 51,
"gyro-read-delay": 3151,
"gyro-moving-threashold": 501,
"formula-max-deviation": 1.7,
"wifi-portal-timeout": 121,
"wifi-connect-timeout": 21,
"formula-calibration-temp": 21,
"push-timeout": 10,
"int-http1": 1,
"int-http2": 2,
"int-http3": 3,
"int-influx": 4,
"int-mqtt": 5
}

View File

@ -3,7 +3,6 @@
"id": "7376ef", "id": "7376ef",
"ota-url": "http://192.168.1.100:80/firmware/gravmon/", "ota-url": "http://192.168.1.100:80/firmware/gravmon/",
"temp-format": "C", "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": "http://192.168.1.10:9090/api/v1/ZYfjlUNeiuyu9N/telemetry",
"http-push-h1": "Auth: Basic T7IF9DD9fF3RDddE=", "http-push-h1": "Auth: Basic T7IF9DD9fF3RDddE=",
"http-push-h2": "Auth: Advanced T7IF9DD9fF3RDddE=", "http-push-h2": "Auth: Advanced T7IF9DD9fF3RDddE=",

View File

@ -37,7 +37,6 @@ json = { "id": id,
"http-push-h2": "", "http-push-h2": "",
"http-push2-h1": "Content-Type: application/json", "http-push2-h1": "Content-Type: application/json",
"http-push2-h2": "", "http-push2-h2": "",
"brewfather-push": "", # Brewfather URL
"influxdb2-push": "", # InfluxDB2 settings "influxdb2-push": "", # InfluxDB2 settings
"influxdb2-org": "", "influxdb2-org": "",
"influxdb2-bucket": "", "influxdb2-bucket": "",

View File

@ -6,9 +6,20 @@
"a2": 45, "a2": 45,
"a4": 55, "a4": 55,
"a5": 30, "a5": 30,
"a6": 30,
"a7": 30,
"a8": 30,
"a9": 30,
"a10": 30,
"g1": 1.000, "g1": 1.000,
"g3": 1.010, "g3": 1.010,
"g2": 1.025, "g2": 1.025,
"g4": 1.040, "g4": 1.040,
"g5": 1.005 "g5": 1.005,
"g6": 1.005,
"g7": 1.005,
"g8": 1.005,
"g9": 1.005,
"g10": 1.005,
"error": ""
} }