Initial version for testing

This commit is contained in:
Magnus 2021-03-26 19:42:58 +01:00
commit 5f04d6e65e
41 changed files with 15774 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.pio/*
.vscode/*
*.map
test/*.md

1
README.md Normal file
View File

@ -0,0 +1 @@
TODO: Write the documentation for this project

BIN
bin/firmware-debug.bin Normal file

Binary file not shown.

BIN
bin/firmware.bin Normal file

Binary file not shown.

1
bin/version.json Normal file
View File

@ -0,0 +1 @@
{ "project":"gravmon", "version":"0.2.0" }

78
data/about.htm Normal file
View File

@ -0,0 +1,78 @@
<!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="/device.htm">Device</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="/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 Magnus Persson</div>
</body>
</html>

1
data/about.min.htm Normal file
View File

@ -0,0 +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="/device.htm">Device</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="/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 Magnus Persson</div></body></html>

330
data/config.htm Normal file
View File

@ -0,0 +1,330 @@
<!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="/device.htm">Device</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.htm">About</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</nav>
<!-- START MAIN INDEX -->
<div class="container">
<hr class="my-2">
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert">
<div id="alert-msg">...</div>
<button type="button" id="alert-btn" class="close" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function showError( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
}
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="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Device settings
</button>
</h2>
</div>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#accordion">
<div class="card-body">
<form action="/api/config/device" method="post">
<input type="text" name="id" id="id1" hidden>
<div class="form-group row">
<label for="mdns" class="col-sm-4 col-form-label">Device name:</label>
<div class="col-sm-8">
<input type="text" maxlength="12" class="form-control" name="mdns" id="mdns">
</div>
</div>
<fieldset class="form-group row">
<legend class="col-form-label col-sm-4 float-sm-left pt-0">Temperature Format:</legend>
<div class="col-sm-8">
<div class="form-check">
<input class="form-check-input" type="radio" name="temp-format" id="temp-format-c" value="C" checked>
<label class="form-check-label" for="temp-format-c">
Celsius
</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="temp-format" id="temp-format-f" value="F">
<label class="form-check-label" for="temp-format-f">
Farenheight
</label>
</div>
</div>
</fieldset>
<div class="form-group row">
<div class="col-sm-8 offset-sm-4">
<button type="submit" class="btn btn-primary" id="device-btn">Save</button>
</div>
</div>
</form>
<div class="form-group row">
<label for="calibrate-btn" class="col-sm-4 col-form-label">Current calibration values:</label>
<label for="calibrate-btn" class="col-sm-4 col-form-label" id="gyro-calibration-data">Loading...</label>
<label for="gyro-calibration-data" class="col-sm-4 col-form-label" id="angle">Loading...</label>
<div class="col-sm-8 offset-sm-4">
<button type="button" class="btn btn-warning" id="calibrate-btn">Calibrate device</button>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingTwo">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
Push settings
</button>
</h2>
</div>
<div id="collapseTwo" class="collapse" aria-labelledby="headingTwo" data-parent="#accordion">
<div class="card-body">
<form action="/api/config/push" method="post">
<input type="text" name="id" id="id2" hidden>
<div class="form-group row">
<label for="http-push" class="col-sm-4 col-form-label">Http:</label>
<div class="col-sm-8">
<input type="url" maxlength="90" class="form-control" name="http-push" id="http-push">
</div>
</div>
<div class="form-group row">
<label for="inputBrewfatherPush" class="col-sm-4 col-form-label">Brewfather:</label>
<div class="col-sm-8">
<input type="url" maxlength="90" class="form-control" name="brewfather-push" id="brewfather-push">
</div>
</div>
<div class="form-group row">
<label for="push-interval" class="col-sm-4 col-form-label">Interval:</label>
<div class="col-sm-8">
<input type="number" min="10" max="10000" class="form-control" name="push-interval" id="push-interval">
</div>
</div>
<div class="form-group row">
<div class="col-sm-8 offset-sm-4">
<button type="submit" class="btn btn-primary" id="push-btn">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingThree">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
Gravity
</button>
</h2>
</div>
<div id="collapseThree" class="collapse" aria-labelledby="headingThree" data-parent="#accordion">
<div class="card-body">
<form action="/api/config/gravity" method="post">
<input type="text" name="id" id="id3" hidden>
<div class="form-group row">
<label for="gravity-formula" class="col-sm-4 col-form-label">Formula</label>
<div class="col-sm-8">
<input type="text" maxlength="200" class="form-control" name="gravity-formula" id="gravity-formula">
</div>
</div>
<label for="gravity-formula"" class="col-sm-8 offset-sm-4 col-form-label" id="gravity">Loading...</label>
<div class="form-group row">
<div class="col-sm-8 offset-sm-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="gravity-temp-adjustment" id="gravity-temp-adjustment">
<label class="form-check-label" for="gravity-temp-adjustment">
Temperature adjust gravity
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-8 offset-sm-4">
<button type="submit" class="btn btn-primary" id="gravity-btn">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="card">
<div class="card-header" id="headingFour">
<h2 class="mb-0">
<button class="btn btn-link btn-block text-left collapsed" type="button" data-toggle="collapse" data-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
Hardware settings
</button>
</h2>
</div>
<div id="collapseFour" class="collapse" aria-labelledby="headingFour" data-parent="#accordion">
<div class="card-body">
<form action="/api/config/hardware" method="post">
<input type="text" name="id" id="id4" hidden>
<div class="form-group row">
<label for="voltage-factor" class="col-sm-4 col-form-label">Voltage factor:</label>
<div class="col-sm-4">
<input type="number" step=".01" class="form-control" name="voltage-factor" id="voltage-factor">
</div>
<label for="voltage-factor" class="col-sm-4 col-form-label" id="battery">Loading...</label>
</div>
<div class="form-group row">
<label for="temp-adjustment-value" class="col-sm-4 col-form-label">Temp Sensor Adj:</label>
<div class="col-sm-8">
<input type="number" step=".1" class="form-control" name="temp-adjustment-value" id="temp-adjustment-value">
</div>
</div>
<div class="form-group row" hidden>
<label for="ota-url" class="col-sm-4 col-form-label">OTA base URL:</label>
<div class="col-sm-8">
<input type="url" maxlength="90" class="form-control" name="ota-url" id="ota-url">
</div>
</div>
<div class="form-group row">
<div class="col-sm-8 offset-sm-4">
<button type="submit" class="btn btn-primary" id="hardware-btn">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<hr class="my-4">
</div>
<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');
}
});
// Trigger the calibration
$("#calibrate-btn").click(function(e){
console.log( "Calibrating..." );
$.ajax( {
type: "POST",
url: "/api/calibrate",
data: { id: $("#id1").val() },
success: function(result) { showSuccess('Calibration of device was completed successfully.'); getConfig(); },
error: function(result) { showError('Unable to calibrate device.'); }
} );
});
function setButtonDisabled( b ) {
$("#device-btn").prop("disabled", b);
$("#calibrate-btn").prop("disabled", b);
$("#push-btn").prop("disabled", b);
$("#gravity-btn").prop("disabled", b);
$("#hardware-btn").prop("disabled", b);
}
// Get the configuration values from the API
function getConfig() {
setButtonDisabled( true );
var url = "/api/config";
//var url = "/test/config.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#id1").val(cfg["id"]);
$("#id2").val(cfg["id"]);
$("#id3").val(cfg["id"]);
$("#id4").val(cfg["id"]);
$("#mdns").val(cfg["mdns"]);
if( cfg["temp-format"] == "C" )
$("#temp-format-c").click();
else
$("#temp-format-f").click();
$("#ota-url").val(cfg["ota-url"]);
$("#http-push").val(cfg["http-push"]);
$("#brewfather-push").val(cfg["brewfather-push"]);
$("#push-interval").val(cfg["push-interval"]);
$("#voltage-factor").val(cfg["voltage-factor"]);
$("#gravity-formula").val(cfg["gravity-formula"]);
$("#temp-adjustment-value").val(cfg["temp-adjustment-value"]);
$("#gravity-temp-adjustment").prop( "checked", cfg["gravity-temp-adjustment"] );
$("#gyro-calibration-data").text( cfg["gyro-calibration-data"]["ax"] + "," + cfg["gyro-calibration-data"]["ay"] + "," + cfg["gyro-calibration-data"]["az"] + "," + cfg["gyro-calibration-data"]["gx"] + "," + cfg["gyro-calibration-data"]["gy"] + "," + cfg["gyro-calibration-data"]["gz"] );
$("#battery").text(cfg["battery"] + " V");
$("#angle").text(cfg["angle"]);
$("#gravity").text(cfg["gravity"] + " SG");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
setButtonDisabled( false );
});
}
</script>
<!-- START FOOTER -->
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021 Magnus Persson</div>
</body>
</html>

1
data/config.min.htm Normal file

File diff suppressed because one or more lines are too long

138
data/device.htm Normal file
View File

@ -0,0 +1,138 @@
<!doctype html>
<html lang="en">
<!--
TODO:
* Add OTA check for new firmware.
* Add button to do upgrade to new firmware.
-->
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body class="py-4">
<!-- START MENU -->
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/device.htm">Device</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.htm">About</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</nav>
<!-- START MAIN INDEX -->
<div class="container">
<hr class="my-4">
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert">
<div id="alert-msg">...</div>
<button type="button" id="alert-btn" class="close" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function showError( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
}
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="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Current version:</div>
<div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div>
</div>
<div class="row mb-3" id="h-app-ver-new" hidden>
<div class="col-md-8 themed-grid-col bg-light">New version:</div>
<div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Host name:</div>
<div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Device ID:</div>
<div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div>
</div>
<!--
<div class="form-group row">
<div class="col-md-4 ">
<button type="button" class="btn btn-warning" id="wifi-reset-btn">Reset Device</button>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-warning" id="wifi-reset-btn">Clear WIFI Settings</button>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-warning" id="wifi-reset-btn">Factory default settings</button>
</div>
</div>
-->
<hr class="my-4">
</div>
<script type="text/javascript">
window.onload = getConfig;
function getConfig() {
var url = "/api/device";
//var url = "/test/device.json";
$('#spinner').show();
$.getJSON(url, function (cfg) {
console.log( cfg );
$("#app-ver").text(cfg["app-ver"]);
$("#mdns").text(cfg["mdns"]);
$("#id").text(cfg["id"]);
})
.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 Magnus Persson</div>
</body>
</html>

17
data/device.min.htm Normal file
View File

@ -0,0 +1,17 @@
<!doctype html><html lang="en"><!--
TODO:
* Add OTA check for new firmware.
* Add button to do upgrade to new firmware.
--><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"><meta name="description" content=""><title>Beer Gravity Monitor</title><link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous"><script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script></head><body class="py-4"><!-- START MENU --><nav class="navbar navbar-expand-sm navbar-dark bg-primary"><a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation"><span class="navbar-toggler-icon"></span></button><div class="collapse navbar-collapse" id="navbar"><ul class="navbar-nav mr-auto"><li class="nav-item"><a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a></li><li class="nav-item active"><a class="nav-link" href="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</a></li></ul></div><div class="spinner-border text-light" id="spinner" role="status"></div></nav><!-- START MAIN INDEX --><div class="container"><hr class="my-4"><div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert"><div id="alert-msg">...</div><button type="button" id="alert-btn" class="close" aria-label="Close"><span aria-hidden="true">&times;</span></button></div><script type="text/javascript">function showError(s){$(".alert").removeClass("alert-success").addClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}function showSuccess(s){$(".alert").addClass("alert-success").removeClass("alert-danger").removeClass("d-none").addClass("show"),$("#alert-msg").text(s)}$("#alert-btn").click(function(s){$(".alert").addClass("d-none").removeClass("show")})</script><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Current version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver">Loading...</div></div><div class="row mb-3" id="h-app-ver-new" hidden><div class="col-md-8 themed-grid-col bg-light">New version:</div><div class="col-md-4 themed-grid-col bg-light" id="app-ver-new">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Host name:</div><div class="col-md-4 themed-grid-col bg-light" id="mdns">Loading...</div></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Device ID:</div><div class="col-md-4 themed-grid-col bg-light" id="id">Loading...</div></div><!--
<div class="form-group row">
<div class="col-md-4 ">
<button type="button" class="btn btn-warning" id="wifi-reset-btn">Reset Device</button>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-warning" id="wifi-reset-btn">Clear WIFI Settings</button>
</div>
<div class="col-md-4">
<button type="button" class="btn btn-warning" id="wifi-reset-btn">Factory default settings</button>
</div>
</div>
--><hr class="my-4"></div><script type="text/javascript">function getConfig(){var n="/api/device";$("#spinner").show(),$.getJSON(n,function(n){console.log(n),$("#app-ver").text(n["app-ver"]),$("#mdns").text(n.mdns),$("#id").text(n.id)}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}window.onload=getConfig</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021 Magnus Persson</div></body></html>

153
data/index.htm Normal file
View File

@ -0,0 +1,153 @@
<!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 active">
<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="/device.htm">Device</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.htm">About</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( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
}
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="" id="id" hidden></div>
<div class="row mb-3">
<div class="col-md-8 themed-grid-col bg-light">Gravity:</div>
<div class="col-md-4 themed-grid-col bg-light" id="gravity">Loading...</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="col-md-12 px-md-5 themed-grid-col bg-light custom-control custom-checkbox">
<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>
<hr class="my-4">
</div>
<script type="text/javascript">
window.onload = start;
$("#sleep-mode").click(function(e){
console.log( "Blocking sleep mode = " + $("#sleep-mode").is(":checked"));
$.ajax( {
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 );
$("#id").text(cfg["id"]);
$("#angle").text(cfg["angle"]);
$("#gravity").text(cfg["gravity"] + " SG");
$("#battery").text(cfg["battery"] + " V");
if( cfg["temp-format"] == "C")
$("#temp").text(cfg["temp-c"] + " C");
else
$("#temp").text(cfg["temp-f"] + " F");
//console.log(cfg["sleep-mode"] );
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-fluid themed-container bg-primary text-light">(C) Copyright 2021 Magnus Persson</div>
</body>
</html>

1
data/index.min.htm Normal file
View File

@ -0,0 +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 active"><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="/device.htm">Device</a></li><li class="nav-item"><a class="nav-link" href="/config.htm">Configuration</a></li><li class="nav-item"><a class="nav-link" href="/about.htm">About</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="" id="id" hidden></div><div class="row mb-3"><div class="col-md-8 themed-grid-col bg-light">Gravity:</div><div class="col-md-4 themed-grid-col bg-light" id="gravity">Loading...</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="col-md-12 px-md-5 themed-grid-col bg-light custom-control custom-checkbox"><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><hr class="my-4"></div><script type="text/javascript">function getStatus(){var e="/api/status";$("#spinner").show(),$.getJSON(e,function(e){console.log(e),$("#id").text(e.id),$("#angle").text(e.angle),$("#gravity").text(e.gravity+" SG"),$("#battery").text(e.battery+" V"),"C"==e["temp-format"]?$("#temp").text(e["temp-c"]+" C"):$("#temp").text(e["temp-f"]+" F"),e["sleep-mode"]?$("#sleep-mode").attr("checked",!0):$("#sleep-mode").attr("checked",!1),$("#sleep-mode").removeAttr("disabled")}).fail(function(){showError("Unable to get data from the device.")}).always(function(){$("#spinner").hide()})}function start(){setInterval(getStatus,3e3)}window.onload=start,$("#sleep-mode").click(function(e){console.log("Blocking sleep mode = "+$("#sleep-mode").is(":checked")),$.ajax({type:"POST",url:"/api/status/sleepmode",data:{id:$("#id").text(),"sleep-mode":$("#sleep-mode").is(":checked")},success:function(e){},error:function(e){showError("Could not update sleep mode for device.")}})})</script><!-- START FOOTER --><div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021 Magnus Persson</div></body></html>

6313
doc/AN3461.pdf Normal file

File diff suppressed because it is too large Load Diff

1474
lib/mpu6050/I2Cdev.cpp Normal file

File diff suppressed because it is too large Load Diff

289
lib/mpu6050/I2Cdev.h Normal file
View File

@ -0,0 +1,289 @@
// I2Cdev library collection - Main I2C device class header file
// Abstracts bit and byte I2C R/W functions into a convenient class
// 2013-06-05 by Jeff Rowberg <jeff@rowberg.net>
//
// Changelog:
// 2020-01-20 - hardija : complete support for Teensy 3.x
// 2015-10-30 - simondlevy : support i2c_t3 for Teensy3.1
// 2013-05-06 - add Francesco Ferrara's Fastwire v0.24 implementation with small modifications
// 2013-05-05 - fix issue with writing bit values to words (Sasquatch/Farzanegan)
// 2012-06-09 - fix major issue with reading > 32 bytes at a time with Arduino Wire
// - add compiler warnings when using outdated or IDE or limited I2Cdev implementation
// 2011-11-01 - fix write*Bits mask calculation (thanks sasquatch @ Arduino forums)
// 2011-10-03 - added automatic Arduino version detection for ease of use
// 2011-10-02 - added Gene Knight's NBWire TwoWire class implementation with small modifications
// 2011-08-31 - added support for Arduino 1.0 Wire library (methods are different from 0.x)
// 2011-08-03 - added optional timeout parameter to read* methods to easily change from default
// 2011-08-02 - added support for 16-bit registers
// - fixed incorrect Doxygen comments on some methods
// - added timeout value for read operations (thanks mem @ Arduino forums)
// 2011-07-30 - changed read/write function structures to return success or byte counts
// - made all methods static for multi-device memory savings
// 2011-07-28 - initial release
/* ============================================
I2Cdev device library code is placed under the MIT license
Copyright (c) 2013 Jeff Rowberg
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 above 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.
===============================================
*/
#ifndef _I2CDEV_H_
#define _I2CDEV_H_
// -----------------------------------------------------------------------------
// I2C interface implementation setting
// -----------------------------------------------------------------------------
#ifndef I2CDEV_IMPLEMENTATION
#define I2CDEV_IMPLEMENTATION I2CDEV_ARDUINO_WIRE
//#define I2CDEV_IMPLEMENTATION I2CDEV_TEENSY_3X_WIRE
//#define I2CDEV_IMPLEMENTATION I2CDEV_BUILTIN_SBWIRE
//#define I2CDEV_IMPLEMENTATION I2CDEV_BUILTIN_FASTWIRE
#endif // I2CDEV_IMPLEMENTATION
// comment this out if you are using a non-optimal IDE/implementation setting
// but want the compiler to shut up about it
#define I2CDEV_IMPLEMENTATION_WARNINGS
// -----------------------------------------------------------------------------
// I2C interface implementation options
// -----------------------------------------------------------------------------
#define I2CDEV_ARDUINO_WIRE 1 // Wire object from Arduino
#define I2CDEV_BUILTIN_NBWIRE 2 // Tweaked Wire object from Gene Knight's NBWire project
// ^^^ NBWire implementation is still buggy w/some interrupts!
#define I2CDEV_BUILTIN_FASTWIRE 3 // FastWire object from Francesco Ferrara's project
#define I2CDEV_I2CMASTER_LIBRARY 4 // I2C object from DSSCircuits I2C-Master Library at https://github.com/DSSCircuits/I2C-Master-Library
#define I2CDEV_BUILTIN_SBWIRE 5 // I2C object from Shuning (Steve) Bian's SBWire Library at https://github.com/freespace/SBWire
#define I2CDEV_TEENSY_3X_WIRE 6 // Teensy 3.x support using i2c_t3 library
// -----------------------------------------------------------------------------
// Arduino-style "Serial.print" debug constant (uncomment to enable)
// -----------------------------------------------------------------------------
//#define I2CDEV_SERIAL_DEBUG
#ifdef ARDUINO
#if ARDUINO < 100
#include "WProgram.h"
#else
#include "Arduino.h"
#endif
#if I2CDEV_IMPLEMENTATION == I2CDEV_ARDUINO_WIRE
#include <Wire.h>
#endif
#if I2CDEV_IMPLEMENTATION == I2CDEV_TEENSY_3X_WIRE
#include <i2c_t3.h>
#endif
#if I2CDEV_IMPLEMENTATION == I2CDEV_I2CMASTER_LIBRARY
#include <I2C.h>
#endif
#if I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_SBWIRE
#include "SBWire.h"
#endif
#endif
#ifdef SPARK
#include <spark_wiring_i2c.h>
#define ARDUINO 101
#endif
// 1000ms default read timeout (modify with "I2Cdev::readTimeout = [ms];")
#define I2CDEV_DEFAULT_READ_TIMEOUT 1000
class I2Cdev {
public:
I2Cdev();
static int8_t readBit(uint8_t devAddr, uint8_t regAddr, uint8_t bitNum, uint8_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readBitW(uint8_t devAddr, uint8_t regAddr, uint8_t bitNum, uint16_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readBits(uint8_t devAddr, uint8_t regAddr, uint8_t bitStart, uint8_t length, uint8_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readBitsW(uint8_t devAddr, uint8_t regAddr, uint8_t bitStart, uint8_t length, uint16_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readByte(uint8_t devAddr, uint8_t regAddr, uint8_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readWord(uint8_t devAddr, uint8_t regAddr, uint16_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data, uint16_t timeout=I2Cdev::readTimeout);
static int8_t readWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t *data, uint16_t timeout=I2Cdev::readTimeout);
static bool writeBit(uint8_t devAddr, uint8_t regAddr, uint8_t bitNum, uint8_t data);
static bool writeBitW(uint8_t devAddr, uint8_t regAddr, uint8_t bitNum, uint16_t data);
static bool writeBits(uint8_t devAddr, uint8_t regAddr, uint8_t bitStart, uint8_t length, uint8_t data);
static bool writeBitsW(uint8_t devAddr, uint8_t regAddr, uint8_t bitStart, uint8_t length, uint16_t data);
static bool writeByte(uint8_t devAddr, uint8_t regAddr, uint8_t data);
static bool writeWord(uint8_t devAddr, uint8_t regAddr, uint16_t data);
static bool writeBytes(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint8_t *data);
static bool writeWords(uint8_t devAddr, uint8_t regAddr, uint8_t length, uint16_t *data);
static uint16_t readTimeout;
};
#if I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_FASTWIRE
//////////////////////
// FastWire 0.24
// This is a library to help faster programs to read I2C devices.
// Copyright(C) 2012
// Francesco Ferrara
//////////////////////
/* Master */
#define TW_START 0x08
#define TW_REP_START 0x10
/* Master Transmitter */
#define TW_MT_SLA_ACK 0x18
#define TW_MT_SLA_NACK 0x20
#define TW_MT_DATA_ACK 0x28
#define TW_MT_DATA_NACK 0x30
#define TW_MT_ARB_LOST 0x38
/* Master Receiver */
#define TW_MR_ARB_LOST 0x38
#define TW_MR_SLA_ACK 0x40
#define TW_MR_SLA_NACK 0x48
#define TW_MR_DATA_ACK 0x50
#define TW_MR_DATA_NACK 0x58
#define TW_OK 0
#define TW_ERROR 1
class Fastwire {
private:
static boolean waitInt();
public:
static void setup(int khz, boolean pullup);
static byte beginTransmission(byte device);
static byte write(byte value);
static byte writeBuf(byte device, byte address, byte *data, byte num);
static byte readBuf(byte device, byte address, byte *data, byte num);
static void reset();
static byte stop();
};
#endif
#if I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_NBWIRE
// NBWire implementation based heavily on code by Gene Knight <Gene@Telobot.com>
// Originally posted on the Arduino forum at http://arduino.cc/forum/index.php/topic,70705.0.html
// Originally offered to the i2cdevlib project at http://arduino.cc/forum/index.php/topic,68210.30.html
#define NBWIRE_BUFFER_LENGTH 32
class TwoWire {
private:
static uint8_t rxBuffer[];
static uint8_t rxBufferIndex;
static uint8_t rxBufferLength;
static uint8_t txAddress;
static uint8_t txBuffer[];
static uint8_t txBufferIndex;
static uint8_t txBufferLength;
// static uint8_t transmitting;
static void (*user_onRequest)(void);
static void (*user_onReceive)(int);
static void onRequestService(void);
static void onReceiveService(uint8_t*, int);
public:
TwoWire();
void begin();
void begin(uint8_t);
void begin(int);
void beginTransmission(uint8_t);
//void beginTransmission(int);
uint8_t endTransmission(uint16_t timeout=0);
void nbendTransmission(void (*function)(int)) ;
uint8_t requestFrom(uint8_t, int, uint16_t timeout=0);
//uint8_t requestFrom(int, int);
void nbrequestFrom(uint8_t, int, void (*function)(int));
void send(uint8_t);
void send(uint8_t*, uint8_t);
//void send(int);
void send(char*);
uint8_t available(void);
uint8_t receive(void);
void onReceive(void (*)(int));
void onRequest(void (*)(void));
};
#define TWI_READY 0
#define TWI_MRX 1
#define TWI_MTX 2
#define TWI_SRX 3
#define TWI_STX 4
#define TW_WRITE 0
#define TW_READ 1
#define TW_MT_SLA_NACK 0x20
#define TW_MT_DATA_NACK 0x30
#define CPU_FREQ 16000000L
#define TWI_FREQ 100000L
#define TWI_BUFFER_LENGTH 32
/* TWI Status is in TWSR, in the top 5 bits: TWS7 - TWS3 */
#define TW_STATUS_MASK (_BV(TWS7)|_BV(TWS6)|_BV(TWS5)|_BV(TWS4)|_BV(TWS3))
#define TW_STATUS (TWSR & TW_STATUS_MASK)
#define TW_START 0x08
#define TW_REP_START 0x10
#define TW_MT_SLA_ACK 0x18
#define TW_MT_SLA_NACK 0x20
#define TW_MT_DATA_ACK 0x28
#define TW_MT_DATA_NACK 0x30
#define TW_MT_ARB_LOST 0x38
#define TW_MR_ARB_LOST 0x38
#define TW_MR_SLA_ACK 0x40
#define TW_MR_SLA_NACK 0x48
#define TW_MR_DATA_ACK 0x50
#define TW_MR_DATA_NACK 0x58
#define TW_ST_SLA_ACK 0xA8
#define TW_ST_ARB_LOST_SLA_ACK 0xB0
#define TW_ST_DATA_ACK 0xB8
#define TW_ST_DATA_NACK 0xC0
#define TW_ST_LAST_DATA 0xC8
#define TW_SR_SLA_ACK 0x60
#define TW_SR_ARB_LOST_SLA_ACK 0x68
#define TW_SR_GCALL_ACK 0x70
#define TW_SR_ARB_LOST_GCALL_ACK 0x78
#define TW_SR_DATA_ACK 0x80
#define TW_SR_DATA_NACK 0x88
#define TW_SR_GCALL_DATA_ACK 0x90
#define TW_SR_GCALL_DATA_NACK 0x98
#define TW_SR_STOP 0xA0
#define TW_NO_INFO 0xF8
#define TW_BUS_ERROR 0x00
//#define _MMIO_BYTE(mem_addr) (*(volatile uint8_t *)(mem_addr))
//#define _SFR_BYTE(sfr) _MMIO_BYTE(_SFR_ADDR(sfr))
#ifndef sbi // set bit
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
#endif // sbi
#ifndef cbi // clear bit
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
#endif // cbi
extern TwoWire Wire;
#endif // I2CDEV_IMPLEMENTATION == I2CDEV_BUILTIN_NBWIRE
#endif /* _I2CDEV_H_ */

3371
lib/mpu6050/MPU6050.cpp Normal file

File diff suppressed because it is too large Load Diff

1041
lib/mpu6050/MPU6050.h Normal file

File diff suppressed because it is too large Load Diff

71
platformio.ini Normal file
View File

@ -0,0 +1,71 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[platformio]
lib_dir = lib
include_dir = lib
[common_env_data]
upload_speed = 921600
monitor_speed = 115200
platform = espressif8266
framework = arduino
board = d1_mini
build_unflags =
build_flags = #-O0 -Wl,-Map,output.map
-D BAUD=${common_env_data.monitor_speed}
#-D ACTIVATE_OTA # TODO: Download html files to FS to reduce image size
-D ACTIVATE_PUSH
#-D SKIP_SLEEPMODE
-D USE_LITTLEFS=true
-D EMBED_HTML
-D CFG_APPVER="\"0.2.0\""
lib_deps =
# https://github.com/jrowberg/i2cdevlib.git # Using local copy of this library
https://github.com/codeplea/tinyexpr
https://github.com/graphitemaster/incbin
https://github.com/khoih-prog/ESP_MultiResetDetector
https://github.com/thijse/Arduino-Log
https://github.com/bblanchon/ArduinoJson
https://github.com/PaulStoffregen/OneWire
https://github.com/tzapu/WiFiManager
https://github.com/milesburton/Arduino-Temperature-Control-Library
[env:gravity-debug]
upload_speed = ${common_env_data.upload_speed}
monitor_speed = ${common_env_data.monitor_speed}
framework = ${common_env_data.framework}
platform = ${common_env_data.platform}
extra_scripts = script/copy_firmware.py
build_unflags = ${common_env_data.build_unflags}
build_flags =
${common_env_data.build_flags}
-D LOG_LEVEL=6
lib_deps =
${common_env_data.lib_deps}
board = ${common_env_data.board}
build_type = debug
board_build.filesystem = littlefs
[env:gravity-release]
upload_speed = ${common_env_data.upload_speed}
monitor_speed = ${common_env_data.monitor_speed}
framework = ${common_env_data.framework}
platform = ${common_env_data.platform}
extra_scripts = script/copy_firmware.py
build_unflags = ${common_env_data.build_unflags}
build_flags =
${common_env_data.build_flags}
-D LOG_LEVEL=5
lib_deps =
${common_env_data.lib_deps}
board = ${common_env_data.board}
build_type = release
board_build.filesystem = littlefs

34
script/copy_firmware.py Normal file
View File

@ -0,0 +1,34 @@
Import("env")
import shutil
def get_build_flag_value(flag_name):
build_flags = env.ParseFlags(env['BUILD_FLAGS'])
flags_with_value_list = [build_flag for build_flag in build_flags.get('CPPDEFINES') if type(build_flag) == list]
defines = {k: v for (k, v) in flags_with_value_list}
return defines.get(flag_name)
def after_build(source, target, env):
print( "Executing custom step " )
dir = env.GetLaunchDir()
name = env.get( "PIOENV" )
source = dir + "\\.pio\\build\\" + name + "\\firmware.bin"
if name == "gravity-debug" :
target = dir + "\\bin\\firmware-debug.bin"
if name == "gravity-release" :
target = dir + "\\bin\\firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
target = dir + "\\bin\\version.json"
ver = get_build_flag_value("CFG_APPVER")
print( "Creating version.json" )
f = open( target, "w" )
f.write( "{ \"project\":\"gravmon\", \"version\":" + ver + " }" )
f.close()
print( "Adding custom build step: ")
#env.AddPreAction("buildprog", after_build)
env.AddPostAction("buildprog", after_build)
#env.AddPreAction("upload", after_build)
#env.AddPostAction("upload", after_build)

99
src/calc.cpp Normal file
View File

@ -0,0 +1,99 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "calc.h"
#include "helper.h"
#include "config.h"
#include "tinyexpr.h"
#include "tempsensor.h"
//#define LOG_LEVEL 5
//
// Calculates gravity according to supplied formula, compatible with iSpindle/Fermentrack formula
//
double calculateGravity( double angle, double temp ) {
const char* formula = myConfig.getGravityFormula();
#if LOG_LEVEL==6
Log.verbose(F("CALC: Calculating gravity for angle %F, temp %F." CR), angle, temp);
Log.verbose(F("CALC: Formula %s." CR), formula);
#endif
if( strlen(formula) == 0 )
return 0.0;
// Store variable names and pointers.
te_variable vars[] = {{"tilt", &angle}, {"temp", &temp}};
int err;
// Compile the expression with variables.
te_expr *expr = te_compile(formula, vars, 2, &err);
if (expr) {
double g = te_eval(expr);
te_free(expr);
#if LOG_LEVEL==6
Log.verbose(F("CALC: Calculated gravity is %F." CR), g);
#endif
return g;
}
Log.error(F("CALC: Failed to parse expression %d." CR), err);
return 0;
}
//
// Do a standard gravity temperature correction. This is a simple way to adjust for differnt worth temperatures
//
double gravityTemperatureCorrection( double gravity, double temp, double calTemp) {
#if LOG_LEVEL==6
Log.verbose(F("CALC: Adjusting gravity based on temperature, gravity %F, temp %F, calTemp %F." CR), gravity, temp, calTemp);
#endif
double tempF = convertCtoF( temp );
double calTempF = convertCtoF(calTemp);
const char* formula = "gravity*((1.00130346-0.000134722124*temp+0.00000204052596*temp^2-0.00000000232820948*temp^3)/(1.00130346-0.000134722124*cal+0.00000204052596*cal^2-0.00000000232820948*cal^3))";
// Store variable names and pointers.
te_variable vars[] = {{"gravity", &gravity}, {"temp", &tempF}, {"cal", &calTempF}};
int err;
// Compile the expression with variables.
te_expr *expr = te_compile(formula, vars, 3, &err);
if (expr) {
double g = te_eval(expr);
te_free(expr);
#if LOG_LEVEL==6
Log.verbose(F("CALC: Corrected gravity is %F." CR), g);
#endif
return g;
}
Log.error(F("CALC: Failed to parse expression %d, no correction has been made." CR), err);
return gravity;
}
// EOF

36
src/calc.h Normal file
View File

@ -0,0 +1,36 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _CALC_H
#define _CALC_H
// Includes
#include "helper.h"
// Functions
double calculateGravity( double angle, double temp );
double gravityTemperatureCorrection( double gravity, double temp, double calTemp = 20 );
#endif // _CALC_H
// EOF

232
src/config.cpp Normal file
View File

@ -0,0 +1,232 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "config.h"
#include "helper.h"
#include <LittleFS.h>
Config myConfig;
//
// Create the config class with default settings.
//
Config::Config() {
// Assiging default values
sprintf(&id[0], "%6x", (unsigned int) ESP.getChipId() );
sprintf(&mDNS[0], "" WIFI_MDNS "%s", getID() );
setTempFormat('C');
setPushInterval(900); // 15 minutes
setVoltageFactor(1.59); // Conversion factor for battery
setTempSensorAdj(0.0);
setGravityTempAdj(false);
gyroCalibration = { 0, 0, 0, 0, 0 ,0 };
saveNeeded = false;
}
//
// Populate the json document with all configuration parameters (used in both web and saving to file)
//
void Config::createJson(DynamicJsonDocument& doc) {
doc[ CFG_PARAM_MDNS ] = getMDNS();
doc[ CFG_PARAM_ID ] = getID();
doc[ CFG_PARAM_OTA ] = getOtaURL();
doc[ CFG_PARAM_TEMPFORMAT ] = String( getTempFormat() );
doc[ CFG_PARAM_PUSH_BREWFATHER ] = getBrewfatherPushTarget();
doc[ CFG_PARAM_PUSH_HTTP ] = getHttpPushTarget();
doc[ CFG_PARAM_PUSH_INTERVAL ] = getPushInterval();
doc[ CFG_PARAM_VOLTAGEFACTOR ] = getVoltageFactor();
doc[ CFG_PARAM_GRAVITY_FORMULA ] = getGravityFormula();
doc[ CFG_PARAM_TEMP_ADJ ] = getTempSensorAdj();
doc[ CFG_PARAM_GRAVITY_TEMP_ADJ ] = isGravityTempAdj();
JsonObject cal = doc.createNestedObject( CFG_PARAM_GYRO_CALIBRATION );
cal["ax"] = gyroCalibration.ax;
cal["ay"] = gyroCalibration.ay;
cal["az"] = gyroCalibration.az;
cal["gx"] = gyroCalibration.gx;
cal["gy"] = gyroCalibration.gy;
cal["gz"] = gyroCalibration.gz;
}
//
// Save json document to file
//
bool Config::saveFile() {
if( !saveNeeded ) {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Skipping save, not needed." CR));
#endif
return true;
}
#if LOG_LEVEL==6
Log.verbose(F("CFG : Saving configuration to file." CR));
#endif
File configFile = LittleFS.open(CFG_FILENAME, "w");
if (!configFile) {
Log.error(F("CFG : Failed to open file " CFG_FILENAME " for save." CR));
return false;
}
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
createJson( doc );
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
serializeJson(doc, configFile);
configFile.flush();
configFile.close();
saveNeeded = false;
myConfig.debug();
Log.notice(F("CFG : Configuration saved to " CFG_FILENAME "." CR));
return true;
}
//
// Load config file from disk
//
bool Config::loadFile() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Loading configuration from file." CR));
#endif
if (!LittleFS.exists(CFG_FILENAME)) {
Log.error(F("CFG : Configuration file does not exist " CFG_FILENAME "." CR));
return false;
}
File configFile = LittleFS.open(CFG_FILENAME, "r");
if (!configFile) {
Log.error(F("CFG : Failed to open " CFG_FILENAME "." CR));
return false;
}
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
DeserializationError err = deserializeJson(doc, configFile);
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
configFile.close();
if( err ) {
Log.error(F("CFG : Failed to parse " CFG_FILENAME " file, Err: %s, %d." CR), err.c_str(), doc.capacity());
return false;
}
#if LOG_LEVEL==6
Log.verbose(F("CFG : Parsed configuration file." CR));
#endif
if( !doc[ CFG_PARAM_OTA ].isNull() )
setOtaURL( doc[ CFG_PARAM_OTA ] );
if( !doc[ CFG_PARAM_MDNS ].isNull() )
setMDNS( doc[ CFG_PARAM_MDNS ] );
if( !doc[ CFG_PARAM_TEMPFORMAT ].isNull() ) {
String s = doc[ CFG_PARAM_TEMPFORMAT ];
setTempFormat( s.charAt(0) );
}
if( !doc[ CFG_PARAM_PUSH_BREWFATHER ].isNull() )
setBrewfatherPushTarget( doc[ CFG_PARAM_PUSH_BREWFATHER ] );
if( !doc[ CFG_PARAM_PUSH_HTTP ].isNull() )
setHttpPushTarget( doc[ CFG_PARAM_PUSH_HTTP ] );
if( !doc[ CFG_PARAM_PUSH_INTERVAL ].isNull() )
setPushInterval( doc[ CFG_PARAM_PUSH_INTERVAL ].as<int>() );
if( !doc[ CFG_PARAM_VOLTAGEFACTOR ].isNull() )
setVoltageFactor( doc[ CFG_PARAM_VOLTAGEFACTOR ].as<float>() );
if( !doc[ CFG_PARAM_GRAVITY_FORMULA ].isNull() )
setGravityFormula( doc[ CFG_PARAM_GRAVITY_FORMULA ] );
if( !doc[ CFG_PARAM_TEMP_ADJ ].isNull() )
setTempSensorAdj( doc[ CFG_PARAM_TEMP_ADJ ].as<float>() );
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["ax"].isNull() )
gyroCalibration.ax = doc[ CFG_PARAM_GYRO_CALIBRATION ]["ax"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["ay"].isNull() )
gyroCalibration.ay = doc[ CFG_PARAM_GYRO_CALIBRATION ]["ay"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["az"].isNull() )
gyroCalibration.az = doc[ CFG_PARAM_GYRO_CALIBRATION ]["az"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["gx"].isNull() )
gyroCalibration.gx = doc[ CFG_PARAM_GYRO_CALIBRATION ]["gx"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["gy"].isNull() )
gyroCalibration.gy = doc[ CFG_PARAM_GYRO_CALIBRATION ]["gy"];
if( !doc[ CFG_PARAM_GYRO_CALIBRATION ]["gz"].isNull() )
gyroCalibration.gz = doc[ CFG_PARAM_GYRO_CALIBRATION ]["gz"];
myConfig.debug();
saveNeeded = false; // Reset save flag
Log.notice(F("CFG : Configuration file " CFG_FILENAME " loaded." CR));
return true;
}
//
// Check if file system can be mounted, if not we format it.
//
void Config::formatFileSystem() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Formating filesystem." CR));
#endif
LittleFS.format();
}
//
// Check if file system can be mounted, if not we format it.
//
void Config::checkFileSystem() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Checking if filesystem is valid." CR));
#endif
if (LittleFS.begin()) {
Log.notice(F("CFG : Filesystem mounted." CR));
} else {
Log.error(F("CFG : Unable to mount file system, formatting..." CR));
LittleFS.format();
}
}
//
// Dump the configuration to the serial port
//
void Config::debug() {
#if LOG_LEVEL==6
Log.verbose(F("CFG : Dumping configration " CFG_FILENAME "." CR));
Log.verbose(F("CFG : ID; '%s'." CR), getID());
Log.verbose(F("CFG : mDNS; '%s'." CR), getMDNS() );
Log.verbose(F("CFG : OTA; '%s'." CR), getOtaURL() );
Log.verbose(F("CFG : Temp; %c." CR), getTempFormat() );
Log.verbose(F("CFG : Temp Adj; %F." CR), getTempSensorAdj() );
Log.verbose(F("CFG : VoltageFactor; %F." CR), getVoltageFactor() );
Log.verbose(F("CFG : Gravity formula; '%s'." CR), getGravityFormula() );
Log.verbose(F("CFG : Push brewfather; '%s'." CR), getBrewfatherPushTarget() );
Log.verbose(F("CFG : Push http; '%s'." CR), getHttpPushTarget() );
Log.verbose(F("CFG : Push interval; %d." CR), getPushInterval() );
// Log.verbose(F("CFG : Accel offset\t%d\t%d\t%d" CR), gyroCalibration.ax, gyroCalibration.ay, gyroCalibration.az );
// Log.verbose(F("CFG : Gyro offset \t%d\t%d\t%d" CR), gyroCalibration.gx, gyroCalibration.gy, gyroCalibration.gz );
#endif
}
// EOF

