219 Commits

Author SHA1 Message Date
e6508b639d GitHub Action Build 2022-04-29 14:46:43 +00:00
1fb8541be6 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-29 16:42:32 +02:00
911b08cd73 Tag build as beta1 2022-04-29 16:42:04 +02:00
7d9228f9d8 #67 2022-04-29 16:41:44 +02:00
891794af7c Added error code -100 2022-04-29 08:59:37 +02:00
56cd730ad4 Removed gyro delay from api 2022-04-29 06:56:38 +02:00
5a9169be64 GitHub Action Build 2022-04-29 04:52:45 +00:00
c45a5174ae Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-29 06:48:40 +02:00
396608bd7e #65 #58 2022-04-29 06:48:17 +02:00
31dc2bae5f Pushing perf-data to influx ssl not supported 2022-04-28 21:28:26 +02:00
dfd5cea53d Disable adv config by default 2022-04-28 20:14:16 +02:00
41507f2bd4 GitHub Action Build 2022-04-28 18:05:57 +00:00
304903564d #62 2022-04-28 20:01:45 +02:00
8dcadf4240 GitHub Action Build 2022-04-27 20:25:31 +00:00
b1474d19c4 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-27 22:21:04 +02:00
d8cb4fe622 Merge branch 'bs5' into dev 2022-04-27 22:19:24 +02:00
e30e3b2cb5 Merge branch 'master' into dev 2022-04-27 22:19:14 +02:00
e91c8af1a5 moved maxfrag to own function 2022-04-27 22:15:35 +02:00
8342341cb9 Specified esp32 fwk ver 2022-04-27 22:14:07 +02:00
149124fc34 bump doc ver 2022-04-26 20:52:25 +02:00
1761ed47ba Updated release notes 2022-04-26 20:13:35 +02:00
1dd4c541b0 Merge branch 'master' into bs5 2022-04-26 20:05:57 +02:00
446cb61e1c Updated BF docs to clarify SG 2022-04-26 20:00:26 +02:00
2f01222582 Merge branch 'dev' into bs5 2022-04-26 19:55:16 +02:00
fa3d68c321 Added wifi advanced config 2022-04-26 19:54:48 +02:00
0fe9bb146d Merge branch 'master' into bs5 2022-04-26 19:52:44 +02:00
d4df0dd272 Fixed bug with rssi key in json 2022-04-26 16:42:13 +02:00
c319afbe9f Added tooltips 2022-04-26 16:24:33 +02:00
cf143c0e73 Added advanced settings 2022-04-25 21:41:00 +02:00
902d123a68 Added SSL support for influxdb 2022-04-24 22:35:10 +02:00
b359f3aba8 Bump ver 2022-04-24 22:06:47 +02:00
fb856dde75 Added used repos to about 2022-04-24 22:05:55 +02:00
c0f7cf2823 Formula fixes 2022-04-24 22:05:40 +02:00
d208b11384 Updated release notes 2022-04-24 18:55:50 +02:00
d83ef79165 Added copy function for standard formats 2022-04-24 18:54:16 +02:00
c685c18b57 Increasing calibration points to 10 2022-04-24 18:15:37 +02:00
7a84042781 Added gif animation of ui 2022-04-23 16:28:33 +02:00
14a73b011e Updated install instructions 2022-04-23 16:13:18 +02:00
121eeea392 Saving partitions.bin for esp32 2022-04-21 07:35:32 +02:00
ed8dc68fc7 GitHub Action Build 2022-04-18 15:14:29 +00:00
3d4c04333e Added perf32 as target 2022-04-18 17:09:02 +02:00
5b267210d2 Added save for additional formula points 2022-04-11 09:27:40 +02:00
e9742888f8 Updated html 2022-04-11 08:16:57 +02:00
5e1d2a73fa Started release notes for 1.0 2022-04-10 19:19:57 +02:00
7eddf35b97 Added 3 more points for formula calculation 2022-04-10 19:13:50 +02:00
a648d54a14 Removed brewfather option 2022-04-10 15:47:44 +02:00
e9229efe56 Removed dev code 2022-04-10 15:43:15 +02:00
635d788ba6 Rough update to bootstrap 5 done 2022-04-10 15:33:00 +02:00
2e3820ca73 GitHub Action Build 2022-04-09 12:09:24 +00:00
d52615f8e3 Update screendumps for 0.9 2022-04-09 14:05:18 +02:00
786b8e9b19 Update docs 2022-04-09 10:49:29 +02:00
10fa71d7ca GitHub Action Build 2022-04-09 07:09:20 +00:00
aeda821396 Fixed bad link to firmware upload 2022-04-09 09:05:28 +02:00
3db3968e07 GitHub Action Build 2022-04-09 07:00:17 +00:00
430f01943a Disable firmware upgrade if no file is selected 2022-04-09 08:56:59 +02:00
ca97a586c1 GitHub Action Build 2022-04-08 18:19:45 +00:00
5c80b780a0 Added check for valid SSID 2022-04-08 20:15:25 +02:00
2e42b86444 Updated hw docs for esp32 2022-04-08 20:14:57 +02:00
acb53bf611 Updated voltage factor for esp32 2022-04-08 20:07:23 +02:00
5b99304d7f GitHub Action Build 2022-04-07 18:40:31 +00:00
d94028a7b9 Updated docs 2022-04-07 20:37:35 +02:00
f4ca86e8ff Added link to firmware updata 2022-04-07 20:36:39 +02:00
2c9f5c72f2 GitHub Action Build 2022-04-07 14:56:20 +00:00
f5aae4f2ea Fixing temp adj in F correcly 2022-04-07 16:52:12 +02:00
51daa23327 GitHub Action Build 2022-04-06 18:21:08 +00:00
a5fb4f40b9 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-06 20:17:46 +02:00
17e9b0d51b added upgrade instructions 2022-04-06 20:17:26 +02:00
2877f91ef8 GitHub Action Build 2022-04-06 17:56:29 +00:00
8dd509214b Added git revision on UI 2022-04-06 19:52:56 +02:00
223ab7f81e GitHub Action Build 2022-04-06 13:50:10 +00:00
b6ba01f6e0 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-06 15:46:32 +02:00
75e9d178a3 Added bug to release notes 2022-04-06 15:44:26 +02:00
e8740364cf Adding leading zero to id 2022-04-06 15:43:01 +02:00
53f1373432 GitHub Action Build 2022-04-05 15:51:46 +00:00
40e7a37aa5 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-05 17:47:32 +02:00
24b2446521 Added perf target for esp32 2022-04-05 17:47:06 +02:00
82f48604a2 GitHub Action Build 2022-04-05 07:10:29 +00:00
f5b627616e Updated docs with ispindel web interface 2022-04-05 09:06:38 +02:00
dc4eb4f4a1 GitHub Action Build 2022-04-05 06:38:20 +00:00
52785871b9 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-05 08:35:06 +02:00
9c92bb9214 Update firmware htm 2022-04-05 08:35:00 +02:00
96295f161a GitHub Action Build 2022-04-05 06:32:51 +00:00
50116e8b45 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-04-05 08:29:27 +02:00
29b243f115 Esp32 support for firmware upload 2022-04-04 23:32:41 +02:00
c779a45ea9 pre-commit updates 2022-04-04 23:16:24 +02:00
2d67a44ad0 Added firmware upload page 2022-04-04 23:03:36 +02:00
8d44a5dcea Moved temp sensor code to correct api 2022-04-04 09:47:20 +02:00
3d3293138f Showing mDNS on wifi config page 2022-04-04 08:32:13 +02:00
cf3e683137 Fix rounding error for temp sensor adj 2022-04-04 08:08:14 +02:00
f2b926cce6 Temp sensor adj when in F 2022-04-03 20:28:59 +02:00
1494b2b3b2 iSpindle temp format error 2022-04-03 20:01:12 +02:00
474f987e73 GitHub Action Build 2022-04-01 06:45:57 +00:00
294b7a7fdd Added remove led on esp32 2022-04-01 08:41:31 +02:00
fda5c6ff27 Enable blue LED when running on ESP32 2022-03-31 16:51:27 +02:00
58144a5187 GitHub Action Build 2022-03-29 05:59:46 +00:00
877afbc26a Merge branch 'master' into dev 2022-03-29 07:56:23 +02:00
98a4c3650f GitHub Action Build 2022-03-27 20:37:11 +00:00
dcbedf5899 Fixed bug in upload api 2022-03-27 22:33:29 +02:00
4fff5ad185 Merge pull request #43 from jinjorge/doc_fix
Minor edits to docs
2022-03-27 11:51:41 +02:00
d1f1e926e7 Minor edits to docs 2022-03-26 23:08:32 -07:00
a20baa6b27 GitHub Action Build 2022-03-24 07:55:57 +00:00
42ca66555a Fixed test setup in htm 2022-03-24 08:52:37 +01:00
13d5280d76 GitHub Action Build 2022-03-21 21:08:33 +00:00
ae4d5eb8a2 Showing only last 6 chars of git rev 2022-03-21 22:05:13 +01:00
d6227b6dad GitHub Action Build 2022-03-21 20:46:07 +00:00
07fefe41fb Added git rev to build info 2022-03-21 21:42:44 +01:00
dab4d0ed22 GitHub Action Build 2022-03-20 14:39:41 +00:00
faba3d7619 Build skip html files in version.json 2022-03-20 15:36:13 +01:00
8637b0f72d Fix crashbug in ota 2022-03-20 15:35:37 +01:00
169798e0eb Remove unused file 2022-03-19 21:26:23 +01:00
04b2721c5d GitHub Action Build 2022-03-19 20:24:00 +00:00
14909b241b Bump to v0.9 2022-03-19 21:20:24 +01:00
f8e72a50e9 GitHub Action Build 2022-03-18 18:29:43 +00:00
5d03a33253 Bump version 2022-03-18 19:25:38 +01:00
65983e638a Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-03-18 19:25:04 +01:00
aae29786bb Added estimated runtime on config 2022-03-18 19:24:28 +01:00
b28797a79b GitHub Action Build 2022-03-16 18:08:46 +00:00
d4452e1d59 Added btn on test page 2022-03-16 19:05:11 +01:00
3717561466 GitHub Action Build 2022-03-14 17:59:26 +00:00
4cad6f888f Added http get option to push 2022-03-14 18:55:12 +01:00
6e9d562977 GitHub Action Build 2022-03-13 18:11:32 +00:00
3e6d698a40 Increased json buffer 2022-03-13 19:07:23 +01:00
dc70b250d8 Updated screenshots for 0.9 2022-03-13 19:07:12 +01:00
276b311194 GitHub Action Build 2022-03-13 16:41:38 +00:00
2609bf840c Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-03-13 17:38:26 +01:00
fe3ca247b9 Split push config into 2 sections 2022-03-13 17:37:53 +01:00
37a42aa2a1 GitHub Action Build 2022-03-13 10:00:37 +00:00
68dfacb07c Fixed bug in build script 2022-03-13 10:57:15 +01:00
a60abdbaa1 Merge branch 'master' into dev 2022-03-13 10:39:52 +01:00
ef3cc5b523 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-03-13 10:39:47 +01:00
99a57978fa Bump version 2022-03-13 10:38:08 +01:00
a2aaeb3f84 Format test using real data 2022-03-13 10:37:21 +01:00
2f8a324bfc Fixed issue with converstion when SG was 0 2022-03-13 10:37:01 +01:00
16e91ec4f5 Merged device and index pages in UI 2022-03-13 10:07:03 +01:00
3407567568 GitHub Action Build 2022-03-12 18:52:27 +00:00
83aa1b2202 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-03-12 19:49:26 +01:00
6193f422e8 Added target with esp32 fwk 2.0 2022-03-12 19:49:04 +01:00
a657f698b8 GitHub Action Build 2022-03-12 17:31:44 +00:00
3d497a1acc Dont start wifi on esp32 is not needed 2022-03-12 18:28:02 +01:00
4e190af499 Adjusted voltage factor on esp32 2022-03-12 17:28:41 +01:00
f07f845dfb GitHub Action Build 2022-03-12 12:57:17 +00:00
f1936b6b0d Fix compile error on linux 2022-03-12 13:53:40 +01:00
aa4e3b5e8d Updated docs to 0.9 2022-03-12 10:37:02 +01:00
7cafedd9bf Added CR on errors in serial console. 2022-03-12 08:25:24 +01:00
1e44d9bd01 Fixed back link in format editor 2022-03-12 08:20:17 +01:00
1bc3abc9f0 Integrated test function 2022-03-12 08:20:02 +01:00
8d51c5ad12 Updated q&a 2022-03-11 22:53:58 +01:00
d9c467d54f Added test option for push targets 2022-03-11 22:50:12 +01:00
07e7cbee1c Update partition info 2022-03-11 15:11:22 +01:00
f5fcf42fbe Fix littlefs header name (case sensitive) 2022-03-11 10:36:26 +01:00
036e10cd5d Updated esp32 target, updated github actions 2022-03-11 08:14:46 +01:00
2d1317af0d Migrated to littlefs on esp32 + fix A0 pin 2022-03-10 11:11:39 +01:00
21fba0481c Added BLE on ESP32 target. 2022-03-10 11:10:54 +01:00
6dfe5a80fd Update wifimgr 2022-03-05 19:55:16 +01:00
c681619be8 Update wifimgr 2022-03-05 17:30:49 +01:00
02cb91e918 Update dbl resetdetector 2022-03-05 17:30:32 +01:00
8a597162ae GitHub Action Build 2022-03-05 14:27:39 +00:00
f49f386569 Updated html for calibration 2022-03-05 15:25:48 +01:00
008ad490a7 GitHub Action Build 2022-03-05 14:17:23 +00:00
3533ee8dac Show tdevice movement in ui 2022-03-05 15:14:46 +01:00
2b9abda873 Bump html version 2022-03-05 14:56:46 +01:00
4bbb558c8b GitHub Action Build 2022-03-05 13:38:39 +00:00
8ebbc6559f Bump version 2022-03-05 14:36:15 +01:00
0af872e743 Updated docs for 0.8 2022-03-05 14:36:04 +01:00
c20f9a534a Remove sled 2022-02-14 21:49:07 +01:00
10ce1fc245 Adding moving info on html 2022-02-14 21:48:34 +01:00
b43874d151 Sorting graph data 2022-02-14 21:47:59 +01:00
1428bec3da GitHub Action Build 2022-02-08 18:07:39 +00:00
f9791dd349 Bump vuild version 2022-02-08 19:05:29 +01:00
1a7f28413c Revert gyro change 2022-02-08 19:05:13 +01:00
d22309bb2e GitHub Action Build 2022-02-06 20:32:15 +00:00
b901a12699 Removed trailing LF from error log 2022-02-06 21:29:58 +01:00
914b4125d8 Fixed tilt calulation error #29 2022-02-06 21:22:46 +01:00
95216ecc54 Fixed ESP32 build 2022-02-06 21:21:55 +01:00
4d83bf8fce GitHub Action Build 2022-02-06 10:21:33 +00:00
e125ca4a10 Added data to upload api 2022-02-06 10:04:53 +01:00
5880d3a6ba GitHub Action Build 2022-02-04 15:35:08 +00:00
cda3a87dd9 Docs updated 2022-02-04 16:22:27 +01:00
4bcacea9d7 Fixed hostname for min ssl buffer 2022-02-04 16:22:14 +01:00
838d062eea validation on header form 2022-02-03 21:48:11 +01:00
77cdbf7649 Updated bootstrap to 4.6.1 2022-02-03 20:33:20 +01:00
f33a58cffe GitHub Action Build 2022-02-03 15:05:54 +00:00
7ca536b216 Changed from s to ms in post timeout 2022-02-03 16:03:39 +01:00
f0ec352538 GitHub Action Build 2022-02-03 08:35:02 +00:00
e336633c38 Mem debug, Variable http timeout and min heapfrag 2022-02-03 09:32:09 +01:00
a130ebd67d GitHub Action Build 2022-02-02 12:32:48 +00:00
4c789a8b37 Minor update 2022-02-02 13:30:48 +01:00
7ab5f451f5 GitHub Action Build 2022-02-02 12:26:05 +00:00
545f274a47 Refactored to free up heap for SSL 2022-02-02 13:23:16 +01:00
9bea54b703 GitHub Action Build 2022-02-01 15:46:41 +00:00
35333469c7 Added thingsspeak setup docs 2022-02-01 16:44:07 +01:00
a9e0d9290a Update release notes 2022-02-01 11:28:28 +01:00
e459ceb2fa Updated docs 2022-02-01 11:27:46 +01:00
2615debe35 Added prel calculation for estimated total runtime 2022-02-01 10:37:17 +01:00
6113a436b0 Added battery percentage to index page 2022-01-31 22:42:41 +01:00
2d5158465f Removed batt from device page 2022-01-31 22:32:52 +01:00
3af52b5464 GitHub Action Build 2022-01-31 17:57:57 +00:00
c116d672d1 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-01-31 18:51:06 +01:00
e1cc54d188 Added token as option in UI #32 2022-01-31 18:50:47 +01:00
761d570d39 Added average runtime + voltage to device page 2022-01-31 18:29:27 +01:00
22ade61af8 Added runtime time logger 2022-01-30 22:54:48 +01:00
928054458a GitHub Action Build 2022-01-30 18:23:44 +00:00
2e67bd1d57 Checking missing params in API #16 2022-01-30 19:21:32 +01:00
83d7aee944 Merge branch 'master' into dev 2022-01-30 17:29:36 +01:00
8bcd27a076 Fix bad link in docs 2022-01-30 17:29:12 +01:00
0912d672fe Merge branch 'master' into dev 2022-01-30 17:27:36 +01:00
01d7c8adb5 Merge branch 'dev' of https://github.com/mp-se/gravitymon into dev 2022-01-30 17:27:29 +01:00
69076d5878 Fixed bad doc_ref 2022-01-30 17:26:13 +01:00
3d939ad733 Merge branch 'master' into dev 2022-01-30 17:23:59 +01:00
f2112cb344 Added intro section to docs 2022-01-30 17:22:38 +01:00
f88fb8241b GitHub Action Build 2022-01-30 13:57:05 +00:00
6eed5f143b Merged patch 0.7.1 into dev 2022-01-30 14:55:00 +01:00
64e582d0e5 GitHub Action Build 2022-01-30 10:52:11 +00:00
5d0f02eb18 Update docs 2022-01-30 11:50:09 +01:00
fbc1eb4e31 Updated docs 2022-01-28 17:15:12 +01:00
700f00f48d GitHub Action Build 2022-01-27 20:33:34 +00:00
9a2f86fed7 Updated docs for 0.8 2022-01-27 21:31:35 +01:00
63fd80e750 Added errlog, custom http headers 2022-01-27 14:00:12 +01:00
b106ebfa20 Extended length of http url 2022-01-25 21:57:22 +01:00
108 changed files with 5572 additions and 7941 deletions

View File

@ -36,9 +36,7 @@ jobs:
git config --global advice.detachedHead false
- name: Run PlatformIO
#run: pio run -e gravity-release -e gravity-perf -e gravity-debug
run: pio run -e gravity-release -e gravity-perf
#run: pio run -e gravity-release
run: pio run -e gravity-release -e gravity-perf -e gravity32-release -e gravity32-perf
- uses: EndBug/add-and-commit@v7 # You can change this to use a specific version. https://github.com/marketplace/actions/add-commit
with:

View File

@ -8,24 +8,31 @@
# Gravity Monitor for Beer Brewing
GravityMon is a replacement firmware for the iSpindle firmware. It's 100% compatible with the iSpindle hardware design so it does not require any hardware changes.
GravityMon is a replacement firmware for the iSpindle firmware. It's 100% compatible with the iSpindle hardware design so it does not require any hardware changes.
Now also works with ESP32 (use ESP32 d1 mini which is compatible with ESP8266)
Installation can be made using https://www.brewflasher.com
Note! If its being flagged as malware, try the older version.
The main differences:
---------------------
* Operates in two modes gravity monitoring and configuration mode (simplify calibration)
* Modern web based UI for configuration (in config mode)
* REST API
* Send data to multiple endpoints when pushing data (2xhttp, brewfather, influxdb v2, mqtt supported)
* Automatic temperature adjustment of gravity reading
* OTA support from local webserver
* Built in function to create gravity formulas, no need for additional software, just enter tilt/gravity.
* Visual graph showing how formula will be interpreted
* Using the temperature sensor in gyro instead of DS18B20 (faster)
* Built in performance measurements (used to optimise code)
* Modern web based user interface for configuration when connected to WIFI
* Efficient software, long lifespan (+45 days with 5min update frequencey)
* Send data to multiple endpoints (http-post, http-get, influxdb v2, mqtt)
* Instructions for service such as; Brewfather, Fermentrack, Ubidots, Home Assistant, Brewers Friend, Brewspy, Thingspeak, Blynk.
* SSL support in standard HTTP and MQTT connections.
* ESP32 support with Bluetooth push
* Customize data format to be pushed
* Automatic temperature adjustment of gravity when enabled
* Use the temperature sensor in gyro instead of DS18B20
* Built in function to create gravity formulas, no need for additional software, just enter tilt/gravity.
* Visual graph showing how gravity formula will be interpreted
* OTA support
* Built in performance measurements (used to optimise code)
* REST API for scripting
No code has been reused from the iSpindle project.

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

Binary file not shown.

Binary file not shown.

BIN
bin/firmware32-perf.bin Normal file

Binary file not shown.

BIN
bin/firmware32.bin Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
bin/partitions32.bin Normal file

Binary file not shown.

1
bin/test.min.htm Normal file

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,120 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Beer Gravity Monitor</title>
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" crossorigin="anonymous"></script>
</head>
<body class="py-4">
<!-- START MENU -->
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar" aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbar">
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/index.htm">Home <span class="sr-only">(current)</span></a>
</li>
<li class="nav-item active">
<a class="nav-link" href="/device.htm">Device</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/calibration.htm">Calibration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.htm">About</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</nav>
<!-- START MAIN INDEX -->
<div class="container">
<hr class="my-4">
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert">
<div id="alert-msg">...</div>
<button type="button" id="alert-btn" class="close" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<script type="text/javascript">
function showError( 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>
<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"] + " (html 0.7.1)");
$("#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-22 Magnus Persson</div>
</body>
</html>

View File

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

195
html/firmware.htm Normal file
View File

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

79
html/firmware.min.htm Normal file
View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -5,158 +5,263 @@
<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>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<style>
.row-margin-10 { margin-top: 1.0em; }
</style>
</head>
<body class="py-4">
<!-- START MENU -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
<nav class="navbar navbar-expand-sm navbar-dark bg-primary">
<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>
<!-- START MENU -->
<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="/calibration.htm">Calibration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.htm">About</a>
</li>
</ul>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container">
<a class="navbar-brand" href="/index.htm">Beer Gravity Monitor</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="#"><b>Home</b></a>
</li>
<li class="nav-item">
<a class="nav-link" href="/config.htm">Configuration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/calibration.htm">Calibration</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/about.htm">About</a>
</li>
</ul>
</div>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</div>
</nav>
<div class="spinner-border text-light" id="spinner" role="status"></div>
</div>
</nav>
<!-- START MAIN INDEX -->
<!-- START MAIN INDEX -->
<div class="container row-margin-10">
<div class="container">
<div class="alert alert-success alert-dismissible hide fade d-none" role="alert">
<div id="alert"></div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<hr class="my-4">
<script type="text/javascript">
function showError( msg ) {
console.log("Error:" + msg);
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
<div class="alert alert-success alert-dismissible fade hide show d-none" role="alert" id="alert">
<div id="alert-msg">...</div>
<button type="button" id="alert-btn" class="close" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
function showSuccess( msg ) {
console.log("Success:" + msg);
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('hide').addClass('show').removeClass('d-none');
$('#alert').text( msg );
}
$("#alert-btn").click(function(e){
console.log("Disable");
$('.alert').addClass('hide').removeClass('show').addClass('d-none');
});
</script>
<div class="accordion" id="accordion">
<div class="accordion-item">
<h2 class="accordion-header" id="headingSoftware">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseSoftware" aria-expanded="true" aria-controls="collapseSoftware">
<b>Device</b>
</button>
</h2>
<div id="collapseSoftware" class="accordion-collapse collapse show" aria-labelledby="headingSoftware" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-4 bg-light">Current version:</div>
<div class="col-md-4 bg-light" id="app-ver">Loading...</div>
</div>
<div class="row mb-3" id="h-app-ver-new" hidden>
<div class="col-md-4 bg-light">New version:</div>
<div class="col-md-4 bg-light" id="app-ver-new">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Host name:</div>
<div class="col-md-4 bg-light" id="mdns">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Device ID:</div>
<div class="col-md-4 bg-light" id="id">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Platform:</div>
<div class="col-md-4 bg-light" id="platform">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">SSID:</div>
<div class="col-md-4 bg-light" id="wifi-ssid">Loading...</div>
</div>
<script>
$("#log-btn").click(function(e){
loadLog();
});
setInterval(function() {
loadLog();
}, 3000); //5 seconds
function loadLog() {
$("#logContent").load("/log");
//$("#logContent").load("/test/status.json");
};
</script>
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="collapse" data-bs-target="#collapseLog" aria-expanded="false" aria-controls="collapseLog" data-bs-toggle="tooltip" title="Load and show the last 10 errors that has occured on the device">
View error log
</button>
<div class="collapse row-margin-10" id="collapseLog">
<div class="card card-body">
<pre><code class="card-text" id="logContent"></code></pre>
</div>
</div>
</div>
</div>
</div>
<h2 class="accordion-header" id="headingData">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseData" aria-expanded="true" aria-controls="collapseData">
<b>Measurement</b>
</button>
</h2>
<div id="collapseData" class="accordion-collapse collapse show" aria-labelledby="headingData" data-bs-parent="#accordion">
<div class="accordion-body">
<div class="row mb-3">
<div class="col-md-4 bg-light">Gravity:</div>
<div class="col-md-4 bg-light" id="gravity">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Temperature:</div>
<div class="col-md-4 bg-light" id="temp">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Angle/Tilt:</div>
<div class="col-md-4 bg-light" id="angle">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Battery:</div>
<div class="col-md-4 bg-light" id="battery">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-4 bg-light">Average runtime:</div>
<div class="col-md-4 bg-light" id="runtime">Loading...</div>
</div>
<div class="row mb-3">
<div class="col-md-8 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>
</div>
</div>
</div>
</div>
<script type="text/javascript">
function showError( msg ) {
$('.alert').removeClass('alert-success').addClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
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 );
$("#app-ver").text(cfg["app-ver"] + " (" + cfg["app-build"] + ")");
$("#mdns").text(cfg["mdns"]);
$("#id").text(cfg["id"]);
$("#platform").text(cfg["platform"]);
$("#wifi-ssid").text(cfg["wifi-ssid"]);
$("#runtime").text(cfg["runtime-average"] + " seconds");
var angle = cfg["angle"];
if(angle==0) {
$("#angle").text("Gyro moving");
$("#gravity").text("Gyro moving");
} else {
$("#angle").text(cfg["angle"]);
if( cfg["gravity-format"] == "G")
$("#gravity").text(cfg["gravity"] + " SG");
else
$("#gravity").text(cfg["gravity"] + " °P");
}
var batt = cfg["battery"];
var charge = 0;
if(batt>4.15) charge = 100;
else if(batt>4.05) charge = 90;
else if(batt>3.97) charge = 80;
else if(batt>3.91) charge = 70;
else if(batt>3.86) charge = 60;
else if(batt>3.81) charge = 50;
else if(batt>3.78) charge = 40;
else if(batt>3.76) charge = 30;
else if(batt>3.73) charge = 20;
else if(batt>3.67) charge = 10;
else if(batt>3.44) charge = 5;
$("#battery").text(batt + " V (" + charge + "%)" );
if( cfg["temp-format"] == "C")
$("#temp").text(cfg["temp-c"] + " C");
else
$("#temp").text(cfg["temp-f"] + " F");
if( cfg["sleep-mode"] )
$("#sleep-mode").attr("checked", true );
else
$("#sleep-mode").attr("checked", false );
$("#sleep-mode").removeAttr("disabled");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
}
function showSuccess( msg ) {
$('.alert').addClass('alert-success').removeClass('alert-danger').removeClass('d-none').addClass('show')
$('#alert-msg').text( msg );
function start() {
setInterval(getStatus, 3000);
}
$("#alert-btn").click(function(e){
$('.alert').addClass('d-none').removeClass('show')
});
</script>
<div class="" id="id" hidden></div>
<!-- START FOOTER -->
<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"]);
if( cfg["gravity-format"] == "G")
$("#gravity").text(cfg["gravity"] + " SG");
else
$("#gravity").text(cfg["gravity"] + " °P");
$("#battery").text(cfg["battery"] + " V");
if( cfg["temp-format"] == "C")
$("#temp").text(cfg["temp-c"] + " C");
else
$("#temp").text(cfg["temp-f"] + " F");
if( cfg["sleep-mode"] )
$("#sleep-mode").attr("checked", true );
else
$("#sleep-mode").attr("checked", false );
$("#sleep-mode").removeAttr("disabled");
})
.fail(function () {
showError('Unable to get data from the device.');
})
.always(function() {
$('#spinner').hide();
});
}
function start() {
setInterval(getStatus, 3000);
}
</script>
<!-- START FOOTER -->
<div class="container-fluid themed-container bg-primary text-light">(C) Copyright 2021-22 Magnus Persson</div>
<div class="container themed-container bg-primary text-light row-margin-10">(C) Copyright 2021-22 Magnus Persson</div>
</body>
</html>

File diff suppressed because one or more lines are too long

229
html/test.htm Normal file
View File

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

1
html/test.min.htm Normal file

File diff suppressed because one or more lines are too long

View File

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

File diff suppressed because one or more lines are too long

View File

