Initial commit with action
Some checks failed
Test compiling project / test (push) Failing after 33s

This commit is contained in:
2023-10-02 15:30:30 +02:00
commit bbe5bfdcee
15 changed files with 888 additions and 0 deletions

View File

@@ -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 ./

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View File

@@ -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"
]
}

1
data/chota.css Normal file

File diff suppressed because one or more lines are too long

60
data/data_export.html Normal file
View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="/chota.css">
<script>
if (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
</head>
<body>
<div class="container">
<nav class="nav" style="margin-bottom: 50px;">
<div class="nav-left">
<a class="brand" href="#">Watermeter</a>
<div class="tabs">
<a href="/">Status</a>
<a href="/settings">Settings</a>
<a class="active" href="/export">Data export</a>
</div>
</div>
<div class="nav-right">
<a class="button outline">Button</a>
</div>
</nav>
<div class="card">
<header>
<h4>JSON</h4>
</header>
Hello json
</div>
<div class="card">
<header>
<h4>MQTT</h4>
</header>
Hello mqtt
</div>
<div class="card">
<header>
<h4>Email alert</h4>
</header>
Hello json
</div>
</div>
</body>
<style>
.card {
margin-bottom: 10px;
}
</style>

0
data/logic.js Normal file
View File

79
data/settings.html Normal file
View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="/chota.css">
<script>
if (window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.body.classList.add('dark');
}
</script>
</head>
<body>
<div class="container">
<nav class="nav" style="margin-bottom: 50px;">
<div class="nav-left">
<a class="brand" href="#">Watermeter</a>
<div class="tabs">
<a href="/">Status</a>
<a class="active" href="/settings">Settings</a>
<a href="/export">Data export</a>
</div>
</div>
<div class="nav-right">
<a class="button outline">Button</a>
</div>
</nav>
<fieldset id="form-settings">
<legend>Settings</legend>
<p>
<label for="input__text">Sensor max height (e.g. 4m)</label>
<input id="input__text" type="number" placeholder="Text Input">
</p>
<p>
<label for="input__password">Water max height</label>
<input id="input__password" type="number">
</p>
<p>
<label for="input__webaddress">Water min height</label>
<input id="input__webaddress" type="number">
</p>
<p>
<label for="input__emailaddress">Email Address</label>
<input id="input__emailaddress" type="number">
</p>
<p>
<button class="button primary" style="width: 100%;">Submit</button>
</p>
</fieldset>
<form action="/update_wifi_credentials" method="post">
<fieldset id="form-settings">
<legend>WiFi Settings</legend>
<p>
<label for="ssid">SSID</label>
<input id="ssid" name="ssid" type="text">
</p>
<p>
<label for="wifi_password">Password</label>
<input id="wifi_password" name="wifi_password" type="password">
</p>
<p>
<button class="button primary" style="width: 100%;">Submit</button>
</p>
</fieldset>
</form>
</div>
</body>

178
data/status.html Normal file
View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<head>
<link rel="stylesheet" href="/chota.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.4/Chart.js"></script>
</head>
<body>
<div class="container">
<nav class="nav" style="margin-bottom: 50px;">
<div class="nav-left">
<a class="brand" href="#">Watermeter</a>
<div class="tabs">
<a class="active" href="/">Status</a>
<a href="/settings">Settings</a>
<a href="/export">Data export</a>
</div>
</div>
<div class="nav-right">
<a class="button outline">Button</a>
</div>
</nav>
<div class="row">
<div class="col-12 col-3-lg">
<!--Sensor card-->
<div class="card">
<header>
<h4>Sensor information</h4>
</header>
<table class="tg">
<thead></thead>
<tbody>
<tr>
<td>Voltage: </td>
<td>12.12V</td>
</tr>
<tr>
<td>Current: </td>
<td>10 mA</td>
</tr>
</tbody>
</table>
</div>
<!--ESP Infor card-->
<div class="card">
<header>
<h4>ESP information</h4>
</header>
<table class="tg">
<thead></thead>
<tbody>
<tr>
<td>Uptime: </td>
<td>132d</td>
</tr>
<tr>
<td>FW version: </td>
<td>0.2</td>
</tr>
<tr>
<td>Time/Date: </td>
<td>1.1.202022</td>
</tr>
</tbody>
</table>
</div>
<!--WIFI card-->
<div class="card">
<header>
<h4>Connection Info</h4>
</header>
<table class="tg">
<thead></thead>
<tbody>
<tr>
<td>Type: </td>
<td>WiFi</td>
</tr>
<tr>
<td>IP: </td>
<td>127.0.0.1</td>
</tr>
<tr>
<td>RSSI: </td>
<td>good</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="col-12 col-7-lg">
<canvas id="myChart" style="width:100%;max-width:700px"></canvas>
</div>
<div class="col-12 col-2-lg">
<div class="outer-wrapper">
<div class="column-wrapper">
<div class="column"></div>
</div>
<div class="percentage">25%</div>
<div class="value">2500 / 10000</div>
</div>
</div>
</div>
</div>
</body>
<style>
.outer-wrapper {
display: inline-block;
margin: 5px 15px;
padding: 25px 15px;
background: #eee;
min-width: 50px;
}
.column-wrapper {
height: 400px;
width: 20px;
background: #CFD8DC;
transform: rotate(180deg);
margin: 0 auto;
}
.column {
width: 20px;
height: 25%;
background: #1900ff;
}
.percentage,
.value {
margin-top: 10px;
padding: 5px 10px;
color: #FFF;
background: #263238;
position: relative;
border-radius: 4px;
text-align: center;
}
.value {
background: #7986CB;
}
.card {
margin-bottom: 10px;
}
</style>
<script>
var xValues = [1,60,70,80,90,100,110,120,130,140,150];
var yValues = [7,8,8,9,9,9,10,11,14,14,15];
new Chart("myChart", {
type: "line",
data: {
labels: xValues,
datasets: [{
backgroundColor: "rgba(0,0,0,1.0)",
borderColor: "rgba(0,0,0,0.1)",
data: yValues
}]
},
options:{}
});
</script>

11
documentation/api_doc.md Normal file
View File

@@ -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
}

View File

@@ -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);
}

39
include/README Normal file
View File

@@ -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

46
lib/README Normal file
View File

@@ -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 <Foo.h>
#include <Bar.h>
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

23
platformio.ini Normal file
View File

@@ -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

388
src/main.cpp Normal file
View File

@@ -0,0 +1,388 @@
#include <Arduino.h>
#include "SPIFFS.h"
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ETH.h>
#include <ArduinoJson.h>
#include <ArduinoLog.h>
#include "AsyncJson.h"
#include <ArduinoOTA.h>
#include "INA226.h"
#include "Wire.h"
#include <Preferences.h>
#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;i<params;i++){
AsyncWebParameter* p = request->getParam(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);
}

11
test/README Normal file
View File

@@ -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