175
src/config.h Normal file
View File

@ -0,0 +1,175 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _CONFIG_H
#define _CONFIG_H
// Includes
#include "helper.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <stdlib.h>
// defintions
#define CFG_JSON_BUFSIZE 1000
#define CFG_APPNAME "GravityMon " // Name of firmware
#define CFG_FILENAME "/gravitymon.json" // Name of config file
#define WIFI_DEFAULT_SSID "GravityMon" // Name of created SSID
#define WIFI_DEFAULT_PWD "password" // Password for created SSID
#define WIFI_MDNS "gravitymon" // Prefix for MDNS name
#define WIFI_PORTAL_TIMEOUT 120 // Number of seconds until the config portal is closed
// These are used in API + Savefile
#define CFG_PARAM_ID "id"
#define CFG_PARAM_MDNS "mdns" // Device name
#define CFG_PARAM_OTA "ota-url" // Base URL for OTA
#define CFG_PARAM_PUSH_BREWFATHER "brewfather-push" // URL (brewfather format)
#define CFG_PARAM_PUSH_HTTP "http-push" // URL (iSpindle format)
#define CFG_PARAM_PUSH_INTERVAL "push-interval" // Time between push
#define CFG_PARAM_TEMPFORMAT "temp-format" // C or F
#define CFG_PARAM_VOLTAGEFACTOR "voltage-factor" // Factor to calculate the battery voltage
#define CFG_PARAM_GRAVITY_FORMULA "gravity-formula" // Formula for calculating gravity
#define CFG_PARAM_GRAVITY_TEMP_ADJ "gravity-temp-adjustment" // True/False. Adjust gravity for temperature
#define CFG_PARAM_TEMP_ADJ "temp-adjustment-value" // Correction value for temp sensor
#define CFG_PARAM_GYRO_CALIBRATION "gyro-calibration-data" // READ ONLY
// These are used in API's
#define CFG_PARAM_APP_NAME "app-name"
#define CFG_PARAM_APP_VER "app-ver"
#define CFG_PARAM_ANGLE "angle"
#define CFG_PARAM_GRAVITY "gravity"
#define CFG_PARAM_TEMP_C "temp-c"
#define CFG_PARAM_TEMP_F "temp-f"
#define CFG_PARAM_BATTERY "battery"
#define CFG_PARAM_SLEEP_MODE "sleep-mode"
#define CFG_PARAM_RSSI "rssi"
// Used for holding sensordata or sensoroffsets
struct RawGyroData {
int16_t ax; // Raw Acceleration
int16_t ay;
int16_t az;
int16_t gx; // Raw Position
int16_t gy;
int16_t gz;
int16_t temp; // Only for information (temperature of chip)
};
// Main configuration class
class Config {
private:
bool saveNeeded;
// Device configuration
char id[10];
char mDNS[30];
char otaURL[200];
char tempFormat; // C, F
float voltageFactor;
float tempSensorAdj; // This value will be added to the read sensor value
// Push target settings
char brewfatherPushTarget[200];
char httpPushTarget[200];
int pushInterval;
// Gravity and temperature calculations
char gravityFormula[200];
bool gravityTempAdj; // true, false
char gravityFormat; // G, P
// Gyro calibration data
RawGyroData gyroCalibration; // Holds the gyro calibration constants (6 * int16_t)
void debug();
void formatFileSystem();
public:
Config();
const char* getID() { return &id[0]; };
const char* getMDNS() { return &mDNS[0]; }
void setMDNS( const char* s ) { strncpy( &mDNS[0], s, sizeof(mDNS)-1); saveNeeded = true; }
const char* getOtaURL() { return &otaURL[0]; }
void setOtaURL( const char* s ) { strncpy( &otaURL[0], s, sizeof(otaURL)-1); saveNeeded = true; }
bool isOtaActive() { return strlen(&otaURL[0])>0?true:false; }
const char* getBrewfatherPushTarget() { return &brewfatherPushTarget[0]; }
void setBrewfatherPushTarget( const char* s ) { strncpy(&brewfatherPushTarget[0], s, sizeof(brewfatherPushTarget)-1); saveNeeded = true; }
bool isBrewfatherActive() { return strlen(&brewfatherPushTarget[0])>0?true:false; }
const char* getHttpPushTarget() { return &httpPushTarget[0]; }
void setHttpPushTarget( const char* s ) { strncpy(&httpPushTarget[0], s, sizeof(httpPushTarget)-1); saveNeeded = true; }
bool isHttpActive() { return strlen(&httpPushTarget[0])>0?true:false; }
int getPushInterval() { return pushInterval; }
void setPushInterval( int v ) { pushInterval = v; saveNeeded = true; }
void setPushInterval( const char* s ) { pushInterval = atoi(s); saveNeeded = true; }
char getTempFormat() { return tempFormat; }
void setTempFormat( char c ) { tempFormat = c; saveNeeded = true; }
bool isTempC() { return tempFormat=='C'?false:true; };
bool isTempF() { return tempFormat=='F'?false:true; };
float getVoltageFactor() { return voltageFactor; }
void setVoltageFactor( float f ) { voltageFactor = f; saveNeeded = true; }
void setVoltageFactor( const char* s ) { voltageFactor = atof(s); saveNeeded = true; }
float getTempSensorAdj() { return tempSensorAdj; }
void setTempSensorAdj( float f ) { tempSensorAdj = f; saveNeeded = true; }
void setTempSensorAdj( const char* s ) { tempSensorAdj = atof(s); saveNeeded = true; }
const char* getGravityFormula() { return &gravityFormula[0]; }
void setGravityFormula( const char* s ) { strncpy(&gravityFormula[0], s, sizeof(gravityFormula)-1); saveNeeded = true; }
bool isGravityTempAdj() { return gravityTempAdj; }
void setGravityTempAdj( bool b ) { gravityTempAdj = b; saveNeeded = true; }
char getGravityFormat() { return gravityFormat; }
void setGravityFormat( char c ) { gravityFormat = c; saveNeeded = true; }
bool isGravitySG() { return gravityFormat=='G'?false:true; };
bool isGravityPlato() { return gravityFormat=='P'?false:true; };
const RawGyroData& getGyroCalibration() { return gyroCalibration; }
void setGyroCalibration( const RawGyroData &r ) { gyroCalibration = r; saveNeeded = true; }
// IO functions
void createJson(DynamicJsonDocument& doc);
bool saveFile();
bool loadFile();
void checkFileSystem();
bool isSaveNeeded() { return saveNeeded; };
void setSaveNeeded() { saveNeeded = true; };
};
// Global instance created
extern Config myConfig;
#endif // _CONFIG_H
// EOF