@ -1,368 +0,0 @@
/****************************************************************************************************************************
ESP_DoubleResetDetector.h
For ESP8266 / ESP32 boards
ESP_DoubleResetDetector is a library for the ESP8266/Arduino platform
to enable trigger configure mode by resetting ESP32 / ESP8266 twice.
Forked from DataCute https://github.com/datacute/DoubleResetDetector
Built by Khoi Hoang https://github.com/khoih-prog/ESP_DoubleResetDetector
Licensed under MIT license
Version: 1.2.1
Version Modified By Date Comments
------- ----------- ---------- -----------
1.0.0 K Hoang 15/12/2019 Initial coding
1.0.1 K Hoang 30/12/2019 Now can use EEPROM or SPIFFS for both ESP8266 and ESP32. RTC still OK for ESP8266
1.0.2 K Hoang 10/04/2020 Fix bug by left-over cpp file and in example.
1.0.3 K Hoang 13/05/2020 Update to use LittleFS for ESP8266 core 2.7.1+
1.1.0 K Hoang 04/12/2020 Add support to LittleFS for ESP32 using LITTLEFS Library
1.1.1 K Hoang 28/12/2020 Suppress all possible compiler warnings
1.1.2 K Hoang 10/10/2021 Update `platform.ini` and `library.json`
1.2.0 K Hoang 26/11/2021 Auto detect ESP32 core and use either built-in LittleFS or LITTLEFS library
1.2.1 K Hoang 26/11/2021 Fix compile error for ESP32 core v1.0.5-
*****************************************************************************************************************************/
#pragma once
#ifndef ESP_DoubleResetDetector_H
#define ESP_DoubleResetDetector_H
#if defined(ARDUINO) && (ARDUINO >= 100)
#include <Arduino.h>
#else
#include <WProgram.h>
#endif
#define ESP_DOUBLE_RESET_DETECTOR_VERSION "ESP_DoubleResetDetector v1.2.1"
#define ESP_DOUBLERESETDETECTOR_VERSION ESP_DOUBLE_RESET_DETECTOR_VERSION
//#define ESP_DRD_USE_EEPROM false
//#define ESP_DRD_USE_LITTLEFS false
//#define ESP_DRD_USE_SPIFFS false
//#define ESP8266_DRD_USE_RTC false //true
#ifdef ESP32
#if (!ESP_DRD_USE_EEPROM && !ESP_DRD_USE_SPIFFS && !ESP_DRD_USE_LITTLEFS)
#warning Neither EEPROM, SPIFFS nor LittleFS selected. Default to EEPROM
#ifdef ESP_DRD_USE_EEPROM
#undef ESP_DRD_USE_EEPROM
#define ESP_DRD_USE_EEPROM true
#endif
#endif
#endif
#ifdef ESP8266
#if (!ESP8266_DRD_USE_RTC && !ESP_DRD_USE_EEPROM && !ESP_DRD_USE_SPIFFS && !ESP_DRD_USE_LITTLEFS)
#warning Neither RTC, EEPROM, LITTLEFS nor SPIFFS selected. Default to EEPROM
#ifdef ESP_DRD_USE_EEPROM
#undef ESP_DRD_USE_EEPROM
#define ESP_DRD_USE_EEPROM true
#endif
#endif
#endif
//default to use EEPROM, otherwise, use LITTLEFS (higher priority), then SPIFFS
#if ESP_DRD_USE_EEPROM
#include <EEPROM.h>
#define FLAG_DATA_SIZE 4
#ifndef EEPROM_SIZE
#define EEPROM_SIZE 512
#endif
#ifndef EEPROM_START
#define EEPROM_START 256
#endif
#elif ( ESP_DRD_USE_LITTLEFS || ESP_DRD_USE_SPIFFS )
#include <FS.h>
#ifdef ESP32
#if ESP_DRD_USE_LITTLEFS
// Check cores/esp32/esp_arduino_version.h and cores/esp32/core_version.h
//#if ( ESP_ARDUINO_VERSION >= ESP_ARDUINO_VERSION_VAL(2, 0, 0) ) //(ESP_ARDUINO_VERSION_MAJOR >= 2)
#if ( defined(ESP_ARDUINO_VERSION_MAJOR) && (ESP_ARDUINO_VERSION_MAJOR >= 2) )
#warning Using ESP32 Core 1.0.6 or 2.0.0+
// The library has been merged into esp32 core from release 1.0.6
#include <LittleFS.h>
#define FileFS LittleFS
#define FS_Name "LittleFS"
#else
#warning Using ESP32 Core 1.0.5-. You must install LITTLEFS library
// The library has been merged into esp32 core from release 1.0.6
#include <LITTLEFS.h> // https://github.com/lorol/LITTLEFS
#define FileFS LITTLEFS
#define FS_Name "LittleFS"
#endif
#else
#include "SPIFFS.h"
// ESP32 core 1.0.4 still uses SPIFFS
#define FileFS SPIFFS
#endif
#else
// From ESP8266 core 2.7.1
#include <LittleFS.h>
#if ESP_DRD_USE_LITTLEFS
#define FileFS LittleFS
#else
#define FileFS SPIFFS
#endif
#endif // #if ESP_DRD_USE_EEPROM
#define DRD_FILENAME "/drd.dat"
#endif //#if ESP_DRD_USE_EEPROM
#ifndef DOUBLERESETDETECTOR_DEBUG
#define DOUBLERESETDETECTOR_DEBUG false
#endif
#define DOUBLERESETDETECTOR_FLAG_SET 0xD0D01234
#define DOUBLERESETDETECTOR_FLAG_CLEAR 0xD0D04321
class DoubleResetDetector
{
public:
DoubleResetDetector(int timeout, int address)
{
#if ESP_DRD_USE_EEPROM
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.printf("EEPROM size = %d, start = %d\n", EEPROM_SIZE, EEPROM_START);
#endif
EEPROM.begin(EEPROM_SIZE);
#elif ( ESP_DRD_USE_LITTLEFS || ESP_DRD_USE_SPIFFS )
// LittleFS / SPIFFS code
if (!FileFS.begin())
{
#if (DOUBLERESETDETECTOR_DEBUG)
#if ESP_DRD_USE_LITTLEFS
Serial.println("LittleFS failed!. Please use SPIFFS or EEPROM.");
#else
Serial.println("SPIFFS failed!. Please use LittleFS or EEPROM.");
#endif
#endif
}
#else
#ifdef ESP8266
//RTC only for ESP8266
#endif
#endif
this->timeout = timeout * 1000;
this->address = address;
doubleResetDetected = false;
waitingForDoubleReset = false;
};
bool detectDoubleReset()
{
doubleResetDetected = detectRecentlyResetFlag();
if (doubleResetDetected)
{
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("doubleResetDetected");
#endif
clearRecentlyResetFlag();
}
else
{
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("No doubleResetDetected");
#endif
setRecentlyResetFlag();
waitingForDoubleReset = true;
}
return doubleResetDetected;
};
void loop()
{
if (waitingForDoubleReset && millis() > timeout)
{
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Stop doubleResetDetecting");
#endif
stop();
}
};
void stop()
{
clearRecentlyResetFlag();
waitingForDoubleReset = false;
};
bool doubleResetDetected;
private:
uint32_t DOUBLERESETDETECTOR_FLAG;
unsigned long timeout;
int address;
bool waitingForDoubleReset;
bool detectRecentlyResetFlag()
{
#if (ESP_DRD_USE_EEPROM)
EEPROM.get(EEPROM_START, DOUBLERESETDETECTOR_FLAG);
doubleResetDetectorFlag = DOUBLERESETDETECTOR_FLAG;
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.printf("EEPROM Flag read = 0x%X\n", DOUBLERESETDETECTOR_FLAG);
#endif
#elif ( ESP_DRD_USE_LITTLEFS || ESP_DRD_USE_SPIFFS )
// LittleFS / SPIFFS code
if (FileFS.exists(DRD_FILENAME))
{
// if config file exists, load
File file = FileFS.open(DRD_FILENAME, "r");
if (!file)
{
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Loading config file failed");
#endif
}
file.readBytes((char *) &DOUBLERESETDETECTOR_FLAG, sizeof(DOUBLERESETDETECTOR_FLAG));
doubleResetDetectorFlag = DOUBLERESETDETECTOR_FLAG;
#if (DOUBLERESETDETECTOR_DEBUG)
#if ESP_DRD_USE_LITTLEFS
Serial.printf("LittleFS Flag read = 0x%X\n", DOUBLERESETDETECTOR_FLAG);
#else
Serial.printf("SPIFFS Flag read = 0x%X\n", DOUBLERESETDETECTOR_FLAG);
#endif
#endif
file.close();
}
#else
#ifdef ESP8266
//RTC only for ESP8266
ESP.rtcUserMemoryRead(address, &doubleResetDetectorFlag, sizeof(doubleResetDetectorFlag));
#endif
#endif
doubleResetDetected = (doubleResetDetectorFlag == DOUBLERESETDETECTOR_FLAG_SET);
return doubleResetDetected;
};
void setRecentlyResetFlag()
{
doubleResetDetectorFlag = DOUBLERESETDETECTOR_FLAG_SET;
DOUBLERESETDETECTOR_FLAG = DOUBLERESETDETECTOR_FLAG_SET;
#if (ESP_DRD_USE_EEPROM)
EEPROM.put(EEPROM_START, DOUBLERESETDETECTOR_FLAG);
EEPROM.commit();
#if (DOUBLERESETDETECTOR_DEBUG)
delay(1000);
EEPROM.get(EEPROM_START, DOUBLERESETDETECTOR_FLAG);
Serial.printf("SetFlag write = 0x%X\n", DOUBLERESETDETECTOR_FLAG);
#endif
#elif ( ESP_DRD_USE_LITTLEFS || ESP_DRD_USE_SPIFFS )
// LittleFS / SPIFFS code
File file = FileFS.open(DRD_FILENAME, "w");
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Saving config file...");
#endif
if (file)
{
file.write((uint8_t *) &DOUBLERESETDETECTOR_FLAG, sizeof(DOUBLERESETDETECTOR_FLAG));
file.close();
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Saving config file OK");
#endif
}
else
{
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Saving config file failed");
#endif
}
#else
#ifdef ESP8266
//RTC only for ESP8266
ESP.rtcUserMemoryWrite(address, &doubleResetDetectorFlag, sizeof(doubleResetDetectorFlag));
#endif
#endif
};
void clearRecentlyResetFlag()
{
doubleResetDetectorFlag = DOUBLERESETDETECTOR_FLAG_CLEAR;
DOUBLERESETDETECTOR_FLAG = DOUBLERESETDETECTOR_FLAG_CLEAR;
#if (ESP_DRD_USE_EEPROM)
//DOUBLERESETDETECTOR_FLAG = DOUBLERESETDETECTOR_FLAG_CLEAR;
EEPROM.put(EEPROM_START, DOUBLERESETDETECTOR_FLAG);
EEPROM.commit();
#if (DOUBLERESETDETECTOR_DEBUG)
delay(1000);
EEPROM.get(EEPROM_START, DOUBLERESETDETECTOR_FLAG);
Serial.printf("ClearFlag write = 0x%X\n", DOUBLERESETDETECTOR_FLAG);
#endif
#elif ( ESP_DRD_USE_LITTLEFS || ESP_DRD_USE_SPIFFS )
// LittleFS / SPIFFS code
File file = FileFS.open(DRD_FILENAME, "w");
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Saving config file...");
#endif
if (file)
{
file.write((uint8_t *) &DOUBLERESETDETECTOR_FLAG, sizeof(DOUBLERESETDETECTOR_FLAG));
file.close();
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Saving config file OK");
#endif
}
else
{
#if (DOUBLERESETDETECTOR_DEBUG)
Serial.println("Saving config file failed");
#endif
}
#else
#ifdef ESP8266
//RTC only for ESP8266
ESP.rtcUserMemoryWrite(address, &doubleResetDetectorFlag, sizeof(doubleResetDetectorFlag));
#endif
#endif
};
uint32_t doubleResetDetectorFlag;
};
#endif // ESP_DoubleResetDetector_H

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -1,127 +0,0 @@
/****************************************************************************************************************************
ESP_WiFiManager_Debug.h
For ESP8266 / ESP32 boards
ESP_WiFiManager is a library for the ESP8266/Arduino platform
(https://github.com/esp8266/Arduino) to enable easy
configuration and reconfiguration of WiFi credentials using a Captive Portal
inspired by:
http://www.esp8266.com/viewtopic.php?f=29&t=2520
https://github.com/chriscook8/esp-arduino-apboot
https://github.com/esp8266/Arduino/blob/master/libraries/DNSServer/examples/CaptivePortalAdvanced/
Modified from Tzapu https://github.com/tzapu/WiFiManager
and from Ken Taylor https://github.com/kentaylor
Built by Khoi Hoang https://github.com/khoih-prog/ESP_WiFiManager
Licensed under MIT license
Version: 1.8.0
Version Modified By Date Comments
------- ----------- ---------- -----------
1.0.0 K Hoang 07/10/2019 Initial coding
1.0.1 K Hoang 13/12/2019 Fix bug. Add features. Add support for ESP32
1.0.2 K Hoang 19/12/2019 Fix bug thatkeeps ConfigPortal in endless loop if Portal/Router SSID or Password is NULL.
1.0.3 K Hoang 05/01/2020 Option not displaying AvailablePages in Info page. Enhance README.md. Modify examples
1.0.4 K Hoang 07/01/2020 Add RFC952 setHostname feature.
1.0.5 K Hoang 15/01/2020 Add configurable DNS feature. Thanks to @Amorphous of https://community.blynk.cc
1.0.6 K Hoang 03/02/2020 Add support for ArduinoJson version 6.0.0+ ( tested with v6.14.1 )
1.0.7 K Hoang 13/04/2020 Reduce start time, fix SPIFFS bug in examples, update README.md
1.0.8 K Hoang 10/06/2020 Fix STAstaticIP issue. Restructure code. Add LittleFS support for ESP8266 core 2.7.1+
1.0.9 K Hoang 29/07/2020 Fix ESP32 STAstaticIP bug. Permit changing from DHCP <-> static IP using Config Portal.
Add, enhance examples (fix MDNS for ESP32)
1.0.10 K Hoang 08/08/2020 Add more features to Config Portal. Use random WiFi AP channel to avoid conflict.
1.0.11 K Hoang 17/08/2020 Add CORS feature. Fix bug in softAP, autoConnect, resetSettings.
1.1.0 K Hoang 28/08/2020 Add MultiWiFi feature to autoconnect to best WiFi at runtime
1.1.1 K Hoang 30/08/2020 Add setCORSHeader function to allow flexible CORS. Fix typo and minor improvement.
1.1.2 K Hoang 17/08/2020 Fix bug. Add example.
1.2.0 K Hoang 09/10/2020 Restore cpp code besides Impl.h code to use if linker error. Fix bug.
1.3.0 K Hoang 04/12/2020 Add LittleFS support to ESP32 using LITTLEFS Library
1.4.1 K Hoang 22/12/2020 Fix staticIP not saved. Add functions. Add complex examples. Sync with ESPAsync_WiFiManager
1.4.2 K Hoang 14/01/2021 Fix examples' bug not using saved WiFi Credentials after losing all WiFi connections.
1.4.3 K Hoang 23/01/2021 Fix examples' bug not saving Static IP in certain cases.
1.5.0 K Hoang 12/02/2021 Add support to new ESP32-S2
1.5.1 K Hoang 26/03/2021 Fix compiler error if setting Compiler Warnings to All. Retest with esp32 core v1.0.6
1.5.2 K Hoang 08/04/2021 Fix example misleading messages.
1.5.3 K Hoang 13/04/2021 Add dnsServer error message.
1.6.0 K Hoang 20/04/2021 Add support to new ESP32-C3 using SPIFFS or EEPROM
1.6.1 K Hoang 25/04/2021 Fix MultiWiFi bug. Fix captive-portal bug if CP AP address is not default 192.168.4.1
1.7.0 K Hoang 06/05/2021 Set _timezoneName. Add support to new ESP32-S2 (METRO_ESP32S2, FUNHOUSE_ESP32S2, etc.)
1.7.1 K Hoang 08/05/2021 Fix Json bug. Fix timezoneName not displayed in Info page.
1.7.2 K Hoang 08/05/2021 Fix warnings with ESP8266 core v3.0.0
1.7.3 K Hoang 29/07/2021 Fix MultiWiFi connection issue with ESP32 core v2.0.0-rc1+
1.7.4 K Hoang 13/08/2021 Add WiFi scanning of hidden SSIDs
1.7.5 K Hoang 10/10/2021 Update `platform.ini` and `library.json`
1.7.6 K Hoang 26/11/2021 Auto detect ESP32 core and use either built-in LittleFS or LITTLEFS library
1.7.7 K Hoang 26/11/2021 Fix compile error for ESP32 core v1.0.5-
1.7.8 K Hoang 30/11/2021 Fix bug to permit using HTTP port different from 80. Fix bug
1.8.0 K Hoang 29/12/2021 Fix `multiple-definitions` linker error and weird bug related to src_cpp
*****************************************************************************************************************************/
#pragma once
#ifndef ESP_WiFiManager_Debug_H
#define ESP_WiFiManager_Debug_H
#ifdef WIFIMGR_DEBUG_PORT
#define WM_DBG_PORT WIFIMGR_DEBUG_PORT
#else
#define WM_DBG_PORT Serial
#endif
// Change _WIFIMGR_LOGLEVEL_ to set tracing and logging verbosity
// 0: DISABLED: no logging
// 1: ERROR: errors
// 2: WARN: errors and warnings
// 3: INFO: errors, warnings and informational (default)
// 4: DEBUG: errors, warnings, informational and debug
#ifndef _WIFIMGR_LOGLEVEL_
#define _WIFIMGR_LOGLEVEL_ 0
#endif
const char WM_MARK[] = "[WM] ";
const char WM_SP[] = " ";
#define WM_PRINT WM_DBG_PORT.print
#define WM_PRINTLN WM_DBG_PORT.println
#define WM_PRINT_MARK WM_PRINT(WM_MARK)
#define WM_PRINT_SP WM_PRINT(WM_SP)
////////////////////////////////////////////////////
#define LOGERROR(x) if(_WIFIMGR_LOGLEVEL_>0) { WM_PRINT_MARK; WM_PRINTLN(x); }
#define LOGERROR0(x) if(_WIFIMGR_LOGLEVEL_>0) { WM_PRINT(x); }
#define LOGERROR1(x,y) if(_WIFIMGR_LOGLEVEL_>0) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINTLN(y); }
#define LOGERROR2(x,y,z) if(_WIFIMGR_LOGLEVEL_>0) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINTLN(z); }
#define LOGERROR3(x,y,z,w) if(_WIFIMGR_LOGLEVEL_>0) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINT(z); WM_PRINT_SP; WM_PRINTLN(w); }
////////////////////////////////////////////////////
#define LOGWARN(x) if(_WIFIMGR_LOGLEVEL_>1) { WM_PRINT_MARK; WM_PRINTLN(x); }
#define LOGWARN0(x) if(_WIFIMGR_LOGLEVEL_>1) { WM_PRINT(x); }
#define LOGWARN1(x,y) if(_WIFIMGR_LOGLEVEL_>1) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINTLN(y); }
#define LOGWARN2(x,y,z) if(_WIFIMGR_LOGLEVEL_>1) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINTLN(z); }
#define LOGWARN3(x,y,z,w) if(_WIFIMGR_LOGLEVEL_>1) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINT(z); WM_PRINT_SP; WM_PRINTLN(w); }
////////////////////////////////////////////////////
#define LOGINFO(x) if(_WIFIMGR_LOGLEVEL_>2) { WM_PRINT_MARK; WM_PRINTLN(x); }
#define LOGINFO0(x) if(_WIFIMGR_LOGLEVEL_>2) { WM_PRINT(x); }
#define LOGINFO1(x,y) if(_WIFIMGR_LOGLEVEL_>2) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINTLN(y); }
#define LOGINFO2(x,y,z) if(_WIFIMGR_LOGLEVEL_>2) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINTLN(z); }
#define LOGINFO3(x,y,z,w) if(_WIFIMGR_LOGLEVEL_>2) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINT(z); WM_PRINT_SP; WM_PRINTLN(w); }
////////////////////////////////////////////////////
#define LOGDEBUG(x) if(_WIFIMGR_LOGLEVEL_>3) { WM_PRINT_MARK; WM_PRINTLN(x); }
#define LOGDEBUG0(x) if(_WIFIMGR_LOGLEVEL_>3) { WM_PRINT(x); }
#define LOGDEBUG1(x,y) if(_WIFIMGR_LOGLEVEL_>3) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINTLN(y); }
#define LOGDEBUG2(x,y,z) if(_WIFIMGR_LOGLEVEL_>3) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINTLN(z); }
#define LOGDEBUG3(x,y,z,w) if(_WIFIMGR_LOGLEVEL_>3) { WM_PRINT_MARK; WM_PRINT(x); WM_PRINT_SP; WM_PRINT(y); WM_PRINT_SP; WM_PRINT(z); WM_PRINT_SP; WM_PRINTLN(w); }
////////////////////////////////////////////////////
#endif //ESP_WiFiManager_Debug_H

File diff suppressed because it is too large Load Diff

13
part32.csv Normal file
View File

@ -0,0 +1,13 @@
# Name,Type,SubType,Offset,Size,Flags
nvs,data,nvs,0x9000,0x5000
otadata,data,ota,0xe000,0x2000
app0,app,ota_0,0x10000,0x1c0000
app1,app,ota_1,0x1d0000,0x1c0000
spiffs,data,spiffs,0x390000,0x70000
# Name, Type, SubType, Offset, Size, Flags
#nvs, data, nvs, 0x9000, 0x5000,
#otadata, data, ota, 0xe000, 0x2000,
#app0, app, ota_0, 0x10000, 0x140000, # 1310720
#app1, app, ota_1, 0x150000,0x140000, # 1310720
#spiffs, data, spiffs, 0x290000,0x170000, # 1507328
1 # Name,Type,SubType,Offset,Size,Flags
2 nvs,data,nvs,0x9000,0x5000
3 otadata,data,ota,0xe000,0x2000
4 app0,app,ota_0,0x10000,0x1c0000
5 app1,app,ota_1,0x1d0000,0x1c0000
6 spiffs,data,spiffs,0x390000,0x70000
7 # Name, Type, SubType, Offset, Size, Flags
8 #nvs, data, nvs, 0x9000, 0x5000,
9 #otadata, data, ota, 0xe000, 0x2000,
10 #app0, app, ota_0, 0x10000, 0x140000, # 1310720
11 #app1, app, ota_1, 0x150000,0x140000, # 1310720
12 #spiffs, data, spiffs, 0x290000,0x170000, # 1507328

View File

@ -20,9 +20,8 @@ framework = arduino
board = d1_mini
build_unflags =
build_flags =
-Wl,-Map,output.map
-D BAUD=${common_env_data.monitor_speed}
-D ACTIVATE_OTA
-D ACTIVATE_OTA
#-D DEBUG_ESP_HTTP_CLIENT
#-D DEBUG_ESP_HTTP_SERVER
#-D DEBUG_ESP_PORT=Serial
@ -34,14 +33,16 @@ build_flags =
-D EMBED_HTML # If this is not used the html files needs to be on the file system (can be uploaded)
-D USER_SSID=\""\"" # =\""myssid\""
-D USER_SSID_PWD=\""\"" # =\""mypwd\""
-D CFG_APPVER="\"0.7.1\""
-D CFG_APPVER="\"1.0.0\""
-D CFG_GITREV=\""beta-1\""
#!python script/git_rev.py
lib_deps = # Switched to forks for better version control.
# Using local copy of these libraries
#https://github.com/jrowberg/i2cdevlib.git#<document>
#https://github.com/khoih-prog/ESP_WiFiManager#<document>
#https://github.com/khoih-prog/ESP_DoubleResetDetector#<document>
#https://github.com/PaulStoffregen/OneWire
#https://github.com/milesburton/Arduino-Temperature-Control-Library
https://github.com/mp-se/ESP_WiFiManager#v1.9.0 # https://github.com/khoih-prog/ESP_WiFiManager
https://github.com/mp-se/ESP_DoubleResetDetector#v1.2.1 # https://github.com/khoih-prog/ESP_DoubleResetDetector
https://github.com/mp-se/tinyexpr # https://github.com/codeplea/tinyexpr
https://github.com/mp-se/incbin # https://github.com/graphitemaster/incbin
https://github.com/mp-se/Arduino-Log#1.1.1 # https://github.com/thijse/Arduino-Log
@ -61,10 +62,12 @@ extra_scripts =
build_unflags =
${common_env_data.build_unflags}
build_flags =
-Wl,-Map,output.map
${common_env_data.build_flags}
#-D PIO_FRAMEWORK_ARDUINO_ENABLE_EXCEPTIONS
#-D SKIP_SLEEPMODE
#-D DOUBLERESETDETECTOR_DEBUG=true
#-D FORCE_GRAVITY_MODE # used to debug gravity mode
-D COLLECT_PERFDATA # This option will collect runtime data for a few defined methods to measure time, dumped to serial and/or influxdb
-D LOG_LEVEL=6 # Maximum log level for the debug build.
-D CFG_DISABLE_LOGGING # Turn off verbose/notice logging to reduce size and dont overload uart.
@ -114,7 +117,7 @@ extra_scripts =
script/create_versionjson.py
build_unflags = ${common_env_data.build_unflags}
build_flags =
${common_env_data.build_flags}
${common_env_data.build_flags}
-D COLLECT_PERFDATA
-D LOG_LEVEL=5
lib_deps =
@ -123,12 +126,9 @@ board = ${common_env_data.board}
build_type = release
board_build.filesystem = littlefs
[env:gravity32-perf]
[env:gravity32-release]
framework = arduino
# platformio only supports v1.0.6 of the esp32 libs.
platform = espressif32
# tasmota port of v2.0.2
#platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.1/platform-tasmota-espressif32-2.0.2.1.zip
platform = espressif32 @ 3.5.0
upload_speed = ${common_env_data.upload_speed}
monitor_speed = ${common_env_data.monitor_speed}
extra_scripts =
@ -138,14 +138,76 @@ extra_scripts =
build_unflags =
${common_env_data.build_unflags}
build_flags =
${common_env_data.build_flags}
-Wl,-Map,output.map
${common_env_data.build_flags}
#-D COLLECT_PERFDATA
-D LOG_LEVEL=5
-D CFG_DISABLE_LOGGING # Turn off verbose/notice logging to reduce size and dont overload uart (applies to LOG_LEVEL6)
-D GYRO_DISABLE_LOGGING
-D CALC_DISABLE_LOGGING
-D HELPER_DISABLE_LOGGING
-D PUSH_DISABLE_LOGGING
-D TSEN_DISABLE_LOGGING
-D WIFI_DISABLE_LOGGING
-D WEB_DISABLE_LOGGING
-D MAIN_DISABLE_LOGGING
lib_deps =
${common_env_data.lib_deps}
https://github.com/lorol/LITTLEFS#1.0.6
https://github.com/mp-se/NimBLE-Arduino#1.3.8 # https://github.com/h2zero/NimBLE-Arduino
board = featheresp32
build_type = release
board_build.partitions = part32.csv
board_build.filesystem = littlefs
monitor_filters = esp32_exception_decoder
[env:gravity32-perf]
framework = arduino
platform = espressif32 @ 3.5.0
upload_speed = ${common_env_data.upload_speed}
monitor_speed = ${common_env_data.monitor_speed}
extra_scripts =
script/copy_html.py
script/copy_firmware.py
script/create_versionjson.py
build_unflags =
${common_env_data.build_unflags}
build_flags =
-Wl,-Map,output.map
${common_env_data.build_flags}
-D COLLECT_PERFDATA
-D LOG_LEVEL=5
lib_deps =
${common_env_data.lib_deps}
board = nodemcu-32s
https://github.com/lorol/LITTLEFS#1.0.6
https://github.com/mp-se/NimBLE-Arduino#1.3.8 # https://github.com/h2zero/NimBLE-Arduino
board = featheresp32
build_type = release
#build_type = debug
board_build.partitions = part32.csv
board_build.filesystem = littlefs
monitor_filters = esp32_exception_decoder
#[env:gravity32-release2]
#framework = arduino
#platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip # build fails
#platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.2/platform-tasmota-espressif32-2.0.2.zip
#platform = espressif32 @ 4.1.0
#upload_speed = ${common_env_data.upload_speed}
#monitor_speed = ${common_env_data.monitor_speed}
#extra_scripts =
# script/copy_html.py
# script/copy_firmware.py
# script/create_versionjson.py
#build_unflags =
# ${common_env_data.build_unflags}
#build_flags =
# ${common_env_data.build_flags}
# -D ESPRESSIF32_20 # v2.0 framework
# -D LOG_LEVEL=5
#lib_deps =
# ${common_env_data.lib_deps}
# https://github.com/mp-se/NimBLE-Arduino#1.3.8 # https://github.com/h2zero/NimBLE-Arduino
#board = featheresp32
#build_type = release
#board_build.partitions = part32.csv
#board_build.filesystem = littlefs
board_build.filesystem = spiffs
monitor_filters = esp32_exception_decoder

View File

@ -11,17 +11,46 @@ 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"
source = dir + "/.pio/build/" + name + "/firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
if name == "gravity-release" :
target = dir + "/bin/firmware.bin"
source = dir + "/.pio/build/" + name + "/firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
if name == "gravity-perf" :
target = dir + "/bin/firmware-perf.bin"
source = dir + "/.pio/build/" + name + "/firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
if name == "gravity32-release" :
target = dir + "/bin/firmware32.bin"
source = dir + "/.pio/build/" + name + "/firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
target = dir + "/bin/partitions32.bin"
source = dir + "/.pio/build/" + name + "/partitions.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
if name == "gravity32-perf" :
target = dir + "/bin/firmware32-perf.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
source = dir + "/.pio/build/" + name + "/firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
if name == "gravity32-release2" :
target = dir + "/bin/firmware32_2.bin"
source = dir + "/.pio/build/" + name + "/firmware.bin"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
print( "Adding custom build step (copy firmware): ")

View File

@ -17,9 +17,6 @@ shutil.copyfile( source + file, target + file )
file = "config.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )
file = "device.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )
file = "index.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )
@ -29,3 +26,9 @@ shutil.copyfile( source + file, target + file )
file = "format.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )
file = "test.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )
file = "firmware.min.htm"
#print( "Copy file: " + source + file + "->" + target + file)
shutil.copyfile( source + file, target + file )

View File

@ -19,42 +19,43 @@ def after_build(source, target, env):
shutil.copyfile( source, target )
# Copy file 2
source = dir + "/data/device.min.htm"
target = dir + "/bin/device.min.htm"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
# Copy file 3
source = dir + "/data/config.min.htm"
target = dir + "/bin/config.min.htm"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
# Copy file 4
# Copy file 3
source = dir + "/data/about.min.htm"
target = dir + "/bin/about.min.htm"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
# Copy file 5
# Copy file 4
source = dir + "/data/calibration.min.htm"
target = dir + "/bin/calibration.min.htm"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
# Copy file 6
# Copy file 5
source = dir + "/data/format.min.htm"
target = dir + "/bin/format.min.htm"
print( "Copy file : " + source + " -> " + target )
shutil.copyfile( source, target )
# Copy file 6
source = dir + "/data/test.min.htm"
target = dir + "/bin/test.min.htm"
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.write( " \"html\": [ \"index.min.htm\", \"device.min.htm\", \"config.min.htm\", \"calibration.min.htm\", \"format.min.htm\", \"about.min.htm\" ] }" )
#f.write( " \"html\": [ \"index.min.htm\", \"config.min.htm\", \"calibration.min.htm\", \"test.min.htm\", \"format.min.htm\", \"about.min.htm\" ] }" )
f.write( " \"html\": [ ] }" )
f.close()

9
script/git_rev.py Normal file
View File

@ -0,0 +1,9 @@
import subprocess
revision = (
subprocess.check_output(["git", "rev-parse", "HEAD"])
.strip()
.decode("utf-8")
)
revision = revision[-6:]
print("-D CFG_GITREV='\"..%s\"'" % revision)

102
src/ble.cpp Normal file
View File

@ -0,0 +1,102 @@
/*
MIT License
Copyright (c) 2021-22 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.
*/
#if defined(ESP32)
#include <ble.hpp>
#include <string>
// Tilt UUID variants and data format, based on tilt-sim
//
// https://github.com/spouliot/tilt-sim
//
// Tilt data format is described here. Only SG and Temp is transmitted over BLE.
// https://kvurd.com/blog/tilt-hydrometer-ibeacon-data-format/
//
// Create ble sender
//
BleSender::BleSender(const char* color) {
BLEDevice::init("");
// boost power to maximum, these might be changed once battery life using BLE
// has been tested.
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_DEFAULT, ESP_PWR_LVL_P9);
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, ESP_PWR_LVL_P9);
esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_SCAN, ESP_PWR_LVL_P9);
_advertising = BLEDevice::getAdvertising();
_color = color;
if (!_color.compareTo("red"))
_uuid = BLEUUID::fromString("A495BB10-C5B1-4B44-B512-1370F02D74DE");
else if (!_color.compareTo("green"))
_uuid = BLEUUID::fromString("A495BB20-C5B1-4B44-B512-1370F02D74DE");
else if (!_color.compareTo("black"))
_uuid = BLEUUID::fromString("A495BB30-C5B1-4B44-B512-1370F02D74DE");
else if (!_color.compareTo("purple"))
_uuid = BLEUUID::fromString("A495BB40-C5B1-4B44-B512-1370F02D74DE");
else if (!_color.compareTo("orange"))
_uuid = BLEUUID::fromString("A495BB50-C5B1-4B44-B512-1370F02D74DE");
else if (!_color.compareTo("blue"))
_uuid = BLEUUID::fromString("A495BB60-C5B1-4B44-B512-1370F02D74DE");
else if (!_color.compareTo("yellow"))
_uuid = BLEUUID::fromString("A495BB70-C5B1-4B44-B512-1370F02D74DE");
else // if (_color.compareTo("pink"))
_uuid = BLEUUID::fromString("A495BB80-C5B1-4B44-B512-1370F02D74DE");
}
//
// Send temp and gravity via BLE
//
void BleSender::sendData(float tempF, float gravSG) {
uint16_t gravity = gravSG * 1000; // SG * 1000 or SG * 10000 for Tilt Pro/HD
uint16_t temperature = tempF; // Deg F _or_ Deg F * 10 for Tilt Pro/HD
BLEBeacon oBeacon = BLEBeacon();
oBeacon.setManufacturerId(
0x4C00); // fake Apple 0x004C LSB (ENDIAN_CHANGE_U16!)
oBeacon.setProximityUUID(_uuid);
oBeacon.setMajor(temperature);
oBeacon.setMinor(gravity);
std::string strServiceData = "";
strServiceData += static_cast<char>(26); // Len
strServiceData += static_cast<char>(0xFF); // Type
strServiceData += oBeacon.getData();
BLEAdvertisementData oAdvertisementData = BLEAdvertisementData();
oAdvertisementData.setFlags(0x04); // BR_EDR_NOT_SUPPORTED 0x04
oAdvertisementData.addData(strServiceData);
BLEAdvertisementData oScanResponseData = BLEAdvertisementData();
_advertising->setAdvertisementData(oAdvertisementData);
_advertising->setScanResponseData(oScanResponseData);
_advertising->setAdvertisementType(BLE_GAP_CONN_MODE_NON);
_advertising->start();
delay(100);
_advertising->stop();
delay(100);
}
#endif // ESP32

