From bbe5bfdceeca17ec7566c9e65379787668ce9a0e Mon Sep 17 00:00:00 2001 From: Tobias Maier Date: Mon, 2 Oct 2023 15:30:30 +0200 Subject: [PATCH] Initial commit with action --- .gitea/workflows/on_push.yaml | 22 ++ .gitignore | 5 + .vscode/extensions.json | 10 + data/chota.css | 1 + data/data_export.html | 60 ++++++ data/logic.js | 0 data/settings.html | 79 +++++++ data/status.html | 178 ++++++++++++++++ documentation/api_doc.md | 11 + documentation/error_codes.md | 15 ++ include/README | 39 ++++ lib/README | 46 ++++ platformio.ini | 23 ++ src/main.cpp | 388 ++++++++++++++++++++++++++++++++++ test/README | 11 + 15 files changed, 888 insertions(+) create mode 100644 .gitea/workflows/on_push.yaml create mode 100644 .gitignore create mode 100644 .vscode/extensions.json create mode 100644 data/chota.css create mode 100644 data/data_export.html create mode 100644 data/logic.js create mode 100644 data/settings.html create mode 100644 data/status.html create mode 100644 documentation/api_doc.md create mode 100644 documentation/error_codes.md create mode 100644 include/README create mode 100644 lib/README create mode 100644 platformio.ini create mode 100644 src/main.cpp create mode 100644 test/README diff --git a/.gitea/workflows/on_push.yaml b/.gitea/workflows/on_push.yaml new file mode 100644 index 0000000..59354ab --- /dev/null +++ b/.gitea/workflows/on_push.yaml @@ -0,0 +1,22 @@ +name: Test compiling project + +on: [push] + +jobs: + test: + runs-on: ubuntu-latest + container: + image: debian:latest + + steps: + - name: Install necessary dependencies + run: apt update && apt install nodejs -y + - uses: actions/setup-python@v4 + with: + python-version: '3.11' + - name: Install Platformio + run: pip install --break-system-packages --upgrade platformio + - name: Checkout Code + uses: actions/checkout@v2 + - name: Run PlatformIO + run: pio ci -c platformio.ini ./ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/data/chota.css b/data/chota.css new file mode 100644 index 0000000..559bed8 --- /dev/null +++ b/data/chota.css @@ -0,0 +1 @@ +/*! chota.css v0.8.1 | MIT License | github.com/jenil/chota */:root{--bg-color:#fff;--bg-secondary-color:#f3f3f6;--color-primary:#14854f;--color-lightGrey:#d2d6dd;--color-grey:#747681;--color-darkGrey:#3f4144;--color-error:#d43939;--color-success:#28bd14;--grid-maxWidth:120rem;--grid-gutter:2rem;--font-size:1.6rem;--font-color:#333;--font-family-sans:-apple-system,BlinkMacSystemFont,Avenir,"Avenir Next","Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue",sans-serif;--font-family-mono:monaco,"Consolas","Lucida Console",monospace}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:62.5%;line-height:1.15}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}*{scrollbar-color:var(--color-lightGrey) var(--bg-primary);scrollbar-width:thin}::-webkit-scrollbar{width:8px}::-webkit-scrollbar-track{background:var(--bg-primary)}::-webkit-scrollbar-thumb{background:var(--color-lightGrey)}body{background-color:var(--bg-color);color:var(--font-color);font-family:Segoe UI,Helvetica Neue,sans-serif;font-family:var(--font-family-sans);font-size:var(--font-size);line-height:1.6;margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-weight:500;margin:.35em 0 .7em}h1{font-size:2em}h2{font-size:1.75em}h3{font-size:1.5em}h4{font-size:1.25em}h5{font-size:1em}h6{font-size:.85em}a{color:var(--color-primary);text-decoration:none}a:hover:not(.button){opacity:.75}button{font-family:inherit}p{margin-top:0}blockquote{background-color:var(--bg-secondary-color);border-left:3px solid var(--color-lightGrey);padding:1.5rem 2rem}dl dt{font-weight:700}hr{background-color:var(--color-lightGrey);height:1px;margin:1rem 0}hr,table{border:none}table{border-collapse:collapse;border-spacing:0;text-align:left;width:100%}table.striped tr:nth-of-type(2n){background-color:var(--bg-secondary-color)}td,th{padding:1.2rem .4rem;vertical-align:middle}thead{border-bottom:2px solid var(--color-lightGrey)}tfoot{border-top:2px solid var(--color-lightGrey)}code,kbd,pre,samp,tt{font-family:var(--font-family-mono)}code,kbd{border-radius:4px;color:var(--color-error);font-size:90%;padding:.2em .4em;white-space:pre-wrap}code,kbd,pre{background-color:var(--bg-secondary-color)}pre{font-size:1em;overflow-x:auto;padding:1rem}pre code{background:none;padding:0}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}img{max-width:100%}fieldset{border:1px solid var(--color-lightGrey)}iframe{border:0}.container{margin:0 auto;max-width:var(--grid-maxWidth);padding:0 calc(var(--grid-gutter)/2);width:96%}.row{-webkit-box-direction:normal;-webkit-box-pack:start;-ms-flex-pack:start;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-flow:row wrap;flex-flow:row wrap;justify-content:flex-start;margin-left:calc(var(--grid-gutter)/-2);margin-right:calc(var(--grid-gutter)/-2)}.row,.row.reverse{-webkit-box-orient:horizontal}.row.reverse{-webkit-box-direction:reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.col{-webkit-box-flex:1;-ms-flex:1;flex:1}.col,[class*=" col-"],[class^=col-]{margin:0 calc(var(--grid-gutter)/2) calc(var(--grid-gutter)/2)}.col-1{-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-1,.col-2{-webkit-box-flex:0}.col-2{-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3{-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-3,.col-4{-webkit-box-flex:0}.col-4{-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5{-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-5,.col-6{-webkit-box-flex:0}.col-6{-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7{-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-7,.col-8{-webkit-box-flex:0}.col-8{-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9{-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10,.col-9{-webkit-box-flex:0}.col-10{-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11{-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-11,.col-12{-webkit-box-flex:0}.col-12{-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}@media screen and (max-width:599px){.container{width:100%}.col,[class*=col-],[class^=col-]{-webkit-box-flex:0;-ms-flex:0 1 100%;flex:0 1 100%;max-width:100%}}@media screen and (min-width:900px){.col-1-md{-webkit-box-flex:0;-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-2-md{-webkit-box-flex:0;-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3-md{-webkit-box-flex:0;-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-4-md{-webkit-box-flex:0;-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5-md{-webkit-box-flex:0;-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-6-md{-webkit-box-flex:0;-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7-md{-webkit-box-flex:0;-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-8-md{-webkit-box-flex:0;-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9-md{-webkit-box-flex:0;-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10-md{-webkit-box-flex:0;-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11-md{-webkit-box-flex:0;-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-12-md{-webkit-box-flex:0;-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}}@media screen and (min-width:1200px){.col-1-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(8.33333% - var(--grid-gutter));flex:0 0 calc(8.33333% - var(--grid-gutter));max-width:calc(8.33333% - var(--grid-gutter))}.col-2-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(16.66667% - var(--grid-gutter));flex:0 0 calc(16.66667% - var(--grid-gutter));max-width:calc(16.66667% - var(--grid-gutter))}.col-3-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(25% - var(--grid-gutter));flex:0 0 calc(25% - var(--grid-gutter));max-width:calc(25% - var(--grid-gutter))}.col-4-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(33.33333% - var(--grid-gutter));flex:0 0 calc(33.33333% - var(--grid-gutter));max-width:calc(33.33333% - var(--grid-gutter))}.col-5-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(41.66667% - var(--grid-gutter));flex:0 0 calc(41.66667% - var(--grid-gutter));max-width:calc(41.66667% - var(--grid-gutter))}.col-6-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(50% - var(--grid-gutter));flex:0 0 calc(50% - var(--grid-gutter));max-width:calc(50% - var(--grid-gutter))}.col-7-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(58.33333% - var(--grid-gutter));flex:0 0 calc(58.33333% - var(--grid-gutter));max-width:calc(58.33333% - var(--grid-gutter))}.col-8-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(66.66667% - var(--grid-gutter));flex:0 0 calc(66.66667% - var(--grid-gutter));max-width:calc(66.66667% - var(--grid-gutter))}.col-9-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(75% - var(--grid-gutter));flex:0 0 calc(75% - var(--grid-gutter));max-width:calc(75% - var(--grid-gutter))}.col-10-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(83.33333% - var(--grid-gutter));flex:0 0 calc(83.33333% - var(--grid-gutter));max-width:calc(83.33333% - var(--grid-gutter))}.col-11-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(91.66667% - var(--grid-gutter));flex:0 0 calc(91.66667% - var(--grid-gutter));max-width:calc(91.66667% - var(--grid-gutter))}.col-12-lg{-webkit-box-flex:0;-ms-flex:0 0 calc(100% - var(--grid-gutter));flex:0 0 calc(100% - var(--grid-gutter));max-width:calc(100% - var(--grid-gutter))}}fieldset{padding:.5rem 2rem}legend{font-size:.8em;letter-spacing:.1rem;text-transform:uppercase}input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]),select,textarea,textarea[type=text]{border:1px solid var(--color-lightGrey);border-radius:4px;display:block;font-family:inherit;font-size:1em;padding:.8rem 1rem;-webkit-transition:all .2s ease;transition:all .2s ease;width:100%}input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]):not(:disabled):hover,select:hover,textarea:hover,textarea[type=text]:hover{border-color:var(--color-grey)}input:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]):focus,select:focus,textarea:focus,textarea[type=text]:focus{border-color:var(--color-primary);-webkit-box-shadow:0 0 1px var(--color-primary);box-shadow:0 0 1px var(--color-primary);outline:none}input.error:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]),textarea.error{border-color:var(--color-error)}input.success:not([type=checkbox]):not([type=radio]):not([type=submit]):not([type=color]):not([type=button]):not([type=reset]),textarea.success{border-color:var(--color-success)}select{-webkit-appearance:none;background:#f3f3f6 no-repeat 100%;background-image:url("data:image/svg+xml;utf8,");background-origin:content-box;background-size:1ex}[type=checkbox],[type=radio]{height:1.6rem;width:1.6rem}.button,[type=button],[type=reset],[type=submit],button{background:var(--color-lightGrey);border:1px solid transparent;border-radius:4px;color:var(--color-darkGrey);cursor:pointer;display:inline-block;font-size:var(--font-size);line-height:1;padding:1rem 2.5rem;text-align:center;text-decoration:none;-webkit-transform:scale(1);transform:scale(1);-webkit-transition:opacity .2s ease;transition:opacity .2s ease}.grouped{display:-webkit-box;display:-ms-flexbox;display:flex}.grouped>:not(:last-child){margin-right:16px}.grouped.gapless>*{border-radius:0!important;margin:0 0 0 -1px!important}.grouped.gapless>:first-child{border-radius:4px 0 0 4px!important;margin:0!important}.grouped.gapless>:last-child{border-radius:0 4px 4px 0!important}.button+.button{margin-left:1rem}.button:hover,[type=button]:hover,[type=reset]:hover,[type=submit]:hover,button:hover{opacity:.8}.button:active,[type=button]:active,[type=reset]:active,[type=submit]:active,button:active{-webkit-transform:scale(.98);transform:scale(.98)}button:disabled,button:disabled:hover,input:disabled,input:disabled:hover{cursor:not-allowed;opacity:.4}.button.dark,.button.error,.button.primary,.button.secondary,.button.success,[type=submit]{background-color:#000;background-color:var(--color-primary);color:#fff;z-index:1}.button.secondary{background-color:var(--color-grey)}.button.dark{background-color:var(--color-darkGrey)}.button.error{background-color:var(--color-error)}.button.success{background-color:var(--color-success)}.button.outline{background-color:transparent;border-color:var(--color-lightGrey)}.button.outline.primary{border-color:var(--color-primary);color:var(--color-primary)}.button.outline.secondary{border-color:var(--color-grey);color:var(--color-grey)}.button.outline.dark{border-color:var(--color-darkGrey);color:var(--color-darkGrey)}.button.clear{background-color:transparent;border-color:transparent;color:var(--color-primary)}.button.icon{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex}.button.icon>img{margin-left:2px}.button.icon-only{padding:1rem}::-webkit-input-placeholder{color:#bdbfc4}::-moz-placeholder{color:#bdbfc4}:-ms-input-placeholder{color:#bdbfc4}::-ms-input-placeholder{color:#bdbfc4}::placeholder{color:#bdbfc4}.nav{-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;display:-webkit-box;display:-ms-flexbox;display:flex;min-height:5rem}.nav img{max-height:3rem}.nav-center,.nav-left,.nav-right,.nav>.container{display:-webkit-box;display:-ms-flexbox;display:flex}.nav-center,.nav-left,.nav-right{-webkit-box-flex:1;-ms-flex:1;flex:1}.nav-left{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.nav-right{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.nav-center{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}@media screen and (max-width:480px){.nav,.nav>.container{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column}.nav-center,.nav-left,.nav-right{-webkit-box-pack:center;-ms-flex-pack:center;-ms-flex-wrap:wrap;flex-wrap:wrap;justify-content:center}}.nav .brand,.nav a{-webkit-box-align:center;-ms-flex-align:center;align-items:center;color:var(--color-darkGrey);display:-webkit-box;display:-ms-flexbox;display:flex;padding:1rem 2rem;text-decoration:none}.nav .active:not(.button),.nav [aria-current=page]:not(.button){color:#000;color:var(--color-primary)}.nav .brand{font-size:1.75em;padding-bottom:0;padding-top:0}.nav .brand img{padding-right:1rem}.nav .button{margin:auto 1rem}.card{background:var(--bg-color);border-radius:4px;-webkit-box-shadow:0 1px 3px var(--color-grey);box-shadow:0 1px 3px var(--color-grey);padding:1rem 2rem}.card p:last-child{margin:0}.card header>*{margin-bottom:1rem;margin-top:0}.tabs{display:-webkit-box;display:-ms-flexbox;display:flex}.tabs a{text-decoration:none}.tabs>.dropdown>summary,.tabs>a{-webkit-box-flex:0;border-bottom:2px solid var(--color-lightGrey);color:var(--color-darkGrey);-ms-flex:0 1 auto;flex:0 1 auto;padding:1rem 2rem;text-align:center}.tabs>a.active,.tabs>a:hover,.tabs>a[aria-current=page]{border-bottom:2px solid var(--color-darkGrey);opacity:1}.tabs>a.active,.tabs>a[aria-current=page]{border-color:var(--color-primary)}.tabs.is-full a{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.tag{border:1px solid var(--color-lightGrey);color:var(--color-grey);display:inline-block;letter-spacing:.5px;line-height:1;padding:.5rem;text-transform:uppercase}.tag.is-small{font-size:.75em;padding:.4rem}.tag.is-large{font-size:1.125em;padding:.7rem}.tag+.tag{margin-left:1rem}details.dropdown{display:inline-block;position:relative}details.dropdown>:last-child{left:0;position:absolute;white-space:nowrap}.bg-primary{background-color:var(--color-primary)!important}.bg-light{background-color:var(--color-lightGrey)!important}.bg-dark{background-color:var(--color-darkGrey)!important}.bg-grey{background-color:var(--color-grey)!important}.bg-error{background-color:var(--color-error)!important}.bg-success{background-color:var(--color-success)!important}.bd-primary{border:1px solid var(--color-primary)!important}.bd-light{border:1px solid var(--color-lightGrey)!important}.bd-dark{border:1px solid var(--color-darkGrey)!important}.bd-grey{border:1px solid var(--color-grey)!important}.bd-error{border:1px solid var(--color-error)!important}.bd-success{border:1px solid var(--color-success)!important}.text-primary{color:var(--color-primary)!important}.text-light{color:var(--color-lightGrey)!important}.text-dark{color:var(--color-darkGrey)!important}.text-grey{color:var(--color-grey)!important}.text-error{color:var(--color-error)!important}.text-success{color:var(--color-success)!important}.text-white{color:#fff!important}.pull-right{float:right!important}.pull-left{float:left!important}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.text-justify{text-align:justify}.text-uppercase{text-transform:uppercase}.text-lowercase{text-transform:lowercase}.text-capitalize{text-transform:capitalize}.is-full-screen{min-height:100vh;width:100%}.is-full-width{width:100%!important}.is-vertical-align{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.is-center,.is-horizontal-align{-webkit-box-pack:center;-ms-flex-pack:center;display:-webkit-box;display:-ms-flexbox;display:flex;justify-content:center}.is-center{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.is-right{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.is-left,.is-right{-webkit-box-align:center;-ms-flex-align:center;align-items:center;display:-webkit-box;display:-ms-flexbox;display:flex}.is-left{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.is-fixed{position:fixed;width:100%}.is-paddingless{padding:0!important}.is-marginless{margin:0!important}.is-pointer{cursor:pointer!important}.is-rounded{border-radius:100%}.clearfix{clear:both;content:"";display:table}.is-hidden{display:none!important}@media screen and (max-width:599px){.hide-xs{display:none!important}}@media screen and (min-width:600px) and (max-width:899px){.hide-sm{display:none!important}}@media screen and (min-width:900px) and (max-width:1199px){.hide-md{display:none!important}}@media screen and (min-width:1200px){.hide-lg{display:none!important}}@media print{.hide-pr{display:none!important}} \ No newline at end of file diff --git a/data/data_export.html b/data/data_export.html new file mode 100644 index 0000000..34a7f85 --- /dev/null +++ b/data/data_export.html @@ -0,0 +1,60 @@ +” + + + + + + + + +
+ + +
+
+

JSON

+
+ Hello json +
+ +
+
+

MQTT

+
+ Hello mqtt +
+ +
+
+

Email alert

+
+ Hello json +
+ + + +
+ + + \ No newline at end of file diff --git a/data/logic.js b/data/logic.js new file mode 100644 index 0000000..e69de29 diff --git a/data/settings.html b/data/settings.html new file mode 100644 index 0000000..a61ceb0 --- /dev/null +++ b/data/settings.html @@ -0,0 +1,79 @@ +” + + + + + + + + +
+ + + +
+ Settings +

+ + +

+

+ + +

+

+ + +

+

+ + +

+ +

+ +

+
+ +
+
+ WiFi Settings +

+ + +

+

+ + +

+ +

+ +

+
+
+ + + + + + +
+ \ No newline at end of file diff --git a/data/status.html b/data/status.html new file mode 100644 index 0000000..6f2750b --- /dev/null +++ b/data/status.html @@ -0,0 +1,178 @@ + + + + + + + + + +
+ + +
+
+ +
+
+

Sensor information

+
+ + + + + + + + + + + + +
Voltage: 12.12V
Current: 10 mA
+
+ +
+
+

ESP information

+
+ + + + + + + + + + + + + + + + +
Uptime: 132d
FW version: 0.2
Time/Date: 1.1.202022
+
+ +
+
+

Connection Info

+
+ + + + + + + + + + + + + + + + +
Type: WiFi
IP: 127.0.0.1
RSSI: good
+
+
+
+ + + +
+
+ +
+
+
+
+
25%
+
2500 / 10000
+
+ +
+
+ + +
+ + + + + + + \ No newline at end of file diff --git a/documentation/api_doc.md b/documentation/api_doc.md new file mode 100644 index 0000000..c80670e --- /dev/null +++ b/documentation/api_doc.md @@ -0,0 +1,11 @@ +## Sensor data API +{ + "voltage": 12.43, + "current": 15.2732, + "error_code": 2, + "level_percent":73.843, + "level_cm": 281.3, + "top_level_cm": 281.3, + "bottom_level_cm": 281.3, + "liter": 24000 +} \ No newline at end of file diff --git a/documentation/error_codes.md b/documentation/error_codes.md new file mode 100644 index 0000000..6ab3b6b --- /dev/null +++ b/documentation/error_codes.md @@ -0,0 +1,15 @@ +## Error codes +1. Voltage Low +2. Voltage High +3. Current low +4. Current high + +else if (voltage_low) { + display_error_code(1); + } else if (voltage_high){ + display_error_code(2); + } else if (current_low) { + display_error_code(3); + } else if (current_high){ + display_error_code(4); + } \ No newline at end of file diff --git a/include/README b/include/README new file mode 100644 index 0000000..194dcd4 --- /dev/null +++ b/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/lib/README b/lib/README new file mode 100644 index 0000000..6debab1 --- /dev/null +++ b/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into executable file. + +The source code of each library should be placed in a an own separate directory +("lib/your_library_name/[here are source files]"). + +For example, see a structure of the following two libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +and a contents of `src/main.c`: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +PlatformIO Library Dependency Finder will find automatically dependent +libraries scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..b75763a --- /dev/null +++ b/platformio.ini @@ -0,0 +1,23 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:ESP32] +platform = espressif32 +board = wemos_d1_mini32 +framework = arduino +monitor_speed = 115200 +lib_deps = + ottowinter/ESPAsyncWebServer-esphome@^3.1.0 + robtillaart/INA226@^0.4.4 + bblanchon/ArduinoJson@^6.21.3 + ArduinoLog + +upload_protocol = espota +upload_port = 192.168.4.18 \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..f93d7ab --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,388 @@ +#include +#include "SPIFFS.h" +#include +#include +#include +#include + +#include +#include +#include "AsyncJson.h" + +#include + +#include "INA226.h" +#include "Wire.h" + +#include + +#define LED_1 4 +#define LED_2 2 +#define LED_3 15 +#define LED_4 13 +#define LED_5 12 +#define LED_RED 14 + +// Define keys to prevent typos +#define ssid_key "ssid" +#define wifi_password_key "wifi_password" + +//Calibration variables +float zero_value = 0.03; //Measured shunt voltage with nothing connected, used to fix measuring offset +float max_water_level_cm = 200; +float min_water_level_cm = 0; +float sensor_range = 2.0; + +bool voltage_high = false; +bool voltage_low = false; +bool current_high = false; +bool current_low = false; + +float rssi; +uint32_t ip_address; +uint8_t failed_connection_attempts = 0; + +struct SensorData { + int percentage; + float voltage; + float current; + float water_height; +}; + +SensorData current_data = SensorData{-1, -1, -1, -1}; + +int64_t mac_address = ESP.getEfuseMac(); + +INA226 ina_sensor(0x40); + +Preferences prefs; + +AsyncWebServer server(80); +#define FORMAT_LITTLEFS_IF_FAILED true + + +void display_percentage(int percentage){ + digitalWrite(LED_RED, 0); + + if (percentage > 20){ + digitalWrite(LED_1, 1); + } else { + digitalWrite(LED_1, 0); + } + if (percentage > 40){ + digitalWrite(LED_2, 1); + } else { + digitalWrite(LED_2, 0); + } + if (percentage > 60){ + digitalWrite(LED_3, 1); + } else { + digitalWrite(LED_3, 0); + } + if (percentage > 80){ + digitalWrite(LED_4, 1); + } else { + digitalWrite(LED_4, 0); + } + if (percentage > 95){ + digitalWrite(LED_5, 1); + } else { + digitalWrite(LED_5, 0); + } +} + +void display_error_code(byte err_code) { + digitalWrite(LED_RED, 1); + digitalWrite(LED_1, bitRead(err_code, 0)); + digitalWrite(LED_2, bitRead(err_code, 1)); + digitalWrite(LED_3, bitRead(err_code, 2)); + digitalWrite(LED_4, bitRead(err_code, 3)); + digitalWrite(LED_5, bitRead(err_code, 4)); + +} + +bool is_error(){ + return voltage_high || voltage_low || current_high || current_low; +} + +void printSuffix(Print* _logOutput, int logLevel) { + _logOutput->print(CR); +} + +void print_prefix(Print* _logOutput, int logLevel) { + _logOutput->print("WATERMETER - C"); + _logOutput->print(xPortGetCoreID()); + _logOutput->print(" - "); + _logOutput->print(pcTaskGetName(xTaskGetCurrentTaskHandle())); + _logOutput->print(" - "); +} + +void display_task(void * parameter ){ + while (true) { + if (!is_error()){ + // We have no error, refresh status display and wait half a second + display_percentage(current_data.percentage); + delay(1000); + } else { + Log.verbose("Error detected"); + // We have an error, display error code for 3 seconds and then water level for 3 seconds + if (voltage_low) { + display_error_code(1); + } else if (voltage_high){ + display_error_code(2); + } else if (current_low) { + display_error_code(3); + } else if (current_high){ + display_error_code(4); + } + delay(3000); + display_percentage(current_data.percentage); + delay(3000); + } + } + +} + +void wifi_task(void * parameter ){ + Log.verbose("Starting WiFi Task"); + while (true) { + if (prefs.getString(ssid_key, "") == "" || failed_connection_attempts > 5 ) { + if (failed_connection_attempts > 5) { + Log.verbose("Failed to connecto to currently saved SSID, starting SoftAP"); + } else { + Log.verbose("No SSID saved, starting SoftAP"); + } + + + String ap_ssid = "Watermeter-" + String(mac_address); + WiFi.softAP(ap_ssid, ""); + + Log.verbose("[WIFI_TASK] Waiting for SSID now..."); + + String old_ssid = prefs.getString(ssid_key, "xxx"); + while(prefs.getString(ssid_key, "") == "" || prefs.getString(ssid_key, "") == old_ssid) { + delay(1000); + } + + + failed_connection_attempts = 0; + } else { + if (WiFi.isConnected() && WiFi.SSID() == prefs.getString(ssid_key, "")) { + failed_connection_attempts = 0; + rssi = WiFi.RSSI(); + ip_address = WiFi.localIP(); + + + + Log.verbose("RSSI: %F, IP Address, %p, SSID: %s", rssi, WiFi.localIP(), prefs.getString(ssid_key, "NOSSID")); + delay(5000); + } else { + Log.verbose("Connecting to %s using password %s", prefs.getString(ssid_key, ""), prefs.getString(wifi_password_key, "")); + WiFi.mode(WIFI_STA); + WiFi.begin(prefs.getString(ssid_key, ""), prefs.getString(wifi_password_key, "")); + failed_connection_attempts++; + delay(5000); + } + } + } + +} + +void ethernet_task(void * parameter ){ + while (true) { + + } + +} + +void setup() { + prefs.begin("waterlevel", false); + Serial.begin(115200); + + Log.begin(LOG_LEVEL_VERBOSE, &Serial); + Log.setSuffix(printSuffix); + Log.setPrefix(print_prefix); + + Log.verbose("Starting WiFi Task now (external)"); + xTaskCreate(wifi_task, "WiFiTask", 10000, NULL, 1, NULL); + + Log.verbose("Init LEDs"); + pinMode(LED_1, OUTPUT); + pinMode(LED_2, OUTPUT); + pinMode(LED_3, OUTPUT); + pinMode(LED_4, OUTPUT); + pinMode(LED_5, OUTPUT); + pinMode(LED_RED, OUTPUT); + display_error_code(17); + + Log.verbose("Beginning SPIFFS"); + SPIFFS.begin(true); + Log.verbose("SPIFFS initialized"); + display_error_code(19); + + Log.verbose("Begin INA"); + ina_sensor.begin(33, 32); + display_error_code(20); + ina_sensor.setMaxCurrentShunt(0.02, 4, false); + ina_sensor.setBusVoltageConversionTime(7); + ina_sensor.setShuntVoltageConversionTime(7); + ina_sensor.setAverage(4); + display_error_code(21); + + Log.verbose("Connecting Ethernet"); + ETH.begin(0, 17, 23, 18); + Log.verbose(ETH.getHostname()); + display_error_code(22); + + /////////////////////////////// ROUTES /////////////////////////////// + Log.verbose("Route Setup"); + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/status.html", "text/html", false); + }); + + server.on("/settings", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/settings.html", "text/html", false); + }); + + server.on("/export", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/data_export.html", "text/html", false); + }); + + server.on("/update_wifi_credentials", HTTP_POST, [](AsyncWebServerRequest *request){ + int params = request->params(); + + // Log.verbose(request->hasParam(ssid_key)); + // Log.verbose(request->hasParam(wifi_password_key)); + + if (request->hasParam(ssid_key, true) && request->hasParam(wifi_password_key, true)){ + Log.verbose("Updating SSID config"); + AsyncWebParameter* ssid_param = request->getParam(ssid_key, true); + AsyncWebParameter* password_param = request->getParam(wifi_password_key, true); + prefs.putString(ssid_key, ssid_param->value().c_str()); + prefs.putString(wifi_password_key, password_param->value().c_str()); + } else { + for(int i=0;igetParam(i); + if(p->isFile()){ //p->isPost() is also true + Log.verbose("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } else if(p->isPost()){ + Log.verbose("POST[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } else { + Log.verbose("GET[%s]: %s\n", p->name().c_str(), p->value().c_str()); + } + } + request->send(400, "text/plain", "Missing parameters"); //TODO add proper error messages + } + request->send(SPIFFS, "/settings.html", "text/html", false); //TODO add proper return templating + }); + + server.on("/sensor_data", HTTP_GET, [](AsyncWebServerRequest *request){ + StaticJsonDocument<128> doc; + doc["percentage"] = current_data.percentage; + doc["voltage"] = current_data.voltage; + doc["current"] = current_data.current; + doc["water_height"] = current_data.water_height; + + String output; + serializeJson(doc, output); + request->send(200, "application/json", output); + }); + + server.on("/chota.css", HTTP_GET, [](AsyncWebServerRequest *request){ + request->send(SPIFFS, "/chota.css", "text/css", false); + }); + + display_error_code(23); + + Log.verbose("Starting webserver"); + server.begin(); + display_error_code(24); + + Log.verbose("OTA Setup"); + ArduinoOTA + .onStart([]() { + String type; + if (ArduinoOTA.getCommand() == U_FLASH) + type = "sketch"; + else // U_SPIFFS + type = "filesystem"; + + // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end() + Log.verbose("Start updating %s", type); + }) + .onEnd([]() { + Log.verbose("\nEnd"); + }) + .onProgress([](unsigned int progress, unsigned int total) { + Serial.printf("Progress: %u%%\r", (progress / (total / 100))); + }) + .onError([](ota_error_t error) { + Serial.printf("Error[%u]: ", error); + if (error == OTA_AUTH_ERROR) Log.verbose("Auth Failed"); + else if (error == OTA_BEGIN_ERROR) Log.verbose("Begin Failed"); + else if (error == OTA_CONNECT_ERROR) Log.verbose("Connect Failed"); + else if (error == OTA_RECEIVE_ERROR) Log.verbose("Receive Failed"); + else if (error == OTA_END_ERROR) Log.verbose("End Failed"); + }); + display_error_code(25); + ArduinoOTA.begin(); + display_error_code(26); + digitalWrite(LED_RED, 0); + Log.verbose(ETH.localIP()); + + xTaskCreate(display_task, "DisplayTask", 10000, NULL, 1, NULL); +} + + +/** + * Updates the global variables from the current ina sensor data + * All other parts just read these variables to prevent incositencies +*/ +void update_sensor_data(){ + float bus_voltage = ina_sensor.getBusVoltage(); + float shunt_voltage = ina_sensor.getShuntVoltage_mV() - zero_value; + float shunt_current = shunt_voltage / 4 ; + + + float mA_per_cm = (20 - 4)/ (sensor_range*100); + + + float min_water_level_mA_over_zero = (min_water_level_cm * mA_per_cm); + float max_water_level_mA_over_zero = (max_water_level_cm * mA_per_cm); + + float min_water_level_mA = 4 + min_water_level_mA_over_zero; + float max_water_level_mA = 4 + max_water_level_mA_over_zero; + + // Over Zero always revers to zero water level + float shunt_current_over_zero = shunt_current - min_water_level_mA; + + int percentage = round(( shunt_current_over_zero / max_water_level_mA_over_zero) * 100); + + + current_low = shunt_current < 3.5; + current_high = shunt_current > 20.5; + voltage_low = bus_voltage < 23; + voltage_high = bus_voltage > 25; + Log.verbose("=========="); + Log.verbose("Shunt current: %F", shunt_current); + Log.verbose("Shunt voltage: %F", shunt_voltage); + Log.verbose("Value percentage: %F", percentage); + + + current_data = SensorData{percentage, bus_voltage, shunt_current, shunt_current_over_zero}; + + + +} + +void loop() { + ArduinoOTA.handle(); + + + update_sensor_data(); + // Log.verbose(WiFi.softAPIP()); + // Log.verbose(ETH.localIP()); + delay(5000); +} + + diff --git a/test/README b/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html