327
src/gyro.cpp Normal file
View File

@ -0,0 +1,327 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "gyro.h"
#include "helper.h"
GyroSensor myGyro;
#define SENSOR_MOVING_THREASHOLD 500
#define SENSOR_READ_COUNT 50
#define SENSOR_READ_DELAY 3150 // us, empirical, to hold sampling to 200 Hz
//#define GYRO_SHOW_MINMAX // Will calculate the min/max values when doing calibration
//#define GYRO_CALIBRATE_STARTUP // Will calibrate sensor at startup
//
// Initialize the sensor chip.
//
bool GyroSensor::setup() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Setting up hardware." CR));
#endif
Wire.begin(D3, D4);
Wire.setClock(400000); // 400kHz I2C clock. Comment this line if having compilation difficulties
accelgyro.initialize();
if( !accelgyro.testConnection() ) {
Log.error(F("GYRO: Failed to connect to MPU6050 (gyro)." CR));
sensorConnected = false;
} else {
Log.notice(F("GYRO: Connected to MPU6050 (gyro)." CR));
sensorConnected = true;
// Configure ethe sensor
accelgyro.setTempSensorEnabled(true);
accelgyro.setFullScaleAccelRange(MPU6050_ACCEL_FS_2);
accelgyro.setFullScaleGyroRange(MPU6050_GYRO_FS_250);
accelgyro.setDLPFMode(MPU6050_DLPF_BW_5);
accelgyro.setRate(17);
// For now we run the calibration at start.
#if defined ( GYRO_CALIBRATE_STARTUP )
calibrateSensor();
#endif
// Once we have calibration values stored we just apply them from the config.
calibrationOffset = myConfig.getGyroCalibration();
applyCalibration();
}
return sensorConnected;
}
//
// Set sensor in sleep mode to conserve battery
//
void GyroSensor::enterSleep() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Setting up hardware." CR));
#endif
accelgyro.setSleepEnabled( true );
}
//
// Do a number of reads to get a more stable value.
//
void GyroSensor::readSensor(RawGyroData &raw, const int noIterations, const int delayTime) {
RawGyroDataL average = { 0, 0, 0, 0, 0, 0 };
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Reading sensor with %d iterations %d us delay." CR), noIterations, delayTime );
#endif
// Set some initial values
#if defined( GYRO_SHOW_MINMAX )
RawGyroData min, max;
//accelgyro.getRotation( &min.gx, &min.gy, &min.gz );
accelgyro.getAcceleration( &min.ax, &min.ay, &min.az );
min.temp = accelgyro.getTemperature();
max = min;
#endif
for(int cnt = 0; cnt < noIterations ; cnt ++) {
accelgyro.getRotation( &raw.gx, &raw.gy, &raw.gz );
accelgyro.getAcceleration( &raw.ax, &raw.ay, &raw.az );
raw.temp = accelgyro.getTemperature();
average.ax += raw.ax;
average.ay += raw.ay;
average.az += raw.az;
average.gx += raw.gx;
average.gy += raw.gy;
average.gz += raw.gz;
average.temp += raw.temp;
// Log what the minium value is
#if defined( GYRO_SHOW_MINMAX )
if( raw.ax < min.ax ) min.ax = raw.ax;
if( raw.ay < min.ay ) min.ay = raw.ay;
if( raw.az < min.az ) min.az = raw.az;
if( raw.gx < min.gx ) min.gx = raw.gx;
if( raw.gy < min.gy ) min.gy = raw.gy;
if( raw.gz < min.gz ) min.gz = raw.gz;
if( raw.temp < min.temp ) min.temp = raw.temp;
// Log what the maximum value is
if( raw.ax > max.ax ) max.ax = raw.ax;
if( raw.ay > max.ay ) max.ay = raw.ay;
if( raw.az > max.az ) max.az = raw.az;
if( raw.gx > max.gx ) max.gx = raw.gx;
if( raw.gy > max.gy ) max.gy = raw.gy;
if( raw.gz > max.gz ) max.gz = raw.gz;
if( raw.temp > max.temp ) max.temp = raw.temp;
#endif
delayMicroseconds( delayTime );
}
raw.ax = average.ax/noIterations;
raw.ay = average.ay/noIterations;
raw.az = average.az/noIterations;
raw.gx = average.gx/noIterations;
raw.gy = average.gy/noIterations;
raw.gz = average.gz/noIterations;
raw.temp = average.temp/noIterations;
#if LOG_LEVEL==6
#if defined( GYRO_SHOW_MINMAX )
Log.verbose(F("GYRO: Min \t%d\t%d\t%d\t%d\t%d\t%d\t%d." CR), min.ax, min.ay, min.az, min.gx, min.gy, min.gz, min.temp );
Log.verbose(F("GYRO: Max \t%d\t%d\t%d\t%d\t%d\t%d\t%d." CR), max.ax, max.ay, max.az, max.gx, max.gy, max.gz, max.temp );
#endif
Log.verbose(F("GYRO: Average\t%d\t%d\t%d\t%d\t%d\t%d\t%d." CR), raw.ax, raw.ay, raw.az, raw.gx, raw.gy, raw.gz, raw.temp );
//Log.verbose(F("GYRO: Result \t%d\t%d\t%d\t%d\t%d\t%d." CR), average.ax/noIterations, average.ay/noIterations, average.az/noIterations,
// average.gx/noIterations, average.gy/noIterations, average.gz/noIterations );
#endif
}
//
// Calcuate the angles (tilt)
//
double GyroSensor::calculateAngle(RawGyroData &raw) {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Calculating the angle." CR) );
#endif
// Source: https://www.nxp.com/docs/en/application-note/AN3461.pdf
double v = (acos( raw.ay / sqrt( raw.ax*raw.ax + raw.ay*raw.ay + raw.az*raw.az ) ) *180.0 / PI);
//Log.notice(F("GYRO: angle = %F." CR), v );
//double v = (acos( raw.az / sqrt( raw.ax*raw.ax + raw.ay*raw.ay + raw.az*raw.az ) ) *180.0 / PI);
//Log.notice(F("GYRO: angle = %F." CR), v );
#if LOG_LEVEL==6
Log.verbose(F("GYRO: angle = %F." CR), v );
#endif
return v;
}
//
// Check if the values are high that indicate that the sensor is moving.
//
bool GyroSensor::isSensorMoving(RawGyroData &raw) {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Checking for sensor movement." CR) );
#endif
int x = abs(raw.gx), y = abs(raw.gy), z = abs(raw.gz);
if( x>SENSOR_MOVING_THREASHOLD || y>SENSOR_MOVING_THREASHOLD || z>SENSOR_MOVING_THREASHOLD ) {
Log.notice(F("GYRO: Movement detected (%d)\t%d\t%d\t%d." CR), SENSOR_MOVING_THREASHOLD, x, y, z);
return true;
}
return false;
}
//
// Read the tilt angle from the gyro.
//
bool GyroSensor::read() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Getting new gyro position." CR) );
#endif
RawGyroData raw;
readSensor( raw, SENSOR_READ_COUNT, SENSOR_READ_DELAY );
// If the sensor is unstable we return false to signal we dont have valid value
if( isSensorMoving(raw) ) {
Log.notice(F("GYRO: Sensor is moving." CR) );
validValue = false;
} else {
validValue = true;
angle = calculateAngle( raw );
//Log.notice(F("GYRO: Calculated angle %F" CR), angle );
}
return validValue;
}
//
// Dump the stored calibration values.
//
void GyroSensor::dumpCalibration() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Accel offset\t%d\t%d\t%d" CR), calibrationOffset.ax, calibrationOffset.ay, calibrationOffset.az );
Log.verbose(F("GYRO: Gyro offset \t%d\t%d\t%d" CR), calibrationOffset.gx, calibrationOffset.gy, calibrationOffset.gz );
#endif
}
//
// Update the sensor with out calculated offsets.
//
void GyroSensor::applyCalibration() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Applying calibration offsets to sensor." CR) );
#endif
if( ( calibrationOffset.ax + calibrationOffset.ay + calibrationOffset.az + calibrationOffset.gx + calibrationOffset.gy + calibrationOffset.gz ) == 0 ) {
Log.error(F("GYRO: No valid calibraion values exist, aborting." CR) );
return;
}
accelgyro.setXAccelOffset( calibrationOffset.ax );
accelgyro.setYAccelOffset( calibrationOffset.ay );
accelgyro.setZAccelOffset( calibrationOffset.az );
accelgyro.setXGyroOffset( calibrationOffset.gx );
accelgyro.setYGyroOffset( calibrationOffset.gy );
accelgyro.setZGyroOffset( calibrationOffset.gz );
}
//
// Calculate the offsets for calibration.
//
void GyroSensor::calibrateSensor() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Calibrating sensor" CR) );
#endif
//accelgyro.PrintActiveOffsets();
//Serial.print( CR );
accelgyro.setDLPFMode(MPU6050_DLPF_BW_5);
accelgyro.CalibrateAccel(6); // 6 = 600 readings
accelgyro.CalibrateGyro(6);
accelgyro.PrintActiveOffsets();
Serial.print( CR );
calibrationOffset.ax = accelgyro.getXAccelOffset();
calibrationOffset.ay = accelgyro.getYAccelOffset();
calibrationOffset.az = accelgyro.getZAccelOffset();
calibrationOffset.gx = accelgyro.getXGyroOffset();
calibrationOffset.gy = accelgyro.getYGyroOffset();
calibrationOffset.gz = accelgyro.getZGyroOffset();
// Save the calibrated values
myConfig.setGyroCalibration( calibrationOffset );
myConfig.saveFile();
}
//
// Calibrate the device.
//
void GyroSensor::debug() {
#if LOG_LEVEL==6
Log.verbose(F("GYRO: Debug - Clock src %d." CR), accelgyro.getClockSource() );
Log.verbose(F("GYRO: Debug - Device ID %d." CR), accelgyro.getDeviceID() );
Log.verbose(F("GYRO: Debug - DHPF Mode %d." CR), accelgyro.getDHPFMode() );
Log.verbose(F("GYRO: Debug - DMP on %s." CR), accelgyro.getDMPEnabled()?"on":"off" );
Log.verbose(F("GYRO: Debug - Acc range %d." CR), accelgyro.getFullScaleAccelRange() );
Log.verbose(F("GYRO: Debug - Gyr range %d." CR), accelgyro.getFullScaleGyroRange() );
Log.verbose(F("GYRO: Debug - Int %s." CR), accelgyro.getIntEnabled()?"on":"off" );
Log.verbose(F("GYRO: Debug - Clock %d." CR), accelgyro.getMasterClockSpeed() );
Log.verbose(F("GYRO: Debug - Rate %d." CR), accelgyro.getRate() );
Log.verbose(F("GYRO: Debug - Gyro range %d." CR), accelgyro.getFullScaleGyroRange() );
// Log.verbose(F("GYRO: Debug - I2C bypass %s." CR), accelgyro.getI2CBypassEnabled()?"on":"off" );
// Log.verbose(F("GYRO: Debug - I2C master %s." CR), accelgyro.getI2CMasterModeEnabled()?"on":"off" );
Log.verbose(F("GYRO: Debug - Acc FactX %d." CR), accelgyro.getAccelXSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Acc FactY %d." CR), accelgyro.getAccelYSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Acc FactZ %d." CR), accelgyro.getAccelZSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Gyr FactX %d." CR), accelgyro.getGyroXSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Gyr FactY %d." CR), accelgyro.getGyroYSelfTestFactoryTrim() );
Log.verbose(F("GYRO: Debug - Gyr FactZ %d." CR), accelgyro.getGyroZSelfTestFactoryTrim() );
switch( accelgyro.getFullScaleAccelRange() ) {
case 0:
Log.verbose(F("GYRO: Debug - Accel range +/- 2g." CR));
break;
case 1:
Log.verbose(F("GYRO: Debug - Accel range +/- 4g." CR));
break;
case 2:
Log.verbose(F("GYRO: Debug - Accel range +/- 8g." CR));
break;
case 3:
Log.verbose(F("GYRO: Debug - Accel range +/- 16g." CR));
break;
}
Log.verbose(F("GYRO: Debug - Acc OffX %d\t%d." CR), accelgyro.getXAccelOffset(), calibrationOffset.az );
Log.verbose(F("GYRO: Debug - Acc OffY %d\t%d." CR), accelgyro.getYAccelOffset(), calibrationOffset.ay );
Log.verbose(F("GYRO: Debug - Acc OffZ %d\t%d." CR), accelgyro.getZAccelOffset(), calibrationOffset.az );
Log.verbose(F("GYRO: Debug - Gyr OffX %d\t%d." CR), accelgyro.getXGyroOffset(), calibrationOffset.gx );
Log.verbose(F("GYRO: Debug - Gyr OffY %d\t%d." CR), accelgyro.getYGyroOffset(), calibrationOffset.gy );
Log.verbose(F("GYRO: Debug - Gyr OffZ %d\t%d." CR), accelgyro.getZGyroOffset(), calibrationOffset.gz );
#endif
}
// EOF