47
src/ble.hpp Normal file
View File

@ -0,0 +1,47 @@
/*
MIT License
Copyright (c) 2021-22 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 SRC_BLE_HPP_
#define SRC_BLE_HPP_
#if defined(ESP32)
#include <NimBLEBeacon.h>
#include <NimBLEDevice.h>
#include <config.hpp>
#include <main.hpp>
class BleSender {
private:
BLEAdvertising* _advertising;
String _color;
BLEUUID _uuid;
public:
explicit BleSender(const char* color);
void sendData(float tempF, float gravSG);
};
#endif // ESP32
#endif // SRC_BLE_HPP_

View File

@ -33,14 +33,16 @@ SOFTWARE.
int createFormula(RawFormulaData &fd, char *formulaBuffer,
int formulaBufferSize, int order) {
int noAngles = 0;
RawFormulaData fd2;
// Check how many valid values we have got
if (fd.a[0] > 0 && fd.a[1] > 0 && fd.a[2] > 0 && fd.a[3] > 0 && fd.a[4] > 0)
noAngles = 5;
else if (fd.a[0] > 0 && fd.a[1] > 0 && fd.a[2] > 0 && fd.a[3] > 0)
noAngles = 4;
else if (fd.a[0] > 0 && fd.a[1] > 0 && fd.a[2] > 0)
noAngles = 3;
// Check how many valid values we have got and make sure we have a full series.
for (int i = 0; i < FORMULA_DATA_SIZE; i++) {
if (fd.a[i]) {
fd2.a[noAngles] = fd.a[i];
fd2.g[noAngles] = fd.g[i];
noAngles++;
}
}
#if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING)
Log.verbose(
@ -48,18 +50,19 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
order, noAngles);
#endif
if (!noAngles) {
Log.error(F("CALC: Not enough values for deriving formula" CR));
if (noAngles <3) {
ErrorFileLog errLog;
errLog.addEntry(F("CALC: Not enough values for deriving formula"));
return ERR_FORMULA_NOTENOUGHVALUES;
} else {
double coeffs[order + 1];
int ret = fitCurve(order, noAngles, fd.a, fd.g,
int ret = fitCurve(order, noAngles, fd2.a, fd2.g,
sizeof(coeffs) / sizeof(double), coeffs);
// Returned value is 0 if no error
if (ret == 0) {
#if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING)
Log.verbose(F("CALC: Finshied processing data points." CR));
Log.verbose(F("CALC: Finshied processing data points, order = %d." CR), order);
#endif
// Print the formula based on 'order'
@ -92,18 +95,19 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
double dev = (g - fd.g[i]) < 0 ? (fd.g[i] - g) : (g - fd.g[i]);
// If the deviation is more than 2 degress we mark it as failed.
if (dev * 1000 > myHardwareConfig.getMaxFormulaCreationDeviation()) {
#if LOG_LEVEL == 6 && !defined(CALC_DISABLE_LOGGING)
if (dev * 1000 > myAdvancedConfig.getMaxFormulaCreationDeviation()) {
char s[20];
snprintf(&s[0], sizeof(s), "%.8f", dev);
Log.verbose(F("CALC: Deviation is: %s" CR), &s[0]);
#endif
Log.error(F("CALC: Deviation to large: %s" CR), &s[0]);
valid = false;
}
}
if (!valid) {
Log.error(F("CALC: Deviation to large, formula rejected." CR));
ErrorFileLog errLog;
errLog.addEntry(
F("CALC: Error validating created formula. Deviation to large, "
"formula rejected."));
return ERR_FORMULA_UNABLETOFFIND;
}
@ -112,7 +116,8 @@ int createFormula(RawFormulaData &fd, char *formulaBuffer,
}
}
Log.error(F("CALC: Internal error finding formula." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CALC: Internal error finding formula."));
return ERR_FORMULA_INTERNAL;
}
@ -157,7 +162,8 @@ double calculateGravity(double angle, double temp, const char *tempFormula) {
return g;
}
Log.error(F("CALC: Failed to parse expression %d." CR), err);
ErrorFileLog errLog;
errLog.addEntry("CALC: Failed to parse gravity expression " + String(err));
return 0;
}
@ -203,9 +209,10 @@ double gravityTemperatureCorrectionC(double gravity, double tempC,
return g;
}
Log.error(
F("CALC: Failed to parse expression %d, no correction has been made." CR),
err);
ErrorFileLog errLog;
errLog.addEntry(
"CALC: Failed to parse expression for gravity temperature correction " +
String(err));
return gravity;
}

View File

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

View File

@ -26,7 +26,7 @@ SOFTWARE.
#include <wifi.hpp>
Config myConfig;
HardwareConfig myHardwareConfig;
AdvancedConfig myAdvancedConfig;
//
// Create the config class with default settings.
@ -34,14 +34,14 @@ HardwareConfig myHardwareConfig;
Config::Config() {
// Assiging default values
char buf[30];
#if defined (ESP8266)
snprintf(&buf[0], sizeof(buf), "%6x", (unsigned int)ESP.getChipId());
#else // defined (ESP32)
#if defined(ESP8266)
snprintf(&buf[0], sizeof(buf), "%06x", (unsigned int)ESP.getChipId());
#else // defined (ESP32)
uint32_t chipId = 0;
for (int i = 0; i < 17; i = i+8) {
for (int i = 0; i < 17; i = i + 8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
snprintf(&buf[0], sizeof(buf), "%6x", chipId);
snprintf(&buf[0], sizeof(buf), "%06x", chipId);
#endif
_id = String(&buf[0]);
snprintf(&buf[0], sizeof(buf), "" WIFI_MDNS "%s", getID());
@ -51,22 +51,6 @@ Config::Config() {
Log.verbose(F("CFG : Created config for %s (%s)." CR), _id.c_str(),
_mDNS.c_str());
#endif
setTempFormat('C');
setGravityFormat('G');
setSleepInterval(900); // 15 minutes
#if defined (ESP8266)
setVoltageFactor(1.59); // Conversion factor for battery on ESP8266
#else // defined (ESP32)
setVoltageFactor(1.43); // Conversion factor for battery on ESP32
#endif
setTempSensorAdjC(0.0);
setGravityTempAdj(false);
_gyroCalibration = {0, 0, 0, 0, 0, 0};
_formulaData = {{0, 0, 0, 0, 0}, {1, 1, 1, 1, 1}};
_gyroTemp = false;
_saveNeeded = false;
_mqttPort = 1883;
}
//
@ -75,14 +59,24 @@ Config::Config() {
//
void Config::createJson(DynamicJsonDocument& doc) {
doc[PARAM_MDNS] = getMDNS();
// doc[PARAM_CONFIG_VER] = getConfigVersion();
doc[PARAM_ID] = getID();
doc[PARAM_OTA] = getOtaURL();
doc[PARAM_SSID] = getWifiSSID();
doc[PARAM_PASS] = getWifiPass();
doc[PARAM_SSID] = getWifiSSID(0);
doc[PARAM_PASS] = getWifiPass(0);
doc[PARAM_SSID2] = getWifiSSID(1);
doc[PARAM_PASS2] = getWifiPass(1);
doc[PARAM_BLE] = getColorBLE();
doc[PARAM_TEMPFORMAT] = String(getTempFormat());
doc[PARAM_PUSH_BREWFATHER] = getBrewfatherPushUrl();
doc[PARAM_PUSH_HTTP] = getHttpPushUrl();
doc[PARAM_PUSH_HTTP2] = getHttpPushUrl2();
doc[PARAM_TOKEN] = getToken();
doc[PARAM_TOKEN2] = getToken2();
doc[PARAM_PUSH_HTTP] = getHttpUrl();
doc[PARAM_PUSH_HTTP_H1] = getHttpHeader(0);
doc[PARAM_PUSH_HTTP_H2] = getHttpHeader(1);
doc[PARAM_PUSH_HTTP2] = getHttp2Url();
doc[PARAM_PUSH_HTTP2_H1] = getHttp2Header(0);
doc[PARAM_PUSH_HTTP2_H2] = getHttp2Header(1);
doc[PARAM_PUSH_HTTP3] = getHttp3Url();
doc[PARAM_PUSH_INFLUXDB2] = getInfluxDb2PushUrl();
doc[PARAM_PUSH_INFLUXDB2_ORG] = getInfluxDb2PushOrg();
doc[PARAM_PUSH_INFLUXDB2_BUCKET] = getInfluxDb2PushBucket();
@ -113,12 +107,22 @@ void Config::createJson(DynamicJsonDocument& doc) {
cal2["a3"] = reduceFloatPrecision(_formulaData.a[2], 2);
cal2["a4"] = reduceFloatPrecision(_formulaData.a[3], 2);
cal2["a5"] = reduceFloatPrecision(_formulaData.a[4], 2);
cal2["a6"] = reduceFloatPrecision(_formulaData.a[5], 2);
cal2["a7"] = reduceFloatPrecision(_formulaData.a[6], 2);
cal2["a8"] = reduceFloatPrecision(_formulaData.a[7], 2);
cal2["a9"] = reduceFloatPrecision(_formulaData.a[8], 2);
cal2["a10"] = reduceFloatPrecision(_formulaData.a[9], 2);
cal2["g1"] = reduceFloatPrecision(_formulaData.g[0], 4);
cal2["g2"] = reduceFloatPrecision(_formulaData.g[1], 4);
cal2["g3"] = reduceFloatPrecision(_formulaData.g[2], 4);
cal2["g4"] = reduceFloatPrecision(_formulaData.g[3], 4);
cal2["g5"] = reduceFloatPrecision(_formulaData.g[4], 4);
cal2["g6"] = reduceFloatPrecision(_formulaData.g[5], 4);
cal2["g7"] = reduceFloatPrecision(_formulaData.g[6], 4);
cal2["g8"] = reduceFloatPrecision(_formulaData.g[7], 4);
cal2["g9"] = reduceFloatPrecision(_formulaData.g[8], 4);
cal2["g10"] = reduceFloatPrecision(_formulaData.g[9], 4);
}
//
@ -139,7 +143,8 @@ bool Config::saveFile() {
File configFile = LittleFS.open(CFG_FILENAME, "w");
if (!configFile) {
Log.error(F("CFG : Failed to open file " CFG_FILENAME " for save." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to save configuration."));
return false;
}
@ -156,7 +161,6 @@ bool Config::saveFile() {
configFile.close();
_saveNeeded = false;
myConfig.debug();
Log.notice(F("CFG : Configuration saved to " CFG_FILENAME "." CR));
return true;
}
@ -170,15 +174,16 @@ bool Config::loadFile() {
#endif
if (!LittleFS.exists(CFG_FILENAME)) {
Log.error(
F("CFG : Configuration file does not exist " CFG_FILENAME "." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Configuration file does not exist."));
return false;
}
File configFile = LittleFS.open(CFG_FILENAME, "r");
if (!configFile) {
Log.error(F("CFG : Failed to open " CFG_FILENAME "." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to load configuration."));
return false;
}
@ -194,8 +199,8 @@ bool Config::loadFile() {
configFile.close();
if (err) {
Log.error(F("CFG : Failed to parse " CFG_FILENAME " file, Err: %s, %d." CR),
err.c_str(), doc.capacity());
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to parse configuration (json)"));
return false;
}
@ -204,19 +209,30 @@ bool Config::loadFile() {
#endif
if (!doc[PARAM_OTA].isNull()) setOtaURL(doc[PARAM_OTA]);
if (!doc[PARAM_MDNS].isNull()) setMDNS(doc[PARAM_MDNS]);
if (!doc[PARAM_SSID].isNull()) setWifiSSID(doc[PARAM_SSID]);
if (!doc[PARAM_PASS].isNull()) setWifiPass(doc[PARAM_PASS]);
if (!doc[PARAM_SSID].isNull()) setWifiSSID(doc[PARAM_SSID], 0);
if (!doc[PARAM_PASS].isNull()) setWifiPass(doc[PARAM_PASS], 0);
if (!doc[PARAM_SSID2].isNull()) setWifiSSID(doc[PARAM_SSID2], 1);
if (!doc[PARAM_PASS2].isNull()) setWifiPass(doc[PARAM_PASS2], 1);
if (!doc[PARAM_BLE].isNull()) setColorBLE(doc[PARAM_BLE]);
if (!doc[PARAM_TEMPFORMAT].isNull()) {
String s = doc[PARAM_TEMPFORMAT];
setTempFormat(s.charAt(0));
}
if (!doc[PARAM_PUSH_BREWFATHER].isNull())
setBrewfatherPushUrl(doc[PARAM_PUSH_BREWFATHER]);
if (!doc[PARAM_PUSH_HTTP].isNull()) setHttpPushUrl(doc[PARAM_PUSH_HTTP]);
if (!doc[PARAM_PUSH_HTTP2].isNull()) setHttpPushUrl2(doc[PARAM_PUSH_HTTP2]);
if (!doc[PARAM_TOKEN].isNull()) setToken(doc[PARAM_TOKEN]);
if (!doc[PARAM_TOKEN2].isNull()) setToken2(doc[PARAM_TOKEN2]);
if (!doc[PARAM_PUSH_HTTP].isNull()) setHttpUrl(doc[PARAM_PUSH_HTTP]);
if (!doc[PARAM_PUSH_HTTP_H1].isNull())
setHttpHeader(doc[PARAM_PUSH_HTTP_H1], 0);
if (!doc[PARAM_PUSH_HTTP_H2].isNull())
setHttpHeader(doc[PARAM_PUSH_HTTP_H2], 1);
if (!doc[PARAM_PUSH_HTTP2].isNull()) setHttp2Url(doc[PARAM_PUSH_HTTP2]);
if (!doc[PARAM_PUSH_HTTP2_H1].isNull())
setHttp2Header(doc[PARAM_PUSH_HTTP2_H1], 0);
if (!doc[PARAM_PUSH_HTTP2_H2].isNull())
setHttp2Header(doc[PARAM_PUSH_HTTP2_H2], 1);
if (!doc[PARAM_PUSH_HTTP3].isNull()) setHttp3Url(doc[PARAM_PUSH_HTTP3]);
if (!doc[PARAM_PUSH_INFLUXDB2].isNull())
setInfluxDb2PushUrl(doc[PARAM_PUSH_INFLUXDB2]);
@ -275,6 +291,16 @@ bool Config::loadFile() {
_formulaData.a[3] = doc[PARAM_FORMULA_DATA]["a4"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a5"].isNull())
_formulaData.a[4] = doc[PARAM_FORMULA_DATA]["a5"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a6"].isNull())
_formulaData.a[5] = doc[PARAM_FORMULA_DATA]["a6"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a7"].isNull())
_formulaData.a[6] = doc[PARAM_FORMULA_DATA]["a7"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a8"].isNull())
_formulaData.a[7] = doc[PARAM_FORMULA_DATA]["a8"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a9"].isNull())
_formulaData.a[8] = doc[PARAM_FORMULA_DATA]["a9"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["a10"].isNull())
_formulaData.a[9] = doc[PARAM_FORMULA_DATA]["a10"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g1"].isNull())
_formulaData.g[0] = doc[PARAM_FORMULA_DATA]["g1"].as<double>();
@ -286,8 +312,25 @@ bool Config::loadFile() {
_formulaData.g[3] = doc[PARAM_FORMULA_DATA]["g4"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g5"].isNull())
_formulaData.g[4] = doc[PARAM_FORMULA_DATA]["g5"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g6"].isNull())
_formulaData.g[5] = doc[PARAM_FORMULA_DATA]["g6"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g7"].isNull())
_formulaData.g[6] = doc[PARAM_FORMULA_DATA]["g7"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g8"].isNull())
_formulaData.g[7] = doc[PARAM_FORMULA_DATA]["g8"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g9"].isNull())
_formulaData.g[8] = doc[PARAM_FORMULA_DATA]["g9"].as<double>();
if (!doc[PARAM_FORMULA_DATA]["g10"].isNull())
_formulaData.g[9] = doc[PARAM_FORMULA_DATA]["g10"].as<double>();
/*if( doc[PARAM_CONFIG_VER].isNull() ) {
// If this parameter is missing we need to reset the gyrocalibaration due to
bug #29 _gyroCalibration.ax = _gyroCalibration.ay = _gyroCalibration.az = 0;
_gyroCalibration.gx = _gyroCalibration.gy = _gyroCalibration.gz = 0;
Log.warning(F("CFG : Old configuration format, clearing gyro calibration."
CR));
}*/
myConfig.debug();
_saveNeeded = false; // Reset save flag
Log.notice(F("CFG : Configuration file " CFG_FILENAME " loaded." CR));
return true;
@ -317,38 +360,10 @@ void Config::checkFileSystem() {
}
}
//
// Dump the configuration to the serial port
//
void Config::debug() {
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
Log.verbose(F("CFG : Dumping configration " CFG_FILENAME "." CR));
Log.verbose(F("CFG : ID; '%s'." CR), getID());
Log.verbose(F("CFG : WIFI; '%s', '%s'." CR), getWifiSSID(), getWifiPass());
Log.verbose(F("CFG : mDNS; '%s'." CR), getMDNS());
Log.verbose(F("CFG : Sleep interval; %d." CR), getSleepInterval());
Log.verbose(F("CFG : OTA; '%s'." CR), getOtaURL());
Log.verbose(F("CFG : Temp Format; %c." CR), getTempFormat());
Log.verbose(F("CFG : Temp Adj; %F." CR), getTempSensorAdjC());
Log.verbose(F("CFG : VoltageFactor; %F." CR), getVoltageFactor());
Log.verbose(F("CFG : Gravity formula; '%s'." CR), getGravityFormula());
Log.verbose(F("CFG : Gravity format; '%c'." CR), getGravityFormat());
Log.verbose(F("CFG : Gravity temp adj; %s." CR),
isGravityTempAdj() ? "true" : "false");
Log.verbose(F("CFG : Gyro temp; %s." CR), isGyroTemp() ? "true" : "false");
Log.verbose(F("CFG : Push brewfather; '%s'." CR), getBrewfatherPushUrl());
Log.verbose(F("CFG : Push http; '%s'." CR), getHttpPushUrl());
Log.verbose(F("CFG : Push http2; '%s'." CR), getHttpPushUrl2());
Log.verbose(F("CFG : InfluxDb2; '%s', '%s', '%s', '%s'." CR),
getInfluxDb2PushUrl(), getInfluxDb2PushOrg(),
getInfluxDb2PushBucket(), getInfluxDb2PushToken());
#endif
}
//
// Save json document to file
//
bool HardwareConfig::saveFile() {
bool AdvancedConfig::saveFile() {
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
Log.verbose(F("CFG : Saving hardware configuration to file." CR));
#endif
@ -356,18 +371,26 @@ bool HardwareConfig::saveFile() {
File configFile = LittleFS.open(CFG_HW_FILENAME, "w");
if (!configFile) {
Log.error(F("CFG : Failed to open file " CFG_HW_FILENAME " for save." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to write hardware configuration "));
return false;
}
DynamicJsonDocument doc(512);
doc[PARAM_HW_GYRO_READ_COUNT] = this->getGyroReadCount();
doc[PARAM_HW_GYRO_READ_DELAY] = this->getGyroReadDelay();
// doc[PARAM_HW_GYRO_READ_DELAY] = this->getGyroReadDelay();
doc[PARAM_HW_GYRO_MOVING_THREASHOLD] = this->getGyroSensorMovingThreashold();
doc[PARAM_HW_FORMULA_DEVIATION] = this->getMaxFormulaCreationDeviation();
doc[PARAM_HW_WIFI_PORTALTIMEOUT] = this->getWifiPortalTimeout();
doc[PARAM_HW_WIFI_PORTAL_TIMEOUT] = this->getWifiPortalTimeout();
doc[PARAM_HW_WIFI_CONNECT_TIMEOUT] = this->getWifiConnectTimeout();
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP] = this->getDefaultCalibrationTemp();
doc[PARAM_HW_PUSH_INTERVAL_HTTP1] = this->getPushIntervalHttp1();
doc[PARAM_HW_PUSH_INTERVAL_HTTP2] = this->getPushIntervalHttp2();
doc[PARAM_HW_PUSH_INTERVAL_HTTP3] = this->getPushIntervalHttp3();
doc[PARAM_HW_PUSH_INTERVAL_INFLUX] = this->getPushIntervalInflux();
doc[PARAM_HW_PUSH_INTERVAL_MQTT] = this->getPushIntervalMqtt();
doc[PARAM_HW_TEMPSENSOR_RESOLUTION] = this->getTempSensorResolution();
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
serializeJson(doc, Serial);
@ -385,7 +408,7 @@ bool HardwareConfig::saveFile() {
//
// Load config file from disk
//
bool HardwareConfig::loadFile() {
bool AdvancedConfig::loadFile() {
#if LOG_LEVEL == 6 && !defined(DISABLE_LOGGING)
Log.verbose(F("CFG : Loading hardware configuration from file." CR));
#endif
@ -399,7 +422,8 @@ bool HardwareConfig::loadFile() {
File configFile = LittleFS.open(CFG_HW_FILENAME, "r");
if (!configFile) {
Log.error(F("CFG : Failed to open " CFG_HW_FILENAME "." CR));
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to read hardware configuration "));
return false;
}
@ -415,9 +439,8 @@ bool HardwareConfig::loadFile() {
configFile.close();
if (err) {
Log.error(
F("CFG : Failed to parse " CFG_HW_FILENAME " file, Err: %s, %d." CR),
err.c_str(), doc.capacity());
ErrorFileLog errLog;
errLog.addEntry(F("CFG : Failed to parse hardware configuration (json)"));
return false;
}
@ -427,8 +450,8 @@ bool HardwareConfig::loadFile() {
if (!doc[PARAM_HW_GYRO_READ_COUNT].isNull())
this->setGyroReadCount(doc[PARAM_HW_GYRO_READ_COUNT].as<int>());
if (!doc[PARAM_HW_GYRO_READ_DELAY].isNull())
this->setGyroReadDelay(doc[PARAM_HW_GYRO_READ_DELAY].as<int>());
// if (!doc[PARAM_HW_GYRO_READ_DELAY].isNull())
// this->setGyroReadDelay(doc[PARAM_HW_GYRO_READ_DELAY].as<int>());
if (!doc[PARAM_HW_GYRO_MOVING_THREASHOLD].isNull())
this->setGyroSensorMovingThreashold(
doc[PARAM_HW_GYRO_MOVING_THREASHOLD].as<int>());
@ -438,8 +461,24 @@ bool HardwareConfig::loadFile() {
if (!doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].isNull())
this->SetDefaultCalibrationTemp(
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP].as<float>());
if (!doc[PARAM_HW_WIFI_PORTALTIMEOUT].isNull())
this->setWifiPortalTimeout(doc[PARAM_HW_WIFI_PORTALTIMEOUT].as<int>());
if (!doc[PARAM_HW_WIFI_PORTAL_TIMEOUT].isNull())
this->setWifiPortalTimeout(doc[PARAM_HW_WIFI_PORTAL_TIMEOUT].as<int>());
if (!doc[PARAM_HW_WIFI_CONNECT_TIMEOUT].isNull())
this->setWifiConnectTimeout(doc[PARAM_HW_WIFI_CONNECT_TIMEOUT].as<int>());
if (!doc[PARAM_HW_PUSH_TIMEOUT].isNull())
this->setPushTimeout(doc[PARAM_HW_PUSH_TIMEOUT].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_HTTP1].isNull())
this->setPushIntervalHttp1(doc[PARAM_HW_PUSH_INTERVAL_HTTP1].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_HTTP2].isNull())
this->setPushIntervalHttp2(doc[PARAM_HW_PUSH_INTERVAL_HTTP2].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_HTTP3].isNull())
this->setPushIntervalHttp3(doc[PARAM_HW_PUSH_INTERVAL_HTTP3].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_INFLUX].isNull())
this->setPushIntervalInflux(doc[PARAM_HW_PUSH_INTERVAL_INFLUX].as<int>());
if (!doc[PARAM_HW_PUSH_INTERVAL_MQTT].isNull())
this->setPushIntervalMqtt(doc[PARAM_HW_PUSH_INTERVAL_MQTT].as<int>());
if (!doc[PARAM_HW_TEMPSENSOR_RESOLUTION].isNull())
this->setTempSensorResolution(doc[PARAM_HW_TEMPSENSOR_RESOLUTION].as<int>());
Log.notice(F("CFG : Configuration file " CFG_HW_FILENAME " loaded." CR));
return true;

View File

@ -29,7 +29,7 @@ SOFTWARE.
#define CFG_JSON_BUFSIZE 3192
#define CFG_APPNAME "GravityMon " // Name of firmware
#define CFG_APPNAME "GravityMon" // Name of firmware
#define CFG_FILENAME "/gravitymon.json" // Name of config file
#define CFG_HW_FILENAME "/hardware.json" // Name of config file for hw
@ -47,86 +47,142 @@ struct RawGyroData {
};
// Used for holding formulaData (used for calculating formula on device)
#define FORMULA_DATA_SIZE 10
struct RawFormulaData {
double a[5];
double g[5];
double a[FORMULA_DATA_SIZE];
double g[FORMULA_DATA_SIZE];
};
class HardwareConfig {
class AdvancedConfig {
private:
int _wifiPortalTimeout = 120;
int _wifiConnectTimeout = 20;
float _maxFormulaCreationDeviation = 1.6;
float _defaultCalibrationTemp = 20.0;
int _gyroSensorMovingThreashold = 500;
int _tempSensorResolution = 9;
int _gyroReadCount = 50;
int _gyroReadDelay = 3150; // us, empirical, to hold sampling to 200 Hz
int _pushTimeout = 10; // seconds
int _pushIntervalHttp1 = 0;
int _pushIntervalHttp2 = 0;
int _pushIntervalHttp3 = 0;
int _pushIntervalInflux = 0;
int _pushIntervalMqtt = 0;
public:
int getWifiPortalTimeout() { return _wifiPortalTimeout; }
void setWifiPortalTimeout(int t) { _wifiPortalTimeout = t; }
int getWifiConnectTimeout() { return _wifiConnectTimeout; }
void setWifiConnectTimeout(int t) { _wifiConnectTimeout = t; }
float getMaxFormulaCreationDeviation() {
return _maxFormulaCreationDeviation;
}
void setMaxFormulaCreationDeviation(float f) {
_maxFormulaCreationDeviation = f;
}
int getTempSensorResolution() { return _tempSensorResolution; }
void setTempSensorResolution(int t) {
if (t>=9 && t<=12)
_tempSensorResolution = t;
}
float getDefaultCalibrationTemp() { return _defaultCalibrationTemp; }
void SetDefaultCalibrationTemp(float t) { _defaultCalibrationTemp = t; }
int getGyroSensorMovingThreashold() { return _gyroSensorMovingThreashold; }
void setGyroSensorMovingThreashold(int t) { _gyroSensorMovingThreashold = t; }
int getGyroReadCount() { return _gyroReadCount; }
void setGyroReadCount(int c) { _gyroReadCount = c; }
int getGyroReadDelay() { return _gyroReadDelay; }
void setGyroReadDelay(int d) { _gyroReadDelay = d; }
int getPushTimeout() { return _pushTimeout; }
void setPushTimeout(int t) { _pushTimeout = t; }
int getPushIntervalHttp1() { return _pushIntervalHttp1; }
void setPushIntervalHttp1(int t) { _pushIntervalHttp1 = t; }
int getPushIntervalHttp2() { return _pushIntervalHttp2; }
void setPushIntervalHttp2(int t) { _pushIntervalHttp2 = t; }
int getPushIntervalHttp3() { return _pushIntervalHttp3; }
void setPushIntervalHttp3(int t) { _pushIntervalHttp3 = t; }
int getPushIntervalInflux() { return _pushIntervalInflux; }
void setPushIntervalInflux(int t) { _pushIntervalInflux = t; }
int getPushIntervalMqtt() { return _pushIntervalMqtt; }
void setPushIntervalMqtt(int t) { _pushIntervalMqtt = t; }
bool isPushIntervalActive() {
return (_pushIntervalHttp1 + _pushIntervalHttp2 + _pushIntervalHttp3 +
_pushIntervalInflux + _pushIntervalMqtt) == 0
? false
: true;
}
bool saveFile();
bool loadFile();
};
class Config {
private:
bool _saveNeeded;
bool _saveNeeded = false;
int _configVersion = 2;
// Device configuration
String _id;
String _mDNS;
String _otaURL;
char _tempFormat;
float _voltageFactor;
float _tempSensorAdjC;
int _sleepInterval;
bool _gyroTemp;
String _id = "";
String _mDNS = "";
String _otaURL = "";
char _tempFormat = 'C';
float _voltageFactor = 1.59;
float _tempSensorAdjC = 0;
int _sleepInterval = 900;
bool _gyroTemp = false;
// Wifi Config
String _wifiSSID;
String _wifiPASS;
String _wifiSSID[2] = {"", ""};
String _wifiPASS[2] = {"", ""};
// Push target settings
String _brewfatherPushUrl;
String _token = "";
String _token2 = "";
String _httpPushUrl;
String _httpPushUrl2;
String _httpUrl = "";
String _httpHeader[2] = {"Content-Type: application/json", ""};
String _http2Url = "";
String _http2Header[2] = {"Content-Type: application/json", ""};
String _http3Url = "";
String _influxDb2Url;
String _influxDb2Org;
String _influxDb2Bucket;
String _influxDb2Token;
String _influxDb2Url = "";
String _influxDb2Org = "";
String _influxDb2Bucket = "";
String _influxDb2Token = "";
String _mqttUrl;
int _mqttPort;
String _mqttUser;
String _mqttPass;
String _mqttUrl = "";
int _mqttPort = 1883;
String _mqttUser = "";
String _mqttPass = "";
// Gravity and temperature calculations
String _gravityFormula;
bool _gravityTempAdj;
char _gravityFormat;
String _gravityFormula = "";
bool _gravityTempAdj = false;
char _gravityFormat = 'G';
// BLE (ESP32 only)
String _colorBLE;
// Gyro calibration and formula calculation data
RawGyroData _gyroCalibration;
RawFormulaData _formulaData;
RawGyroData _gyroCalibration = {0, 0, 0, 0, 0, 0};
RawFormulaData _formulaData = {{0, 0, 0, 0, 0}, {1, 1, 1, 1, 1}};
void debug();
void formatFileSystem();
public:
@ -139,6 +195,8 @@ class Config {
_saveNeeded = true;
}
int getConfigVersion() { return _configVersion; }
const bool isGyroTemp() { return _gyroTemp; }
void setGyroTemp(bool b) {
_gyroTemp = b;
@ -151,41 +209,80 @@ class Config {
_saveNeeded = true;
}
bool isOtaActive() { return _otaURL.length() ? true : false; }
bool isOtaSSL() { return _otaURL.startsWith("https://"); }
const char* getWifiSSID() { return _wifiSSID.c_str(); }
void setWifiSSID(String s) {
_wifiSSID = s;
const char* getWifiSSID(int idx) { return _wifiSSID[idx].c_str(); }
void setWifiSSID(String s, int idx) {
_wifiSSID[idx] = s;
_saveNeeded = true;
}
const char* getWifiPass() { return _wifiPASS.c_str(); }
void setWifiPass(String s) {
_wifiPASS = s;
const char* getWifiPass(int idx) { return _wifiPASS[idx].c_str(); }
void setWifiPass(String s, int idx) {
_wifiPASS[idx] = s;
_saveNeeded = true;
}
bool dualWifiConfigured() {
return _wifiSSID[0].length() > 0 && _wifiSSID[1].length() > 0 ? true
: false;
}
void swapPrimaryWifi() {
String s = _wifiSSID[0];
_wifiSSID[0] = _wifiSSID[1];
_wifiSSID[1] = s;
String p = _wifiPASS[0];
_wifiPASS[0] = _wifiPASS[1];
_wifiPASS[1] = p;
_saveNeeded = true;
}
// Brewfather
const char* getBrewfatherPushUrl() { return _brewfatherPushUrl.c_str(); }
void setBrewfatherPushUrl(String s) {
_brewfatherPushUrl = s;
// Token parameter
const char* getToken() { return _token.c_str(); }
void setToken(String s) {
_token = s;
_saveNeeded = true;
}
bool isBrewfatherActive() {
return _brewfatherPushUrl.length() ? true : false;
const char* getToken2() { return _token2.c_str(); }
void setToken2(String s) {
_token2 = s;
_saveNeeded = true;
}
// Standard HTTP
const char* getHttpPushUrl() { return _httpPushUrl.c_str(); }
void setHttpPushUrl(String s) {
_httpPushUrl = s;
const char* getHttpUrl() { return _httpUrl.c_str(); }
void setHttpUrl(String s) {
_httpUrl = s;
_saveNeeded = true;
}
bool isHttpActive() { return _httpPushUrl.length() ? true : false; }
const char* getHttpPushUrl2() { return _httpPushUrl2.c_str(); }
void setHttpPushUrl2(String s) {
_httpPushUrl2 = s;
const char* getHttpHeader(int idx) { return _httpHeader[idx].c_str(); }
void setHttpHeader(String s, int idx) {
_httpHeader[idx] = s;
_saveNeeded = true;
}
bool isHttpActive2() { return _httpPushUrl2.length() ? true : false; }
bool isHttpActive() { return _httpUrl.length() ? true : false; }
bool isHttpSSL() { return _httpUrl.startsWith("https://"); }
const char* getHttp2Url() { return _http2Url.c_str(); }
void setHttp2Url(String s) {
_http2Url = s;
_saveNeeded = true;
}
const char* getHttp2Header(int idx) { return _http2Header[idx].c_str(); }
void setHttp2Header(String s, int idx) {
_http2Header[idx] = s;
_saveNeeded = true;
}
bool isHttp2Active() { return _http2Url.length() ? true : false; }
bool isHttp2SSL() { return _http2Url.startsWith("https://"); }
const char* getHttp3Url() { return _http3Url.c_str(); }
void setHttp3Url(String s) {
_http3Url = s;
_saveNeeded = true;
}
bool isHttp3Active() { return _http3Url.length() ? true : false; }
bool isHttp3SSL() { return _http3Url.startsWith("https://"); }
// InfluxDB2
const char* getInfluxDb2PushUrl() { return _influxDb2Url.c_str(); }
@ -194,6 +291,7 @@ class Config {
_saveNeeded = true;
}
bool isInfluxDb2Active() { return _influxDb2Url.length() ? true : false; }
bool isInfluxSSL() { return _influxDb2Url.startsWith("https://"); }
const char* getInfluxDb2PushOrg() { return _influxDb2Org.c_str(); }
void setInfluxDb2PushOrg(String s) {
_influxDb2Org = s;
@ -211,12 +309,14 @@ class Config {
}
// MQTT
bool isMqttActive() { return _mqttUrl.length() ? true : false; }
const char* getMqttUrl() { return _mqttUrl.c_str(); }
void setMqttUrl(String s) {
_mqttUrl = s;
_saveNeeded = true;
}
bool isMqttActive() { return _mqttUrl.length() ? true : false; }
bool isMqttSSL() { return _mqttPort > 8000 ? true : false; }
int getMqttPort() { return _mqttPort; }
void setMqttPort(String s) {
_mqttPort = s.toInt();
@ -272,12 +372,12 @@ class Config {
_tempSensorAdjC = f;
_saveNeeded = true;
}
void setTempSensorAdjC(String s) {
_tempSensorAdjC = s.toFloat();
void setTempSensorAdjC(String s, float adjustC = 0) {
_tempSensorAdjC = s.toFloat() + adjustC;
_saveNeeded = true;
}
void setTempSensorAdjF(String s) {
_tempSensorAdjC = convertFtoC(s.toFloat());
void setTempSensorAdjF(String s, float adjustF = 0) {
_tempSensorAdjC = convertFtoC(s.toFloat() + adjustF);
_saveNeeded = true;
}
@ -303,6 +403,19 @@ class Config {
bool isGravitySG() { return _gravityFormat == 'G'; }
bool isGravityPlato() { return _gravityFormat == 'P'; }
const char* getColorBLE() { return _colorBLE.c_str(); }
void setColorBLE(String c) {
_colorBLE = c;
_saveNeeded = true;
}
bool isBLEActive() { return _colorBLE.length() ? true : false; }
bool isWifiPushActive() {
return (isHttpActive() || isHttp2Active() || isHttp3Active() ||
isInfluxDb2Active() || isMqttActive())
? true
: false;
}
const RawGyroData& getGyroCalibration() { return _gyroCalibration; }
void setGyroCalibration(const RawGyroData& r) {
_gyroCalibration = r;
@ -325,7 +438,7 @@ class Config {
};
extern Config myConfig;
extern HardwareConfig myHardwareConfig;
extern AdvancedConfig myAdvancedConfig;
#endif // SRC_CONFIG_HPP_

View File

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

View File

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

View File

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

View File

@ -21,6 +21,7 @@ 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 <ble.hpp>
#include <calc.hpp>
#include <config.hpp>
#include <gyro.hpp>
@ -31,6 +32,8 @@ SOFTWARE.
#include <webserver.hpp>
#include <wifi.hpp>
//#define FORCE_GRAVITY_MODE
// Define constats for this program
#ifdef DEACTIVATE_SLEEPMODE
const int interval = 1000; // ms, time to wait between changes to output
@ -40,12 +43,11 @@ int interval = 200; // ms, time to wait between changes to output
bool sleepModeAlwaysSkip =
false; // Flag set in web interface to override normal behaviour
uint32_t loopMillis = 0; // Used for main loop to run the code every _interval_
uint32_t pushMillis = 0; // Used to control how often we will send push data
uint32_t runtimeMillis; // Used to calculate the total time since start/wakeup
uint32_t stableGyroMillis; // Used to calculate the total time since last
// stable gyro reading
enum RunMode { gravityMode = 0, configurationMode = 1, wifiSetupMode = 2 };
RunMode runMode = RunMode::gravityMode;
//
@ -58,6 +60,12 @@ void checkSleepMode(float angle, float volt) {
return;
#endif
#if defined(FORCE_GRAVITY_MODE)
Log.notice(F("MAIN: Forcing device into gravity mode for debugging" CR));
runMode = RunMode::gravityMode;
return;
#endif
const RawGyroData &g = myConfig.getGyroCalibration();
if (!g.ax && !g.ay && !g.az && !g.gx && !g.gy && !g.gz) {
@ -110,23 +118,25 @@ void setup() {
#if LOG_LEVEL == 6 && !defined(MAIN_DISABLE_LOGGING)
// Add a delay so that serial is started.
// delay(3000);
#if defined (ESP8266)
#if defined(ESP8266)
Log.verbose(F("Main: Reset reason %s." CR), ESP.getResetInfo().c_str());
#else // defined (ESP32)
#else // defined (ESP32)
#endif
#endif
// Main startup
#if defined (ESP8266)
#if defined(ESP8266)
Log.notice(F("Main: Started setup for %s." CR),
String(ESP.getChipId(), HEX).c_str());
#else // defined (ESP32)
#else // defined (ESP32)
char buf[20];
uint32_t chipId = 0;
for (int i = 0; i < 17; i = i+8) {
for (int i = 0; i < 17; i = i + 8) {
chipId |= ((ESP.getEfuseMac() >> (40 - i)) & 0xff) << i;
}
snprintf(&buf[0], sizeof(buf), "%6x", chipId);
Log.notice(F("Main: Started setup for %s." CR), &buf[0]);
pinMode(PIN_LED, OUTPUT);
digitalWrite(PIN_LED, HIGH);
#endif
printBuildOptions();
@ -134,14 +144,14 @@ void setup() {
myConfig.checkFileSystem();
myConfig.loadFile();
myWifi.init();
myHardwareConfig.loadFile();
myAdvancedConfig.loadFile();
LOG_PERF_STOP("main-config-load");
// Setup watchdog
#if defined (ESP8266)
#if defined(ESP8266)
ESP.wdtDisable();
ESP.wdtEnable(5000); // 5 seconds
#else // defined (ESP32)
#else // defined (ESP32)
#endif
// No stored config, move to portal
@ -157,6 +167,9 @@ void setup() {
runMode = RunMode::wifiSetupMode;
}
bool needWifi = true; // Under ESP32 we dont need wifi if only BLE is active
// in gravityMode
// Do this setup for all modes exect wifi setup
switch (runMode) {
case RunMode::wifiSetupMode:
@ -164,16 +177,10 @@ void setup() {
break;
default:
LOG_PERF_START("main-wifi-connect");
myWifi.connect();
LOG_PERF_STOP("main-wifi-connect");
LOG_PERF_START("main-temp-setup");
myTempSensor.setup();
LOG_PERF_STOP("main-temp-setup");
if (!myGyro.setup()) {
Log.error(F("Main: Failed to initialize the gyro." CR));
ErrorFileLog errLog;
errLog.addEntry(
F("MAIN: Failed to initialize the gyro, is it connected?"));
} else {
LOG_PERF_START("main-gyro-read");
myGyro.read();
@ -182,6 +189,25 @@ void setup() {
myBatteryVoltage.read();
checkSleepMode(myGyro.getAngle(), myBatteryVoltage.getVoltage());
#if defined(ESP32)
if (!myConfig.isWifiPushActive() && runMode == RunMode::gravityMode) {
Log.notice(
F("Main: Wifi is not needed in gravity mode, skipping "
"connection." CR));
needWifi = false;
}
#endif
if (needWifi) {
LOG_PERF_START("main-wifi-connect");
myWifi.connect();
LOG_PERF_STOP("main-wifi-connect");
}
LOG_PERF_START("main-temp-setup");
myTempSensor.setup();
LOG_PERF_STOP("main-temp-setup");
break;
}
@ -189,6 +215,7 @@ void setup() {
switch (runMode) {
case RunMode::configurationMode:
if (myWifi.isConnected()) {
Log.notice(F("Main: Activating web server." CR));
#if defined(ACTIVATE_OTA)
LOG_PERF_START("main-wifi-ota");
if (myWifi.checkFirmwareVersion()) myWifi.updateFirmware();
@ -207,7 +234,8 @@ void setup() {
LOG_PERF_STOP("main-setup");
Log.notice(F("Main: Setup completed." CR));
stableGyroMillis = millis(); // Dont include time for wifi connection
pushMillis = stableGyroMillis =
millis(); // Dont include time for wifi connection
}
//
@ -241,18 +269,41 @@ bool loopReadGravity() {
#if LOG_LEVEL == 6 && !defined(MAIN_DISABLE_LOGGING)
Log.verbose(F("Main: Sensor values gyro angle=%F, temp=%FC, gravity=%F, "
"corr_gravity=%F." CR),
angle, tempC, gravity, corrGravity);
angle, tempC, gravitySG, corrGravitySG);
#endif
LOG_PERF_START("loop-push");
// Force the transmission if we are going to sleep
myPushTarget.send(angle, gravitySG, corrGravitySG, tempC,
(millis() - runtimeMillis) / 1000,
runMode == RunMode::gravityMode ? true : false);
LOG_PERF_STOP("loop-push");
bool pushExpired = (abs((int32_t)(millis() - pushMillis)) >
(myConfig.getSleepInterval() * 1000));
if (pushExpired || runMode == RunMode::gravityMode) {
pushMillis = millis();
LOG_PERF_START("loop-push");
#if defined(ESP32)
if (myConfig.isBLEActive()) {
BleSender ble(myConfig.getColorBLE());
ble.sendData(convertCtoF(tempC), gravitySG);
Log.notice(F("MAIN: Broadcast data over bluetooth." CR));
}
#endif
if (myWifi.isConnected()) { // no need to try if there is no wifi
// connection.
PushTarget push;
push.sendAll(angle, gravitySG, corrGravitySG, tempC,
(millis() - runtimeMillis) / 1000);
}
LOG_PERF_STOP("loop-push");
// Send stats to influx after each push run.
if (runMode == RunMode::configurationMode) {
LOG_PERF_PUSH();
}
}
return true;
} else {
Log.error(F("Main: No gyro value." CR));
Log.error(F("MAIN: No gyro value found, the device might be moving." CR));
}
return false;
}
@ -265,7 +316,7 @@ void loopGravityOnInterval() {
if (abs((int32_t)(millis() - loopMillis)) > interval) {
loopReadGravity();
loopMillis = millis();
printHeap();
// printHeap("MAIN");
LOG_PERF_START("loop-gyro-read");
myGyro.read();
LOG_PERF_STOP("loop-gyro-read");
@ -274,6 +325,8 @@ void loopGravityOnInterval() {
}
}
bool skipRunTimeLog = false;
//
// Main loop that determines if device should go to sleep
//
@ -281,6 +334,11 @@ void goToSleep(int sleepInterval) {
float volt = myBatteryVoltage.getVoltage();
float runtime = (millis() - runtimeMillis);
if (!skipRunTimeLog) {
FloatHistoryLog runLog(RUNTIME_FILENAME);
runLog.addEntry(runtime);
}
Log.notice(F("MAIN: Entering deep sleep for %ds, run time %Fs, "
"battery=%FV." CR),
sleepInterval, reduceFloatPrecision(runtime / 1000, 2), volt);
@ -298,15 +356,21 @@ void goToSleep(int sleepInterval) {
void loop() {
switch (runMode) {
case RunMode::configurationMode:
myWebServerHandler.loop();
if (myWifi.isConnected()) myWebServerHandler.loop();
myWifi.loop();
loopGravityOnInterval();
// If we switched mode, dont include this in the log.
if (runMode != RunMode::configurationMode) skipRunTimeLog = true;
break;
case RunMode::gravityMode:
// If we didnt get a wifi connection, we enter sleep for a short time to
// conserve battery.
if (!myWifi.isConnected()) { // no connection to wifi
if (!myWifi.isConnected() &&
myConfig.isWifiPushActive()) { // no connection to wifi and we have
// defined push targets.
Log.notice(
F("MAIN: No connection to wifi established, sleeping for 60s." CR));
myWifi.stopDoubleReset();

View File

@ -29,21 +29,33 @@ SOFTWARE.
#include <ArduinoLog.h>
#include <stdlib.h>
#if defined (ESP8266)
enum RunMode { gravityMode = 0, configurationMode = 1, wifiSetupMode = 2 };
extern RunMode runMode;
#if defined(ESP8266)
#include <LittleFS.h>
#define ESP_RESET ESP.reset
#define PIN_SDA D3
#define PIN_SCL D4
#define PIN_DS D6
#else // defined (ESP32)
#define LittleFS SPIFFS
#define PIN_LED 2
// #define PIN_A0 A0
#else // defined (ESP32)
#if defined(ESPRESSIF32_20)
#include <LittleFS.h>
#else
#include <LITTLEFS.h>
#define LittleFS LITTLEFS
#endif
#include <FS.h>
#define ESPhttpUpdate httpUpdate
#define ESP_RESET ESP.restart
#define ESP8266WebServer WebServer
#include <spiffs.h>
#define PIN_SDA 17
#define PIN_SCL 16
#define PIN_DS 19
#define PIN_A0 36
#define PIN_LED 2
#endif
#define PIN_LED 2

View File

@ -22,10 +22,8 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#include <ESP8266mDNS.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#endif
#include <MQTT.h>
@ -34,79 +32,163 @@ SOFTWARE.
#include <pushtarget.hpp>
#include <wifi.hpp>
PushTarget myPushTarget;
#define PUSHINT_FILENAME "/push.dat"
//
// Decrease counters
//
void PushIntervalTracker::update(const int index, const int defaultValue) {
if (_counters[index] <= 0)
_counters[index] = defaultValue;
else
_counters[index]--;
}
//
// Load data from file
//
void PushIntervalTracker::load() {
File intFile = LittleFS.open(PUSHINT_FILENAME, "r");
if (intFile) {
String line = intFile.readStringUntil('\n');
Log.notice(F("PUSH: Read interval tracker %s." CR), line.c_str());
char temp[80];
char *s, *p = &temp[0];
int i = 0;
snprintf(&temp[0], sizeof(temp), "%s", line.c_str());
while ((s = strtok_r(p, ":", &p)) != NULL) {
_counters[i++] = atoi(s);
}
intFile.close();
}
#if !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: Parsed trackers: %d:%d:%d:%d:%d." CR), _counters[0],
_counters[1], _counters[2], _counters[3], _counters[4]);
#endif
}
//
// Update and save counters
//
void PushIntervalTracker::save() {
update(0, myAdvancedConfig.getPushIntervalHttp1());
update(1, myAdvancedConfig.getPushIntervalHttp2());
update(2, myAdvancedConfig.getPushIntervalHttp3());
update(3, myAdvancedConfig.getPushIntervalInflux());
update(4, myAdvancedConfig.getPushIntervalMqtt());
// If this feature is disabled we skip saving the file
if (!myAdvancedConfig.isPushIntervalActive()) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Variabled push interval disabled." CR));
#endif
LittleFS.remove(PUSHINT_FILENAME);
} else {
Log.notice(
F("PUSH: Variabled push interval enabled, updating counters." CR));
File intFile = LittleFS.open(PUSHINT_FILENAME, "w");
if (intFile) {
// Format=http1:http2:http3:influx:mqtt
intFile.printf("%d:%d:%d:%d:%d\n", _counters[0], _counters[1],
_counters[2], _counters[3], _counters[4]);
intFile.close();
}
}
}
//
// Send the data to targets
//
void PushTarget::send(float angle, float gravitySG, float corrGravitySG,
float tempC, float runTime, bool force) {
uint32_t timePassed = abs((int32_t)(millis() - _ms));
uint32_t interval = myConfig.getSleepInterval() * 1000;
if ((timePassed < interval) && !force) {
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: Timer has not expired %l vs %l." CR), timePassed,
interval);
#endif
return;
}
_ms = millis();
#if defined(ESP8266)
if (ESP.getFreeContStack() < 1500) {
Log.error(F("PUSH: Low on memory, skipping push since it will crasch. "
"(stack=%d, heap=%d)." CR),
ESP.getFreeContStack(), ESP.getFreeHeap());
myWifi.closeWifiClient();
return;
}
#endif
void PushTarget::sendAll(float angle, float gravitySG, float corrGravitySG,
float tempC, float runTime) {
printHeap("PUSH");
_http.setReuse(false);
_httpSecure.setReuse(false);
TemplatingEngine engine;
engine.initialize(angle, gravitySG, corrGravitySG, tempC, runTime);
if (myConfig.isBrewfatherActive()) {
LOG_PERF_START("push-brewfather");
sendBrewfather(engine);
LOG_PERF_STOP("push-brewfather");
}
PushIntervalTracker intDelay;
intDelay.load();
if (myConfig.isHttpActive()) {
if (myConfig.isHttpActive() && intDelay.useHttp1()) {
LOG_PERF_START("push-http");
sendHttp(engine, 0);
sendHttpPost(engine, myConfig.isHttpSSL(), 0);
LOG_PERF_STOP("push-http");
}
if (myConfig.isHttpActive2()) {
if (myConfig.isHttp2Active() && intDelay.useHttp2()) {
LOG_PERF_START("push-http2");
sendHttp(engine, 1);
sendHttpPost(engine, myConfig.isHttp2SSL(), 1);
LOG_PERF_STOP("push-http2");
}
if (myConfig.isInfluxDb2Active()) {
if (myConfig.isHttp3Active() && intDelay.useHttp3()) {
LOG_PERF_START("push-http3");
sendHttpGet(engine, myConfig.isHttp3SSL());
LOG_PERF_STOP("push-http3");
}
if (myConfig.isInfluxDb2Active() && intDelay.useInflux()) {
LOG_PERF_START("push-influxdb2");
sendInfluxDb2(engine);
sendInfluxDb2(engine, myConfig.isInfluxSSL());
LOG_PERF_STOP("push-influxdb2");
}
if (myConfig.isMqttActive()) {
if (myConfig.isMqttActive() && intDelay.useMqtt()) {
LOG_PERF_START("push-mqtt");
sendMqtt(engine);
sendMqtt(engine, myConfig.isMqttSSL());
LOG_PERF_STOP("push-mqtt");
}
LOG_PERF_PUSH();
intDelay.save();
}
//
// Check if the server can reduce the buffer size to save memory (ESP8266 only)
//
void PushTarget::probeMaxFragement(String& serverPath) {
#if defined(ESP8266) // Looks like this is feature is not supported by influxdb
// Format: http:://servername:port/path
int port = 443;
String host = serverPath.substring(8); // remove the prefix or the probe will
// fail, it needs a pure host name.
// Remove the path if it exist
int idx = host.indexOf("/");
if (idx != -1) host = host.substring(0, idx);
// If a server port is defined, lets extract that part
idx = host.indexOf(":");
if (idx != -1) {
String p = host.substring(idx + 1);
port = p.toInt();
host = host.substring(0, idx);
}
Log.notice(F("PUSH: Probing server to max fragment %s:%d" CR), host.c_str(),
port);
if (_wifiSecure.probeMaxFragmentLength(host, port, 512)) {
Log.notice(F("PUSH: Server supports smaller SSL buffer." CR));
_wifiSecure.setBufferSizes(512, 512);
}
#endif
}
//
// Send to influx db v2
//
void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
void PushTarget::sendInfluxDb2(TemplatingEngine& engine, bool isSecure) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to influxdb2." CR));
#endif
_lastCode = 0;
_lastSuccess = false;
String serverPath =
String(myConfig.getInfluxDb2PushUrl()) +
@ -114,136 +196,257 @@ void PushTarget::sendInfluxDb2(TemplatingEngine& engine) {
"&bucket=" + String(myConfig.getInfluxDb2PushBucket());
String doc = engine.create(TemplatingEngine::TEMPLATE_INFLUX);
HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: data %s." CR), doc.c_str());
#endif
// Send HTTP POST request
String auth = "Token " + String(myConfig.getInfluxDb2PushToken());
http.addHeader(F("Authorization"), auth.c_str());
int httpResponseCode = http.POST(doc);
if (httpResponseCode == 204) {
Log.notice(F("PUSH: InfluxDB2 push successful, response=%d" CR),
httpResponseCode);
if (isSecure) {
#if defined( ESP8266 )
if (runMode == RunMode::configurationMode) {
Log.notice(F("PUSH: Skipping InfluxDB since SSL is enabled and we are in config mode." CR));
_lastCode = -100;
return;
}
#endif
Log.notice(F("PUSH: InfluxDB, SSL enabled without validation." CR));
_wifiSecure.setInsecure();
probeMaxFragement(serverPath);
_httpSecure.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_httpSecure.begin(_wifiSecure, serverPath);
_httpSecure.addHeader(F("Authorization"), auth.c_str());
_httpSecure.setReuse(true);
_lastCode = _httpSecure.POST(doc);
} else {
Log.error(F("PUSH: InfluxDB2 push failed, response=%d" CR),
httpResponseCode);
_http.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_http.begin(_wifi, serverPath);
_http.addHeader(F("Authorization"), auth.c_str());
_lastCode = _http.POST(doc);
}
http.end();
myWifi.closeWifiClient();
if (_lastCode == 204) {
_lastSuccess = true;
Log.notice(F("PUSH: InfluxDB2 push successful, response=%d" CR), _lastCode);
} else {
ErrorFileLog errLog;
errLog.addEntry("PUSH: Influxdb push failed response=" + String(_lastCode));
}
if (isSecure) {
_httpSecure.end();
_wifiSecure.stop();
} else {
_http.end();
_wifi.stop();
}
tcp_cleanup();
}
//
// Send data to brewfather
// Add HTTP header to request
//
void PushTarget::sendBrewfather(TemplatingEngine& engine) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to brewfather" CR));
#endif
void PushTarget::addHttpHeader(HTTPClient& http, String header) {
if (!header.length()) return;
String serverPath = myConfig.getBrewfatherPushUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_BREWFATHER);
// Your Domain name with URL path or IP address with path
HTTPClient http;
http.begin(myWifi.getWifiClient(), serverPath);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json"));
int httpResponseCode = http.POST(doc);
if (httpResponseCode == 200) {
Log.notice(F("PUSH: Brewfather push successful, response=%d" CR),
httpResponseCode);
int i = header.indexOf(":");
if (i) {
String name = header.substring(0, i);
String value = header.substring(i + 1);
value.trim();
Log.notice(F("PUSH: Adding header '%s': '%s'" CR), name.c_str(),
value.c_str());
http.addHeader(name, value);
} else {
Log.error(F("PUSH: Brewfather push failed, response=%d" CR),
httpResponseCode);
ErrorFileLog errLog;
errLog.addEntry("PUSH: Unable to set header, invalid value " + header);
}
http.end();
myWifi.closeWifiClient();
}
//
// Send data to http target
// Send data to http target using POST
//
void PushTarget::sendHttp(TemplatingEngine& engine, int index) {
void PushTarget::sendHttpPost(TemplatingEngine& engine, bool isSecure,
int index) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to http (%s)" CR),
index ? "http2" : "http1");
index ? "http2" : "http");
#endif
_lastCode = 0;
_lastSuccess = false;
String serverPath, doc;
if (index == 0) {
serverPath = myConfig.getHttpPushUrl();
serverPath = myConfig.getHttpUrl();
doc = engine.create(TemplatingEngine::TEMPLATE_HTTP1);
} else {
serverPath = myConfig.getHttpPushUrl2();
serverPath = myConfig.getHttp2Url();
doc = engine.create(TemplatingEngine::TEMPLATE_HTTP2);
}
HTTPClient http;
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
http.begin(myWifi.getWifiClientSecure(), serverPath);
} else {
http.begin(myWifi.getWifiClient(), serverPath);
}
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
Log.verbose(F("PUSH: json %s." CR), doc.c_str());
#endif
// Send HTTP POST request
http.addHeader(F("Content-Type"), F("application/json"));
int httpResponseCode = http.POST(doc);
if (isSecure) {
#if defined( ESP8266 )
if (runMode == RunMode::configurationMode) {
Log.notice(F("PUSH: Skipping HTTP since SSL is enabled and we are in config mode." CR));
_lastCode = -100;
return;
}
#endif
if (httpResponseCode == 200) {
Log.notice(F("PUSH: HTTP push successful, response=%d" CR),
httpResponseCode);
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
_wifiSecure.setInsecure();
probeMaxFragement(serverPath);
_httpSecure.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_httpSecure.begin(_wifiSecure, serverPath);
if (index == 0) {
addHttpHeader(_httpSecure, myConfig.getHttpHeader(0));
addHttpHeader(_httpSecure, myConfig.getHttpHeader(1));
} else {
addHttpHeader(_httpSecure, myConfig.getHttp2Header(0));
addHttpHeader(_httpSecure, myConfig.getHttp2Header(1));
}
_lastCode = _httpSecure.POST(doc);
} else {
Log.error(F("PUSH: HTTP push failed, response=%d" CR), httpResponseCode);
_http.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_http.begin(_wifi, serverPath);
if (index == 0) {
addHttpHeader(_http, myConfig.getHttpHeader(0));
addHttpHeader(_http, myConfig.getHttpHeader(1));
} else {
addHttpHeader(_http, myConfig.getHttp2Header(0));
addHttpHeader(_http, myConfig.getHttp2Header(1));
}
_lastCode = _http.POST(doc);
}
http.end();
myWifi.closeWifiClient();
if (_lastCode == 200) {
_lastSuccess = true;
Log.notice(F("PUSH: HTTP post successful, response=%d" CR), _lastCode);
} else {
ErrorFileLog errLog;
errLog.addEntry("PUSH: HTTP post failed response=" + String(_lastCode) +
String(index == 0 ? " (http)" : " (http2)"));
}
if (isSecure) {
_httpSecure.end();
_wifiSecure.stop();
} else {
_http.end();
_wifi.stop();
}
tcp_cleanup();
}
//
// Send data to http target
// Send data to http target using GET
//
void PushTarget::sendMqtt(TemplatingEngine& engine) {
void PushTarget::sendHttpGet(TemplatingEngine& engine, bool isSecure) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to http3" CR));
#endif
_lastCode = 0;
_lastSuccess = false;
String serverPath;
serverPath = myConfig.getHttp3Url();
serverPath += engine.create(TemplatingEngine::TEMPLATE_HTTP3);
#if LOG_LEVEL == 6 && !defined(PUSH_DISABLE_LOGGING)
Log.verbose(F("PUSH: url %s." CR), serverPath.c_str());
#endif
if (isSecure) {
#if defined( ESP8266 )
if (runMode == RunMode::configurationMode) {
Log.notice(F("PUSH: Skipping HTTP since SSL is enabled and we are in config mode." CR));
_lastCode = -100;
return;
}
#endif
Log.notice(F("PUSH: HTTP, SSL enabled without validation." CR));
_wifiSecure.setInsecure();
probeMaxFragement(serverPath);
_httpSecure.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_httpSecure.begin(_wifiSecure, serverPath);
_lastCode = _httpSecure.GET();
} else {
_http.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
_http.begin(_wifi, serverPath);
_lastCode = _http.GET();
}
if (_lastCode == 200) {
_lastSuccess = true;
Log.notice(F("PUSH: HTTP get successful, response=%d" CR), _lastCode);
} else {
ErrorFileLog errLog;
errLog.addEntry("PUSH: HTTP get failed response=" + String(_lastCode));
}
if (isSecure) {
_httpSecure.end();
_wifiSecure.stop();
} else {
_http.end();
_wifi.stop();
}
tcp_cleanup();
}
//
// Send data to mqtt target
//
void PushTarget::sendMqtt(TemplatingEngine& engine, bool isSecure) {
#if !defined(PUSH_DISABLE_LOGGING)
Log.notice(F("PUSH: Sending values to mqtt." CR));
#endif
_lastCode = 0;
_lastSuccess = false;
MQTTClient mqtt(512);
String url = myConfig.getMqttUrl();
String host = myConfig.getMqttUrl();
String doc = engine.create(TemplatingEngine::TEMPLATE_MQTT);
int port = myConfig.getMqttPort();
// if (url.endsWith(":8883")) {
if (port > 8000) {
// Allow secure channel, but without certificate validation
myWifi.getWifiClientSecure().setInsecure();
if (myConfig.isMqttSSL()) {
#if defined( ESP8266 )
if (runMode == RunMode::configurationMode) {
Log.notice(F("PUSH: Skipping MQTT since SSL is enabled and we are in config mode." CR));
_lastCode = -100;
return;
}
#endif
Log.notice(F("PUSH: MQTT, SSL enabled without validation." CR));
mqtt.begin(url.c_str(), port, myWifi.getWifiClientSecure());
_wifiSecure.setInsecure();
#if defined(ESP8266)
if (_wifiSecure.probeMaxFragmentLength(host, port, 512)) {
Log.notice(F("PUSH: MQTT server supports smaller SSL buffer." CR));
_wifiSecure.setBufferSizes(512, 512);
}
#endif
mqtt.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
mqtt.begin(host.c_str(), port, _wifiSecure);
} else {
mqtt.begin(myConfig.getMqttUrl(), port, myWifi.getWifiClient());
mqtt.setTimeout(myAdvancedConfig.getPushTimeout() * 1000);
mqtt.begin(host.c_str(), port, _wifi);
}
mqtt.connect(myConfig.getMDNS(), myConfig.getMqttUser(),
@ -254,9 +457,6 @@ void PushTarget::sendMqtt(TemplatingEngine& engine) {
Log.verbose(F("PUSH: data %s." CR), doc.c_str());
#endif
// Send MQQT message(s)
mqtt.setTimeout(10); // 10 seconds timeout
int lines = 1;
// Find out how many lines are in the document. Each line is one
// topic/message. | is used as new line.
@ -277,10 +477,14 @@ void PushTarget::sendMqtt(TemplatingEngine& engine) {
value.c_str());
#endif
if (mqtt.publish(topic, value)) {
_lastSuccess = true;
Log.notice(F("PUSH: MQTT publish successful on %s" CR), topic.c_str());
_lastCode = 0;
} else {
Log.error(F("PUSH: MQTT publish failed err=%d, ret=%d" CR),
mqtt.lastError(), mqtt.returnCode());
_lastCode = mqtt.lastError();
ErrorFileLog errLog;
errLog.addEntry("PUSH: MQTT push on " + topic +
" failed error=" + String(mqtt.lastError()));
}
index = next + 1;
@ -288,7 +492,12 @@ void PushTarget::sendMqtt(TemplatingEngine& engine) {
}
mqtt.disconnect();
myWifi.closeWifiClient();
if (isSecure) {
_wifiSecure.stop();
} else {
_wifi.stop();
}
tcp_cleanup();
}
// EOF

View File

@ -26,22 +26,60 @@ SOFTWARE.
#include <templating.hpp>
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#include <WiFiClientSecure.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#endif
class PushTarget {
private:
uint32_t _ms; // Used to check that we do not post to often
WiFiClient _wifi;
WiFiClientSecure _wifiSecure;
HTTPClient _http;
HTTPClient _httpSecure;
int _lastCode;
bool _lastSuccess;
void sendBrewfather(TemplatingEngine& engine);
void sendHttp(TemplatingEngine& engine, int index);
void sendInfluxDb2(TemplatingEngine& engine);
void sendMqtt(TemplatingEngine& engine);
void sendHttpPost(TemplatingEngine& engine, bool isSecure, int index);
void sendHttpGet(TemplatingEngine& engine, bool isSecure);
void addHttpHeader(HTTPClient& http, String header);
void probeMaxFragement( String& serverPath );
public:
PushTarget() { _ms = millis(); }
void send(float angle, float gravitySG, float corrGravitySG, float tempC,
float runTime, bool force = false);
void sendAll(float angle, float gravitySG, float corrGravitySG, float tempC,
float runTime);
void sendHttp1(TemplatingEngine& engine, bool isSecure) {
sendHttpPost(engine, isSecure, 0);
}
void sendHttp2(TemplatingEngine& engine, bool isSecure) {
sendHttpPost(engine, isSecure, 1);
}
void sendHttp3(TemplatingEngine& engine, bool isSecure) {
sendHttpGet(engine, isSecure);
}
void sendInfluxDb2(TemplatingEngine& engine, bool isSecure);
void sendMqtt(TemplatingEngine& engine, bool isSecure);
int getLastCode() { return _lastCode; }
bool getLastSuccess() { return _lastSuccess; }
};
extern PushTarget myPushTarget;
class PushIntervalTracker {
private:
int _counters[5] = { 0, 0, 0, 0, 0 };
void update(const int index, const int defaultValue);
public:
bool useHttp1() { return _counters[0] == 0 ? true : false; }
bool useHttp2() { return _counters[1] == 0 ? true : false; }
bool useHttp3() { return _counters[2] == 0 ? true : false; }
bool useInflux() { return _counters[3] == 0 ? true : false; }
bool useMqtt() { return _counters[4] == 0 ? true : false; }
void load();
void save();
};
#endif // SRC_PUSHTARGET_HPP_

View File

@ -21,7 +21,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#if defined (ESP8266)
#if defined(ESP8266)
#define INCBIN_OUTPUT_SECTION ".irom.text"
#endif
#include <incbin.h>
@ -31,14 +31,15 @@ SOFTWARE.
#if defined(EMBED_HTML)
// 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(CalibrationHtm, "data/calibration.min.htm");
INCBIN(FormatHtm, "data/format.min.htm");
INCBIN(TestHtm, "data/test.min.htm");
INCBIN(AboutHtm, "data/about.min.htm");
#else
// Minium web interface for uploading htm files
INCBIN(UploadHtm, "data/upload.min.htm");
#endif
INCBIN(FirmwareHtm, "data/firmware.min.htm");
// EOF

View File

@ -27,12 +27,22 @@ SOFTWARE.
// Common strings used in json formats.
#define PARAM_ID "id"
#define PARAM_MDNS "mdns"
#define PARAM_CONFIG_VER "config-version"
#define PARAM_OTA "ota-url"
#define PARAM_SSID "wifi-ssid"
#define PARAM_PASS "wifi-pass"
#define PARAM_PUSH_BREWFATHER "brewfather-push"
#define PARAM_SSID2 "wifi-ssid2"
#define PARAM_PASS2 "wifi-pass2"
#define PARAM_RUNTIME_AVERAGE "runtime-average"
#define PARAM_TOKEN "token"
#define PARAM_TOKEN2 "token2"
#define PARAM_PUSH_HTTP "http-push"
#define PARAM_PUSH_HTTP_H1 "http-push-h1"
#define PARAM_PUSH_HTTP_H2 "http-push-h2"
#define PARAM_PUSH_HTTP2 "http-push2"
#define PARAM_PUSH_HTTP2_H1 "http-push2-h1"
#define PARAM_PUSH_HTTP2_H2 "http-push2-h2"
#define PARAM_PUSH_HTTP3 "http-push3"
#define PARAM_PUSH_INFLUXDB2 "influxdb2-push"
#define PARAM_PUSH_INFLUXDB2_ORG "influxdb2-org"
#define PARAM_PUSH_INFLUXDB2_BUCKET "influxdb2-bucket"
@ -51,8 +61,11 @@ SOFTWARE.
#define PARAM_GYRO_CALIBRATION "gyro-calibration-data"
#define PARAM_GYRO_TEMP "gyro-temp"
#define PARAM_FORMULA_DATA "formula-calculation-data"
#define PARAM_APP_NAME "app-name"
#define PARAM_FILES "files"
#define PARAM_FILE_NAME "file-name"
#define PARAM_FILE_SIZE "file-size"
#define PARAM_APP_VER "app-ver"
#define PARAM_APP_BUILD "app-build"
#define PARAM_ANGLE "angle"
#define PARAM_GRAVITY "gravity"
#define PARAM_TEMP_C "temp-c"
@ -61,16 +74,30 @@ SOFTWARE.
#define PARAM_SLEEP_MODE "sleep-mode"
#define PARAM_RSSI "rssi"
#define PARAM_ERROR "error"
#define PARAM_PLATFORM "platform"
#define PARAM_BLE "ble"
#define PARAM_HW_GYRO_READ_COUNT "gyro-read-count"
#define PARAM_HW_GYRO_READ_DELAY "gyro-read-delay"
// #define PARAM_HW_GYRO_READ_DELAY "gyro-read-delay"
#define PARAM_HW_GYRO_MOVING_THREASHOLD "gyro-moving-threashold"
#define PARAM_HW_FORMULA_DEVIATION "formula-max-deviation"
#define PARAM_HW_FORMULA_CALIBRATION_TEMP "formula-calibration-temp"
#define PARAM_HW_WIFI_PORTALTIMEOUT "wifi-portaltimeout"
#define PARAM_HW_WIFI_PORTAL_TIMEOUT "wifi-portal-timeout"
#define PARAM_HW_WIFI_CONNECT_TIMEOUT "wifi-connect-timeout"
#define PARAM_HW_TEMPSENSOR_RESOLUTION "tempsensor-resolution"
#define PARAM_HW_PUSH_TIMEOUT "push-timeout"
#define PARAM_HW_PUSH_INTERVAL_HTTP1 "int-http1"
#define PARAM_HW_PUSH_INTERVAL_HTTP2 "int-http2"
#define PARAM_HW_PUSH_INTERVAL_HTTP3 "int-http3"
#define PARAM_HW_PUSH_INTERVAL_INFLUX "int-influx"
#define PARAM_HW_PUSH_INTERVAL_MQTT "int-mqtt"
#define PARAM_FORMAT_HTTP1 "http-1"
#define PARAM_FORMAT_HTTP2 "http-2"
#define PARAM_FORMAT_BREWFATHER "brewfather"
#define PARAM_FORMAT_HTTP3 "http-3"
#define PARAM_FORMAT_INFLUXDB "influxdb"
#define PARAM_FORMAT_MQTT "mqtt"
#define PARAM_PUSH_FORMAT "format"
#define PARAM_PUSH_SUCCESS "success"
#define PARAM_PUSH_CODE "code"
#define PARAM_PUSH_ENABLED "enabled"
#endif // SRC_RESOURCES_HPP_

View File

@ -21,73 +21,76 @@ 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 <templating.hpp>
#include <config.hpp>
#include <templating.hpp>
#if defined (ESP8266)
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#else // defined (ESP32)
#else // defined (ESP32)
#include <WiFi.h>
#endif
// Use iSpindle format for compatibility
const char iSpindleFormat[] PROGMEM =
"{"
// Use iSpindle format for compatibility, HTTP POST
const char iSpindleFormat[] PROGMEM =
"{"
"\"name\" : \"${mdns}\", "
"\"ID\": \"${id}\", "
"\"token\" : \"gravmon\", "
"\"interval\": ${sleep-interval}, "
"\"temperature\": ${temp}, "
"\"temp-units\": \"${temp-unit}\", "
"\"gravity\": ${gravity}, "
"\"angle\": ${angle}, "
"\"battery\": ${battery}, "
"\"rssi\": ${rssi}, "
"\"corr-gravity\": ${corr-gravity}, "
"\"gravity-unit\": \"${gravity-unit}\", "
"\"run-time\": ${run-time} "
"}";
const char brewfatherFormat[] PROGMEM =
"{"
"\"name\": \"${mdns}\","
"\"temp\": ${temp}, "
"\"aux_temp\": 0, "
"\"ext_temp\": 0, "
"\"temp_unit\": \"${temp-unit}\", "
"\"ID\": \"${id}\", "
"\"token\" : \"${token}\", "
"\"interval\": ${sleep-interval}, "
"\"temperature\": ${temp}, "
"\"temp_units\": \"${temp-unit}\", "
"\"gravity\": ${gravity}, "
"\"gravity_unit\": \"${gravity-unit}\", "
"\"pressure\": 0, "
"\"pressure_unit\": \"PSI\", "
"\"ph\": 0, "
"\"bpm\": 0, "
"\"comment\": \"\", "
"\"beer\": \"\", "
"\"battery\": ${battery}"
"}";
"\"angle\": ${angle}, "
"\"battery\": ${battery}, "
"\"RSSI\": ${rssi}, "
"\"corr-gravity\": ${corr-gravity}, "
"\"gravity-unit\": \"${gravity-unit}\", "
"\"run-time\": ${run-time} "
"}";
const char influxDbFormat[] PROGMEM =
"measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-format=${gravity-unit} "
"gravity=${gravity},corr-gravity=${corr-gravity},angle=${angle},temp=${temp},battery=${battery},"
"rssi=${rssi}\n";
// Format for an HTTP GET
const char iHttpGetFormat[] PROGMEM =
"?name=${mdns}"
"&id=${id}"
"&token=${token2}"
"&interval=${sleep-interval}"
"&temperature=${temp}"
"&temp-units=${temp-unit}"
"&gravity=${gravity}"
"&angle=${angle}"
"&battery=${battery}"
"&rssi=${rssi}"
"&corr-gravity=${corr-gravity}"
"&gravity-unit=${gravity-unit}"
"&run-time=${run-time}";
const char mqttFormat[] PROGMEM =
"ispindel/${mdns}/tilt:${angle}|"
"ispindel/${mdns}/temperature:${temp}|"
"ispindel/${mdns}/temp_units:${temp-unit}|"
"ispindel/${mdns}/battery:${battery}|"
"ispindel/${mdns}/gravity:${gravity}|"
"ispindel/${mdns}/interval:${sleep-interval}|"
"ispindel/${mdns}/RSSI:${rssi}|";
const char influxDbFormat[] PROGMEM =
"measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-"
"format=${gravity-unit} "
"gravity=${gravity},corr-gravity=${corr-gravity},angle=${angle},temp=${"
"temp},battery=${battery},"
"rssi=${rssi}\n";
const char mqttFormat[] PROGMEM =
"ispindel/${mdns}/tilt:${angle}|"
"ispindel/${mdns}/temperature:${temp}|"
"ispindel/${mdns}/temp_units:${temp-unit}|"
"ispindel/${mdns}/battery:${battery}|"
"ispindel/${mdns}/gravity:${gravity}|"
"ispindel/${mdns}/interval:${sleep-interval}|"
"ispindel/${mdns}/RSSI:${rssi}|";
//
// Initialize the variables
//
void TemplatingEngine::initialize(float angle, float gravitySG, float corrGravitySG, float tempC, float runTime) {
void TemplatingEngine::initialize(float angle, float gravitySG,
float corrGravitySG, float tempC,
float runTime) {
// Names
setVal(TPL_MDNS, myConfig.getMDNS());
setVal(TPL_ID, myConfig.getID());
setVal(TPL_TOKEN, myConfig.getToken());
setVal(TPL_TOKEN2, myConfig.getToken2());
// Temperature
if (myConfig.isTempC()) {
@ -116,8 +119,7 @@ void TemplatingEngine::initialize(float angle, float gravitySG, float corrGravit
if (myConfig.isGravitySG()) {
setVal(TPL_GRAVITY, gravitySG, 4);
setVal(TPL_GRAVITY_CORR, corrGravitySG, 4);
}
else {
} else {
setVal(TPL_GRAVITY, convertToPlato(gravitySG), 1);
setVal(TPL_GRAVITY_CORR, convertToPlato(corrGravitySG), 1);
}
@ -138,36 +140,37 @@ void TemplatingEngine::initialize(float angle, float gravitySG, float corrGravit
//
const String& TemplatingEngine::create(TemplatingEngine::Templates idx) {
String fname;
baseTemplate.reserve(600);
// Load templates from memory
switch (idx) {
case TEMPLATE_HTTP1:
baseTemplate = String(iSpindleFormat);
fname = TPL_FNAME_HTTP1;
break;
break;
case TEMPLATE_HTTP2:
baseTemplate = String(iSpindleFormat);
fname = TPL_FNAME_HTTP2;
break;
case TEMPLATE_BREWFATHER:
baseTemplate = String(brewfatherFormat);
//fname = TPL_FNAME_BREWFATHER;
break;
break;
case TEMPLATE_HTTP3:
baseTemplate = String(iHttpGetFormat);
fname = TPL_FNAME_HTTP3;
break;
case TEMPLATE_INFLUX:
baseTemplate = String(influxDbFormat);
fname = TPL_FNAME_INFLUXDB;
break;
break;
case TEMPLATE_MQTT:
baseTemplate = String(mqttFormat);
fname = TPL_FNAME_MQTT;
break;
break;
}
// TODO: Add code to load templates from disk if they exist.
File file = LittleFS.open(fname, "r");
if (file) {
char buf[file.size()+1];
memset(&buf[0], 0, file.size()+1);
char buf[file.size() + 1];
memset(&buf[0], 0, file.size() + 1);
file.readBytes(&buf[0], file.size());
baseTemplate = String(&buf[0]);
file.close();
@ -175,14 +178,14 @@ const String& TemplatingEngine::create(TemplatingEngine::Templates idx) {
}
#if LOG_LEVEL == 6
//Log.verbose(F("TPL : Base '%s'." CR), baseTemplate.c_str());
// Log.verbose(F("TPL : Base '%s'." CR), baseTemplate.c_str());
#endif
// Insert data into template.
transform(baseTemplate);
#if LOG_LEVEL == 6
//Log.verbose(F("TPL : Transformed '%s'." CR), baseTemplate.c_str());
// Log.verbose(F("TPL : Transformed '%s'." CR), baseTemplate.c_str());
#endif
return baseTemplate;

View File

@ -26,39 +26,42 @@ SOFTWARE.
// Includes
#include <Arduino.h>
#include <main.hpp>
#include <helper.hpp>
#include <algorithm>
#include <helper.hpp>
#include <main.hpp>
// Templating variables
#define TPL_MDNS "${mdns}"
#define TPL_ID "${id}"
#define TPL_TOKEN "${token}"
#define TPL_TOKEN2 "${token2}"
#define TPL_SLEEP_INTERVAL "${sleep-interval}"
#define TPL_TEMP "${temp}"
#define TPL_TEMP_C "${temp-c}"
#define TPL_TEMP_F "${temp-f}"
#define TPL_TEMP_UNITS "${temp-unit}" // C or F
#define TPL_TEMP_UNITS "${temp-unit}" // C or F
#define TPL_BATTERY "${battery}"
#define TPL_RSSI "${rssi}"
#define TPL_RUN_TIME "${run-time}"
#define TPL_ANGLE "${angle}"
#define TPL_TILT "${tilt}" // same as angle
#define TPL_TILT "${tilt}" // same as angle
#define TPL_GRAVITY "${gravity}"
#define TPL_GRAVITY_G "${gravity-sg}"
#define TPL_GRAVITY_P "${gravity-plato}"
#define TPL_GRAVITY_CORR "${corr-gravity}"
#define TPL_GRAVITY_CORR_G "${corr-gravity-sg}"
#define TPL_GRAVITY_CORR_P "${corr-gravity-plato}"
#define TPL_GRAVITY_UNIT "${gravity-unit}" // G or P
#define TPL_GRAVITY_UNIT "${gravity-unit}" // G or P
#define TPL_FNAME_HTTP1 "/http-1.tpl"
#define TPL_FNAME_HTTP2 "/http-2.tpl"
// #define TPL_FNAME_BREWFATHER "/brewfather.tpl"
#define TPL_FNAME_HTTP3 "/http-3.tpl"
#define TPL_FNAME_INFLUXDB "/influxdb.tpl"
#define TPL_FNAME_MQTT "/mqtt.tpl"
extern const char iSpindleFormat[] PROGMEM;
extern const char brewfatherFormat[] PROGMEM;
extern const char iHttpGetFormat[] PROGMEM;
extern const char influxDbFormat[] PROGMEM;
extern const char mqttFormat[] PROGMEM;
@ -70,36 +73,30 @@ class TemplatingEngine {
String val;
};
KeyVal items[19] = {
{ TPL_MDNS, "" },
{ TPL_ID, "" },
{ TPL_SLEEP_INTERVAL, "" },
{ TPL_TEMP, "" },
{ TPL_TEMP_C, "" },
{ TPL_TEMP_F, "" },
{ TPL_TEMP_UNITS, "" },
{ TPL_BATTERY, "" },
{ TPL_RSSI, "" },
{ TPL_RUN_TIME, "" },
{ TPL_ANGLE, "" },
{ TPL_TILT, "" },
{ TPL_GRAVITY, "" },
{ TPL_GRAVITY_G, "" },
{ TPL_GRAVITY_P, "" },
{ TPL_GRAVITY_CORR, "" },
{ TPL_GRAVITY_CORR_G, "" },
{ TPL_GRAVITY_CORR_P, "" },
{ TPL_GRAVITY_UNIT, "" }
};
KeyVal items[21] = {{TPL_MDNS, ""}, {TPL_ID, ""},
{TPL_SLEEP_INTERVAL, ""}, {TPL_TEMP, ""},
{TPL_TEMP_C, ""}, {TPL_TEMP_F, ""},
{TPL_TEMP_UNITS, ""}, {TPL_BATTERY, ""},
{TPL_RSSI, ""}, {TPL_RUN_TIME, ""},
{TPL_ANGLE, ""}, {TPL_TILT, ""},
{TPL_GRAVITY, ""}, {TPL_GRAVITY_G, ""},
{TPL_GRAVITY_P, ""}, {TPL_GRAVITY_CORR, ""},
{TPL_GRAVITY_CORR_G, ""}, {TPL_GRAVITY_CORR_P, ""},
{TPL_GRAVITY_UNIT, ""}, {TPL_TOKEN, ""},
{TPL_TOKEN2, ""}};
char buffer[20];
String baseTemplate;
void setVal(String key, float val, int dec = 2) { String s = convertFloatToString(val, &buffer[0], dec); s.trim(); setVal(key, s); }
void setVal(String key, float val, int dec = 2) {
String s = convertFloatToString(val, &buffer[0], dec);
s.trim();
setVal(key, s);
}
void setVal(String key, int val) { setVal(key, String(val)); }
void setVal(String key, char val) { setVal(key, String(val)); }
void setVal(String key, String val) {
int max = sizeof(items)/sizeof(KeyVal);
int max = sizeof(items) / sizeof(KeyVal);
for (int i = 0; i < max; i++) {
if (items[i].key.equals(key)) {
items[i].val = val;
@ -107,11 +104,11 @@ class TemplatingEngine {
}
}
Log.error(F("TPL : Key not found %s." CR), key.c_str());
Log.warning(F("TPL : Key not found %s." CR), key.c_str());
}
void transform(String& s) {
int max = sizeof(items)/sizeof(KeyVal);
int max = sizeof(items) / sizeof(KeyVal);
for (int i = 0; i < max; i++) {
while (s.indexOf(items[i].key) != -1)
s.replace(items[i].key, items[i].val);
@ -119,13 +116,13 @@ class TemplatingEngine {
}
void dumpAll() {
int max = sizeof(items)/sizeof(KeyVal);
int max = sizeof(items) / sizeof(KeyVal);
for (int i = 0; i < max; i++) {
Serial.print( "Key=\'" );
Serial.print( items[i].key.c_str() );
Serial.print( "\', Val=\'" );
Serial.print( items[i].val.c_str() );
Serial.println( "\'" );
Serial.print("Key=\'");
Serial.print(items[i].key.c_str());
Serial.print("\', Val=\'");
Serial.print(items[i].val.c_str());
Serial.println("\'");
}
}
@ -133,12 +130,13 @@ class TemplatingEngine {
enum Templates {
TEMPLATE_HTTP1 = 0,
TEMPLATE_HTTP2 = 1,
TEMPLATE_BREWFATHER = 2,
TEMPLATE_HTTP3 = 2,
TEMPLATE_INFLUX = 3,
TEMPLATE_MQTT = 4
};
void initialize(float angle, float gravitySG, float corrGravitySG, float tempC, float runTime);
void initialize(float angle, float gravitySG, float corrGravitySG,
float tempC, float runTime);
const String& create(TemplatingEngine::Templates idx);
};

View File

@ -32,7 +32,6 @@ SOFTWARE.
OneWire myOneWire(PIN_DS);
DallasTemperature mySensors(&myOneWire);
#define TEMPERATURE_PRECISION 9
TempSensor myTempSensor;
@ -52,10 +51,10 @@ void TempSensor::setup() {
if (mySensors.getDS18Count()) {
#if !defined(TSEN_DISABLE_LOGGING)
Log.notice(F("TSEN: Found %d temperature sensor(s)." CR),
mySensors.getDS18Count());
Log.notice(F("TSEN: Found %d temperature sensor(s). Using %d resolution" CR),
mySensors.getDS18Count(), myAdvancedConfig.getTempSensorResolution());
#endif
mySensors.setResolution(TEMPERATURE_PRECISION);
mySensors.setResolution(myAdvancedConfig.getTempSensorResolution());
}
// Set the temp sensor adjustment values

View File

@ -26,6 +26,7 @@ SOFTWARE.
#include <gyro.hpp>
#include <helper.hpp>
#include <main.hpp>
#include <pushtarget.hpp>
#include <resources.hpp>
#include <templating.hpp>
#include <tempsensor.hpp>
@ -36,49 +37,36 @@ WebServerHandler myWebServerHandler; // My wrapper class fr webserver functions
extern bool sleepModeActive;
extern bool sleepModeAlwaysSkip;
//
// Callback from webServer when / has been accessed.
//
void WebServerHandler::webHandleDevice() {
LOG_PERF_START("webserver-api-device");
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
Log.verbose(F("WEB : webServer callback for /api/device." CR));
#endif
DynamicJsonDocument doc(100);
doc[PARAM_ID] = myConfig.getID();
doc[PARAM_APP_NAME] = CFG_APPNAME;
doc[PARAM_APP_VER] = CFG_APPVER;
doc[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());
LOG_PERF_STOP("webserver-api-device");
}
//
// Callback from webServer when / has been accessed.
//
void WebServerHandler::webHandleConfig() {
LOG_PERF_START("webserver-api-config");
Log.notice(F("WEB : webServer callback for /api/config." CR));
Log.notice(F("WEB : webServer callback for /api/config(get)." CR));
DynamicJsonDocument doc(CFG_JSON_BUFSIZE);
myConfig.createJson(doc);
doc[PARAM_PASS] = ""; // dont show the wifi password
double angle = myGyro.getAngle();
double angle = 0;
if (myGyro.hasValue()) angle = myGyro.getAngle();
double tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
double gravity = calculateGravity(angle, tempC);
doc[PARAM_ANGLE] = reduceFloatPrecision(angle);
doc[PARAM_GRAVITY_FORMAT] = String(myConfig.getGravityFormat());
// Format the adjustment so we get rid of rounding errors
if (myConfig.isTempF())
// We want the delta value (32F = 0C).
doc[PARAM_TEMP_ADJ] =
reduceFloatPrecision(convertCtoF(myConfig.getTempSensorAdjC()) - 32, 1);
else
doc[PARAM_TEMP_ADJ] = reduceFloatPrecision(myConfig.getTempSensorAdjC(), 1);
if (myConfig.isGravityTempAdj()) {
gravity =
gravityTemperatureCorrectionC(gravity, tempC, myConfig.getTempFormat());
@ -92,12 +80,23 @@ void WebServerHandler::webHandleConfig() {
doc[PARAM_BATTERY] = reduceFloatPrecision(myBatteryVoltage.getVoltage());
FloatHistoryLog runLog(RUNTIME_FILENAME);
doc[PARAM_RUNTIME_AVERAGE] = reduceFloatPrecision(
runLog.getAverage() ? runLog.getAverage() / 1000 : 0, 1);
#if defined(ESP8266)
doc[PARAM_PLATFORM] = "esp8266";
#else
doc[PARAM_PLATFORM] = "esp32";
#endif
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
Serial.print(CR);
#endif
String out;
out.reserve(CFG_JSON_BUFSIZE);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-config");
@ -108,15 +107,49 @@ void WebServerHandler::webHandleConfig() {
//
void WebServerHandler::webHandleUpload() {
LOG_PERF_START("webserver-api-upload");
Log.notice(F("WEB : webServer callback for /api/upload." CR));
DynamicJsonDocument doc(100);
Log.notice(F("WEB : webServer callback for /api/upload(get)." CR));
DynamicJsonDocument doc(300);
doc["index"] = checkHtmlFile(WebServerHandler::HTML_INDEX);
doc["device"] = checkHtmlFile(WebServerHandler::HTML_DEVICE);
doc["config"] = checkHtmlFile(WebServerHandler::HTML_CONFIG);
doc["calibration"] = checkHtmlFile(WebServerHandler::HTML_CALIBRATION);
doc["format"] = checkHtmlFile(WebServerHandler::HTML_FORMAT);
doc["about"] = checkHtmlFile(WebServerHandler::HTML_ABOUT);
doc["test"] = checkHtmlFile(WebServerHandler::HTML_TEST);
#if defined(ESP8266)
JsonArray files = doc.createNestedArray(PARAM_FILES);
// Show files in the filessytem at startup
FSInfo fs;
LittleFS.info(fs);
Dir dir = LittleFS.openDir("/");
while (dir.next()) {
JsonObject obj = files.createNestedObject();
obj[PARAM_FILE_NAME] = dir.fileName();
obj[PARAM_FILE_SIZE] = dir.fileSize();
}
#else // defined(ESP32)
JsonArray files = doc.createNestedArray(PARAM_FILES);
File dir = LittleFS.open("/");
while (true) {
File entry = dir.openNextFile();
if (!entry) {
// no more files
break;
}
if (!entry.isDirectory()) {
JsonObject obj = files.createNestedObject();
obj[PARAM_FILE_NAME] = entry.name();
obj[PARAM_FILE_SIZE] = entry.size();
}
entry.close();
}
dir.close();
#endif
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
@ -124,6 +157,7 @@ void WebServerHandler::webHandleUpload() {
#endif
String out;
out.reserve(300);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-upload");
@ -134,45 +168,106 @@ void WebServerHandler::webHandleUpload() {
//
void WebServerHandler::webHandleUploadFile() {
LOG_PERF_START("webserver-api-upload-file");
Log.notice(F("WEB : webServer callback for /api/upload/file." CR));
Log.verbose(F("WEB : webServer callback for /api/upload(post)." CR));
HTTPUpload& upload = _server->upload();
String f = upload.filename;
bool validFilename = false;
bool firmware = false;
if (f.equalsIgnoreCase("index.min.htm") ||
f.equalsIgnoreCase("device.min.htm") ||
f.equalsIgnoreCase("calibration.min.htm") ||
f.equalsIgnoreCase("config.min.htm") ||
f.equalsIgnoreCase("format.min.htm") ||
f.equalsIgnoreCase("test.min.htm") ||
f.equalsIgnoreCase("about.min.htm")) {
validFilename = true;
}
if (f.endsWith(".bin")) {
validFilename = true;
firmware = true;
}
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
Log.verbose(F("WEB : webServer callback for /api/upload, receiving file %s, "
"valid=%s." CR),
f.c_str(), validFilename ? "yes" : "no");
Log.verbose(
F("WEB : webServer callback for /api/upload, receiving file %s, %d(%d) "
"valid=%s, firmware=%s." CR),
f.c_str(), upload.currentSize, upload.totalSize,
validFilename ? "yes" : "no", firmware ? "yes" : "no");
#endif
if (upload.status == UPLOAD_FILE_START) {
Log.notice(F("WEB : Start upload." CR));
if (validFilename) _uploadFile = LittleFS.open(f, "w");
} else if (upload.status == UPLOAD_FILE_WRITE) {
Log.notice(F("WEB : Writing upload." CR));
if (_uploadFile)
_uploadFile.write(
upload.buf,
upload.currentSize); // Write the received bytes to the file
} else if (upload.status == UPLOAD_FILE_END) {
Log.notice(F("WEB : Finish upload." CR));
if (_uploadFile) {
_uploadFile.close();
Log.notice(F("WEB : File uploaded %d bytes." CR), upload.totalSize);
if (firmware) {
// Handle firmware update, hardcode since function return wrong value.
// (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
uint32_t maxSketchSpace = MAX_SKETCH_SPACE;
if (upload.status == UPLOAD_FILE_START) {
_uploadReturn = 200;
Log.notice(F("WEB : Start firmware upload, max sketch size %d kb." CR),
maxSketchSpace / 1024);
if (!Update.begin(maxSketchSpace, U_FLASH, PIN_LED)) {
ErrorFileLog errLog;
errLog.addEntry(
F("WEB : Not enough space to store for this firmware."));
_uploadReturn = 500;
}
} else if (upload.status == UPLOAD_FILE_WRITE) {
Log.notice(F("WEB : Writing firmware upload %d (%d)." CR),
upload.totalSize, maxSketchSpace);
if (upload.totalSize > maxSketchSpace) {
Log.error(F("WEB : Firmware file is to large." CR));
_uploadReturn = 500;
} else if (Update.write(upload.buf, upload.currentSize) !=
upload.currentSize) {
Log.warning(F("WEB : Firmware write was unsuccessful." CR));
_uploadReturn = 500;
}
} else if (upload.status == UPLOAD_FILE_END) {
Log.notice(F("WEB : Finish firmware upload." CR));
if (Update.end(true)) {
_server->send(200);
delay(500);
ESP_RESET();
} else {
ErrorFileLog errLog;
errLog.addEntry("WEB : Failed to finish firmware flashing error=" +
String(Update.getError()));
_uploadReturn = 500;
}
} else {
Update.end();
Log.notice(F("WEB : Firmware flashing aborted." CR));
_uploadReturn = 500;
}
_server->sendHeader("Location", "/");
_server->send(303);
delay(0);
} else {
_server->send(500, "text/plain", "Couldn't create file.");
// Handle HTML file upload
if (upload.status == UPLOAD_FILE_START) {
_uploadReturn = 200;
Log.notice(F("WEB : Start html upload." CR));
if (validFilename) _uploadFile = LittleFS.open(f, "w");
} else if (upload.status == UPLOAD_FILE_WRITE) {
Log.notice(F("WEB : Writing html upload." CR));
if (_uploadFile) _uploadFile.write(upload.buf, upload.currentSize);
} else if (upload.status == UPLOAD_FILE_END) {
Log.notice(F("WEB : Finish html upload." CR));
if (_uploadFile) {
_uploadFile.close();
Log.notice(F("WEB : Html file uploaded %d bytes." CR),
upload.totalSize);
}
_server->sendHeader("Location", "/");
_server->send(303);
} else {
_server->send(500, "text/plain", "Couldn't upload html file.");
}
}
LOG_PERF_STOP("webserver-api-upload-file");
}
@ -199,13 +294,21 @@ void WebServerHandler::webHandleCalibrate() {
//
// Callback from webServer when / has been accessed.
//
void WebServerHandler::webHandleFactoryReset() {
void WebServerHandler::webHandleFactoryDefaults() {
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/factory." CR));
if (!id.compareTo(myConfig.getID())) {
_server->send(200, "text/plain", "Doing reset...");
_server->send(200, "text/plain",
"Removing configuration and restarting...");
LittleFS.remove(CFG_FILENAME);
LittleFS.remove(CFG_HW_FILENAME);
LittleFS.remove(ERR_FILENAME);
LittleFS.remove(RUNTIME_FILENAME);
LittleFS.remove(TPL_FNAME_HTTP1);
LittleFS.remove(TPL_FNAME_HTTP2);
LittleFS.remove(TPL_FNAME_INFLUXDB);
LittleFS.remove(TPL_FNAME_MQTT);
LittleFS.end();
delay(500);
ESP_RESET();
@ -219,18 +322,21 @@ void WebServerHandler::webHandleFactoryReset() {
//
void WebServerHandler::webHandleStatus() {
LOG_PERF_START("webserver-api-status");
Log.notice(F("WEB : webServer callback for /api/status." CR));
Log.notice(F("WEB : webServer callback for /api/status(get)." CR));
DynamicJsonDocument doc(256);
DynamicJsonDocument doc(512);
double angle = 0;
if (myGyro.hasValue()) angle = myGyro.getAngle();
double angle = myGyro.getAngle();
double tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
double gravity = calculateGravity(angle, tempC);
doc[PARAM_ID] = myConfig.getID();
doc[PARAM_ANGLE] = reduceFloatPrecision(angle);
if (myConfig.isGravityTempAdj()) {
gravity = gravityTemperatureCorrectionC(gravity, tempC); //
gravity = gravityTemperatureCorrectionC(gravity, tempC);
}
if (myConfig.isGravityPlato()) {
doc[PARAM_GRAVITY] = reduceFloatPrecision(convertToPlato(gravity), 1);
@ -244,6 +350,24 @@ void WebServerHandler::webHandleStatus() {
doc[PARAM_GRAVITY_FORMAT] = String(myConfig.getGravityFormat());
doc[PARAM_SLEEP_MODE] = sleepModeAlwaysSkip;
doc[PARAM_RSSI] = WiFi.RSSI();
doc[PARAM_SLEEP_INTERVAL] = myConfig.getSleepInterval();
doc[PARAM_TOKEN] = myConfig.getToken();
doc[PARAM_TOKEN2] = myConfig.getToken2();
doc[PARAM_APP_VER] = CFG_APPVER;
doc[PARAM_APP_BUILD] = CFG_GITREV;
doc[PARAM_MDNS] = myConfig.getMDNS();
doc[PARAM_SSID] = WiFi.SSID();
FloatHistoryLog runLog(RUNTIME_FILENAME);
doc[PARAM_RUNTIME_AVERAGE] = reduceFloatPrecision(
runLog.getAverage() ? runLog.getAverage() / 1000 : 0, 1);
#if defined(ESP8266)
doc[PARAM_PLATFORM] = "esp8266";
#else
doc[PARAM_PLATFORM] = "esp32";
#endif
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
@ -251,6 +375,7 @@ void WebServerHandler::webHandleStatus() {
#endif
String out;
out.reserve(300);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-status");
@ -266,6 +391,11 @@ void WebServerHandler::webHandleClearWIFI() {
if (!id.compareTo(myConfig.getID())) {
_server->send(200, "text/plain",
"Clearing WIFI credentials and doing reset...");
myConfig.setWifiPass("", 0);
myConfig.setWifiSSID("", 0);
myConfig.setWifiPass("", 1);
myConfig.setWifiSSID("", 1);
myConfig.saveFile();
delay(1000);
WiFi.disconnect(); // Clear credentials
ESP_RESET();
@ -280,7 +410,7 @@ void WebServerHandler::webHandleClearWIFI() {
void WebServerHandler::webHandleStatusSleepmode() {
LOG_PERF_START("webserver-api-sleepmode");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/status/sleepmode." CR));
Log.notice(F("WEB : webServer callback for /api/status/sleepmode(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -308,7 +438,7 @@ void WebServerHandler::webHandleStatusSleepmode() {
void WebServerHandler::webHandleConfigDevice() {
LOG_PERF_START("webserver-api-config-device");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/device." CR));
Log.notice(F("WEB : webServer callback for /api/config/device(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -322,11 +452,14 @@ void WebServerHandler::webHandleConfigDevice() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setMDNS(_server->arg(PARAM_MDNS).c_str());
myConfig.setTempFormat(_server->arg(PARAM_TEMPFORMAT).charAt(0));
myConfig.setSleepInterval(_server->arg(PARAM_SLEEP_INTERVAL).c_str());
if (_server->hasArg(PARAM_MDNS))
myConfig.setMDNS(_server->arg(PARAM_MDNS).c_str());
if (_server->hasArg(PARAM_TEMPFORMAT))
myConfig.setTempFormat(_server->arg(PARAM_TEMPFORMAT).charAt(0));
if (_server->hasArg(PARAM_SLEEP_INTERVAL))
myConfig.setSleepInterval(_server->arg(PARAM_SLEEP_INTERVAL).c_str());
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseOne", true);
_server->sendHeader("Location", "/config.htm#collapseDevice", true);
_server->send(302, "text/plain", "Device config updated");
LOG_PERF_STOP("webserver-api-config-device");
}
@ -337,7 +470,7 @@ void WebServerHandler::webHandleConfigDevice() {
void WebServerHandler::webHandleConfigPush() {
LOG_PERF_START("webserver-api-config-push");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/push." CR));
Log.notice(F("WEB : webServer callback for /api/config/push(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -350,21 +483,47 @@ void WebServerHandler::webHandleConfigPush() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setHttpPushUrl(_server->arg(PARAM_PUSH_HTTP).c_str());
myConfig.setHttpPushUrl2(_server->arg(PARAM_PUSH_HTTP2).c_str());
myConfig.setBrewfatherPushUrl(_server->arg(PARAM_PUSH_BREWFATHER).c_str());
myConfig.setInfluxDb2PushUrl(_server->arg(PARAM_PUSH_INFLUXDB2).c_str());
myConfig.setInfluxDb2PushOrg(_server->arg(PARAM_PUSH_INFLUXDB2_ORG).c_str());
myConfig.setInfluxDb2PushBucket(
_server->arg(PARAM_PUSH_INFLUXDB2_BUCKET).c_str());
myConfig.setInfluxDb2PushToken(
_server->arg(PARAM_PUSH_INFLUXDB2_AUTH).c_str());
myConfig.setMqttUrl(_server->arg(PARAM_PUSH_MQTT).c_str());
myConfig.setMqttPort(_server->arg(PARAM_PUSH_MQTT_PORT).c_str());
myConfig.setMqttUser(_server->arg(PARAM_PUSH_MQTT_USER).c_str());
myConfig.setMqttPass(_server->arg(PARAM_PUSH_MQTT_PASS).c_str());
if (_server->hasArg(PARAM_TOKEN))
myConfig.setToken(_server->arg(PARAM_TOKEN).c_str());
if (_server->hasArg(PARAM_TOKEN2))
myConfig.setToken2(_server->arg(PARAM_TOKEN2).c_str());
if (_server->hasArg(PARAM_PUSH_HTTP))
myConfig.setHttpUrl(_server->arg(PARAM_PUSH_HTTP).c_str());
if (_server->hasArg(PARAM_PUSH_HTTP_H1))
myConfig.setHttpHeader(_server->arg(PARAM_PUSH_HTTP_H1).c_str(), 0);
if (_server->hasArg(PARAM_PUSH_HTTP_H2))
myConfig.setHttpHeader(_server->arg(PARAM_PUSH_HTTP_H2).c_str(), 1);
if (_server->hasArg(PARAM_PUSH_HTTP2))
myConfig.setHttp2Url(_server->arg(PARAM_PUSH_HTTP2).c_str());
if (_server->hasArg(PARAM_PUSH_HTTP2_H1))
myConfig.setHttp2Header(_server->arg(PARAM_PUSH_HTTP2_H1).c_str(), 0);
if (_server->hasArg(PARAM_PUSH_HTTP2_H2))
myConfig.setHttp2Header(_server->arg(PARAM_PUSH_HTTP2_H2).c_str(), 1);
if (_server->hasArg(PARAM_PUSH_HTTP3))
myConfig.setHttp3Url(_server->arg(PARAM_PUSH_HTTP3).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2))
myConfig.setInfluxDb2PushUrl(_server->arg(PARAM_PUSH_INFLUXDB2).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_ORG))
myConfig.setInfluxDb2PushOrg(
_server->arg(PARAM_PUSH_INFLUXDB2_ORG).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_BUCKET))
myConfig.setInfluxDb2PushBucket(
_server->arg(PARAM_PUSH_INFLUXDB2_BUCKET).c_str());
if (_server->hasArg(PARAM_PUSH_INFLUXDB2_AUTH))
myConfig.setInfluxDb2PushToken(
_server->arg(PARAM_PUSH_INFLUXDB2_AUTH).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT))
myConfig.setMqttUrl(_server->arg(PARAM_PUSH_MQTT).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT_PORT))
myConfig.setMqttPort(_server->arg(PARAM_PUSH_MQTT_PORT).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT_USER))
myConfig.setMqttUser(_server->arg(PARAM_PUSH_MQTT_USER).c_str());
if (_server->hasArg(PARAM_PUSH_MQTT_PASS))
myConfig.setMqttPass(_server->arg(PARAM_PUSH_MQTT_PASS).c_str());
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseTwo", true);
String section("/config.htm#");
section += _server->arg("section");
_server->sendHeader("Location", section.c_str(), true);
_server->send(302, "text/plain", "Push config updated");
LOG_PERF_STOP("webserver-api-config-push");
}
@ -394,7 +553,7 @@ String WebServerHandler::getRequestArguments() {
void WebServerHandler::webHandleConfigGravity() {
LOG_PERF_START("webserver-api-config-gravity");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/gravity." CR));
Log.notice(F("WEB : webServer callback for /api/config/gravity(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -408,13 +567,16 @@ void WebServerHandler::webHandleConfigGravity() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setGravityFormat(_server->arg(PARAM_GRAVITY_FORMAT).charAt(0));
myConfig.setGravityFormula(_server->arg(PARAM_GRAVITY_FORMULA).c_str());
myConfig.setGravityTempAdj(
_server->arg(PARAM_GRAVITY_TEMP_ADJ).equalsIgnoreCase("on") ? true
: false);
if (_server->hasArg(PARAM_GRAVITY_FORMAT))
myConfig.setGravityFormat(_server->arg(PARAM_GRAVITY_FORMAT).charAt(0));
if (_server->hasArg(PARAM_GRAVITY_FORMULA))
myConfig.setGravityFormula(_server->arg(PARAM_GRAVITY_FORMULA).c_str());
if (_server->hasArg(PARAM_GRAVITY_TEMP_ADJ))
myConfig.setGravityTempAdj(
_server->arg(PARAM_GRAVITY_TEMP_ADJ).equalsIgnoreCase("on") ? true
: false);
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseThree", true);
_server->sendHeader("Location", "/config.htm#collapseGravity", true);
_server->send(302, "text/plain", "Gravity config updated");
LOG_PERF_STOP("webserver-api-config-gravity");
}
@ -425,7 +587,7 @@ void WebServerHandler::webHandleConfigGravity() {
void WebServerHandler::webHandleConfigHardware() {
LOG_PERF_START("webserver-api-config-hardware");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/config/hardware." CR));
Log.notice(F("WEB : webServer callback for /api/config/hardware(post)." CR));
if (!id.equalsIgnoreCase(myConfig.getID())) {
Log.error(F("WEB : Wrong ID received %s, expected %s" CR), id.c_str(),
@ -439,34 +601,42 @@ void WebServerHandler::webHandleConfigHardware() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
myConfig.setVoltageFactor(_server->arg(PARAM_VOLTAGEFACTOR).toFloat());
if (myConfig.isTempC()) {
myConfig.setTempSensorAdjC(_server->arg(PARAM_TEMP_ADJ));
} else {
myConfig.setTempSensorAdjF(_server->arg(PARAM_TEMP_ADJ));
if (_server->hasArg(PARAM_VOLTAGEFACTOR))
myConfig.setVoltageFactor(_server->arg(PARAM_VOLTAGEFACTOR).toFloat());
if (_server->hasArg(PARAM_TEMP_ADJ)) {
if (myConfig.isTempC()) {
myConfig.setTempSensorAdjC(_server->arg(PARAM_TEMP_ADJ));
} else {
// Data is delta so we add 32 in order to conver to C.
myConfig.setTempSensorAdjF(_server->arg(PARAM_TEMP_ADJ), 32);
}
}
myConfig.setOtaURL(_server->arg(PARAM_OTA).c_str());
myConfig.setGyroTemp(
_server->arg(PARAM_GYRO_TEMP).equalsIgnoreCase("on") ? true : false);
if (_server->hasArg(PARAM_BLE))
myConfig.setColorBLE(_server->arg(PARAM_BLE).c_str());
if (_server->hasArg(PARAM_OTA))
myConfig.setOtaURL(_server->arg(PARAM_OTA).c_str());
if (_server->hasArg(PARAM_GYRO_TEMP))
myConfig.setGyroTemp(
_server->arg(PARAM_GYRO_TEMP).equalsIgnoreCase("on") ? true : false);
myConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseFour", true);
_server->sendHeader("Location", "/config.htm#collapseHardware", true);
_server->send(302, "text/plain", "Hardware config updated");
LOG_PERF_STOP("webserver-api-config-hardware");
}
//
// Update device parameters.
// Update advanced settings.
//
void WebServerHandler::webHandleDeviceParam() {
LOG_PERF_START("webserver-api-device-param");
void WebServerHandler::webHandleConfigAdvancedWrite() {
LOG_PERF_START("webserver-api-config-advanced");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/device/param." CR));
Log.notice(F("WEB : webServer callback for /api/config/advaced(post)." CR));
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.");
LOG_PERF_STOP("webserver-api-device-param");
LOG_PERF_STOP("webserver-api-config-advanced");
return;
}
@ -474,39 +644,82 @@ void WebServerHandler::webHandleDeviceParam() {
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
for (int i = 0; i < _server->args(); i++) {
String s = _server->arg(i);
if (_server->hasArg(PARAM_HW_GYRO_READ_COUNT))
myAdvancedConfig.setGyroReadCount(
_server->arg(PARAM_HW_GYRO_READ_COUNT).toInt());
// if (_server->hasArg(PARAM_HW_GYRO_READ_DELAY))
// myAdvancedConfig.setGyroReadDelay(
// _server->arg(PARAM_HW_GYRO_READ_DELAY).toInt());
if (_server->hasArg(PARAM_HW_GYRO_MOVING_THREASHOLD))
myAdvancedConfig.setGyroSensorMovingThreashold(
_server->arg(PARAM_HW_GYRO_MOVING_THREASHOLD).toInt());
if (_server->hasArg(PARAM_HW_FORMULA_DEVIATION))
myAdvancedConfig.setMaxFormulaCreationDeviation(
_server->arg(PARAM_HW_FORMULA_DEVIATION).toFloat());
if (_server->hasArg(PARAM_HW_FORMULA_CALIBRATION_TEMP))
myAdvancedConfig.SetDefaultCalibrationTemp(
_server->arg(PARAM_HW_FORMULA_CALIBRATION_TEMP).toFloat());
if (_server->hasArg(PARAM_HW_WIFI_PORTAL_TIMEOUT))
myAdvancedConfig.setWifiPortalTimeout(
_server->arg(PARAM_HW_WIFI_PORTAL_TIMEOUT).toInt());
if (_server->hasArg(PARAM_HW_WIFI_CONNECT_TIMEOUT))
myAdvancedConfig.setWifiConnectTimeout(
_server->arg(PARAM_HW_WIFI_CONNECT_TIMEOUT).toInt());
if (_server->hasArg(PARAM_HW_PUSH_TIMEOUT))
myAdvancedConfig.setPushTimeout(
_server->arg(PARAM_HW_PUSH_TIMEOUT).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_HTTP1))
myAdvancedConfig.setPushIntervalHttp1(
_server->arg(PARAM_HW_PUSH_INTERVAL_HTTP1).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_HTTP2))
myAdvancedConfig.setPushIntervalHttp2(
_server->arg(PARAM_HW_PUSH_INTERVAL_HTTP2).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_HTTP3))
myAdvancedConfig.setPushIntervalHttp3(
_server->arg(PARAM_HW_PUSH_INTERVAL_HTTP3).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_INFLUX))
myAdvancedConfig.setPushIntervalInflux(
_server->arg(PARAM_HW_PUSH_INTERVAL_INFLUX).toInt());
if (_server->hasArg(PARAM_HW_PUSH_INTERVAL_MQTT))
myAdvancedConfig.setPushIntervalMqtt(
_server->arg(PARAM_HW_PUSH_INTERVAL_MQTT).toInt());
if (_server->hasArg(PARAM_HW_TEMPSENSOR_RESOLUTION))
myAdvancedConfig.setTempSensorResolution(
_server->arg(PARAM_HW_TEMPSENSOR_RESOLUTION).toInt());
if (_server->argName(i).equalsIgnoreCase(PARAM_HW_GYRO_READ_COUNT))
myHardwareConfig.setGyroReadCount(s.toInt());
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_GYRO_READ_DELAY))
myHardwareConfig.setGyroReadDelay(s.toInt());
else if (_server->argName(i).equalsIgnoreCase(
PARAM_HW_GYRO_MOVING_THREASHOLD))
myHardwareConfig.setGyroSensorMovingThreashold(s.toInt());
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_FORMULA_DEVIATION))
myHardwareConfig.setMaxFormulaCreationDeviation(s.toFloat());
else if (_server->argName(i).equalsIgnoreCase(
PARAM_HW_FORMULA_CALIBRATION_TEMP))
myHardwareConfig.SetDefaultCalibrationTemp(s.toFloat());
else if (_server->argName(i).equalsIgnoreCase(PARAM_HW_WIFI_PORTALTIMEOUT))
myHardwareConfig.setWifiPortalTimeout(s.toInt());
}
myAdvancedConfig.saveFile();
_server->sendHeader("Location", "/config.htm#collapseAdvanced", true);
_server->send(302, "text/plain", "Advanced config updated");
LOG_PERF_STOP("webserver-api-config-advanced");
}
myHardwareConfig.saveFile();
//
// Read advanced settings
//
void WebServerHandler::webHandleConfigAdvancedRead() {
LOG_PERF_START("webserver-api-config-advanced");
Log.notice(F("WEB : webServer callback for /api/config/advanced(get)." CR));
// Return the current configuration.
DynamicJsonDocument doc(512);
doc[PARAM_HW_GYRO_READ_COUNT] = myHardwareConfig.getGyroReadCount();
doc[PARAM_HW_GYRO_READ_DELAY] = myHardwareConfig.getGyroReadDelay();
doc[PARAM_HW_GYRO_READ_COUNT] = myAdvancedConfig.getGyroReadCount();
// doc[PARAM_HW_GYRO_READ_DELAY] = myAdvancedConfig.getGyroReadDelay();
doc[PARAM_HW_GYRO_MOVING_THREASHOLD] =
myHardwareConfig.getGyroSensorMovingThreashold();
myAdvancedConfig.getGyroSensorMovingThreashold();
doc[PARAM_HW_FORMULA_DEVIATION] =
myHardwareConfig.getMaxFormulaCreationDeviation();
doc[PARAM_HW_WIFI_PORTALTIMEOUT] = myHardwareConfig.getWifiPortalTimeout();
myAdvancedConfig.getMaxFormulaCreationDeviation();
doc[PARAM_HW_WIFI_PORTAL_TIMEOUT] = myAdvancedConfig.getWifiPortalTimeout();
doc[PARAM_HW_WIFI_CONNECT_TIMEOUT] = myAdvancedConfig.getWifiConnectTimeout();
doc[PARAM_HW_PUSH_TIMEOUT] = myAdvancedConfig.getPushTimeout();
doc[PARAM_HW_FORMULA_CALIBRATION_TEMP] =
myHardwareConfig.getDefaultCalibrationTemp();
myAdvancedConfig.getDefaultCalibrationTemp();
doc[PARAM_HW_PUSH_INTERVAL_HTTP1] = myAdvancedConfig.getPushIntervalHttp1();
doc[PARAM_HW_PUSH_INTERVAL_HTTP2] = myAdvancedConfig.getPushIntervalHttp2();
doc[PARAM_HW_PUSH_INTERVAL_HTTP3] = myAdvancedConfig.getPushIntervalHttp3();
doc[PARAM_HW_PUSH_INTERVAL_INFLUX] = myAdvancedConfig.getPushIntervalInflux();
doc[PARAM_HW_PUSH_INTERVAL_MQTT] = myAdvancedConfig.getPushIntervalMqtt();
doc[PARAM_HW_TEMPSENSOR_RESOLUTION] =
myAdvancedConfig.getTempSensorResolution();
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
@ -514,9 +727,10 @@ void WebServerHandler::webHandleDeviceParam() {
#endif
String out;
out.reserve(512);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-device-param");
LOG_PERF_STOP("webserver-api-config-advanced");
}
//
@ -526,7 +740,7 @@ void WebServerHandler::webHandleFormulaRead() {
LOG_PERF_START("webserver-api-formula-read");
Log.notice(F("WEB : webServer callback for /api/formula(get)." CR));
DynamicJsonDocument doc(250);
DynamicJsonDocument doc(512);
const RawFormulaData& fd = myConfig.getFormulaData();
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
@ -559,6 +773,11 @@ void WebServerHandler::webHandleFormulaRead() {
doc["a3"] = reduceFloatPrecision(fd.a[2], 2);
doc["a4"] = reduceFloatPrecision(fd.a[3], 2);
doc["a5"] = reduceFloatPrecision(fd.a[4], 2);
doc["a6"] = reduceFloatPrecision(fd.a[5], 2);
doc["a7"] = reduceFloatPrecision(fd.a[6], 2);
doc["a8"] = reduceFloatPrecision(fd.a[7], 2);
doc["a9"] = reduceFloatPrecision(fd.a[8], 2);
doc["a10"] = reduceFloatPrecision(fd.a[9], 2);
if (myConfig.isGravityPlato()) {
doc["g1"] = reduceFloatPrecision(convertToPlato(fd.g[0]), 1);
@ -566,12 +785,22 @@ void WebServerHandler::webHandleFormulaRead() {
doc["g3"] = reduceFloatPrecision(convertToPlato(fd.g[2]), 1);
doc["g4"] = reduceFloatPrecision(convertToPlato(fd.g[3]), 1);
doc["g5"] = reduceFloatPrecision(convertToPlato(fd.g[4]), 1);
doc["g6"] = reduceFloatPrecision(convertToPlato(fd.g[5]), 1);
doc["g7"] = reduceFloatPrecision(convertToPlato(fd.g[6]), 1);
doc["g8"] = reduceFloatPrecision(convertToPlato(fd.g[7]), 1);
doc["g9"] = reduceFloatPrecision(convertToPlato(fd.g[8]), 1);
doc["g10"] = reduceFloatPrecision(convertToPlato(fd.g[9]), 1);
} else {
doc["g1"] = reduceFloatPrecision(fd.g[0], 4);
doc["g2"] = reduceFloatPrecision(fd.g[1], 4);
doc["g3"] = reduceFloatPrecision(fd.g[2], 4);
doc["g4"] = reduceFloatPrecision(fd.g[3], 4);
doc["g5"] = reduceFloatPrecision(fd.g[4], 4);
doc["g6"] = reduceFloatPrecision(fd.g[5], 4);
doc["g7"] = reduceFloatPrecision(fd.g[6], 4);
doc["g8"] = reduceFloatPrecision(fd.g[7], 4);
doc["g9"] = reduceFloatPrecision(fd.g[8], 4);
doc["g10"] = reduceFloatPrecision(fd.g[9], 4);
}
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
@ -580,6 +809,7 @@ void WebServerHandler::webHandleFormulaRead() {
#endif
String out;
out.reserve(256);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-formula-read");
@ -611,28 +841,95 @@ void WebServerHandler::webHandleConfigFormatWrite() {
success = writeFile(TPL_FNAME_HTTP1, _server->arg(PARAM_FORMAT_HTTP1));
} else if (_server->hasArg(PARAM_FORMAT_HTTP2)) {
success = writeFile(TPL_FNAME_HTTP2, _server->arg(PARAM_FORMAT_HTTP2));
} else if (_server->hasArg(PARAM_FORMAT_HTTP3)) {
success = writeFile(TPL_FNAME_HTTP3, _server->arg(PARAM_FORMAT_HTTP3));
} else if (_server->hasArg(PARAM_FORMAT_INFLUXDB)) {
success =
writeFile(TPL_FNAME_INFLUXDB, _server->arg(PARAM_FORMAT_INFLUXDB));
} else if (_server->hasArg(PARAM_FORMAT_MQTT)) {
success = writeFile(TPL_FNAME_MQTT, _server->arg(PARAM_FORMAT_MQTT));
}
/*else if (_server->hasArg(PARAM_FORMAT_BREWFATHER)) {
success = writeFile(TPL_FNAME_BREWFATHER,
_server->arg(PARAM_FORMAT_BREWFATHER));
}*/
if (success) {
_server->sendHeader("Location", "/format.htm", true);
_server->send(302, "text/plain", "Format updated");
} else {
Log.error(F("WEB : Unable to store format file" CR));
ErrorFileLog errLog;
errLog.addEntry(F("WEB : Unable to store format file"));
_server->send(400, "text/plain", "Unable to store format in file.");
}
LOG_PERF_STOP("webserver-api-config-format-write");
}
//
// Get format with real data
//
void WebServerHandler::webHandleTestPush() {
LOG_PERF_START("webserver-api-test-push");
String id = _server->arg(PARAM_ID);
Log.notice(F("WEB : webServer callback for /api/test/push." CR));
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.");
LOG_PERF_STOP("webserver-api-test-push");
return;
}
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
Log.verbose(F("WEB : %s." CR), getRequestArguments().c_str());
#endif
float angle = myGyro.getAngle();
float tempC = myTempSensor.getTempC(myConfig.isGyroTemp());
float gravitySG = calculateGravity(angle, tempC);
float corrGravitySG = gravityTemperatureCorrectionC(gravitySG, tempC);
TemplatingEngine engine;
engine.initialize(angle, gravitySG, corrGravitySG, tempC, 2.1);
const String& type = _server->arg(PARAM_PUSH_FORMAT);
PushTarget push;
bool enabled = false;
if (!type.compareTo(PARAM_FORMAT_HTTP1) && myConfig.isHttpActive()) {
push.sendHttp1(engine, myConfig.isHttpSSL());
enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_HTTP2) && myConfig.isHttp2Active()) {
push.sendHttp2(engine, myConfig.isHttp2SSL());
enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_HTTP3) && myConfig.isHttp3Active()) {
push.sendHttp3(engine, myConfig.isHttp3SSL());
enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_INFLUXDB) &&
myConfig.isInfluxDb2Active()) {
push.sendInfluxDb2(engine, myConfig.isInfluxSSL());
enabled = true;
} else if (!type.compareTo(PARAM_FORMAT_MQTT) && myConfig.isMqttActive()) {
push.sendMqtt(engine, myConfig.isMqttSSL());
enabled = true;
}
DynamicJsonDocument doc(100);
doc[PARAM_PUSH_ENABLED] = enabled;
doc[PARAM_PUSH_SUCCESS] = push.getLastSuccess();
doc[PARAM_PUSH_CODE] = push.getLastCode();
String out;
out.reserve(100);
serializeJson(doc, out);
#if LOG_LEVEL == 6 && !defined(WEB_DISABLE_LOGGING)
serializeJson(doc, Serial);
Serial.print(CR);
#endif
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-test-push");
}
//
// Write file to disk, if there is no data then delete the current file (if it
// exists) = reset to default.
@ -701,11 +998,11 @@ void WebServerHandler::webHandleConfigFormatRead() {
else
doc[PARAM_FORMAT_HTTP2] = urlencode(String(&iSpindleFormat[0]));
/*s = readFile(TPL_FNAME_BREWFATHER);
s = readFile(TPL_FNAME_HTTP3);
if (s.length())
doc[PARAM_FORMAT_BREWFATHER] = urlencode(s);
doc[PARAM_FORMAT_HTTP3] = urlencode(s);
else
doc[PARAM_FORMAT_BREWFATHER] = urlencode(&brewfatherFormat[0]);*/
doc[PARAM_FORMAT_HTTP3] = urlencode(String(&iHttpGetFormat[0]));
s = readFile(TPL_FNAME_INFLUXDB);
if (s.length())
@ -725,6 +1022,7 @@ void WebServerHandler::webHandleConfigFormatRead() {
#endif
String out;
out.reserve(2048);
serializeJson(doc, out);
_server->send(200, "application/json", out.c_str());
LOG_PERF_STOP("webserver-api-config-format-read");
@ -756,6 +1054,11 @@ void WebServerHandler::webHandleFormulaWrite() {
fd.a[2] = _server->arg("a3").toDouble();
fd.a[3] = _server->arg("a4").toDouble();
fd.a[4] = _server->arg("a5").toDouble();
fd.a[5] = _server->arg("a6").toDouble();
fd.a[6] = _server->arg("a7").toDouble();
fd.a[7] = _server->arg("a8").toDouble();
fd.a[8] = _server->arg("a9").toDouble();
fd.a[9] = _server->arg("a10").toDouble();
if (myConfig.isGravityPlato()) {
fd.g[0] = convertToSG(_server->arg("g1").toDouble());
@ -763,12 +1066,22 @@ void WebServerHandler::webHandleFormulaWrite() {
fd.g[2] = convertToSG(_server->arg("g3").toDouble());
fd.g[3] = convertToSG(_server->arg("g4").toDouble());
fd.g[4] = convertToSG(_server->arg("g5").toDouble());
fd.g[5] = convertToSG(_server->arg("g6").toDouble());
fd.g[6] = convertToSG(_server->arg("g7").toDouble());
fd.g[7] = convertToSG(_server->arg("g8").toDouble());
fd.g[8] = convertToSG(_server->arg("g9").toDouble());
fd.g[9] = convertToSG(_server->arg("g10").toDouble());
} else {
fd.g[0] = _server->arg("g1").toDouble();
fd.g[1] = _server->arg("g2").toDouble();
fd.g[2] = _server->arg("g3").toDouble();
fd.g[3] = _server->arg("g4").toDouble();
fd.g[4] = _server->arg("g5").toDouble();
fd.g[5] = _server->arg("g6").toDouble();
fd.g[6] = _server->arg("g7").toDouble();
fd.g[7] = _server->arg("g8").toDouble();
fd.g[8] = _server->arg("g9").toDouble();
fd.g[9] = _server->arg("g10").toDouble();
}
myConfig.setFormulaData(fd);
@ -818,8 +1131,6 @@ const char* WebServerHandler::getHtmlFileName(HtmlFile item) {
switch (item) {
case HTML_INDEX:
return "index.min.htm";
case HTML_DEVICE:
return "device.min.htm";
case HTML_CONFIG:
return "config.min.htm";
case HTML_CALIBRATION:
@ -828,6 +1139,8 @@ const char* WebServerHandler::getHtmlFileName(HtmlFile item) {
return "format.min.htm";
case HTML_ABOUT:
return "about.min.htm";
case HTML_TEST:
return "test.min.htm";
}
return "";
@ -873,8 +1186,6 @@ bool WebServerHandler::setupWebServer() {
_server->on("/", std::bind(&WebServerHandler::webReturnIndexHtm, this));
_server->on("/index.htm",
std::bind(&WebServerHandler::webReturnIndexHtm, this));
_server->on("/device.htm",
std::bind(&WebServerHandler::webReturnDeviceHtm, this));
_server->on("/config.htm",
std::bind(&WebServerHandler::webReturnConfigHtm, this));
_server->on("/calibration.htm",
@ -883,6 +1194,8 @@ bool WebServerHandler::setupWebServer() {
std::bind(&WebServerHandler::webReturnFormatHtm, this));
_server->on("/about.htm",
std::bind(&WebServerHandler::webReturnAboutHtm, this));
_server->on("/test.htm",
std::bind(&WebServerHandler::webReturnTestHtm, this));
#else
// Show files in the filessytem at startup
@ -898,16 +1211,16 @@ bool WebServerHandler::setupWebServer() {
// Check if the html files exist, if so serve them, else show the static
// upload page.
if (checkHtmlFile(HTML_INDEX) && checkHtmlFile(HTML_DEVICE) &&
checkHtmlFile(HTML_CONFIG) && checkHtmlFile(HTML_CALIBRATION) &&
checkHtmlFile(HTML_FORMAT) && checkHtmlFile(HTML_ABOUT)) {
if (checkHtmlFile(HTML_INDEX) && checkHtmlFile(HTML_CONFIG) &&
checkHtmlFile(HTML_CALIBRATION) && checkHtmlFile(HTML_FORMAT) &&
checkHtmlFile(HTML_ABOUT) && checkHtmlFile(HTML_TEST)) {
Log.notice(F("WEB : All html files exist, starting in normal mode." CR));
_server->serveStatic("/", LittleFS, "/index.min.htm");
_server->serveStatic("/index.htm", LittleFS, "/index.min.htm");
_server->serveStatic("/device.htm", LittleFS, "/device.min.htm");
_server->serveStatic("/config.htm", LittleFS, "/config.min.htm");
_server->serveStatic("/about.htm", LittleFS, "/about.min.htm");
_server->serveStatic("/test.htm", LittleFS, "/test.min.htm");
_server->serveStatic("/calibration.htm", LittleFS, "/calibration.min.htm");
_server->serveStatic("/format.htm", LittleFS, "/format.min.htm");
@ -920,14 +1233,15 @@ bool WebServerHandler::setupWebServer() {
_server->on("/", std::bind(&WebServerHandler::webReturnUploadHtm, this));
}
#endif
_server->on("/firmware.htm",
std::bind(&WebServerHandler::webReturnFirmwareHtm, this));
_server->serveStatic("/log", LittleFS, ERR_FILENAME);
_server->serveStatic("/runtime", LittleFS, RUNTIME_FILENAME);
// Dynamic content
_server->on(
"/api/config", HTTP_GET,
std::bind(&WebServerHandler::webHandleConfig, this)); // Get config.json
_server->on(
"/api/device", HTTP_GET,
std::bind(&WebServerHandler::webHandleDevice, this)); // Get device.json
_server->on("/api/formula", HTTP_GET,
std::bind(&WebServerHandler::webHandleFormulaRead,
this)); // Get formula.json (calibration page)
@ -938,7 +1252,7 @@ bool WebServerHandler::setupWebServer() {
std::bind(&WebServerHandler::webHandleCalibrate,
this)); // Run calibration routine (param id)
_server->on("/api/factory", HTTP_GET,
std::bind(&WebServerHandler::webHandleFactoryReset,
std::bind(&WebServerHandler::webHandleFactoryDefaults,
this)); // Reset the device
_server->on("/api/status", HTTP_GET,
std::bind(&WebServerHandler::webHandleStatus,
@ -975,9 +1289,15 @@ bool WebServerHandler::setupWebServer() {
_server->on("/api/config/format", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigFormatWrite,
this)); // Change template formats
_server->on("/api/device/param", HTTP_GET,
std::bind(&WebServerHandler::webHandleDeviceParam,
this)); // Change device params
_server->on("/api/config/advanced", HTTP_GET,
std::bind(&WebServerHandler::webHandleConfigAdvancedRead,
this)); // Read advanced settings
_server->on("/api/config/advanced", HTTP_POST,
std::bind(&WebServerHandler::webHandleConfigAdvancedWrite,
this)); // Change advanced params
_server->on("/api/test/push", HTTP_GET,
std::bind(&WebServerHandler::webHandleTestPush,
this)); //
_server->onNotFound(
std::bind(&WebServerHandler::webHandlePageNotFound, this));

View File

@ -24,52 +24,58 @@ SOFTWARE.
#ifndef SRC_WEBSERVER_HPP_
#define SRC_WEBSERVER_HPP_
#if defined (ESP8266)
#if defined(ESP8266)
#include <ESP8266WebServer.h>
#include <ESP8266WiFi.h>
#include <ESP8266mDNS.h>
#else // defined (ESP32)
#define MAX_SKETCH_SPACE 1044464
#else // defined (ESP32)
#include <ESPmDNS.h>
#include <WebServer.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#include <Update.h>
#define MAX_SKETCH_SPACE 1835008
#endif
#include <incbin.h>
#if defined(EMBED_HTML)
INCBIN_EXTERN(IndexHtm);
INCBIN_EXTERN(DeviceHtm);
INCBIN_EXTERN(ConfigHtm);
INCBIN_EXTERN(CalibrationHtm);
INCBIN_EXTERN(FormatHtm);
INCBIN_EXTERN(TestHtm);
INCBIN_EXTERN(AboutHtm);
#else
INCBIN_EXTERN(UploadHtm);
#endif
INCBIN_EXTERN(FirmwareHtm);
class WebServerHandler {
private:
ESP8266WebServer* _server = 0;
File _uploadFile;
int _lastFormulaCreateError = 0;
int _uploadReturn = 200;
void webHandleConfig();
void webHandleFormulaWrite();
void webHandleFormulaRead();
void webHandleConfigAdvancedRead();
void webHandleConfigAdvancedWrite();
void webHandleConfigHardware();
void webHandleConfigGravity();
void webHandleConfigPush();
void webHandleConfigDevice();
void webHandleConfigFormatRead();
void webHandleConfigFormatWrite();
void webHandleTestPush();
void webHandleStatusSleepmode();
void webHandleClearWIFI();
void webHandleStatus();
void webHandleFactoryReset();
void webHandleFactoryDefaults();
void webHandleCalibrate();
void webHandleUploadFile();
void webHandleUpload();
void webHandleDevice();
void webHandleDeviceParam();
void webHandlePageNotFound();
String readFile(String fname);
@ -78,16 +84,12 @@ class WebServerHandler {
String getRequestArguments();
// Inline functions.
void webReturnOK() { _server->send(200); }
void webReturnOK() { _server->send(_uploadReturn); }
#if defined(EMBED_HTML)
void webReturnIndexHtm() {
_server->send_P(200, "text/html", (const char*)gIndexHtmData,
gIndexHtmSize);
}
void webReturnDeviceHtm() {
_server->send_P(200, "text/html", (const char*)gDeviceHtmData,
gDeviceHtmSize);
}
void webReturnConfigHtm() {
_server->send_P(200, "text/html", (const char*)gConfigHtmData,
gConfigHtmSize);
@ -104,21 +106,28 @@ class WebServerHandler {
_server->send_P(200, "text/html", (const char*)gAboutHtmData,
gAboutHtmSize);
}
void webReturnTestHtm() {
_server->send_P(200, "text/html", (const char*)gTestHtmData, gTestHtmSize);
}
#else
void webReturnUploadHtm() {
_server->send_P(200, "text/html", (const char*)gUploadHtmData,
gUploadHtmSize);
}
#endif
void webReturnFirmwareHtm() {
_server->send_P(200, "text/html", (const char*)gFirmwareHtmData,
gFirmwareHtmSize);
}
public:
enum HtmlFile {
HTML_INDEX = 0,
HTML_DEVICE = 1,
HTML_CONFIG = 2,
HTML_ABOUT = 3,
HTML_CALIBRATION = 4,
HTML_FORMAT = 5
HTML_CONFIG = 1,
HTML_ABOUT = 2,
HTML_CALIBRATION = 3,
HTML_FORMAT = 4,
HTML_TEST = 5
};
bool setupWebServer();

View File

@ -21,27 +21,24 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
#if defined (ESP8266)
#include <ESP8266HTTPClient.h>
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266httpUpdate.h>
#else // defined (ESP32)
#include <WiFi.h>
#include <HTTPClient.h>
#else // defined (ESP32)
#include <HTTPUpdate.h>
#include <WiFi.h>
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#endif
#include <incbin.h>
#include <config.hpp>
#include <main.hpp>
#include <wifi.hpp>
// Settings for DRD
#if defined (ESP8266)
#define ESP_DRD_USE_LITTLEFS true
#define ESP_DRD_USE_SPIFFS false
#else // defined (ESP32)
#define ESP_DRD_USE_LITTLEFS false
#define ESP_DRD_USE_SPIFFS true
#endif
#define ESP_DRD_USE_EEPROM false
#include <ESP_DoubleResetDetector.h>
#define DRD_TIMEOUT 3
@ -53,22 +50,10 @@ SOFTWARE.
#define USING_CORS_FEATURE false
#define NUM_WIFI_CREDENTIALS 1
#define USE_STATIC_IP_CONFIG_IN_CP false
#define _WIFIMGR_LOGLEVEL_ 3
#include <ESP_WiFiManager.h>
// Override the look and feel of the standard ui (hide secondary forms)
const char WM_HTTP_FORM_START[] PROGMEM =
"<form method='get' "
"action='wifisave'><fieldset><div><label>SSID</label><input id='s' "
"name='s' length=32 "
"placeholder='SSID'><div></div></div><div><label>Password</label><input "
"id='p' name='p' length=64 placeholder='password'><div></div></div><div "
"hidden><label>SSID1</label><input id='s1' name='s1' length=32 "
"placeholder='SSID1'><div></div></div><div "
"hidden><label>Password</label><input id='p1' name='p1' length=64 "
"placeholder='password1'><div></div></div></fieldset>";
#include <ESP_WiFiManager-Impl.h>
ESP_WiFiManager *myWifiManager;
DoubleResetDetector *myDRD;
WifiConnection myWifi;
const char *userSSID = USER_SSID;
@ -85,15 +70,21 @@ void WifiConnection::init() {
// Check if we have a valid wifi configuration
//
bool WifiConnection::hasConfig() {
if (strlen(myConfig.getWifiSSID())) return true;
if (strlen(myConfig.getWifiSSID(0))) return true;
if (strlen(userSSID)) return true;
// Check if there are stored WIFI Settings we can use.
// Check if there are stored WIFI Settings we can use.
#if defined(ESP32)
#warning \
"Cant read SSID on ESP32 until a connection has been made, this part will not work, change to WifiManager"
#endif
String ssid = WiFi.SSID();
if (ssid.length()) {
Log.notice(F("WIFI: Found credentials in EEPORM." CR));
myConfig.setWifiSSID(WiFi.SSID());
myConfig.setWifiPass(WiFi.psk());
myConfig.setWifiSSID(ssid, 0);
if (WiFi.psk().length()) myConfig.setWifiPass(WiFi.psk(), 0);
myConfig.saveFile();
return true;
}
@ -137,18 +128,37 @@ void WifiConnection::startPortal() {
myWifiManager->setMinimumSignalQuality(-1);
myWifiManager->setConfigPortalChannel(0);
myWifiManager->setConfigPortalTimeout(
myHardwareConfig.getWifiPortalTimeout());
myAdvancedConfig.getWifiPortalTimeout());
if (myWifiManager->startConfigPortal(WIFI_DEFAULT_SSID, WIFI_DEFAULT_PWD)) {
Log.notice(F("WIFI: Exited portal, connected to wifi. Rebooting..." CR));
myConfig.setWifiSSID(myWifiManager->getSSID());
myConfig.setWifiPass(myWifiManager->getPW());
String mdns("<p>Default mDNS name is: http://");
mdns += myConfig.getMDNS();
mdns += ".local<p>";
ESP_WMParameter deviceName(mdns.c_str());
myWifiManager->addParameter(&deviceName);
myWifiManager->startConfigPortal(WIFI_DEFAULT_SSID, WIFI_DEFAULT_PWD);
if (myWifiManager->getSSID(0).length()) {
myConfig.setWifiSSID(myWifiManager->getSSID(0), 0);
myConfig.setWifiPass(myWifiManager->getPW(0), 0);
myConfig.setWifiSSID(myWifiManager->getSSID(1), 1);
myConfig.setWifiPass(myWifiManager->getPW(1), 1);
// If the same SSID has been used, lets delete the second
if (!strcmp(myConfig.getWifiSSID(0), myConfig.getWifiSSID(1))) {
myConfig.setWifiSSID("", 1);
myConfig.setWifiPass("", 1);
}
Log.notice(F("WIFI: Stored SSID1:'%s' SSID2:'%s'" CR),
myConfig.getWifiSSID(0), myConfig.getWifiSSID(1));
myConfig.saveFile();
} else {
Log.notice(
F("WIFI: Exited portal, no connection to wifi. Rebooting..." CR));
F("WIFI: Could not find first SSID so assuming we got a timeout." CR));
}
Log.notice(F("WIFI: Exited wifi config portal. Rebooting..." CR));
stopDoubleReset();
delay(500);
ESP_RESET();
@ -162,7 +172,7 @@ void WifiConnection::loop() { myDRD->loop(); }
//
// Connect to last known access point, non blocking mode.
//
void WifiConnection::connectAsync() {
void WifiConnection::connectAsync(int wifiIndex) {
WiFi.persistent(true);
WiFi.mode(WIFI_STA);
if (strlen(userSSID)) {
@ -170,9 +180,10 @@ void WifiConnection::connectAsync() {
userSSID);
WiFi.begin(userSSID, userPWD);
} else {
Log.notice(F("WIFI: Connecting to wifi using stored settings %s." CR),
myConfig.getWifiSSID());
WiFi.begin(myConfig.getWifiSSID(), myConfig.getWifiPass());
Log.notice(F("WIFI: Connecting to wifi (%d) using stored settings %s." CR),
wifiIndex, myConfig.getWifiSSID(wifiIndex));
WiFi.begin(myConfig.getWifiSSID(wifiIndex),
myConfig.getWifiPass(wifiIndex));
}
}
@ -191,25 +202,100 @@ bool WifiConnection::waitForConnection(int maxTime) {
if (i++ >
(maxTime * 10)) { // Try for maxTime seconds. Since delay is 100ms.
Log.error(F("WIFI: Failed to connect to wifi %d, aborting %s." CR),
WiFi.status(), getIPAddress().c_str());
ErrorFileLog errLog;
errLog.addEntry("WIFI: Failed to connect to wifi " +
String(WiFi.status()));
WiFi.disconnect();
Serial.print(CR);
return false; // Return to main that we have failed to connect.
}
}
Serial.print(CR);
Log.notice(F("WIFI: Connected to wifi ip=%s." CR), getIPAddress().c_str());
Log.notice(F("WIFI: Connected to wifi %s ip=%s." CR), WiFi.SSID().c_str(),
getIPAddress().c_str());
Log.notice(F("WIFI: Using mDNS name %s." CR), myConfig.getMDNS());
return true;
}
//
// Check what network is the strongest.
//
int WifiConnection::findStrongestNetwork() {
if (!myConfig.dualWifiConfigured()) {
Log.notice(F("WIFI: Only one wifi SSID is configured, skipping scan." CR));
return -1;
}
Log.notice(F("WIFI: Scanning for wifi." CR));
int noNetwork = WiFi.scanNetworks(false, false);
int strenght[2] = {-100, -100};
for (int i = 0; i < noNetwork; i++) {
int rssi = WiFi.RSSI(i);
String ssid = WiFi.SSID(i);
if (ssid.compareTo(myConfig.getWifiSSID(0)))
strenght[0] = rssi;
else if (ssid.compareTo(myConfig.getWifiSSID(1)))
strenght[1] = rssi;
#if LOG_LEVEL == 6
Log.verbose(F("WIFI: Found %s %d." CR), ssid.c_str(), rssi);
#endif
}
if (strenght[0] == -100 && strenght[1] == -100)
return -1; // None of the stored networks can be seen
if (strenght[0] >= strenght[1])
return 0; // First network is strongest or they have equal strength
return 1; // Second network is the strongest
}
//
// Connect to last known access point, blocking mode.
//
bool WifiConnection::connect() {
connectAsync();
return waitForConnection(20); // 20 seconds.
/*
// Alternative code for connecting to strongest wifi.
// Takes approximatly 2 seconds to scan for available network
int i = findStrongestNetwork();
if (i != -1) {
Log.notice(F("WIFI: Wifi %d:'%s' is strongest." CR), i,
myConfig.getWifiSSID(i)); } else { i = 0; // Use first SSID as default.
}
connectAsync(i);
return waitForConnection(myAdvancedConfig.getWifiConnectTimeout());
*/
// Alternative code for using seconday wifi settings as fallback.
// If success to seconday is successful this is used as standard
int timeout = myAdvancedConfig.getWifiConnectTimeout();
connectAsync(0);
if (!waitForConnection(timeout)) {
Log.warning(F("WIFI: Failed to connect to first SSID %s." CR),
myConfig.getWifiSSID(0));
connectAsync(1);
if (waitForConnection(timeout)) {
Log.notice(
F("WIFI: Connected to second SSID %s, making secondary default." CR),
myConfig.getWifiSSID(1));
myConfig.swapPrimaryWifi();
myConfig.saveFile();
return true;
}
Log.warning(F("WIFI: Failed to connect to any SSID." CR));
return false;
}
return true;
}
//
@ -236,67 +322,62 @@ bool WifiConnection::updateFirmware() {
Log.verbose(F("WIFI: Updating firmware." CR));
#endif
String serverPath = myConfig.getOtaURL();
serverPath += "firmware.bin";
WiFiClient wifi;
WiFiClientSecure wifiSecure;
HTTPUpdateResult ret;
String serverPath = myConfig.getOtaURL();
#if defined(ESP8266)
serverPath += "firmware.bin";
#else // defined (ESP32)
serverPath += "firmware32.bin";
#endif
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
wifiSecure.setInsecure();
Log.notice(F("WIFI: OTA, SSL enabled without validation." CR));
ret = ESPhttpUpdate.update(myWifi.getWifiClientSecure(), serverPath);
ret = ESPhttpUpdate.update(wifiSecure, serverPath);
} else {
ret = ESPhttpUpdate.update(myWifi.getWifiClient(), serverPath);
ret = ESPhttpUpdate.update(wifi, serverPath);
}
switch (ret) {
case HTTP_UPDATE_FAILED:
Log.error(F("WIFI: OTA update failed %d, %s." CR),
ESPhttpUpdate.getLastError(),
ESPhttpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_FAILED: {
ErrorFileLog errLog;
errLog.addEntry("WIFI: OTA update failed " +
String(ESPhttpUpdate.getLastError()));
} break;
case HTTP_UPDATE_NO_UPDATES:
break;
case HTTP_UPDATE_OK:
case HTTP_UPDATE_OK: {
Log.notice("WIFI: OTA Update sucesfull, rebooting.");
delay(100);
ESP_RESET();
break;
}
}
return false;
}
//
// Download and save file
//
void WifiConnection::downloadFile(const char *fname) {
void WifiConnection::downloadFile(HTTPClient &http, String &fname) {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Download file %s." CR), fname);
#endif
HTTPClient http;
String serverPath = myConfig.getOtaURL();
serverPath += fname;
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
Log.notice(F("WIFI: OTA, SSL enabled without validation." CR));
http.begin(myWifi.getWifiClientSecure(), serverPath);
} else {
http.begin(myWifi.getWifiClient(), serverPath);
}
int httpResponseCode = http.GET();
if (httpResponseCode == 200) {
File f = LittleFS.open(fname, "w");
http.writeToStream(&f);
f.close();
Log.notice(F("WIFI: Downloaded file %s." CR), fname);
Log.notice(F("WIFI: Downloaded file %s." CR), fname.c_str());
} else {
Log.error(F("WIFI: Failed to download file, respone=%d" CR),
httpResponseCode);
ErrorFileLog errLog;
errLog.addEntry("WIFI: Failed to download html-file " +
String(httpResponseCode));
}
http.end();
myWifi.closeWifiClient();
}
//
@ -306,20 +387,23 @@ bool WifiConnection::checkFirmwareVersion() {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Checking if new version exist." CR));
#endif
WiFiClient wifi;
WiFiClientSecure wifiSecure;
HTTPClient http;
String serverPath = myConfig.getOtaURL();
serverPath += "version.json";
// Your Domain name with URL path or IP address with path
if (serverPath.startsWith("https://")) {
myWifi.getWifiClientSecure().setInsecure();
if (myConfig.isOtaSSL()) {
wifiSecure.setInsecure();
Log.notice(F("WIFI: OTA, SSL enabled without validation." CR));
http.begin(myWifi.getWifiClientSecure(), serverPath);
http.begin(wifiSecure, serverPath);
} else {
http.begin(myWifi.getWifiClient(), serverPath);
http.begin(wifi, serverPath);
}
// Send HTTP GET request
DynamicJsonDocument ver(300);
int httpResponseCode = http.GET();
if (httpResponseCode == 200) {
@ -329,10 +413,10 @@ bool WifiConnection::checkFirmwareVersion() {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Payload %s." CR), payload.c_str());
#endif
DynamicJsonDocument ver(300);
DeserializationError err = deserializeJson(ver, payload);
if (err) {
Log.error(F("WIFI: Failed to parse version.json, %s" CR), err);
ErrorFileLog errLog;
errLog.addEntry(F("WIFI: Failed to parse version.json"));
} else {
#if LOG_LEVEL == 6 && !defined(WIFI_DISABLE_LOGGING)
Log.verbose(F("WIFI: Project %s version %s." CR),
@ -351,25 +435,26 @@ bool WifiConnection::checkFirmwareVersion() {
// Compare major version
if (newVer[0] > curVer[0]) _newFirmware = true;
// Compare minor version
if (newVer[0] == curVer[0] && newVer[1] > curVer[1])
else 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])
else if (newVer[0] == curVer[0] && newVer[1] == curVer[1] &&
newVer[2] > curVer[2])
_newFirmware = true;
}
}
// Download new html files to filesystem if they are present.
if (!ver["html"].isNull() && _newFirmware) {
Log.notice(F("WIFI: OTA downloading new html files." CR));
Log.notice(
F("WIFI: OTA checking if html files should be downloaded." CR));
JsonArray htmlFiles = ver["html"].as<JsonArray>();
for (JsonVariant v : htmlFiles) {
String s = v;
#if LOG_LEVEL == 6
Log.verbose(F("WIFI: OTA listed html file %s" CR), s.c_str());
#endif
downloadFile(s.c_str());
downloadFile(http, s);
}
}
}
@ -378,11 +463,12 @@ bool WifiConnection::checkFirmwareVersion() {
httpResponseCode);
}
http.end();
myWifi.closeWifiClient();
#if LOG_LEVEL == 6
Log.verbose(F("WIFI: OTA found new version %s." CR),
_newFirmware ? "true" : "false");
#endif
return _newFirmware;
}

View File

@ -24,11 +24,10 @@ SOFTWARE.
#ifndef SRC_WIFI_HPP_
#define SRC_WIFI_HPP_
#if defined (ESP8266)
#include <ESP8266WiFi.h>
#else // defined (ESP32)
#include <WiFiClient.h>
#include <WiFiClientSecure.h>
#if defined(ESP8266)
#include <ESP8266HTTPClient.h>
#else // defined (ESP32)
#include <HTTPClient.h>
#endif
#define WIFI_DEFAULT_SSID "GravityMon" // Name of created SSID
@ -36,22 +35,18 @@ SOFTWARE.
#define WIFI_MDNS "gravitymon" // Prefix for MDNS name
// tcp cleanup, to avoid memory crash.
struct tcp_pcb;
extern struct tcp_pcb* tcp_tw_pcbs;
extern "C" void tcp_abort(struct tcp_pcb* pcb);
class WifiConnection {
private:
// WIFI
WiFiClient _client;
WiFiClientSecure _secureClient;
// OTA
bool _newFirmware = false;
bool parseFirmwareVersionString(int (&num)[3], const char* version);
void downloadFile(const char* fname);
void connectAsync();
void downloadFile(HTTPClient& http, String& fname);
// WIFI
void connectAsync(int wifiIndex);
bool waitForConnection(int maxTime = 20);
int findStrongestNetwork();
public:
// WIFI
@ -67,16 +62,6 @@ class WifiConnection {
void startPortal();
void loop();
WiFiClient& getWifiClient() { return _client; }
WiFiClientSecure& getWifiClientSecure() { return _secureClient; }
void closeWifiClient() {
_client.stop();
_secureClient.stop();
// Cleanup memory allocated by open tcp connetions.
while (tcp_tw_pcbs) tcp_abort(tcp_tw_pcbs);
}
// OTA
bool updateFirmware();
bool checkFirmwareVersion();

View File

@ -35,6 +35,12 @@ These are the format keys available for use in the format.
* - ${mdns}
- Name of the device
- gravmon2
* - ${token}
- Token
- any value
* - ${token2}
- Token 2
- any value
* - ${id}
- Unique id of the device
- e422a3

View File

@ -12,6 +12,7 @@ Retrive the current configuation of the device via an HTTP GET command. Payload
* ``temp-format`` can be either ``C`` or ``F``
* ``gravity-format`` is always ``G`` (plato is not yet supported)
* ``ble`` is used to enable ble data transmission (only on esp32) simulating a tilt. Valid color names are; red, green, black, purple, orange, blue, yellow, pink
Other parameters are the same as in the configuration guide.
@ -22,13 +23,16 @@ Other parameters are the same as in the configuration guide.
"id": "ee1bfc",
"ota-url": "http://192.168.1.50:80/firmware/gravmon/",
"temp-format": "C",
"brewfather-push": "http://log.brewfather.net/stream?id=Qwerty",
"ble": "color",
"token": "token",
"token2": "token2",
"http-push": "http://192.168.1.50:9090/api/v1/Qwerty/telemetry",
"http-push-h1": "header: value",
"http-push-h2": "header: value",
"http-push2": "http://192.168.1.50/ispindel",
"http-push2-h1": "header: value",
"http-push2-h2": "header: value",
"http-push3": "http://192.168.1.50/ispindel",
"influxdb2-push": "http://192.168.1.50:8086",
"influxdb2-org": "org",
"influxdb2-bucket": "bucket_id",
@ -52,25 +56,38 @@ Other parameters are the same as in the configuration guide.
"gy": -6,
"gz": 4
},
"formula-calculation-data": {
"a1":25,
"a2":30,
"a3":35,
"a4":40,
"a5":45,
"a5":0,
"a6":0,
"a7":0,
"a8":0,
"g1":1,
"g2":1.01,
"g3":1.02,
"g4":1.03,
"g4":1.04,
"g5":1,
"g6":1,
"g7":1,
"g8":1
},
"angle": 90.93,
"gravity": 1.105,
"battery": 0.04
"battery": 0.04,
"platform": "esp8266",
"runtime-average": 3.12
}
GET: /api/device
================
Retrive the current device settings via an HTTP GET command. Payload is in JSON format.
.. code-block:: json
{
"app-name": "GravityMon",
"app-ver": "0.0.0",
"id": "ee1bfc",
"mdns": "gravmon"
}
This API has been removed from 0.9 and merged with /api/status
GET: /api/status
@ -79,6 +96,7 @@ GET: /api/status
Retrive the current device status via an HTTP GET command. Payload is in JSON format.
* ``temp-format`` can be either ``C`` or ``F``
* ``platform`` can be either ``esp8266`` or ``esp32``
Other parameters are the same as in the configuration guide.
@ -88,13 +106,19 @@ Other parameters are the same as in the configuration guide.
"id": "ee1bfc",
"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
"token": "token",
"token2": "token2",
"rssi": -56,
"app-ver": "0.0.0",
"mdns": "gravmon",
"sleep-interval": 30,
"platform": "esp8266",
"runtime-average": 3.12
}
@ -103,8 +127,8 @@ GET: /api/config/formula
Retrive the data used for formula calculation data via an HTTP GET command. Payload is in JSON format.
* ``a1``-``a4`` are the angles/tilt readings (up to 5 are currently supported)
* ``g1``-``g4`` are the corresponding gravity reaadings in SG or Plato depending on the device-format.
* ``a1``-``a8`` are the angles/tilt readings (up to 8 are currently supported)
* ``g1``-``g8`` are the corresponding gravity reaadings in SG or Plato depending on the device-format.
.. code-block:: json
@ -115,16 +139,97 @@ Retrive the data used for formula calculation data via an HTTP GET command. Payl
"a3": 58,
"a4": 0,
"a5": 0,
"a6": 0,
"a7": 0,
"a8": 0,
"g1": 1.000,
"g2": 1.053,
"g3": 1.062,
"g4": 1,
"g5": 1,
"g6": 1,
"g7": 1,
"g8": 1,
"error": "Potential error message",
"gravity-format": "G",
"gravity-formula": "0.0*tilt^3+0.0*tilt^2+0.0017978*tilt+0.9436"
}
GET: /api/config/advanced
=========================
Used for adjusting some internal constants and other advanced settings. Should be used with caution.
.. code-block:: json
{
"gyro-read-count": 50,
"gyro-read-delay": 3150,
"gyro-moving-threashold": 500,
"formula-max-deviation": 1.6,
"wifi-portaltimeout": 120,
"formula-calibration-temp": 20,
"int-http1": 0,
"int-http2": 0,
"int-http3": 0,
"int-influx": 0,
"int-mqtt": 0
}
POST: /api/config/advanced
==========================
Same parameters as above.
Payload should be in standard format used for posting a form
GET: /api/clearwifi
===================
Will reset the wifi settings both in the configuration file and eeprom, leaving the rest of the configuration.
For this to work you will need to supply the device id as a parameter in the request:
::
http://mygravity.local/api/clearwifi?id=<mydeviceid>
GET: /api/factory
=================
Will do a reset to factory defaults and delete all data except wifi settings.
For this to work you will need to supply the device id as a parameter in the request:
::
http://mygravity.local/api/factory?id=<mydeviceid>
GET: /api/test/push
===================
Trigger a push on one of the targets, used to validate the configuration from the UI.
Requires to parameters to function /api/test/push?id=<deviceid>&format=<format>
* ``format`` defines which endpoint to test, valid values are; http-1, http-2, http-3, influxdb, mqtt
The response is an json message with the following values.
* ``code`` is the return code from the push function, typically http responsecode or error code from mqtt library.
.. code-block:: json
{
"success": false,
"enabled": true,
"code": -3
}
POST: /api/config/device
========================
@ -152,13 +257,15 @@ Payload should be in standard format used for posting a form. Such as as: `id=va
.. code-block::
id=ee1bfc
token=
token2=
http-push=http://192.168.1.50/ispindel
http-push2=
http-push3=
http-push-h1=
http-push-h2=
http-push2-h1=
http-push2-h2=
brewfather-push=
influxdb2-push=http://192.168.1.50:8086
influxdb2-org=
influxdb2-bucket=
@ -207,6 +314,7 @@ Payload should be in standard format used for posting a form. Such as as: `id=va
id=ee1bfc
voltage-factor=1.59
temp-adjustment=0
ble=red
gyro-temp=off
ota-url=http://192.168.1.50/firmware/gravmon/
@ -216,8 +324,8 @@ POST: /api/config/formula
Used to update formula calculation data via an HTTP POST command. Payload is in JSON format.
* ``a1``-``a4`` are the angles/tilt readings (up to 5 are currently supported)
* ``g1``-``g4`` are the corresponding gravity reaadings (in SG)
* ``a1``-``a8`` are the angles/tilt readings (up to 5 are currently supported)
* ``g1``-``g8`` are the corresponding gravity reaadings (in SG)
Payload should be in standard format used for posting a form. Such as as: `id=value&mdns=value` etc. Key value pairs are shown below.
@ -229,11 +337,17 @@ Payload should be in standard format used for posting a form. Such as as: `id=va
a3=58
a4=0
a5=0
a6=0
a7=0
a8=0
g1=1.000
g2=1.053
g3=1.062
g4=1
g5=1
g6=1
g7=1
g8=1
Calling the API's from Python
@ -271,14 +385,16 @@ The requests package converts the json to standard form post format.
url = "http://" + host + "/api/config/push"
json = { "id": id,
"token": "",
"token2": "",
"http-push": "http://192.168.1.1/ispindel",
"http-push2": "",
"http-push-h1": "",
"http-push-h2": "",
"http-push2-h1": "",
"http-push2-h2": "",
"brewfather-push": "",
"influxdb2-push": "",
"http-push2": "",
"http-push3": "",
"http-push-h1": "",
"http-push-h2": "",
"http-push2-h1": ""
"http-push2-h2": "",
"influxdb2-push": "",
"influxdb2-org": "",
"influxdb2-bucket": "",
"influxdb2-auth": "",
@ -302,6 +418,7 @@ The requests package converts the json to standard form post format.
"voltage-factor": 1.59, # Default value for voltage calculation
"temp-adjustment": 0, # If temp sensor needs to be corrected
"gyro-temp": "on", # Use the temp sensor in the gyro instead (on/off)
"ble": "red", # Enable ble on esp32
"ota-url": "" # if the device should seach for a new update when active
}
set_config( url, json )
@ -313,10 +430,16 @@ The requests package converts the json to standard form post format.
"a3": 58,
"a4": 0,
"a5": 0,
"a6": 0,
"a7": 0,
"a8": 0,
"g1": 1.000,
"g2": 1.053,
"g3": 1.062,
"g4": 1,
"g5": 1
"g5": 1,
"g6": 1,
"g7": 1,
"g8": 1
}
set_config( url, json )

View File

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

View File

@ -22,38 +22,23 @@ URL: (http://gravmon.local)
:width: 800
:alt: Index page
Configuration is accessed by entering the URL for the device, this will be the mDNS name *device.local* or the IP adress. The following chapter assumes the device name is *gravmon*.
The main page shows the device readings; gravity, angle, temperature and battery charge. If the checkbox is active then the device will never go into sleep mode. This is useful if
you are collecting angle/tilt for calibration. If this is unchecked the device will change mode as explained before.
You can also view the average time a gravity measurement takes. Under optimal setting this should be around 1.5 - 2.0 seconds. If this is higher than 2 seconds this is most likley connected to slow wifi
connection. It will show 0 if data has not been collected yet.
.. tip::
If you are connected to the device via a serial console (speed: 115200) you can see the connection sequence and get the Unique ID and IP adress from there.
.. tip::
Device
======
URL: (http://gravmon.local/device)
.. image:: images/device.png
:width: 800
:alt: Device Settings
* **Version:**
Installed version of the code and html files.
* **Device name:**
This is unique name of the device which is set in the configuration, also known as MDNS name.
* **Device ID:**
This is unique identifier for the device (ESP8266 id), this is required when using the API as an API Key to safeguard
against faulty requests. This is the ESP8266 chip ID, so it should be unique.
The button `view error log` will show the last 15 errors on the device. This can be useful for checking errors without
the need to connect to the serial port or to check what errors has occured while in `gravity mode`.
Configuration
@ -70,27 +55,28 @@ Device Setting
* **Device name:**
This is unique name for the device. It will be used in pushing data as well as mDNS name on the network (<name>.local)
This is unique name for the device. It will be used in pushing data as well as mDNS name on the network (<name>.local)
* **Temperature format:**
Choose between Celsius and Farenheight when displaying temperature.
Choose between Celsius and Farenheight when displaying temperature.
* **Interval:**
This defines how long the device should be sleeping between the readings when in `gravity monitoring` mode. You will also see
the values in minutes/seconds to easier set the interval. 900s is a recommended interval. The sleep interval can
be set between 10 - 3600 seconds (60 minutes).
This defines how long the device should be sleeping between the readings when in `gravity monitoring` mode. You will also see
the values in minutes/seconds to easier set the interval. 900s is a recommended interval. The sleep interval can
be set between 10 - 3600 seconds (60 minutes).
.. note::
A low value such as 30s will give a lifespan of 1-2 weeks and 300s (5 min) would last for 3+ weeks. This assumes that
there is good wifi connection that takes less than 1s to reconnect. Poor wifi connection is the main reason for battery drain.
there is good wifi connection that takes less than 1s to reconnect. Poor wifi connection is the main reason for battery drain.
The device will show the estimated lifespan based on the average connection time, if no data exist it will not be shown.
* **Calibration values:**
These are calibration data for the gyro. Place the device flat on a table and press the button to save the default orientation values. Without this calibration we cannot calculate the correct angle/tilt.
These are calibration data for the gyro. Place the device flat on a table and press the button to save the default orientation values. Without this calibration we cannot calculate the correct angle/tilt.
.. warning::
@ -108,59 +94,97 @@ Push Settings
When enabling SSL this will not validate the root CA of the remote service, this is a design decision based on two aspects. Enabling CA validation will take 3-4s extra on each connection which means way less
battery life, so the decision is to prioritize battery life over security. The data transmitted is not really that sensitive anyway so I belive this is a good balance.
* **HTTP 1 (POST):**
* **HTTP URL 1:**
Endpoint to send data via http. Default format used Format used :ref:`data-formats-ispindle`. You can customize the format using :ref:`format-editor`.
Endpoint to send data via http. Default format used Format used :ref:`data-formats-ispindle`. You can customize the format using :ref:`format-editor`.
If you add the prefix `https://` then the device will use SSL when sending data.
If you add the prefix `https://` then the device will use SSL when sending data.
* **HTTP 2 (POST):**
* **HTTP URL 2:**
Endpoint to send data via http. Default format used :ref:`data-formats-ispindle`. You can customize the format using :ref:`format-editor`.
Endpoint to send data via http. Default format used :ref:`data-formats-ispindle`. You can customize the format using :ref:`format-editor`.
If you add the prefix `https://` then the device will use SSL when sending data.
If you add the prefix `https://` then the device will use SSL when sending data.
* **Token:**
* **Brewfather URL:**
The token is included in the iSpindle JSON format and will be used for both HTTP targets. If you
need to have 2 different tokens please use the :ref:`format-editor` to customize the data format.
Endpoint to send data via http to brewfather. Format used :ref:`data-formats-brewfather`
* **HTTP 3 (GET):**
SSL is not supported for this target.
Endpoint to send data via http. This is using an HTTP GET request instead of a post. This means that the values are appended to the URL like; http://endpoint?param=value&param2=value2. You can customize the format using :ref:`format-editor`.
If you add the prefix `https://` then the device will use SSL when sending data.
* **Token 2:**
The token is included in the default format for the HTTP GET url but can be used for any of the formats. For HTTP GET use can use this for an authorization token with for instance ubidots or blynk http api.
* **HTTP Headers**
.. image:: images/config-popup1.png
:width: 300
:alt: HTTP Headers
You can define 2 http headers per push target. This is available via a pop-up window but dont forget
to press the save buttons on the post section to save the values. One common header is content type which is the
default setting for http targets.
The input must have the format **'<header>: <value>'** for it to work. The UI will accept any value so errors
will not show until the device tries to push data.
::
Content-Type: application/json
X-Auth-Token: <api-token>
Mozilla has a good guide on what headers are valid; `HTTP Headers <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers>`_
Push Settings (2)
+++++++++++++++++
.. image:: images/config2b.png
:width: 800
:alt: Push Settings
* **Influx DB v2 URL:**
Endpoint to send data via http to InfluxDB. Format used :ref:`data-formats-influxdb2`. You can customize the format using :ref:`format-editor`.
Endpoint to send data via http to InfluxDB. Format used :ref:`data-formats-influxdb2`. You can customize the format using :ref:`format-editor`.
SSL is not supported for this target. Raise a issue on github if this is wanted.
SSL is not supported for this target. Raise a issue on github if this is wanted.
* **Influx DB v2 Organisation:**
Name of organisation in Influx.
Name of organisation in Influx.
* **Influx DB v2 Bucket:**
Identifier for bucket.
Identifier for bucket.
* **Influx DB v2 Token:**
Token with write access to bucket.
Token with write access to bucket.
* **MQTT server:**
IP or name of server to send data to. Default format used :ref:`data-formats-mqtt`. You can customize the format using :ref:`format-editor`.
IP or name of server to send data to. Default format used :ref:`data-formats-mqtt`. You can customize the format using :ref:`format-editor`.
* **MQTT Port:**
Which port should be used for communication, default is 1883 (standard port). For SSL use 8883 (any port over 8000 is treated as SSL).
Which port should be used for communication, default is 1883 (standard port). For SSL use 8883 (any port over 8000 is treated as SSL).
* **MQTT user:**
Username or blank if anonymous is accepted
Username or blank if anonymous is accepted
* **MQTT password:**
Password or blank if anonymous is accepted
Password or blank if anonymous is accepted
Gravity Settings
++++++++++++++++
@ -171,25 +195,26 @@ Gravity Settings
* **Gravity format:**
Gravity format can be eihter `SG` or `Plato`. The device will use SG Internally and convert to Plato when displaying or sending data.
Gravity format can be eihter `SG` or `Plato`. The device will use SG Internally and convert to Plato when displaying or sending data.
* **Gravity formula:**
Gravity formula is compatible with standard iSpindle formulas so any existing calculation option can be used. You can also use
the feature to create the formula by supplying the raw data. See :ref:`create-formula`
Gravity formula is compatible with standard iSpindle formulas so any existing calculation option can be used. You can also use
the feature to create the formula by supplying the raw data. See :ref:`create-formula`
The gravity formula accepts to paramaters, **tilt** for the angle or **temp** for temperature (temperature inserted into the formula
will be in celsius). I would recommend to use the formula calculation feature instead since this is much easier.
The gravity formula accepts to paramaters, **tilt** for the angle or **temp** for temperature (temperature inserted into the formula
will be in celsius). I would recommend to use the formula calculation feature instead since this is much easier.
* **Temperature correct gravity:**
Will apply a temperature calibration formula to the gravity as a second step after gravity has been calculated. It's also possible to
build this into the gravity formula.
Will apply a temperature calibration formula to the gravity as a second step after gravity has been calculated. It's also possible to
build this into the gravity formula.
.. warning::
This formula assumes that the calibration has been done at 20°C / 68°F.
Formula used in temperature correction.
Formula used in temperature correction.
::
@ -206,33 +231,58 @@ Hardware Settings
* **Voltage factor:**
Factor used to calcualate the battery voltage. If you get a too low/high voltage you can adjust this value.
Factor used to calcualate the battery voltage. If you get a too low/high voltage you can adjust this value.
* **Temperature correction:**
This value will be added to the temperature reading (negative value will reduce temperature reading). This is applied
when the device starts. So changing this will not take affect until the device is restarted.
This value will be added to the temperature reading (negative value will reduce temperature reading). This is applied
when the device starts. So changing this will not take affect until the device is restarted.
* **Gyro Temperature:**
Enable this feature will use the temp sensor i the gyro instead of the DS18B20, the benefit is shorter run time and
longer battery life (this is an experimental feature). The value used is the first temperature reading from when the
device is activated, since the gyro should be cool this is reflecting the surronding temperature. After it has
been running the value would be totally off.
Enable this feature will use the temp sensor i the gyro instead of the DS18B20, the benefit is shorter run time and
longer battery life (this is an experimental feature). The value used is the first temperature reading from when the
device is activated, since the gyro should be cool this is reflecting the surronding temperature. After it has
been running the value would be totally off.
* **Bluetooth: (Only ESP32)**
If the build is using an ESP32 then you can send data over BLE, simulating a Tilt device. Choose the color that you want the device to simulate.
* **OTA URL:**
Should point to a URL where the firmware.bin file + version.json file are located.
Should point to a URL where the firmware.bin file + version.json file are located. For an ESP32 target the firmware should be named firmware32.bin.
For the OTA to work, place the following files (version.json + firmware.bin) at the location that you pointed out in OTA URL. If the version number in the json file is newer than in the
code the update will be done during startup.
For the OTA to work, place the following files (version.json + firmware.bin) at the location that you pointed out in OTA URL. If the version number in the json file is newer than in the
code the update will be done during startup.
If you have the previx `https://` then the device will use secure transfer without CA validation.
If you have the previx `https://` then the device will use secure transfer without CA validation.
Example; OTA URL (don't forget trailing dash), the name of the file should be firmware.bin
Example; OTA URL (don't forget trailing dash), the name of the file should be firmware.bin
.. code-block::
http://192.168.1.1/firmware/gravmon/
https://192.168.1.1/firmware/gravmon/
* **Upload Firmware**
This option gives you the possibility to install an new version of the firmware (or any firmware that uses the standard flash layout).
.. image:: images/firmware.png
:width: 600
:alt: Update firmware
Advanded Settings
+++++++++++++++++
.. image:: images/config5.png
:width: 800
:alt: Advanced Settings
* **Header:**
To be described

View File

@ -5,8 +5,8 @@ Data Formats
.. _data-formats-ispindle:
iSpindle format
===============
HTTP Post, iSpindle format
==========================
This is the format used for standard http posts.
@ -22,11 +22,11 @@ This is the format used for standard http posts.
"token" : "gravmon",
"interval": 900,
"temperature": 20.5,
"temp-units": "C",
"temp_units": "C",
"gravity": 1.0050,
"angle": 45.34,
"battery": 3.67,
"rssi": -12,
"RSSI": -12,
"corr-gravity": 1.0050,
"gravity-unit": "G",
@ -43,38 +43,38 @@ This is the format template used to create the json above.
"token" : "gravmon",
"interval": ${sleep-interval},
"temperature": ${temp},
"temp-units": "${temp-unit}",
"temp_units": "${temp-unit}",
"gravity": ${gravity},
"angle": ${angle},
"battery": ${battery},
"rssi": ${rssi},
"RSSI": ${rssi},
"corr-gravity": ${corr-gravity},
"gravity-unit": "${gravity-unit}",
"run-time": ${run-time}
}
.. _data-formats-brewfather:
Brewfather format
=================
This is the format for Brewfather. See: `Brewfather API docs <https://docs.brewfather.app/integrations/custom-stream>`_
.. code-block:: json
{
"name" : "gravmon",
"temp": 20.5,
"temp_unit": "C",
"battery": 3.67,
"gravity": 1.0050,
"gravity_unit": "G",
}
.. _data-formats-influxdb2:
HTTP Get
========
This is the format added to the URL when using HTTP get
.. code-block::
?name=<mdns>,id=<id>,token=<token>&interval=300&temperature=20.1&temp-units=<C|F>&
gravity=$1.004&angle=45.5&battery=3.96&rssi=-18&corr-gravity=1.004&gravity-unit=<G|P>&run-time=2.1
This is the format template used to create the data above.
.. code-block::
?name=${mdns}&id=${id}&token=${token2}&interval=${sleep-interval}&temperature=${temp}&
temp-units=${temp-unit}&gravity=${gravity}&angle=${angle}&battery=${battery}&rssi=${rssi}&
corr-gravity=${corr-gravity}&gravity-unit=${gravity-unit}&run-time=${run-time}
Influx DB v2
============
@ -82,14 +82,17 @@ This is the format for InfluxDB v2
.. code-block::
measurement,host=<mdns>,device=<id>,temp-format=<C|F>,gravity-format=SG,gravity=1.0004,corr-gravity=1.0004,angle=45.45,temp=20.1,battery=3.96,rssi=-18
measurement,host=<mdns>,device=<id>,temp-format=<C|F>,gravity-format=SG,
gravity=1.0004,corr-gravity=1.0004,angle=45.45,temp=20.1,battery=3.96,rssi=-18
This is the format template used to create the json above.
.. code-block::
measurement,host=${mdns},device=${id},temp-format=${temp-unit},gravity-format=${gravity-unit} gravity=${gravity},corr-gravity=${corr-gravity},angle=${angle},temp=${temp},battery=${battery},rssi=${rssi}
measurement,host=${mdns},device=${id},temp-format=${temp-unit},
gravity-format=${gravity-unit} gravity=${gravity},corr-gravity=${corr-gravity},
angle=${angle},temp=${temp},battery=${battery},rssi=${rssi}
.. _data-formats-mqtt:
@ -130,7 +133,10 @@ This is a format template that is compatible with v0.6. Just replace the `topic`
.. code-block::
topic:{"name":"gravmon","ID":"${id}","token":"gravmon","interval": ${sleep-interval},"temperature": ${temp},"temp-units": "${temp-unit}","gravity":${gravity},"angle": ${angle},"battery":${battery},"rssi": ${rssi},"corr-gravity":${corr-gravity},"gravity-unit": "${gravity-unit}","run-time": ${run-time}}|
topic:{"name":"gravmon", "ID":"${id}", "token":"gravmon", "interval": ${sleep-interval},
"temperature": ${temp}, "temp_units": "${temp-unit}", "gravity":${gravity},
"angle": ${angle}, "battery":${battery}, "rssi": ${rssi}, "corr-gravity":${corr-gravity},
"gravity-unit": "${gravity-unit}", "run-time": ${run-time}}|
version.json
@ -147,7 +153,7 @@ they can be uploaded manually afterwards.
"version":"0.7.0",
"html": [
"index.min.htm",
"device.min.htm",
"test.min.htm",
"config.min.htm",
"format.min.htm",
"calibration.min.htm",

View File

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

View File

@ -0,0 +1,26 @@
.. _hardware:
Hardware
########
There are lots of resouces out there on how to build the hardware for an iSpindle so I will not go into details on that part. Here are two of my builds using the iSpindle PCB v4.
.. image:: images/ispindel.jpg
:width: 500
:alt: Builds of iSpindel
It's possible to use this PCB and mount an ESP32 on top of that. It must be an pin compatible ESP32 and the one I used was called *ESP32 d1 mini*. Since this is the same width as the PCB you need to
mount it really close to the PCB in order for it to fit in the PET tube/container. I also had to smooth the edge of the ESP32 in order for it to fit.
I would suggest that you try how it fits into the PET tube before soldering it to the PCB. Make sure that the battery is attached since this will be a really tight fit.
You also need to desolder (remove) the RED ON LED from the ESP32 or the battery power will be reduced a lot.
Final thing is to add a resistor between A0 (Analog PIN) and ground of 470k. The reason is that the esp8266 has a build in resistor which
the esp32 does not have. So in order to get a valid voltage (less than 3.2V) on the A0 pin this is needed. Once the modification is done you might
need to adjust the voltage factor so the battery reading is correct.
.. image:: images/esp32.jpg
:width: 500
:alt: Mounting esp32

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 931 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

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

View File

@ -1,6 +1,14 @@
.. _installation:
Installation
------------
You have these 3 options for flashing this firmware.
* Brewflasher via USB serial
* Esptool via USB serial
* iSpindel web interface
Brewflasher
===========
@ -14,7 +22,7 @@ on both Windows and Mac. You can download the latest version from here: `Brewfla
Binaries
********
In the /bin directory you will find 2 different firmware builds;
In the /bin directory you will find 3 different firmware builds;
* **firmware.bin**
@ -24,12 +32,16 @@ In the /bin directory you will find 2 different firmware builds;
This version also submits performance data to an influx database with detailed execution times.
* **firmware32.bin**
This is the standard release build for an ESP32 variant
In these versions all the html files are embedded in the binaries. The file system is currently only used for storing
the configuration file.
If the software becomes so large the html files can be moved to the file system, but this is not enabled by
default (see compiling for details). This approach makes installation much easier and ensure that html files
and code is in sync.
and code is in sync.
Esptool
=======
@ -47,6 +59,40 @@ If there are issues you can try do erase the flash first using this command;
``esptool.py --port COM4 erase_flash``
iSpindel
========
If you already have the device flashed with iSpindel firmware you can go into the configuration mode where you will find
an option for updating firmware. The option is under the maintence meny.
Select the esp8266 version of the firmware called firmware.bin and press upload.
Updating firmware
=================
You can use the above options for updating the firmware as well. But if you want to do updates without connecting the USB cable these
are your options.
OTA Option
**********
You can use the OTA option by adding this URL to your configuration and when the device starts up in configuration mode it
will check for a new version and if it finds a newer version it will do an update.
``https://mp-se.github.io/gravitymon/release/``
Manual update
*************
When the device in is configuration mode you can manually update with a new firmware. Just open this URL in the web
browser and select the firmware.bin file that corresponds to the version you want to flash.
``http://<device_name>/firmware.htm``
.. _serial_monitoring:
Serial Monitoring
=================
@ -57,18 +103,39 @@ Just select a baud rate of 115200, 8N1.
:width: 800
:alt: Serial output
.. _setup_wifi:
Configuring WIFI
================
When the device is flashed it will need to have WIFI configuration in order to work. If you have used other software on
the device its possible that wifi settings exist.
the device its possible that wifi settings already exist.
If this is not configured in the device it will create an wirless access point called `GravMon`. The default password is `password`.
Connect to this AP and enter the SSID and password you want to use. If the web page dont open automatically you can enter the following adress
in the browser: **http://192.168.4.1**
Before pressing save on the network infomration, make a note of the devicename that is shown on the screen, this will be the name that is used
in the next step to access the configuration pages. The link would look like this: **http://gravitymon56EA34.local**
.. image:: images/wifi.png
:width: 200
:alt: Wifi page
.. _setup_ip:
Finding the device adress
=========================
Once the wifi network settings have been added then the device will reboot and connect to your network. If the blue led is flashing then it's up and running and is ready to be configured.
If your computer supports mDNS the adress you saw before can be used in your web browser to connect to the device. Windows does not have the best support for mDNS so if you are having issues
with finding the network name you can try the following:
* Check your wireless router for the IP adress and use that to connect instead, for example; http://192.168.1.56
* Download an IP scanner / Port Scanner on your Windows computer or mobile device and use that to find what devices are listening on port 80.
Once you can access the user interface then proceed to the next step.

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

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

View File

@ -7,3 +7,8 @@ My device is no going in to sleep after fully charged
- Check the angle/tilt. If the device is reporting 90 degress then its not going into sleep. Tilt the device and check if sleep works.
- Check in reported voltage of the battery in the web interface. If this is higher than 4.15V the device belives its beeing charged. In that case adjust the voltage factor under hardware so the voltage drops below 4.15V.
- Check if the `always on` option is activated in the web interface.
My device reports a temperature of -273C or -491F
-------------------------------------------------
- The DS18B20 temperature sensor cannot be found and this is the default value reported in this case.
- Check the orienation of the sensor and soldering.

View File

@ -3,6 +3,70 @@
Releases
########
v1.0.0
------
* Upgraded to bootstrap v5.1 for web pages.
* Added tooltips to all fields in user interface
* Removed brewfather option (can use standard HTTP options), the old apporach can still be used via changing format template.
* Added 5 more points for formula creation, so a total of 10 angles/gravity values can be stored.
* Added function on format page so that it's easy to copy a format template from the docs (simplify service integration).
* Added https support for Influxdb
* Added possibility to have variable push intervals for different endpoints so that different frequency can be used, for example; 5min mqtt, 15min brewfather.
* Added advanced settings to configuration for adjusting some internal values (gyro reads, accepted formula deviation, timeouts, moving detection etc).
* Added additional http error codes to troubleshooting documentation
* Installation instructions updated on how to find the device after wifi has been configured.
* Documentation on brewfather has been updated to adress SG/Plato conversion
* BUG: Fixed issue in formula calculation in case there were a gap in the data series
* BUG: Field name for wifi strenght changed from "rssi" to "RSSI"
* TODO: Fix documentation for advanced settings.
v0.9.0
------
* Added one http push target that uses HTTP GET. This can be used with ubidots or blynk api's.
* Added function to test push targets from configuration page. It will send data and show the return code as a first step.
* Added documetation on how to integrate with Blynk.io using http get.
* Config page now shows the estimated runtime for the device (based on a full battery and previous average runtime)
* Experimental release of firmware using an esp32 instead of esp8266
* Merged index and device pages into one so that all the needed information is available on the index page.
* Removed api for device (/api/device), it's now merged into the /api/status api.
* Test function in format editor now uses real data and not fake.
* Split push configuration into two sections to make it fit better on smaller devices
* Updated WifiManager and DoubleReset libraries
* Updated esp32 target with littlefs support
* Updated esp32 target with BLE send support (it will simulate a tilt)
* Mounted esp32 d1 mini mounted to a iSpindle PCB 4.0 (CherryPhilip) which worked fine.
* Documented hardware changes on esp32
* Default mDNS name is now shown on WIFI setup page.
* Added option to manually update/downgrade firmware under hardware settings.
* BUG: Corrected PIN for voltage read on ESP32
* BUG: If using plato and not gravity formula was defined the value was set to null.
* BUG: Temp format name was incorrect in iSpindle format causing receiver to incorrectly read temperature.
* BUG: Temperature sensor adjusmemnt value was not handled properly when using Farenheight.
* BUG: If the ID was to low the device id could end up with a leading space causing errors in data post. Added leading zero to ID.
* BUG: Entering wifi setup and a timeout occured the wifi settings could be deleted.
v0.8.0
------
* Added option to set http headers (2 per http endpoint), these can be used for
other http formats than json (default) and for adding authentication headers.
* Added possibility to view last 10 errors on device page.
* Added possibility to define token parameter used in iSpindle format.
* Added instructions for how to configure integration with Brewspy
* Added instructions for how to configure integration with Thingspeak
* Added option to do a factory reset via API.
* Added logging of the runtime, how long a measurement take (last 10 are stored). This can be
used to check how good the wifi connection is and estimate the lifetime when on battery.
Check the device page in the UI for this information.
* Refactored code to free up more RAM to make SSL more stable.
* Before connecting to an SSL endpoint the device will try to use a new SSL feature
called MFLN (Maximum Fragment Length Negotiation) that allow us to reduce the buffers
from 16k to 2k. This can make a huge difference on a device with only 40k RAM. Not all
servers might support this feature.
* Updated documentation pages.
* Tested batterylife, 47 days using an update frequency of 5 min
v0.7.1
------

View File

@ -10,14 +10,55 @@ Brewfather
Brewfather is an all in one service that allows you to manage you recepies and brews.
Just enter the http adress found on brewfather, not other settings are needed. The endpoint has the following format:
**Option 1** - iSpindle Endpoint
This opion makes use of the standard http (1 or 2) endpoints in the push section. If you are using SG then the device name needs to end with [SG] or brewfather will assume
that the data is in plato. You can also modify the format template using the following options:
Update the following part `"gravity": ${gravity-plato},` or `"name" : "${mdns}[SG]",``
This makes use of the standard format template, no changes needed.
.. code-block::
http://log.brewfather.net/http://log.brewfather.net/stream?id=<yourid>
http://log.brewfather.net/ispindel?id=<yourid>
The URL is found under settings.
Documentation on this can be found under `Brewfather iSpindle Endpoint <https://docs.brewfather.app/integrations/ispindel>`_
**Option 2** - Custom Stream
This option makes use of the http push endpoint with a custom format template. Just enter the http stream adress found
on brewfather, not other settings are needed. The stream endpoint URL has the following format:
.. code-block::
http://log.brewfather.net/stream?id=<yourid>
Documentation on this can be found under `Brewfather Custom Endpoint <https://docs.brewfather.app/integrations/custom-stream>`_
The implementation is basically a http request with the following format template:
.. code-block::
{
"name": "${mdns}",
"temp": ${temp},
"aux_temp": 0,
"ext_temp": 0,
"temp_unit": "${temp-unit}",
"gravity": ${gravity},
"gravity_unit": "${gravity-unit}",
"pressure": 0,
"pressure_unit": "PSI",
"ph": 0,
"bpm": 0,
"comment": "",
"beer": "",
"battery": ${battery}
}
Fermentrack
+++++++++++
@ -48,6 +89,27 @@ Enter the following as URL:
http://industrial.api.ubidots.com/api/v1.6/devices/<devicename>/?token=<api-token>
This is the less secure option.
**Option 2** - token as the http header
Enter the following as URL, use either standard or ssl.
.. code-block::
http://industrial.api.ubidots.com/api/v1.6/devices/<devicename>
https://industrial.api.ubidots.com/api/v1.6/devices/<devicename>
Under `Headers` (button after the http url in the UI) enter the following string:
.. code-block::
X-Auth-Token: <api-token>
This is the more secure option.
Even though ubidots can handle the default ispindle format it probably better to just post the data you want. This is an example of a
format template that can be used. For information on customizing the format see :ref:`format-editor`.
@ -105,7 +167,7 @@ Brewer's friend is an all in one service that allows you to manage you recepies
.. warning::
I dont have an account for brewers friend so I have not been able to verfy this completely. Its based on
the available documentation.
the available documentation. If this works please let
You can find you API key when logged in to the service. Follow these `instructions <https://docs.brewersfriend.com/devices/ispindel>`_
@ -128,9 +190,74 @@ format for the endpoint. Just add you API key after token.
"token" : "[API KEY]",
"interval": ${sleep-interval},
"temperature": ${temp},
"temp-units": "${temp-unit}",
"temp_units": "${temp-unit}",
"gravity": ${gravity},
"angle": ${angle},
"battery": ${battery},
"rssi": ${rssi}
}
Brewspy
+++++++
BrewSpy is a service that can show the history and manage the brew.
You need to enter the Token found in brewspy. The field is found under the field for http configuration.
.. code-block::
http://brew-spy.com/api/ispindel
Thingspeak
++++++++++
Thingspeak is an IoT platform for receiving data which can be visualized.
In order to use this platform you need to create a channel (channel = device) and get the APIKEY for
writing to the channel. Each channel can handle up to 8 measurements. In the http field enter the following URL.
.. code-block::
http://api.thingspeak.com/update.json
You also need to create a custom format for the selected endpoint where the field1-field8 contains the data
you want to include. The example below sends 5 different values to the channel identified by the API key.
.. code-block::
{
"api_key": "<your write api key for channel>",
"field1": ${gravity},
"field2": ${temp},
"field3": ${angle},
"field4": ${battery},
"field5": ${rssi}
}
Blynk.io
++++++++
Blynk is an IoT service which can be updated via http get. It also has an mobile device that can be used to show the data.
In order to use this platform you need to create a device which can be used to receive the data. Each device will have a
unique token that is used to identify it. You need to use the HTTP GET option (http-3) on the device for this to work. Enter the
following URL in the UI. This will allow us to update several data points at once. I usually enter the token in the ``token2`` field
so the format template does not contain any sensitive data and it's easier to switch to another device.
.. code-block::
http://blynk.cloud/external/api/batch/update
In the format editor you can enter this template which will send 3 values to blynk. You can add as many as you want but make sure
these are configured on the device with the correct validation option such as data type and range. The value should be on one line
starting with a ``?``. This string will be added to the URL above when doing the post. You can add more values if you want.
.. code-block::
?token=${token2}&v1=${temp}&v2=${gravity}&v3=${angle}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

15
test/adv.json Normal file
View File

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

Some files were not shown because too many files have changed in this diff Show More