79
src/gyro.h Normal file
View File

@ -0,0 +1,79 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _GYRO_H
#define _GYRO_H
#define I2CDEV_IMPLEMENTATION I2CDEV_ARDUINO_WIRE
//#define I2CDEV_IMPLEMENTATION I2CDEV_BUILTIN_SBWIRE
// Includes
#include <arduino.h>
#include "MPU6050.h"
#include "config.h"
// Classes
struct RawGyroDataL { // Used for average multiple readings
long ax; // Raw Acceleration
long ay;
long az;
long gx; // Raw Position
long gy;
long gz;
long temp; // Only for information (temperature of chip)
};
class GyroSensor {
private:
MPU6050 accelgyro;
bool sensorConnected = false;
bool validValue = false;
double angle = 0;
RawGyroData calibrationOffset;
void debug();
void applyCalibration();
void dumpCalibration();
void readSensor(RawGyroData &raw, const int noIterations = 100, const int delayTime = 1);
bool isSensorMoving(RawGyroData &raw);
double calculateAngle(RawGyroData &raw);
public:
bool setup();
bool read();
void calibrateSensor();
double getAngle() { return angle; };
bool isConnected() { return sensorConnected; };
bool hasValue() { return validValue; };
void enterSleep();
};
// Global instance created
extern GyroSensor myGyro;
#endif // _GYRO_H
// EOF

110
src/helper.cpp Normal file
View File

@ -0,0 +1,110 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "helper.h"
#include "config.h"
SerialDebug mySerial;
BatteryVoltage myBatteryVoltage;
//
// Enter deep sleep for the defined duration (Argument is seconds)
//
void deepSleep(int t) {
#if LOG_LEVEL==6
Log.verbose(F("HELP: Entering sleep mode for %ds." CR), t );
#endif
uint64_t wake = t * 1000000;
ESP.deepSleep( wake );
}
//
// Print the build options used
//
void printBuildOptions() {
Log.notice( F("Build options: %s LOGLEVEL %d "
#ifdef ACTIVATE_PUSH
"PUSH "
#endif
#ifdef SKIP_SLEEPMODE
"SKIP_SLEEP "
#endif
#ifdef ACTIVATE_OTA
"OTA "
#endif
CR), CFG_APPVER, LOG_LEVEL );
}
//
// Configure serial debug output
//
SerialDebug::SerialDebug(const long serialSpeed) {
// Start serial with auto-detected rate (default to defined BAUD)
Serial.flush();
Serial.begin(serialSpeed);
getLog()->begin(LOG_LEVEL, &Serial, true);
getLog()->setPrefix(printTimestamp);
getLog()->notice(F("SDBG: Serial logging started at %l." CR), serialSpeed);
}
//
// Print the timestamp (ms since start of device)
//
void printTimestamp(Print* _logOutput) {
char c[12];
sprintf(c, "%10lu ", millis());
_logOutput->print(c);
}
//
// Read and calculate the battery voltage
//
void BatteryVoltage::read() {
// The analog pin can only handle 3.3V maximum voltage so we need to reduce the voltage (from max 5V)
float factor = myConfig.getVoltageFactor(); // Default value is 1.63
int v = analogRead( A0 );
batteryLevel = ((3.3/1023)*v)*factor;
#if LOG_LEVEL==6
Log.verbose(F("BATT: Reading voltage level. Factor=%F Value=%d, Voltage=%F." CR), factor, v, batteryLevel );
#endif
}
//
// Convert float to formatted string with n decimals. Buffer should be at least 10 chars.
//
char* convertFloatToString( float f, char *buffer, int dec ) {
dtostrf(f, 6, dec, buffer);
return buffer;
}
//
// Reduce precision to n decimals
//
float reduceFloatPrecision( float f, int dec ) {
char buffer[5];
dtostrf(f, 6, dec, &buffer[0]);
return atof(&buffer[0]);
}
// EOF

66
src/helper.h Normal file
View File

@ -0,0 +1,66 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _HELPER_H
#define _HELPER_H
// Includes
#include <ArduinoLog.h>
// Sleep mode
void deepSleep(int t);
// Show build options
void printBuildOptions();
// Float to String
char* convertFloatToString( float f, char* buf, int dec = 2);
float reduceFloatPrecision( float f, int dec = 2 );
// Logging via serial
void printTimestamp(Print* _logOutput);
void printNewline(Print* _logOutput);
// Classes
class SerialDebug {
public:
SerialDebug(const long serialSpeed = 115200L);
static Logging* getLog() { return &Log; };
};
class BatteryVoltage {
private:
float batteryLevel;
public:
void read();
float getVoltage() { return batteryLevel; };
};
// Global instance created
extern SerialDebug mySerial;
extern BatteryVoltage myBatteryVoltage;
#endif // _HELPER_H
// EOF

214
src/main.cpp Normal file
View File

@ -0,0 +1,214 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "helper.h"
#include "gyro.h"
#include "config.h"
#include "wifi.h"
#include "webserver.h"
#include "calc.h"
#include "tempsensor.h"
#include "pushtarget.h"
#include <LittleFS.h>
// Settings for double reset detector.
#define ESP_MRD_USE_LITTLEFS true
#define ESP_MRD_USE_SPIFFS false
#define ESP_MRD_USE_EEPROM false
#define MRD_TIMES 3
#define MRD_TIMEOUT 10
#define MRD_ADDRESS 0
#define MULTIRESETDETECTOR_DEBUG true
#include <ESP_MultiResetDetector.h>
MultiResetDetector *mrd;
// Define constats for this program
#if LOG_LEVEL==6
const int interval = 1000; // ms, time to wait between changes to output
bool sleepModeAlwaysSkip = true; // Web interface can override normal behaviour
#else
const int interval = 100; // ms, time to wait between changes to output
bool sleepModeAlwaysSkip = false; // Web interface can override normal behaviour
#endif
unsigned long lastMillis = 0;
unsigned long startMillis;
bool sleepModeActive = false;
//
// Check if we should be in sleep mode
//
void checkSleepMode( float angle, float volt ) {
#if defined( SKIP_SLEEPMODE )
sleepModeActive = false;
Log.verbose(F("MAIN: Skipping sleep mode (SKIP_SLEEPMODE is defined)." CR) );
return;
#endif
const RawGyroData &g = myConfig.getGyroCalibration();
// Will not enter sleep mode if: no calibration data
if( g.ax==0 && g.ay==0 && g.az==0 && g.gx==0 && g.gy==0 && g.gz==0 ) {
Log.notice(F("MAIN: Missing calibration data, so forcing webserver to be active." CR) );
sleepModeAlwaysSkip = true;
}
if( sleepModeAlwaysSkip ) {
Log.notice(F("MAIN: Sleep mode disabled from web interface." CR) );
sleepModeActive = false;
return;
}
// Will not enter sleep mode if: charger is connected
sleepModeActive = volt<4.15 ? true : false;
// sleep mode active when flat
//sleepModeActive = ( angle<85 && angle>5 ) ? true : false;
#if LOG_LEVEL==6
Log.verbose(F("MAIN: Deep sleep mode %s (angle=%F volt=%F)." CR), sleepModeActive ? "true":"false", angle, volt );
#endif
}
//
// Setup
//
void setup() {
startMillis = millis();
mrd = new MultiResetDetector(MRD_TIMEOUT, MRD_ADDRESS);
bool dt = mrd->detectMultiReset();
#if LOG_LEVEL==6
Log.verbose(F("Main: Reset reason %s." CR), ESP.getResetInfo().c_str() );
#endif
// Main startup
Log.notice(F("Main: Started setup for %s." CR), String( ESP.getChipId(), HEX).c_str() );
printBuildOptions();
Log.notice(F("Main: Loading configuration." CR));
myConfig.checkFileSystem();
myConfig.loadFile();
// Setup watchdog
ESP.wdtDisable();
ESP.wdtEnable( interval*2 );
myTempSensor.setup();
// Setup Gyro
if( !myGyro.setup() )
Log.error(F("Main: Failed to initialize the gyro." CR));
if( dt )
Log.notice(F("Main: Detected doubletap on reset." CR));
Log.notice(F("Main: Connecting to wifi." CR));
myWifi.connect( dt );
Log.notice(F("Main: WIFI connected." CR));
myGyro.read();
myBatteryVoltage.read();
checkSleepMode( myGyro.getAngle(), myBatteryVoltage.getVoltage() );
if( myWifi.isConnected() ) {
Log.notice(F("Main: Connected to wifi ip=%s." CR), myWifi.getIPAddress().c_str() );
if( !sleepModeActive )
if( myWebServer.setupWebServer() )
Log.notice(F("Main: Webserver is running." CR) );
}
#if defined( ACTIVATE_OTA )
if( !sleepModeActive && myWifi.isConnected() && myWifi.checkFirmwareVersion() ) {
myWifi.updateFirmware();
}
#endif
}
//
// Main loops
//
void loop() {
mrd->loop();
if( sleepModeActive || abs(millis() - lastMillis) > interval ) {
float angle = 90;
float volt = myBatteryVoltage.getVoltage();
#if LOG_LEVEL==6
Log.verbose(F("Main: Entering main loop." CR) );
#endif
// If we dont get any readings we just skip this and try again the next interval.
if( myGyro.hasValue() ) {
angle = myGyro.getAngle();
float temp = myTempSensor.getValueCelcius(); // The code is build around using C for temp.
float gravity = calculateGravity( angle, temp );
#if LOG_LEVEL==6
Log.verbose(F("Main: Sensor values gyro=%F, temp=%F, gravity=%F." CR), angle, temp, gravity );
#endif
if( myConfig.isGravityTempAdj() ) {
gravity = gravityTemperatureCorrection( gravity, temp); // Use default correction temperature of 20C
#if LOG_LEVEL==6
Log.verbose(F("Main: Temp adjusted gravity=%F." CR), gravity );
#endif
}
Log.notice(F("Main: Gyro angle=%F, temp=%F, gravity=%F, batt=%F." CR), angle, temp, gravity, volt );
#if defined( ACTIVATE_PUSH )
unsigned long runTime = millis() - startMillis;
myPushTarget.send( angle, gravity, temp, runTime/1000, sleepModeActive ); // Force the transmission if we are going to sleep
#endif
} else {
Log.error(F("Main: No gyro value." CR) );
}
if( sleepModeActive ) {
unsigned long runTime = millis() - startMillis;
// Enter sleep mode...
Log.notice(F("MAIN: Entering deep sleep, run time %l s." CR), runTime/1000 );
LittleFS.end();
myGyro.enterSleep();
mrd->stop();
delay(100);
deepSleep( myConfig.getPushInterval() );
}
#if LOG_LEVEL==6
Log.verbose(F("Main: Sleep mode not active." CR) );
#endif
// Do these checks if we are running in normal mode (not sleep mode)
checkSleepMode( angle, volt );
myGyro.read();
myBatteryVoltage.read();
lastMillis = millis();
#if LOG_LEVEL==6
Log.verbose(F("Main: Heap %d kb FreeSketch %d kb." CR), ESP.getFreeHeap()/1024, ESP.getFreeSketchSpace()/1024 );
Log.verbose(F("Main: HeapFrag %d %%." CR), ESP.getHeapFragmentation() );
#endif
}
myWebServer.loop();
}
// EOF

170
src/pushtarget.cpp Normal file
View File

@ -0,0 +1,170 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "pushtarget.h"
#include "config.h"
#if defined( ACTIVATE_PUSH )
PushTarget myPushTarget;
//
// Send the pressure value
//
void PushTarget::send(float angle, float gravity, float temp, float runTime, bool force ) {
unsigned long timePassed = abs( millis() - ms );
unsigned long interval = myConfig.getPushInterval()*1000;
if( ( timePassed < interval ) && !force) {
#if LOG_LEVEL==6
Log.verbose(F("PUSH: Timer has not expired %l vs %l." CR), timePassed, interval );
#endif
return;
}
#if LOG_LEVEL==6
Log.verbose(F("PUSH: Sending data." CR) );
#endif
ms = millis();
if( myConfig.isBrewfatherActive() )
sendBrewfather( angle, gravity, temp );
if( myConfig.isHttpActive() )
sendHttp( angle, gravity, temp, runTime );
}
//
// Send data to brewfather
//
void PushTarget::sendBrewfather(float angle, float gravity, float temp ) {
Log.notice(F("PUSH: Sending values to brewfather angle=%F, gravity=%F, temp=%F." CR), angle, gravity, temp );
DynamicJsonDocument doc(300);
//
// {
// "name": "YourDeviceName", // Required field, this will be the ID in Brewfather
// "temp": 20.32,
// "aux_temp": 15.61, // Fridge Temp
// "ext_temp": 6.51, // Room Temp
// "temp_unit": "C", // C, F, K
// "gravity": 1.042,
// "gravity_unit": "G", // G, P
// "pressure": 10,
// "pressure_unit": "PSI", // PSI, BAR, KPA
// "ph": 4.12,
// "bpm": 123, // Bubbles Per Minute
// "comment": "Hello World",
// "beer": "Pale Ale"
// "battery": 4.98
// }
//
doc["name"] = myConfig.getMDNS();
doc["temp"] = reduceFloatPrecision( temp, 1);
//doc["aux_temp"] = 0;
//doc["ext_temp"] = 0;
doc["temp_unit"] = String( myConfig.getTempFormat() );
//doc["pressure"] = ;
//doc["pressure_unit"] = ;
doc["battery"] = reduceFloatPrecision( myBatteryVoltage.getVoltage(), 2 );
doc["gravity"] = reduceFloatPrecision( gravity, 4 );
doc["gravity_unit"] = myConfig.isGravitySG()?"G":"P";
//doc["ph"] = 0;
//doc["bpm"] = 0;
//doc["comment"] = "";
//doc["beer"] = "";
WiFiClient client;
HTTPClient http;
String serverPath = myConfig.getBrewfatherPushTarget();
// Your Domain name with URL path or IP address with path
http.begin( client, serverPath);
String json;
serializeJson(doc, json);
#if LOG_LEVEL==6
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), json.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json") );
int httpResponseCode = http.POST(json);
if (httpResponseCode==200) {
Log.notice(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
} else {
Log.error(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
}
http.end();
}
//
// Send data to http target
//
void PushTarget::sendHttp(float angle, float gravity, float temp, float runTime ) {
Log.notice(F("PUSH: Sending values to http angle=%F, gravity=%F, temp=%F." CR), angle, gravity, temp );
DynamicJsonDocument doc(256);
doc["name"] = myConfig.getMDNS();
doc["temp"] = reduceFloatPrecision( temp, 1 );
doc["temp-unit"] = String( myConfig.getTempFormat() );
doc["gravity"] = reduceFloatPrecision( gravity, 4 );
doc["angle"] = reduceFloatPrecision( angle, 2);
doc["battery"] = reduceFloatPrecision( myBatteryVoltage.getVoltage(), 2 );
doc["rssi"] = WiFi.RSSI();
// Some debug information
doc["run-time"] = reduceFloatPrecision( runTime, 2 );
WiFiClient client;
HTTPClient http;
String serverPath = myConfig.getHttpPushTarget();
// Your Domain name with URL path or IP address with path
http.begin( client, serverPath);
String json;
serializeJson(doc, json);
#if LOG_LEVEL==6
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), json.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json") );
int httpResponseCode = http.POST(json);
if (httpResponseCode==200) {
Log.notice(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
} else {
Log.error(F("PUSH: HTTP Response code %d" CR), httpResponseCode);
}
http.end();
}
#endif // ACTIVATE_PUSH
// EOF

52
src/pushtarget.h Normal file
View File

@ -0,0 +1,52 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _PUSHTARGET_H
#define _PUSHTARGET_H
// Includes
#include "helper.h"
#include <Arduino.h>
#include <ArduinoJson.h>
#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
// Classes
class PushTarget {
private:
unsigned long ms; // Used to check that we do not post to often
void sendBrewfather(float angle, float gravity, float temp );
void sendHttp(float angle, float gravity, float temp, float runTime );
public:
PushTarget() { ms = millis(); }
void send(float angle, float gravity, float temp, float runTime, bool force = false );
};
extern PushTarget myPushTarget;
#endif // _PUSHTARGET_H
// EOF

41
src/resources.cpp Normal file
View File

@ -0,0 +1,41 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include <incbin.h>
#if defined( EMBED_HTML )
/*INCBIN(IndexHtm, "data/index.htm" );
INCBIN(DeviceHtm, "data/device.htm" );
INCBIN(ConfigHtm, "data/config.htm" );
INCBIN(AboutHtm, "data/about.htm" );*/
// Using minify to reduce memory usage. Reducing RAM memory usage with about 7%
INCBIN(IndexHtm, "data/index.min.htm" );
INCBIN(DeviceHtm, "data/device.min.htm" );
INCBIN(ConfigHtm, "data/config.min.htm" );
INCBIN(AboutHtm, "data/about.min.htm" );
#endif
// EOF

108
src/tempsensor.cpp Normal file
View File

@ -0,0 +1,108 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "tempsensor.h"
#include "helper.h"
#include "config.h"
#include <onewire.h>
#include <DallasTemperature.h>
#include <Wire.h>
//
// Conversion between C and F
//
float convertCtoF( float t ) {
return (t * 1.8 ) + 32.0;
}
OneWire myOneWire(D6);
DallasTemperature mySensors(&myOneWire);
TempSensor myTempSensor;
#define TEMPERATURE_PRECISION 9
//
// Setup temp sensors
//
void TempSensor::setup() {
#if defined( SIMULATE_TEMP )
hasSensors = true;
return;
#endif
if( mySensors.getDeviceCount() )
return;
#if LOG_LEVEL==6
Log.verbose(F("TSEN: Looking for temp sensors." CR));
#endif
mySensors.begin();
if( mySensors.getDeviceCount() ) {
Log.notice(F("TSEN: Found %d sensors." CR), mySensors.getDeviceCount());
mySensors.setResolution(TEMPERATURE_PRECISION);
}
float t = myConfig.getTempSensorAdj();
// Set the temp sensor adjustment values
if( myConfig.isTempC() ) {
tempSensorAdjF = t * 1.8; // Convert the adjustment value to C
tempSensorAdjC = t;
} else {
tempSensorAdjF = t;
tempSensorAdjC = t * 0.556; // Convert the adjustent value to F
}
#if LOG_LEVEL==6
Log.verbose(F("TSEN: Adjustment values for temp sensor %F C, %F F." CR), tempSensorAdjC, tempSensorAdjF );
#endif
}
//
// Retrieving value from sensor
//
float TempSensor::getValue() {
float c = 0;
#if defined( SIMULATE_TEMP )
return 21;
#endif
// Read the sensors
mySensors.requestTemperatures();
if( mySensors.getDeviceCount() >= 1) {
c = mySensors.getTempCByIndex(0);
#if LOG_LEVEL==6
Log.verbose(F("TSEN: Reciving temp value for sensor %F C." CR), c);
#endif
hasSensor = true;
}
return c;
}
// EOF

50
src/tempsensor.h Normal file
View File

@ -0,0 +1,50 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _TEMPSENSOR_H
#define _TEMPSENSOR_H
// definitions
float convertCtoF( float t );
// classes
class TempSensor {
private:
bool hasSensor = false;
float tempSensorAdjF = 0;
float tempSensorAdjC = 0;
float getValue();
public:
void setup();
bool isSensorAttached() { return hasSensor; };
float getValueCelcius() { return getValue() + tempSensorAdjC; }
float getValueFarenheight() { return convertCtoF(getValue()) + tempSensorAdjF; };
};
// Global instance created
extern TempSensor myTempSensor;
#endif // _TEMPSENSOR_H
// EOF

382
src/webserver.cpp Normal file
View File

@ -0,0 +1,382 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
//#define DEBUG_ESP_HTTP_SERVER
#include "webserver.h"
#include "config.h"
#include "helper.h"
#include "gyro.h"
#include "calc.h"
#include "tempsensor.h"
#include <ArduinoJson.h>
#include <incbin.h>
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <LittleFS.h>
// Binary resouces
#if defined( EMBED_HTML )
INCBIN_EXTERN(IndexHtm);
INCBIN_EXTERN(DeviceHtm);
INCBIN_EXTERN(ConfigHtm);
INCBIN_EXTERN(AboutHtm);
#endif
WebServer myWebServer;
ESP8266WebServer server(80);
extern bool sleepModeActive;
extern bool sleepModeAlwaysSkip;
//
// Callback from webServer when / has been accessed.
//
void webHandleDevice() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config." CR));
#endif
DynamicJsonDocument doc(100);
doc[ CFG_PARAM_ID ] = myConfig.getID();
doc[ CFG_PARAM_APP_NAME ] = CFG_APPNAME;
doc[ CFG_PARAM_APP_VER ] = CFG_APPVER;
doc[ CFG_PARAM_MDNS ] = myConfig.getMDNS();
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
String out;
serializeJson(doc, out);
server.send(200, "application/json", out.c_str() );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleConfig() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config." CR));
#endif
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
myConfig.createJson( doc );
double angle = myGyro.getAngle();
double temp = myTempSensor.getValueCelcius();
double gravity = calculateGravity( angle, temp );
doc[ CFG_PARAM_ANGLE ] = reduceFloatPrecision( angle);
doc[ CFG_PARAM_GRAVITY ] = reduceFloatPrecision( gravityTemperatureCorrection( gravity, temp ), 4);
doc[ CFG_PARAM_BATTERY ] = reduceFloatPrecision( myBatteryVoltage.getVoltage());
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
String out;
serializeJson(doc, out);
server.send(200, "application/json", out.c_str() );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleCalibrate() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/calibrate." CR));
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
myGyro.calibrateSensor();
server.send(200, "text/plain", "Device calibrated" );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleFactoryReset() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/factory." CR));
#endif
if( !id.compareTo( myConfig.getID() ) ) {
server.send(200, "text/plain", "Doing reset...");
LittleFS.remove(CFG_FILENAME);
LittleFS.end();
delay(500);
ESP.reset();
} else {
server.send(400, "text/plain", "Unknown ID.");
}
}
//
// Callback from webServer when / has been accessed.
//
void webHandleStatus() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/status." CR));
#endif
DynamicJsonDocument doc(256);
double angle = myGyro.getAngle();
double temp = myTempSensor.getValueCelcius();
double gravity = calculateGravity( angle, temp );
doc[ CFG_PARAM_ID ] = myConfig.getID();
doc[ CFG_PARAM_ANGLE ] = reduceFloatPrecision( angle);
doc[ CFG_PARAM_GRAVITY ] = reduceFloatPrecision( gravityTemperatureCorrection( gravity, temp ), 4);
doc[ CFG_PARAM_TEMP_C ] = reduceFloatPrecision( temp, 1);
doc[ CFG_PARAM_TEMP_F ] = reduceFloatPrecision( myTempSensor.getValueFarenheight(), 1);
doc[ CFG_PARAM_BATTERY ] = reduceFloatPrecision( myBatteryVoltage.getVoltage());
doc[ CFG_PARAM_TEMPFORMAT ] = String( myConfig.getTempFormat() );
doc[ CFG_PARAM_SLEEP_MODE ] = sleepModeAlwaysSkip;
doc[ CFG_PARAM_RSSI ] = WiFi.RSSI();
#if LOG_LEVEL==6
serializeJson(doc, Serial);
Serial.print( CR );
#endif
String out;
serializeJson(doc, out);
server.send(200, "application/json", out.c_str() );
}
//
// Callback from webServer when / has been accessed.
//
void webHandleClearWIFI() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/clearwifi." CR));
#endif
if( !id.compareTo( myConfig.getID() ) ) {
server.send(200, "text/plain", "Clearing WIFI credentials and doing reset...");
delay(1000);
WiFi.disconnect(); // Clear credentials
ESP.reset();
} else {
server.send(400, "text/plain", "Unknown ID.");
}
}
//
// Used to force the device to never sleep.
//
void webHandleStatusSleepmode() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/status/sleepmode." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : sleep-mode=%s." CR), server.arg( CFG_PARAM_SLEEP_MODE ).c_str() );
#endif
if( server.arg( CFG_PARAM_SLEEP_MODE ).equalsIgnoreCase( "true" ) )
sleepModeAlwaysSkip = true;
else
sleepModeAlwaysSkip = false;
server.send(200, "text/plain", "Sleep mode updated" );
}
//
// Update device settings.
//
void webHandleConfigDevice() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/device." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : mdns=%s, temp-format=%s." CR), server.arg( CFG_PARAM_MDNS ).c_str(), server.arg( CFG_PARAM_TEMPFORMAT ).c_str() );
#endif
myConfig.setMDNS( server.arg( CFG_PARAM_MDNS ).c_str() );
myConfig.setTempFormat( server.arg( CFG_PARAM_TEMPFORMAT ).charAt(0) );
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseOne", true);
server.send(302, "text/plain", "Device config updated" );
}
//
// Update push settings.
//
void webHandleConfigPush() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/push." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : http=%s, bf=%s interval=%s." CR), server.arg( CFG_PARAM_PUSH_HTTP ).c_str(), server.arg( CFG_PARAM_PUSH_BREWFATHER ).c_str(), server.arg( CFG_PARAM_PUSH_INTERVAL ).c_str() );
#endif
myConfig.setHttpPushTarget( server.arg( CFG_PARAM_PUSH_HTTP ).c_str() );
myConfig.setBrewfatherPushTarget( server.arg( CFG_PARAM_PUSH_BREWFATHER ).c_str() );
myConfig.setPushInterval( server.arg( CFG_PARAM_PUSH_INTERVAL ).c_str() );
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseTwo", true);
server.send(302, "text/plain", "Push config updated" );
}
//
// Update gravity settings.
//
void webHandleConfigGravity() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/gravity." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : formula=%s, temp-corr=%s." CR), server.arg( CFG_PARAM_GRAVITY_FORMULA ).c_str(), server.arg( CFG_PARAM_GRAVITY_TEMP_ADJ ).c_str() );
#endif
myConfig.setGravityFormula( server.arg( CFG_PARAM_GRAVITY_FORMULA ).c_str() );
myConfig.setGravityTempAdj( server.arg( CFG_PARAM_GRAVITY_TEMP_ADJ ).equalsIgnoreCase( "on" ) ? true:false);
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseThree", true);
server.send(302, "text/plain", "Gravity config updated" );
}
//
// Update hardware settings.
//
void webHandleConfigHardware() {
String id = server.arg( CFG_PARAM_ID );
#if LOG_LEVEL==6
Log.verbose(F("WEB : webServer callback for /api/config/hardware." CR) );
#endif
if( !id.equalsIgnoreCase( myConfig.getID() ) ) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(), myConfig.getID());
server.send(400, "text/plain", "Invalid ID.");
return;
}
#if LOG_LEVEL==6
Log.verbose(F("WEB : vf=%s, tempadj=%s, ota=%s." CR), server.arg( CFG_PARAM_VOLTAGEFACTOR ).c_str(), server.arg( CFG_PARAM_TEMP_ADJ ).c_str(), server.arg( CFG_PARAM_OTA ).c_str() );
#endif
myConfig.setVoltageFactor( server.arg( CFG_PARAM_VOLTAGEFACTOR ).toFloat() );
myConfig.setTempSensorAdj( server.arg( CFG_PARAM_TEMP_ADJ ).toFloat() );
myConfig.setOtaURL( server.arg( CFG_PARAM_OTA ).c_str() );
myConfig.saveFile();
server.sendHeader("Location", "/config.htm#collapseFour", true);
server.send(302, "text/plain", "Hardware config updated" );
}
//
// Setup the Web Server callbacks and start it
//
bool WebServer::setupWebServer() {
#if LOG_LEVEL==6
Log.verbose(F("WEB : Setting up web server." CR));
#endif
Log.notice(F("WEB : Web server setup started." CR));
MDNS.begin( myConfig.getMDNS() );
MDNS.addService("http", "tcp", 80);
// Static content
#if defined( EMBED_HTML )
server.on("/",[]() {
server.send_P(200, "text/html", (const char*) gIndexHtmData, gIndexHtmSize );
} );
server.on("/index.htm",[]() {
server.send_P(200, "text/html", (const char*) gIndexHtmData, gIndexHtmSize );
} );
server.on("/device.htm",[]() {
server.send_P(200, "text/html", (const char*) gDeviceHtmData, gDeviceHtmSize );
} );
server.on("/config.htm",[]() {
server.send_P(200, "text/html", (const char*) gConfigHtmData, gConfigHtmSize );
} );
server.on("/about.htm",[]() {
server.send_P(200, "text/html", (const char*) gAboutHtmData, gAboutHtmSize );
} );
#else
// Show files in the filessytem at startup
FSInfo fs;
LittleFS.info(fs);
Log.notice( F("File system: Total=%d, Used=%d." CR), fs.totalBytes, fs.usedBytes );
Dir dir = LittleFS.openDir("/");
while( dir.next() ) {
Log.notice( F("File: %s, %d bytes" CR), dir.fileName().c_str(), dir.fileSize() );
}
server.serveStatic("/", LittleFS, "/index.htm" );
server.serveStatic("/index.htm", LittleFS, "/index.htm" );
server.serveStatic("/device.htm", LittleFS, "/device.htm" );
server.serveStatic("/config.htm", LittleFS, "/config.htm" );
server.serveStatic("/about.htm", LittleFS, "/about.htm" );
#endif
// Dynamic content
server.on("/api/config", webHandleConfig); // Get config.json
server.on("/api/device", webHandleDevice); // Get device.json
server.on("/api/calibrate", webHandleCalibrate); // Run calibration routine (param id)
server.on("/api/factory", webHandleFactoryReset); // Reset the device
server.on("/api/status", webHandleStatus); // Get the status.json
server.on("/api/clearwifi", webHandleClearWIFI); // Clear wifi settings
server.on("/api/status/sleepmode", webHandleStatusSleepmode);
server.on("/api/config/device", webHandleConfigDevice);
server.on("/api/config/push", webHandleConfigPush);
server.on("/api/config/gravity", webHandleConfigGravity);
server.on("/api/config/hardware", webHandleConfigHardware);
server.onNotFound( []() {
Log.error(F("WEB : URL not found %s received." CR), server.uri().c_str());
server.send(404, "text/plain", F("URL not found") );
} );
server.begin();
Log.notice(F("WEB : Web server started." CR));
return true;
}
//
// called from main loop
//
void WebServer::loop() {
// Dont put serial debug output in this call
server.handleClient();
MDNS.update();
}
// EOF

41
src/webserver.h Normal file
View File

@ -0,0 +1,41 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _WEBSERVER_H
#define _WEBSERVER_H
// Include
// classes
class WebServer {
public:
bool setupWebServer();
void loop();
};
// Global instance created
extern WebServer myWebServer;
#endif // _WEBSERVER_H
// EOF

203
src/wifi.cpp Normal file
View File

@ -0,0 +1,203 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#include "wifi.h"
#include "config.h"
#include "helper.h"
#include "gyro.h"
#include "calc.h"
#include "tempsensor.h"
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#include <ArduinoJson.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>
#include <LittleFS.h>
#include <incbin.h>
Wifi myWifi;
WiFiManager myWifiManager;
// TODO: ADD MDNS setting to WIFI portal.....
// TODO: Download html files during OTA update to reduce image size.
//
// Connect to last known access point or create one if connection is not working.
//
bool Wifi::connect( bool showPortal ) {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Connecting to WIFI via connection manager (portal=%s)." CR), showPortal?"true":"false");
myWifiManager.setDebugOutput(true);
#else
myWifiManager.setDebugOutput(false);
#endif
unsigned long startMillis = millis();
myWifiManager.setConfigPortalTimeout( WIFI_PORTAL_TIMEOUT );
if( showPortal ) {
Log.notice(F("WIFI: Starting wifi portal." CR));
connectedFlag = myWifiManager.startConfigPortal( WIFI_DEFAULT_SSID, WIFI_DEFAULT_PWD );
}
else
connectedFlag = myWifiManager.autoConnect( WIFI_DEFAULT_SSID, WIFI_DEFAULT_PWD );
Log.notice( F("WIFI: Connect time %d s" CR), abs(millis() - startMillis)/1000);
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Connect returned %s." CR), connectedFlag?"True":"False" );
#endif
return connectedFlag;
}
//
// This will erase the stored credentials and forcing the WIFI manager to AP mode.
//
bool Wifi::disconnect() {
Log.notice(F("WIFI: Erasing stored WIFI credentials." CR));
// Erase WIFI credentials
return WiFi.disconnect();
}
#if defined( ACTIVATE_OTA )
//
//
//
bool Wifi::updateFirmware() {
if( !newFirmware ) {
Log.notice(F("WIFI: No newer version exist, skipping update." CR));
return false;
}
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Updating firmware." CR));
#endif
WiFiClient client;
String serverPath = myConfig.getOtaURL();
serverPath += "firmware.bin";
HTTPUpdateResult ret = ESPhttpUpdate.update(client, serverPath);
switch(ret) {
case HTTP_UPDATE_FAILED:
Log.error(F("WIFI: Updating failed %d, %s." CR), ESPhttpUpdate.getLastError(), ESPhttpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
break;
case HTTP_UPDATE_OK:
Log.notice("WIFI: Updated succesfull, rebooting." );
ESP.reset();
break;
}
return false;
}
//
// Check what firmware version is available over OTA
//
bool Wifi::checkFirmwareVersion() {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Checking if new version exist." CR));
#endif
WiFiClient client;
HTTPClient http;
String serverPath = myConfig.getOtaURL();
serverPath += "version.json";
// Your Domain name with URL path or IP address with path
http.begin( client, serverPath);
// Send HTTP GET request
int httpResponseCode = http.GET();
if (httpResponseCode==200) {
Log.notice(F("WIFI: HTTP Response code %d" CR), httpResponseCode);
String payload = http.getString();
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Payload %s." CR), payload.c_str());
#endif
DynamicJsonDocument ver(256);
DeserializationError err = deserializeJson(ver, payload);
if( err ) {
Log.error(F("WIFI: Failed to parse json" CR));
} else {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Project %s version %s." CR), ver["project"].as<char*>(), ver["version"].as<char*>());
#endif
int newVer[3];
int curVer[3];
if( parseFirmwareVersionString( newVer, (const char*) ver["version"] ) ) {
if( parseFirmwareVersionString( curVer, CFG_APPVER) ) {
#if LOG_LEVEL==6
Log.verbose(F("OTA : Checking New=%d.%d.%d Cur=%d.%d.%d" CR), newVer[0], newVer[1], newVer[2], curVer[0], curVer[1], curVer[2] );
#endif
// Compare major version
if( newVer[0] > curVer[0] )
newFirmware = true;
// Compare minor version
if( newVer[0] == curVer[0] && newVer[1] > curVer[1] )
newFirmware = true;
// Compare patch version
if( newVer[0] == curVer[0] && newVer[1] == curVer[1] && newVer[2] > curVer[2] )
newFirmware = true;
}
}
}
} else {
Log.error(F("WIFI: HTTP Response code %d" CR), httpResponseCode);
}
http.end();
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Found new version %s." CR), newFirmware?"true":"false");
#endif
return newFirmware;
}
//
// Parse a version string in the format M.m.p (eg. 1.2.10)
//
bool Wifi::parseFirmwareVersionString( int (&num)[3], const char *version ) {
#if LOG_LEVEL==6
Log.verbose(F("WIFI: Parsing version number %s." CR), version);
#endif
char temp[80];
char *s;
char *p = &temp[0];
int i = 0;
strcpy( &temp[0], version );
// TODO: Do some error checking on the string, lenght etc.
while ((s = strtok_r(p, ".", &p)) != NULL) {
num[i++] = atoi( s );
}
return true;
}
#endif // ACTIVATE_OTA
// EOF

57
src/wifi.h Normal file
View File

@ -0,0 +1,57 @@
/*
MIT License
Copyright (c) 2021 Magnus
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 above 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.
*/
#ifndef _WIFI_H
#define _WIFI_H
// Include
#include <WiFiManager.h>
// classes
class Wifi {
private:
// WIFI
bool connectedFlag = false;
// OTA
bool newFirmware = false;
bool parseFirmwareVersionString( int (&num)[3], const char *version );
public:
// WIFI
bool connect( bool showPortal = false );
bool disconnect();
bool isConnected() { return connectedFlag; };
String getIPAddress() { return WiFi.localIP().toString(); };
// OTA
bool updateFirmware();
bool checkFirmwareVersion();
};
// Global instance created
extern Wifi myWifi;
#endif // _WIFI_H
// EOF

1
test/config.json Normal file
View File

@ -0,0 +1 @@
{"mdns":"gravitymon2","id":"ee1bfc","ota-url":"","temp-format":"C","brewfather-push":"","http-push":"http://192.168.1.16:9090/api/v1/V7s7vRXLqnsW3HdxxRuD/telemetry","push-interval":30,"voltage-factor":1.59,"gravity-formula":"0.0*tilt^3+0.0*tilt^2+0.0017978*tilt+0.9436","temp-adjustment-value":0,"gravity-temp-adjustment":false,"gyro-calibration-data":{"ax":-4814,"ay":1143,"az":2270,"gx":47,"gy":-25,"gz":47},"angle":90.38,"gravity":1.1106,"battery":4.24}

1
test/device.json Normal file
View File

@ -0,0 +1 @@
{"app-name":"GravityMon ","app-ver":"0.0.0","id":"7376ef","mdns":"gravmon"}

12
test/status.json Normal file
View File

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