mirror of
https://github.com/jomjol/AI-on-the-edge-device.git
synced 2025-12-31 03:48:25 +03:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71766e1c7b | ||
|
|
d739ad4c91 | ||
|
|
71577039f8 | ||
|
|
39de778ffe | ||
|
|
de4ae71864 | ||
|
|
2b09ce6e44 | ||
|
|
eb812282d2 | ||
|
|
f3e3ce504e | ||
|
|
ba03a7dc38 | ||
|
|
73afc07652 | ||
|
|
c320e4c921 | ||
|
|
43b29f3408 | ||
|
|
b1c65c0a71 | ||
|
|
00091fc3f9 | ||
|
|
69a43fb068 | ||
|
|
82f28cb5bc | ||
|
|
34818c0dc1 | ||
|
|
0b3a6e1057 | ||
|
|
f06ef7b80e | ||
|
|
962a674058 | ||
|
|
c57cd83948 | ||
|
|
6991c41060 | ||
|
|
8bb274cd84 | ||
|
|
168ec5b485 | ||
|
|
7a0a34e32e | ||
|
|
64bf79b288 | ||
|
|
61085d3861 | ||
|
|
8494f36069 | ||
|
|
3e67aeec0d | ||
|
|
e85e92762e | ||
|
|
eb9bf3c7c1 |
27
.github/workflows/build.yaml
vendored
27
.github/workflows/build.yaml
vendored
@@ -1,6 +1,10 @@
|
|||||||
name: Build and Pack
|
name: Build and Pack
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
release:
|
||||||
|
types: [released] # Only trigger on published releases (not drafts or pre-released)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
@@ -286,6 +290,7 @@ jobs:
|
|||||||
rm -rf ./sd-card/html
|
rm -rf ./sd-card/html
|
||||||
rm -rf ./sd-card/demo
|
rm -rf ./sd-card/demo
|
||||||
cp -r ./html ./sd-card/ # Overwrite the Web UI with the preprocessed files
|
cp -r ./html ./sd-card/ # Overwrite the Web UI with the preprocessed files
|
||||||
|
rm -f ./sd-card/Readme.md
|
||||||
cp -r ./demo ./sd-card/
|
cp -r ./demo ./sd-card/
|
||||||
cd sd-card; rm -rf html/param-tooltips; zip -r ../manual_setup/sd-card.zip *; cd ..
|
cd sd-card; rm -rf html/param-tooltips; zip -r ../manual_setup/sd-card.zip *; cd ..
|
||||||
cd ./manual_setup
|
cd ./manual_setup
|
||||||
@@ -303,7 +308,7 @@ jobs:
|
|||||||
prepare-release:
|
prepare-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [pack-for-update, pack-for-manual_setup, pack-for-remote_setup]
|
needs: [pack-for-update, pack-for-manual_setup, pack-for-remote_setup]
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: github.event_name == 'release' # Only run when the trigger is a release
|
||||||
|
|
||||||
# Sets permissions of the GITHUB_TOKEN to allow updating the branches
|
# Sets permissions of the GITHUB_TOKEN to allow updating the branches
|
||||||
permissions:
|
permissions:
|
||||||
@@ -405,7 +410,7 @@ jobs:
|
|||||||
#########################################################################################
|
#########################################################################################
|
||||||
# Make sure to also update update-webinstaller.yml!
|
# Make sure to also update update-webinstaller.yml!
|
||||||
update-web-installer:
|
update-web-installer:
|
||||||
if: github.event_name == 'release' && github.event.action == 'published' # Only run on release but not on prerelease
|
if: github.event_name == 'release' # Only run when the trigger is a release
|
||||||
needs: [prepare-release]
|
needs: [prepare-release]
|
||||||
environment:
|
environment:
|
||||||
name: github-pages
|
name: github-pages
|
||||||
@@ -434,22 +439,22 @@ jobs:
|
|||||||
- name: Add binary to Web Installer and update manifest
|
- name: Add binary to Web Installer and update manifest
|
||||||
run: |
|
run: |
|
||||||
echo "Updating Web installer to use firmware from ${{ steps.last_release.outputs.tag_name }}..."
|
echo "Updating Web installer to use firmware from ${{ steps.last_release.outputs.tag_name }}..."
|
||||||
rm -f docs/binary/firmware.bin
|
rm -f webinstaller/binary/firmware.bin
|
||||||
wget ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.last_release.outputs.tag_name }}/AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
wget ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.last_release.outputs.tag_name }}/AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
||||||
unzip AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
unzip AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
||||||
cp -f firmware.bin docs/binary/firmware.bin
|
cp -f firmware.bin webinstaller/binary/firmware.bin
|
||||||
echo "Updating index and manifest file..."
|
echo "Updating index and manifest file..."
|
||||||
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' docs/index.html
|
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' webinstaller/index.html
|
||||||
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' docs/manifest.json
|
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' webinstaller/manifest.json
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v4
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v2
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: 'docs'
|
path: 'webinstaller'
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v3 # Note: v4 does not work!
|
uses: actions/deploy-pages@v4.0.5 # Note: v4 does not work!
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# This updates the Web Installer with the files from the docs folder and the binary of the latest release
|
# This updates the Web Installer with the files from the webinstaller folder and the binary of the latest release
|
||||||
# it only gets run on:
|
# it only gets run on:
|
||||||
# - Manually triggered
|
# - Manually triggered
|
||||||
# Make sure to also update the lower part of build.yml!
|
# Make sure to also update the lower part of build.yml!
|
||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
# branches:
|
# branches:
|
||||||
# - rolling
|
# - rolling
|
||||||
# paths:
|
# paths:
|
||||||
# - docs # The path filter somehow does not work, so lets run it on every change to rolling
|
# - webinstaller # The path filter somehow does not work, so lets run it on every change to rolling
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
manually-update-web-installer:
|
manually-update-web-installer:
|
||||||
@@ -42,13 +42,13 @@ jobs:
|
|||||||
- name: Add binary to Web Installer and update manifest
|
- name: Add binary to Web Installer and update manifest
|
||||||
run: |
|
run: |
|
||||||
echo "Updating Web installer to use firmware from ${{ steps.last_release.outputs.tag_name }}..."
|
echo "Updating Web installer to use firmware from ${{ steps.last_release.outputs.tag_name }}..."
|
||||||
rm -f docs/binary/firmware.bin
|
rm -f webinstaller/binary/firmware.bin
|
||||||
wget https://github.com/jomjol/AI-on-the-edge-device/releases/download/${{ steps.last_release.outputs.tag_name }}/AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
wget ${{ github.server_url }}/${{ github.repository }}/releases/download/${{ steps.last_release.outputs.tag_name }}/AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
||||||
unzip AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
unzip AI-on-the-edge-device__update__${{ steps.last_release.outputs.tag_name }}.zip
|
||||||
cp -f firmware.bin docs/binary/firmware.bin
|
cp -f firmware.bin webinstaller/binary/firmware.bin
|
||||||
echo "Updating index and manifest file..."
|
echo "Updating index and manifest file..."
|
||||||
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' docs/index.html
|
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' webinstaller/index.html
|
||||||
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' docs/manifest.json
|
sed -i 's/$VERSION/${{ steps.last_release.outputs.tag_name }}/g' webinstaller/manifest.json
|
||||||
|
|
||||||
- name: Setup Pages
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v5
|
uses: actions/configure-pages@v5
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
path: 'docs'
|
path: 'webinstaller'
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
|
|||||||
2
.github/workflows/reply-bot.yaml
vendored
2
.github/workflows/reply-bot.yaml
vendored
@@ -18,7 +18,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
comment:
|
comment:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
26
Changelog.md
26
Changelog.md
@@ -1,4 +1,28 @@
|
|||||||
# [16.0.0] - 2024-03-15
|
# [16.1.0-RC1] - 2025-12-29
|
||||||
|
|
||||||
|
For a full list of changes see [Full list of changes](https://github.com/jomjol/AI-on-the-edge-device/compare/v16.0.0...v16.1.0)
|
||||||
|
|
||||||
|
### Known issues
|
||||||
|
No software is perfect. We know that our software has some quirks. If you have an issue, please first check the [issues](https://github.com/jomjol/AI-on-the-edge-device/issues) and
|
||||||
|
[discussions](https://github.com/jomjol/AI-on-the-edge-device/discussions) before reporting a new issue.
|
||||||
|
|
||||||
|
|
||||||
|
### Core Changes and Bug fixes
|
||||||
|
- Various minor improvements and changes
|
||||||
|
- Various documentation corrections/improvements
|
||||||
|
- Parameter `ChangeRateThreshold` can now also be zero [#3668](https://github.com/jomjol/AI-on-the-edge-device/pull/3668)
|
||||||
|
- MQTT: Added a proper `device_class` (duration) to "uptime" [#3659](https://github.com/jomjol/AI-on-the-edge-device/pull/3659)
|
||||||
|
- MQTT: Added `default_entity_id` to MQTT HA autodiscovery payload [#3970](https://github.com/jomjol/AI-on-the-edge-device/pull/3970)
|
||||||
|
- Added new models
|
||||||
|
- Updated SDCard Manufacturer List [#3730](https://github.com/jomjol/AI-on-the-edge-device/pull/3730)
|
||||||
|
- Added option for gal/min rate for Home Assistant MQTT Autodiscovery[#3868](https://github.com/jomjol/AI-on-the-edge-device/pull/3868)
|
||||||
|
- Fixed Setup Wizard [#3906](https://github.com/jomjol/AI-on-the-edge-device/pull/3906)
|
||||||
|
- `checkDigitConsistency`: added ESP_LOG_DEBUG output [#3962](https://github.com/jomjol/AI-on-the-edge-device/pull/3962)
|
||||||
|
- Added "Draw from center" option to Analog ROI editor [#3975](https://github.com/jomjol/AI-on-the-edge-device/pull/3975)
|
||||||
|
- Enabled OV3660 support in sdkconfig.defaults [#3996](https://github.com/jomjol/AI-on-the-edge-device/pull/3996)
|
||||||
|
|
||||||
|
|
||||||
|
# [16.0.0] - 2025-03-15
|
||||||
|
|
||||||
For a full list of changes see [Full list of changes](https://github.com/jomjol/AI-on-the-edge-device/compare/v15.7.0...v16.0.0)
|
For a full list of changes see [Full list of changes](https://github.com/jomjol/AI-on-the-edge-device/compare/v15.7.0...v16.0.0)
|
||||||
|
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ Various 3D-printable housings can be found here:
|
|||||||
- ⚡ [Power Meter](https://www.thingiverse.com/thing:5028229)
|
- ⚡ [Power Meter](https://www.thingiverse.com/thing:5028229)
|
||||||
- 🔥 [Gas Meter](https://www.thingiverse.com/thing:5224101)
|
- 🔥 [Gas Meter](https://www.thingiverse.com/thing:5224101)
|
||||||
- 📷 [ESP32-cam housing only](https://www.thingiverse.com/thing:4571627)
|
- 📷 [ESP32-cam housing only](https://www.thingiverse.com/thing:4571627)
|
||||||
|
- 📡 [Top cover and Antenna Holder](https://www.printables.com/model/452139-cover-for-esp32-cam-water-meter)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
```
|
```
|
||||||
git clone https://github.com/jomjol/AI-on-the-edge-device.git
|
git clone https://github.com/jomjol/AI-on-the-edge-device.git
|
||||||
cd AI-on-the-edge-device
|
cd AI-on-the-edge-device
|
||||||
git checkout rolling
|
git checkout main
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -12,10 +12,10 @@ git submodule update --init
|
|||||||
```
|
```
|
||||||
cd /components/submodule-name (e.g. tflite-micro-example)
|
cd /components/submodule-name (e.g. tflite-micro-example)
|
||||||
git checkout VERSION (e.g. HASH of latest tflite-micro-example build)
|
git checkout VERSION (e.g. HASH of latest tflite-micro-example build)
|
||||||
cd ../../ (auf Ebene von code)
|
cd ../../ (at the code level)
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
```
|
```
|
||||||
Evt. muss man vorher noch einige Verzeichnisse in compenents von Hand löschen, da sie beim checkout nicht gelöscht wurden (vor update -- init)
|
You may need to manually delete some directories in the 'components' folder beforehand, as they were not deleted during checkout (before update -- init)
|
||||||
|
|
||||||
## Build and Flash within terminal
|
## Build and Flash within terminal
|
||||||
See further down to build it within an IDE.
|
See further down to build it within an IDE.
|
||||||
@@ -51,7 +51,7 @@ pio device monitor -p /dev/ttyUSB0
|
|||||||
```
|
```
|
||||||
git clone https://github.com/jomjol/AI-on-the-edge-device.git
|
git clone https://github.com/jomjol/AI-on-the-edge-device.git
|
||||||
cd AI-on-the-edge-device
|
cd AI-on-the-edge-device
|
||||||
git checkout rolling
|
git checkout main
|
||||||
git submodule update --init
|
git submodule update --init
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -161,6 +161,9 @@ bool ClassFlowMQTT::ReadParameter(FILE* pfile, string& aktparamgraph)
|
|||||||
else if (toUpper(splitted[1]) == "WATER_GAL") {
|
else if (toUpper(splitted[1]) == "WATER_GAL") {
|
||||||
mqttServer_setMeterType("water", "gal", "h", "gal/h");
|
mqttServer_setMeterType("water", "gal", "h", "gal/h");
|
||||||
}
|
}
|
||||||
|
else if (toUpper(splitted[1]) == "WATER_GAL_MIN") {
|
||||||
|
mqttServer_setMeterType("water", "gal", "min", "gal/min"); // min = Minutes
|
||||||
|
}
|
||||||
else if (toUpper(splitted[1]) == "GAS_M3") {
|
else if (toUpper(splitted[1]) == "GAS_M3") {
|
||||||
mqttServer_setMeterType("gas", "m³", "h", "m³/h");
|
mqttServer_setMeterType("gas", "m³", "h", "m³/h");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -758,17 +758,8 @@ string ClassFlowPostProcessing::ShiftDecimal(string in, int _decShift) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ClassFlowPostProcessing::doFlow(string zwtime) {
|
bool ClassFlowPostProcessing::doFlow(string zwtime) {
|
||||||
string result = "";
|
|
||||||
string digit = "";
|
|
||||||
string analog = "";
|
|
||||||
string zwvalue;
|
string zwvalue;
|
||||||
string zw;
|
time_t imagetime = flowTakeImage->getTimeImageTaken();
|
||||||
time_t imagetime = 0;
|
|
||||||
string rohwert;
|
|
||||||
|
|
||||||
// Update decimal point, as the decimal places can also change when changing from CNNType Auto --> xyz:
|
|
||||||
|
|
||||||
imagetime = flowTakeImage->getTimeImageTaken();
|
|
||||||
|
|
||||||
if (imagetime == 0) {
|
if (imagetime == 0) {
|
||||||
time(&imagetime);
|
time(&imagetime);
|
||||||
@@ -794,6 +785,7 @@ bool ClassFlowPostProcessing::doFlow(string zwtime) {
|
|||||||
// double LastValueTimeDifference = difftime(imagetime, NUMBERS[j]->timeStampLastValue); // in seconds
|
// double LastValueTimeDifference = difftime(imagetime, NUMBERS[j]->timeStampLastValue); // in seconds
|
||||||
double LastPreValueTimeDifference = difftime(imagetime, NUMBERS[j]->timeStampLastPreValue); // in seconds
|
double LastPreValueTimeDifference = difftime(imagetime, NUMBERS[j]->timeStampLastPreValue); // in seconds
|
||||||
|
|
||||||
|
// Update decimal point, as the decimal places can also change when changing from CNNType Auto --> xyz:
|
||||||
UpdateNachkommaDecimalShift();
|
UpdateNachkommaDecimalShift();
|
||||||
|
|
||||||
int previous_value = -1;
|
int previous_value = -1;
|
||||||
@@ -884,12 +876,15 @@ bool ClassFlowPostProcessing::doFlow(string zwtime) {
|
|||||||
|
|
||||||
if (NUMBERS[j]->checkDigitIncreaseConsistency) {
|
if (NUMBERS[j]->checkDigitIncreaseConsistency) {
|
||||||
if (flowDigit) {
|
if (flowDigit) {
|
||||||
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Before checkDigitConsistency: value=" + std::to_string(NUMBERS[j]->Value));
|
||||||
NUMBERS[j]->Value = checkDigitConsistency(NUMBERS[j]->Value, NUMBERS[j]->DecimalShift, NUMBERS[j]->analog_roi != NULL, NUMBERS[j]->PreValue);
|
NUMBERS[j]->Value = checkDigitConsistency(NUMBERS[j]->Value, NUMBERS[j]->DecimalShift, NUMBERS[j]->analog_roi != NULL, NUMBERS[j]->PreValue);
|
||||||
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "After checkDigitConsistency: value=" + std::to_string(NUMBERS[j]->Value));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
#ifdef SERIAL_DEBUG
|
#ifdef SERIAL_DEBUG
|
||||||
ESP_LOGD(TAG, "checkDigitIncreaseConsistency = true - no digit numbers defined!");
|
ESP_LOGD(TAG, "checkDigitIncreaseConsistency = true - no digit numbers defined!");
|
||||||
#endif
|
#endif
|
||||||
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "checkDigitIncreaseConsistency = true - no digit numbers defined!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1128,6 +1123,7 @@ float ClassFlowPostProcessing::checkDigitConsistency(double input, int _decilams
|
|||||||
#ifdef SERIAL_DEBUG
|
#ifdef SERIAL_DEBUG
|
||||||
ESP_LOGD(TAG, "checkDigitConsistency: pot=%d, decimalshift=%d", pot, _decilamshift);
|
ESP_LOGD(TAG, "checkDigitConsistency: pot=%d, decimalshift=%d", pot, _decilamshift);
|
||||||
#endif
|
#endif
|
||||||
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "checkDigitConsistency: pot=" + std::to_string(pot) + ", decimalshift=" + std::to_string(_decilamshift));
|
||||||
|
|
||||||
pot_max = ((int) log10(input)) + 1;
|
pot_max = ((int) log10(input)) + 1;
|
||||||
|
|
||||||
@@ -1159,6 +1155,7 @@ float ClassFlowPostProcessing::checkDigitConsistency(double input, int _decilams
|
|||||||
#ifdef SERIAL_DEBUG
|
#ifdef SERIAL_DEBUG
|
||||||
ESP_LOGD(TAG, "checkDigitConsistency: input=%f", input);
|
ESP_LOGD(TAG, "checkDigitConsistency: input=%f", input);
|
||||||
#endif
|
#endif
|
||||||
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "checkDigitConsistency: input=" + std::to_string(input));
|
||||||
|
|
||||||
pot++;
|
pot++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -806,11 +806,21 @@ struct SDCard_Manufacturer_database sd_database[] = {
|
|||||||
.id = 0x03,
|
.id = 0x03,
|
||||||
.manufacturer = "SanDisk",
|
.manufacturer = "SanDisk",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.type = "sd",
|
||||||
|
.id = 0x05,
|
||||||
|
.manufacturer = "Lenovo",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.type = "sd",
|
.type = "sd",
|
||||||
.id = 0x08,
|
.id = 0x08,
|
||||||
.manufacturer = "Silicon Power",
|
.manufacturer = "Silicon Power",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.type = "sd",
|
||||||
|
.id = 0x09,
|
||||||
|
.manufacturer = "ATP",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.type = "sd",
|
.type = "sd",
|
||||||
.id = 0x18,
|
.id = 0x18,
|
||||||
@@ -894,7 +904,27 @@ struct SDCard_Manufacturer_database sd_database[] = {
|
|||||||
{
|
{
|
||||||
.type = "sd",
|
.type = "sd",
|
||||||
.id = 0x89,
|
.id = 0x89,
|
||||||
.manufacturer = "Unknown",
|
.manufacturer = "Netac",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.type = "sd",
|
||||||
|
.id = 0x9f,
|
||||||
|
.manufacturer = "Kingston/Kodak/Silicon Power",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.type = "sd",
|
||||||
|
.id = 0xad,
|
||||||
|
.manufacturer = "Amazon Basics/Lexar/OV",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.type = "sd",
|
||||||
|
.id = 0xdf,
|
||||||
|
.manufacturer = "Lenovo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
.type = "sd",
|
||||||
|
.id = 0xfe,
|
||||||
|
.manufacturer = "Bekit/Cloudisk/HP/Reletech",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1218,7 +1248,7 @@ bool replaceString(std::string &s, std::string const &toReplace, std::string con
|
|||||||
|
|
||||||
if (logIt)
|
if (logIt)
|
||||||
{
|
{
|
||||||
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line '" + old + "' to '" + s + "'");
|
LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Migrated Configfile line '" + old + "' to '" + s + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ bool MQTTPublish(std::string _key, std::string _content, int qos, bool retained_
|
|||||||
_content.append("..");
|
_content.append("..");
|
||||||
}
|
}
|
||||||
|
|
||||||
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Published topic: " + _key + ", content: " + _content + " (msg_id=" + std::to_string(msg_id) + ")");
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "Published topic: " + _key + ", content: " + _content);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -465,7 +465,7 @@ void MQTTconnected(){
|
|||||||
if (subscribeFunktionMap != NULL) {
|
if (subscribeFunktionMap != NULL) {
|
||||||
for(std::map<std::string, std::function<bool(std::string, char*, int)>>::iterator it = subscribeFunktionMap->begin(); it != subscribeFunktionMap->end(); ++it) {
|
for(std::map<std::string, std::function<bool(std::string, char*, int)>>::iterator it = subscribeFunktionMap->begin(); it != subscribeFunktionMap->end(); ++it) {
|
||||||
int msg_id = esp_mqtt_client_subscribe(client, it->first.c_str(), 0);
|
int msg_id = esp_mqtt_client_subscribe(client, it->first.c_str(), 0);
|
||||||
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "topic " + it->first + " subscribe successful, msg_id=" + std::to_string(msg_id));
|
LogFile.WriteToFile(ESP_LOG_DEBUG, TAG, "topic " + it->first + " subscribe successful");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ bool sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
|
|||||||
std::string topicFull;
|
std::string topicFull;
|
||||||
std::string configTopic;
|
std::string configTopic;
|
||||||
std::string payload;
|
std::string payload;
|
||||||
|
std::string component;
|
||||||
|
|
||||||
configTopic = field;
|
configTopic = field;
|
||||||
|
|
||||||
@@ -80,6 +81,16 @@ bool sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
|
|||||||
name = group + " " + name;
|
name = group + " " + name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (field == "problem") { // Special case: Binary sensor which is based on error topic
|
||||||
|
component = "binary_sensor";
|
||||||
|
}
|
||||||
|
else if (field == "flowstart") { // Special case: Button
|
||||||
|
component = "button";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
component = "sensor";
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* homeassistant needs the MQTT discovery topic according to the following structure:
|
* homeassistant needs the MQTT discovery topic according to the following structure:
|
||||||
* <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
|
* <discovery_prefix>/<component>/[<node_id>/]<object_id>/config
|
||||||
@@ -87,21 +98,14 @@ bool sendHomeAssistantDiscoveryTopic(std::string group, std::string field,
|
|||||||
* This means a maintopic "home/test/watermeter" is transformed to the discovery topic "homeassistant/sensor/watermeter/..."
|
* This means a maintopic "home/test/watermeter" is transformed to the discovery topic "homeassistant/sensor/watermeter/..."
|
||||||
*/
|
*/
|
||||||
std::string node_id = createNodeId(maintopic);
|
std::string node_id = createNodeId(maintopic);
|
||||||
if (field == "problem") { // Special case: Binary sensor which is based on error topic
|
topicFull = "homeassistant/" + component + "/" + node_id + "/" + configTopic + "/config";
|
||||||
topicFull = "homeassistant/binary_sensor/" + node_id + "/" + configTopic + "/config";
|
|
||||||
}
|
|
||||||
else if (field == "flowstart") { // Special case: Button
|
|
||||||
topicFull = "homeassistant/button/" + node_id + "/" + configTopic + "/config";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
topicFull = "homeassistant/sensor/" + node_id + "/" + configTopic + "/config";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* See https://www.home-assistant.io/docs/mqtt/discovery/ */
|
/* See https://www.home-assistant.io/docs/mqtt/discovery/ */
|
||||||
payload = string("{") +
|
payload = string("{") +
|
||||||
"\"~\": \"" + maintopic + "\"," +
|
"\"~\": \"" + maintopic + "\"," +
|
||||||
"\"unique_id\": \"" + maintopic + "-" + configTopic + "\"," +
|
"\"unique_id\": \"" + maintopic + "-" + configTopic + "\"," +
|
||||||
"\"object_id\": \"" + maintopic + "_" + configTopic + "\"," + // This used to generate the Entity ID
|
"\"object_id\": \"" + maintopic + "_" + configTopic + "\"," + // Default entity ID; required for HA <= 2025.10
|
||||||
|
"\"default_entity_id\": \"" + component + "." + maintopic + "_" + configTopic + "\"," + // Default entity ID; required in HA >=2026.4
|
||||||
"\"name\": \"" + name + "\"," +
|
"\"name\": \"" + name + "\"," +
|
||||||
"\"icon\": \"mdi:" + icon + "\",";
|
"\"icon\": \"mdi:" + icon + "\",";
|
||||||
|
|
||||||
@@ -174,7 +178,7 @@ bool MQTThomeassistantDiscovery(int qos) {
|
|||||||
int aFreeInternalHeapSizeBefore = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
|
int aFreeInternalHeapSizeBefore = heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL);
|
||||||
|
|
||||||
// Group | Field | User Friendly Name | Icon | Unit | Device Class | State Class | Entity Category
|
// Group | Field | User Friendly Name | Icon | Unit | Device Class | State Class | Entity Category
|
||||||
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "uptime", "Uptime", "clock-time-eight-outline", "s", "", "", "diagnostic", qos);
|
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "uptime", "Uptime", "progress-clock", "s", "duration", "measurement", "diagnostic", qos);
|
||||||
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "MAC", "MAC Address", "network-outline", "", "", "", "diagnostic", qos);
|
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "MAC", "MAC Address", "network-outline", "", "", "", "diagnostic", qos);
|
||||||
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "fwVersion", "Firmware Version", "application-outline", "", "", "", "diagnostic", qos);
|
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "fwVersion", "Firmware Version", "application-outline", "", "", "", "diagnostic", qos);
|
||||||
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "hostname", "Hostname", "network-outline", "", "", "", "diagnostic", qos);
|
allSendsSuccessed |= sendHomeAssistantDiscoveryTopic("", "hostname", "Hostname", "network-outline", "", "", "", "diagnostic", qos);
|
||||||
|
|||||||
@@ -635,7 +635,7 @@ void migrateConfiguration(void) {
|
|||||||
CamZoom_value = false;
|
CamZoom_value = false;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
ESP_LOGE(TAG, "splitted[1]: %s", splitted[1].c_str());
|
// ESP_LOGI(TAG, "splitted[1]: %s", splitted[1].c_str());
|
||||||
CamZoom_value = alphanumericToBoolean(splitted[1]);
|
CamZoom_value = alphanumericToBoolean(splitted[1]);
|
||||||
}
|
}
|
||||||
CamZoom_found = true;
|
CamZoom_found = true;
|
||||||
@@ -858,22 +858,22 @@ void migrateConfiguration(void) {
|
|||||||
else {
|
else {
|
||||||
configLines[CamZoom_lines] = ("CamZoom = false");
|
configLines[CamZoom_lines] = ("CamZoom = false");
|
||||||
}
|
}
|
||||||
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line 'Zoom' to 'CamZoom'");
|
LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Migrated Configfile line 'Zoom' to 'CamZoom'");
|
||||||
migrated = true;
|
migrated = true;
|
||||||
}
|
}
|
||||||
if (CamZoomSize_lines > 0) {
|
if (CamZoomSize_lines > 0) {
|
||||||
configLines[CamZoomSize_lines] = ("CamZoomSize = " + std::to_string(CamZoomSize_value));
|
configLines[CamZoomSize_lines] = ("CamZoomSize = " + std::to_string(CamZoomSize_value));
|
||||||
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line 'ZoomMode' to 'CamZoomSize'");
|
LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Migrated Configfile line 'ZoomMode' to 'CamZoomSize'");
|
||||||
migrated = true;
|
migrated = true;
|
||||||
}
|
}
|
||||||
if (CamZoomOffsetX_lines > 0) {
|
if (CamZoomOffsetX_lines > 0) {
|
||||||
configLines[CamZoomOffsetX_lines] = ("CamZoomOffsetX = " + std::to_string(CamZoomOffsetX_value));
|
configLines[CamZoomOffsetX_lines] = ("CamZoomOffsetX = " + std::to_string(CamZoomOffsetX_value));
|
||||||
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line 'ZoomOffsetX' to 'CamZoomOffsetX'");
|
LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Migrated Configfile line 'ZoomOffsetX' to 'CamZoomOffsetX'");
|
||||||
migrated = true;
|
migrated = true;
|
||||||
}
|
}
|
||||||
if (CamZoomOffsetY_lines > 0) {
|
if (CamZoomOffsetY_lines > 0) {
|
||||||
configLines[CamZoomOffsetY_lines] = ("CamZoomOffsetY = " + std::to_string(CamZoomOffsetY_value));
|
configLines[CamZoomOffsetY_lines] = ("CamZoomOffsetY = " + std::to_string(CamZoomOffsetY_value));
|
||||||
LogFile.WriteToFile(ESP_LOG_ERROR, TAG, "Migrated Configfile line 'ZoomOffsetY' to 'CamZoomOffsetY'");
|
LogFile.WriteToFile(ESP_LOG_INFO, TAG, "Migrated Configfile line 'ZoomOffsetY' to 'CamZoomOffsetY'");
|
||||||
migrated = true;
|
migrated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ CONFIG_CAMERA_CORE1=y
|
|||||||
CONFIG_OV7670_SUPPORT=n
|
CONFIG_OV7670_SUPPORT=n
|
||||||
CONFIG_OV7725_SUPPORT=n
|
CONFIG_OV7725_SUPPORT=n
|
||||||
CONFIG_NT99141_SUPPORT=n
|
CONFIG_NT99141_SUPPORT=n
|
||||||
CONFIG_OV3660_SUPPORT=n
|
CONFIG_OV3660_SUPPORT=y
|
||||||
CONFIG_OV2640_SUPPORT=y
|
CONFIG_OV2640_SUPPORT=y
|
||||||
CONFIG_OV5640_SUPPORT=y
|
CONFIG_OV5640_SUPPORT=y
|
||||||
CONFIG_GC2145_SUPPORT=n
|
CONFIG_GC2145_SUPPORT=n
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ Default Value: `undefined`
|
|||||||
Dedicated definition of the field for InfluxDB use for saving in the Influx database (e.g.: "watermeter/value").
|
Dedicated definition of the field for InfluxDB use for saving in the Influx database (e.g.: "watermeter/value").
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.Field`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.Field`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ Default Value: `undefined`
|
|||||||
Field for InfluxDB v2 to use for saving.
|
Field for InfluxDB v2 to use for saving.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.Field`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.Field`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -10,19 +10,23 @@ Alternatively you can set the parameter `DecimalShift` to `3` so the value is co
|
|||||||
List of supported options:
|
List of supported options:
|
||||||
|
|
||||||
- `other`
|
- `other`
|
||||||
- `water_m3` (uses `m^3/min` as rate)
|
- `water_m3` (uses `m^3/h` as rate)
|
||||||
- `water_l` (uses `l/h` as rate, not officially supported by Homeassistant!)
|
- `water_l` (uses `l/h` as rate) **⚠️ Not supported by Homeassistant 2025.7 and later! ⚠️**
|
||||||
- `water_gal` (uses `gal/h` as rate, not officially supported by Homeassistant!)
|
- `water_gal` (uses `gal/h` as rate) **⚠️ Not supported by Homeassistant 2025.7 and later! ⚠️**
|
||||||
|
- `water_gal_min` (uses `gal/min` as rate)
|
||||||
- `water_ft3` (uses `ft^3/min` as rate)
|
- `water_ft3` (uses `ft^3/min` as rate)
|
||||||
- `gas_m3` (uses `m^3/min` as rate)
|
- `gas_m3` (uses `m^3/h` as rate)
|
||||||
- `gas_ft3` (uses `ft^3/min` as rate)
|
- `gas_ft3` (uses `ft^3/min` as rate)
|
||||||
- `energy_wh` (uses `W` as rate)
|
- `energy_wh` (uses `W` as rate)
|
||||||
- `energy_kwh` (uses `KW` as rate)
|
- `energy_kwh` (uses `KW` as rate)
|
||||||
- `energy_mwh` (uses `MW` as rate)
|
- `energy_mwh` (uses `MW` as rate)
|
||||||
- `energy_gj` (uses `GJ/h` as rate, not officially supported by Homeassistant!)
|
- `energy_gj` (uses `GJ/h` as rate) **⚠️ Not supported by Homeassistant 2025.7 and later! ⚠️**
|
||||||
- `temperature_c` (uses `+C/min` as rate)
|
- `temperature_c` (uses `+C/min` as rate)
|
||||||
- `temperature_f` (uses `°F/min` as rate)
|
- `temperature_f` (uses `°F/min` as rate)
|
||||||
- `temperature_k` (uses `K/min` as rate)
|
- `temperature_k` (uses `K/min` as rate)
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Not all options are supported by Homeassistant, see `SensorDeviceClass.VOLUME_FLOW_RATE` in [https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes](https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes)!
|
Not all options are supported by Homeassistant, see `SensorDeviceClass.VOLUME_FLOW_RATE` in [https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes](https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes)!
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Since Homeassistant 2025.7, the unsupported options will no longer work, see change in Homeassistant: [Ensure MQTT sensor has a valid native unit of measurement](https://github.com/home-assistant/core/pull/146722).
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ Default Value: `0`
|
|||||||
The Idx number for the counter device. Can be obtained from the devices setup page on the Domoticz system.
|
The Idx number for the counter device. Can be obtained from the devices setup page on the Domoticz system.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.DomoticzIDX`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.DomoticzIDX`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ Allow a meter to count backwards (decreasing values).
|
|||||||
This is unusual (it means there is a negative rate) and not wanted in most cases!
|
This is unusual (it means there is a negative rate) and not wanted in most cases!
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.AllowNegativeRates`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.AllowNegativeRates`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -9,4 +9,4 @@ See [here](../Watermeter-specific-analog---digit-transition) for details.
|
|||||||
Range: `6.0` .. `9.9`.
|
Range: `6.0` .. `9.9`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.AnalogToDigitTransitionStart`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.AnalogToDigitTransitionStart`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Parameter `ChangeRateThreshold`
|
# Parameter `ChangeRateThreshold`
|
||||||
Default Value: `2`
|
Default Value: `2`
|
||||||
|
|
||||||
Range: `1` .. `9`.
|
Range: `0` .. `9`.
|
||||||
|
|
||||||
Threshold parameter for change rate detection.<br>
|
Threshold parameter for change rate detection.<br>
|
||||||
This parameter is intended to compensate for small reading fluctuations that occur when the meter does not change its value for a long time (e.g. at night) or slightly turns backwards. This can eg. happen on watermeters.
|
This parameter is intended to compensate for small reading fluctuations that occur when the meter does not change its value for a long time (e.g. at night) or slightly turns backwards. This can eg. happen on watermeters.
|
||||||
@@ -10,7 +10,7 @@ It is only applied to the last digit of the read value (See example below).
|
|||||||
If the read value is within PreValue +/- Threshold, no further calculation is carried out and the Value/Prevalue remains at the old value.
|
If the read value is within PreValue +/- Threshold, no further calculation is carried out and the Value/Prevalue remains at the old value.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.ChangeRateThreshold`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.ChangeRateThreshold`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ An additional consistency check.
|
|||||||
It especially improves the zero crossing check between digits.
|
It especially improves the zero crossing check between digits.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.CheckDigitIncreaseConsistency`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.CheckDigitIncreaseConsistency`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ Shift the decimal separator (positiv or negativ).
|
|||||||
Eg. to move from `m³` to `liter` (`1 m³` equals `1000 liters`), you need to set it to `+3`.
|
Eg. to move from `m³` to `liter` (`1 m³` equals `1000 liters`), you need to set it to `+3`.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.DecimalShift`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.DecimalShift`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ Use the decimal place of the last analog counter for increased accuracy.
|
|||||||
This parameter is only supported on the `*-class*` and `*-const` models! See [Choosing-the-Model](../Choosing-the-Model) for details.
|
This parameter is only supported on the `*-class*` and `*-const` models! See [Choosing-the-Model](../Choosing-the-Model) for details.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.ExtendedResolution`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.ExtendedResolution`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ This is only relevant for models which use `N`!
|
|||||||
See [here](../Choosing-the-Model) for details.
|
See [here](../Choosing-the-Model) for details.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.IgnoreLeadingNaN`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.IgnoreLeadingNaN`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -5,4 +5,4 @@ Defines if the **Change Rate** is calculated as the difference between the last
|
|||||||
as the difference normalized to the interval (`RateChange` = difference per minute).
|
as the difference normalized to the interval (`RateChange` = difference per minute).
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.MaxRateType`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.MaxRateType`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ Maximum allowed change between two readings, if exceeded the last reading will b
|
|||||||
If negative rate is disallowed and no maximum rate value is set, one false high reading will lead to a period of missing measurements until the measurement reaches the previous false high reading. E.g. if the counter is at `600,00` and it's read incorrectly as` 610,00`, all measurements will be skipped until the counter reaches `610,00`. Setting the MaxRateValue to `0,05` leads to a rejection of all readings with a difference `> 0,05`, in this case `610,00`. The rejection also applies to correct readings with a difference `> 0,05`!
|
If negative rate is disallowed and no maximum rate value is set, one false high reading will lead to a period of missing measurements until the measurement reaches the previous false high reading. E.g. if the counter is at `600,00` and it's read incorrectly as` 610,00`, all measurements will be skipped until the counter reaches `610,00`. Setting the MaxRateValue to `0,05` leads to a rejection of all readings with a difference `> 0,05`, in this case `610,00`. The rejection also applies to correct readings with a difference `> 0,05`!
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This parameter must be prefixed with `<NUMBER>` followed by a dot (eg. `main.MaxRateValue`). `<NUMBER>` is the name of the number sequence defined in the ROI's.
|
If you edit the config file manually, you must prefix this parameter with `<NUMBER>` followed by a dot (eg. `main.MaxRateValue`). The reason is that this parameter is specific for each `<NUMBER>` (`<NUMBER>` is the name of the number sequence defined in the ROI's).
|
||||||
|
|||||||
5
sd-card/Readme.md
Normal file
5
sd-card/Readme.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# SD Card content
|
||||||
|
This folder contains the files which are required to setup the SD card.
|
||||||
|
|
||||||
|
❗ Do not directly copy this folder onto your SD card, **it will not work!** Instead, you can use any of the artifacts generaded in any of the Pipeline runs of the [Build-Pipeline](https://github.com/jomjol/AI-on-the-edge-device/actions/workflows/build.yaml).
|
||||||
|
The files in the `html` folder here only serve as templates. The real `html` folder get generated using the Github actions.
|
||||||
BIN
sd-card/config/ana-cont_1500_s2_q.tflite
Normal file
BIN
sd-card/config/ana-cont_1500_s2_q.tflite
Normal file
Binary file not shown.
@@ -43,7 +43,7 @@ AlignmentAlgo = default
|
|||||||
/config/ref1.jpg 442 142
|
/config/ref1.jpg 442 142
|
||||||
|
|
||||||
[Digits]
|
[Digits]
|
||||||
Model = /config/dig-cont_0712_s3_q.tflite
|
Model = /config/dig-cont_0900_s3_q.tflite
|
||||||
CNNGoodThreshold = 0.5
|
CNNGoodThreshold = 0.5
|
||||||
;ROIImagesLocation = /log/digit
|
;ROIImagesLocation = /log/digit
|
||||||
;ROIImagesRetention = 3
|
;ROIImagesRetention = 3
|
||||||
@@ -52,7 +52,7 @@ main.dig2 343 126 30 54 false
|
|||||||
main.dig3 391 126 30 54 false
|
main.dig3 391 126 30 54 false
|
||||||
|
|
||||||
[Analog]
|
[Analog]
|
||||||
Model = /config/ana-cont_1300_s2.tflite
|
Model = /config/ana-cont_1500_s2_q.tflite
|
||||||
CNNGoodThreshold = 0.5
|
CNNGoodThreshold = 0.5
|
||||||
;ROIImagesLocation = /log/analog
|
;ROIImagesLocation = /log/analog
|
||||||
;ROIImagesRetention = 3
|
;ROIImagesRetention = 3
|
||||||
|
|||||||
BIN
sd-card/config/dig-class100-0180-s2-q.tflite
Normal file
BIN
sd-card/config/dig-class100-0180-s2-q.tflite
Normal file
Binary file not shown.
BIN
sd-card/config/dig-class100-0182-s2_q.tflite
Normal file
BIN
sd-card/config/dig-class100-0182-s2_q.tflite
Normal file
Binary file not shown.
BIN
sd-card/config/dig-cont_0900_s3_q.tflite
Normal file
BIN
sd-card/config/dig-cont_0900_s3_q.tflite
Normal file
Binary file not shown.
@@ -43,14 +43,14 @@ AlignmentAlgo = default
|
|||||||
/config/ref1.jpg 536 113
|
/config/ref1.jpg 536 113
|
||||||
|
|
||||||
[Digits]
|
[Digits]
|
||||||
Model = /config/dig-cont_0710_s3_q.tflite
|
Model = /config/dig-cont_0810_s3_q.tflite
|
||||||
CNNGoodThreshold = 0.5
|
CNNGoodThreshold = 0.5
|
||||||
;ROIImagesLocation = /log/digit
|
;ROIImagesLocation = /log/digit
|
||||||
;ROIImagesRetention = 3
|
;ROIImagesRetention = 3
|
||||||
main.dig1 438 62 49 71 false
|
main.dig1 438 62 49 71 false
|
||||||
|
|
||||||
[Analog]
|
[Analog]
|
||||||
Model = /config/ana-cont_1300_s2.tflite
|
Model = /config/ana-cont_1400_s2_q.tflite
|
||||||
;ROIImagesLocation = /log/analog
|
;ROIImagesLocation = /log/analog
|
||||||
;ROIImagesRetention = 3
|
;ROIImagesRetention = 3
|
||||||
main.ana1 452 199 120 120 false
|
main.ana1 452 199 120 120 false
|
||||||
@@ -67,7 +67,7 @@ main.AllowNegativeRates = true
|
|||||||
main.ExtendedResolution = true
|
main.ExtendedResolution = true
|
||||||
main.IgnoreLeadingNaN = false
|
main.IgnoreLeadingNaN = false
|
||||||
ErrorMessage = true
|
ErrorMessage = true
|
||||||
CheckDigitIncreaseConsistency = false
|
main.CheckDigitIncreaseConsistency = false
|
||||||
|
|
||||||
;[MQTT]
|
;[MQTT]
|
||||||
;Uri = mqtt://IP-ADRESS:1883
|
;Uri = mqtt://IP-ADRESS:1883
|
||||||
@@ -76,11 +76,14 @@ CheckDigitIncreaseConsistency = false
|
|||||||
;user = USERNAME
|
;user = USERNAME
|
||||||
;password = PASSWORD
|
;password = PASSWORD
|
||||||
RetainMessages = false
|
RetainMessages = false
|
||||||
|
;DomoticzTopicIn = undefined
|
||||||
|
;main.DomoticzIDX = undefined
|
||||||
HomeassistantDiscovery = false
|
HomeassistantDiscovery = false
|
||||||
;MeterType = other
|
;MeterType = other
|
||||||
;CACert = /config/certs/RootCA.pem
|
;CACert = /config/certs/RootCA.pem
|
||||||
;ClientCert = /config/certs/client.pem.crt
|
;ClientCert = /config/certs/client.pem.crt
|
||||||
;ClientKey = /config/certs/client.pem.key
|
;ClientKey = /config/certs/client.pem.key
|
||||||
|
;ValidateServerCert = true
|
||||||
|
|
||||||
;[InfluxDB]
|
;[InfluxDB]
|
||||||
;Uri = undefined
|
;Uri = undefined
|
||||||
@@ -133,4 +136,3 @@ TimeZone = CET-1CEST,M3.5.0,M10.5.0/3
|
|||||||
RSSIThreshold = -75
|
RSSIThreshold = -75
|
||||||
CPUFrequency = 160
|
CPUFrequency = 160
|
||||||
SetupMode = false
|
SetupMode = false
|
||||||
|
|
||||||
|
|||||||
@@ -156,6 +156,7 @@ The following settings are only used for easier setup, they are <b>not</b> persi
|
|||||||
<input type="checkbox" id="showall" name="showall" value="1" onclick="draw()" checked tabindex=9><label for="showall"> Show all ROIs</label><br>
|
<input type="checkbox" id="showall" name="showall" value="1" onclick="draw()" checked tabindex=9><label for="showall"> Show all ROIs</label><br>
|
||||||
<input type="checkbox" id="lockAspectRatio" name="lockAspectRatio" value="1" onclick="changelockAspectRatio()" checked tabindex=6><label for="lockAspectRatio"> Lock aspect ratio </label><br>
|
<input type="checkbox" id="lockAspectRatio" name="lockAspectRatio" value="1" onclick="changelockAspectRatio()" checked tabindex=6><label for="lockAspectRatio"> Lock aspect ratio </label><br>
|
||||||
<input type="checkbox" id="lockSizes" name="lockSizes" value="1" onclick="changelockSizes()" checked tabindex=7><label for="lockSizes"> Synchronize y, Δx and Δy between ROIs</label><br>
|
<input type="checkbox" id="lockSizes" name="lockSizes" value="1" onclick="changelockSizes()" checked tabindex=7><label for="lockSizes"> Synchronize y, Δx and Δy between ROIs</label><br>
|
||||||
|
<input type="checkbox" id="drawFromCenter" name="drawFromCenter" value="1" onclick="changeDrawFromCenter()" checked tabindex=8><label for="drawFromCenter"> Draw from center</label><br>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<table>
|
<table>
|
||||||
@@ -196,6 +197,7 @@ The following settings are only used for easier setup, they are <b>not</b> persi
|
|||||||
enhanceCon = false,
|
enhanceCon = false,
|
||||||
lockAspectRatio = true,
|
lockAspectRatio = true,
|
||||||
lockSizes = false,
|
lockSizes = false,
|
||||||
|
drawFromCenter = true,
|
||||||
domainname = getDomainname();
|
domainname = getDomainname();
|
||||||
|
|
||||||
function doReboot() {
|
function doReboot() {
|
||||||
@@ -319,6 +321,10 @@ The following settings are only used for easier setup, they are <b>not</b> persi
|
|||||||
UpdateROIs();
|
UpdateROIs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeDrawFromCenter() {
|
||||||
|
drawFromCenter = document.getElementById("drawFromCenter").checked;
|
||||||
|
}
|
||||||
|
|
||||||
function changeCCW() {
|
function changeCCW() {
|
||||||
var sel = document.getElementById("Numbers_value1");
|
var sel = document.getElementById("Numbers_value1");
|
||||||
var _number = sel.options[sel.selectedIndex].text;
|
var _number = sel.options[sel.selectedIndex].text;
|
||||||
@@ -800,6 +806,10 @@ The following settings are only used for easier setup, they are <b>not</b> persi
|
|||||||
zw = getCoords(this)
|
zw = getCoords(this)
|
||||||
rect.startX = e.pageX - zw.left;
|
rect.startX = e.pageX - zw.left;
|
||||||
rect.startY = e.pageY - zw.top;
|
rect.startY = e.pageY - zw.top;
|
||||||
|
if (drawFromCenter) {
|
||||||
|
rect.centerX = rect.startX;
|
||||||
|
rect.centerY = rect.startY;
|
||||||
|
}
|
||||||
document.getElementById("refx").value = rect.startX;
|
document.getElementById("refx").value = rect.startX;
|
||||||
document.getElementById("refy").value = rect.startY;
|
document.getElementById("refy").value = rect.startY;
|
||||||
drag = true;
|
drag = true;
|
||||||
@@ -822,16 +832,33 @@ The following settings are only used for easier setup, they are <b>not</b> persi
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mouseMove(e) {
|
function mouseMove(e) {
|
||||||
|
const mouseX = e.pageX - zw.left;
|
||||||
|
const mouseY = e.pageY - zw.top;
|
||||||
|
|
||||||
if (drag) {
|
if (drag) {
|
||||||
zw = getCoords(this)
|
zw = getCoords(this)
|
||||||
|
|
||||||
|
if (drawFromCenter) {
|
||||||
if (lockAspectRatio) {
|
if (lockAspectRatio) {
|
||||||
rect.h = (e.pageY - zw.top) - rect.startY;
|
rect.h = Math.abs(mouseY - rect.centerY) * 2;
|
||||||
rect.w = Math.round(rect.h * ROIInfo[aktindex]["ar"]);
|
rect.w = Math.round(rect.h * ROIInfo[aktindex]["ar"]);
|
||||||
|
} else {
|
||||||
|
rect.w = Math.abs(mouseX - rect.centerX) * 2;
|
||||||
|
rect.h = Math.abs(mouseY - rect.centerY) * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.startX = rect.centerX - rect.w / 2;
|
||||||
|
rect.startY = rect.centerY - rect.h / 2;
|
||||||
|
document.getElementById("refx").value = Math.round(rect.startX);
|
||||||
|
document.getElementById("refy").value = Math.round(rect.startY);
|
||||||
|
} else {
|
||||||
|
if (lockAspectRatio) {
|
||||||
|
rect.h = mouseY - rect.startY;
|
||||||
|
rect.w = Math.round(rect.h * ROIInfo[aktindex]["ar"]);
|
||||||
|
} else {
|
||||||
|
rect.w = mouseX - rect.startX;
|
||||||
|
rect.h = mouseY - rect.startY;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
rect.w = (e.pageX - zw.left) - rect.startX;
|
|
||||||
rect.h = (e.pageY - zw.top) - rect.startY;
|
|
||||||
}
|
}
|
||||||
document.getElementById("refdx").value = rect.w;
|
document.getElementById("refdx").value = rect.w;
|
||||||
document.getElementById("refdy").value = rect.h;
|
document.getElementById("refdy").value = rect.h;
|
||||||
@@ -843,8 +870,8 @@ The following settings are only used for easier setup, they are <b>not</b> persi
|
|||||||
var context = canvas.getContext('2d');
|
var context = canvas.getContext('2d');
|
||||||
|
|
||||||
zw = getCoords(this);
|
zw = getCoords(this);
|
||||||
x = e.pageX - zw.left;
|
x = mouseX;
|
||||||
y = e.pageY - zw.top;
|
y = mouseY;
|
||||||
|
|
||||||
context.lineWidth = 2;
|
context.lineWidth = 2;
|
||||||
context.strokeStyle = "#00FF00";
|
context.strokeStyle = "#00FF00";
|
||||||
|
|||||||
@@ -969,7 +969,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input required type="number" id="PostProcessing_ChangeRateThreshold_value1" step="1" min="0" max="9" value="2"
|
<input required type="number" id="PostProcessing_ChangeRateThreshold_value1" step="1" min="0" max="9" value="2"
|
||||||
oninput="(!validity.rangeUnderflow||(value=1)) && (!validity.rangeOverflow||(value=9)) &&
|
oninput="(!validity.rangeUnderflow||(value=0)) && (!validity.rangeOverflow||(value=9)) &&
|
||||||
(!validity.stepMismatch||(value=parseInt(this.value)));">
|
(!validity.stepMismatch||(value=parseInt(this.value)));">
|
||||||
</td>
|
</td>
|
||||||
<td>$TOOLTIP_PostProcessing_NUMBER.ChangeRateThreshold</td>
|
<td>$TOOLTIP_PostProcessing_NUMBER.ChangeRateThreshold</td>
|
||||||
@@ -1183,15 +1183,16 @@
|
|||||||
<select class="select_large" id="MQTT_MeterType_value1"> <!-- See https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes -->
|
<select class="select_large" id="MQTT_MeterType_value1"> <!-- See https://developers.home-assistant.io/docs/core/entity/sensor/#available-device-classes -->
|
||||||
<option value="other" selected>Other (no Units)</option>
|
<option value="other" selected>Other (no Units)</option>
|
||||||
<option value="water_m3">Watermeter (Value: m³, Rate: m³/h)</option>
|
<option value="water_m3">Watermeter (Value: m³, Rate: m³/h)</option>
|
||||||
<option value="water_l">Watermeter (Value: l, Rate: l/h) *Not officially supported by Homeassistant!*</option>
|
<option value="water_l">Watermeter (Value: l, Rate: l/h) ⚠️ Not supported by Homeassistant 2025.7 and later! ⚠️</option>
|
||||||
<option value="water_gal">Watermeter (Value: gal, Rate: gal/h) *Not officially supported by Homeassistant!*</option>
|
<option value="water_gal">Watermeter (Value: gal, Rate: gal/h) ⚠️ Not supported by Homeassistant 2025.7 and later! ⚠️</option>
|
||||||
|
<option value="water_gal_min">Watermeter (Value: gal, Rate: gal/min)</option>
|
||||||
<option value="water_ft3">Watermeter (Value: ft³, Rate: ft³/min)</option>
|
<option value="water_ft3">Watermeter (Value: ft³, Rate: ft³/min)</option>
|
||||||
<option value="gas_m3">Gasmeter (Value: m³, Rate: m³/h)</option>
|
<option value="gas_m3">Gasmeter (Value: m³, Rate: m³/h)</option>
|
||||||
<option value="gas_ft3">Gasmeter (Value: ft³, Rate: ft³/min)</option>
|
<option value="gas_ft3">Gasmeter (Value: ft³, Rate: ft³/min)</option>
|
||||||
<option value="energy_wh">Energymeter (Value: Wh, Rate: W)</option>
|
<option value="energy_wh">Energymeter (Value: Wh, Rate: W)</option>
|
||||||
<option value="energy_kwh">Energymeter (Value: kWh, Rate: kW)</option>
|
<option value="energy_kwh">Energymeter (Value: kWh, Rate: kW)</option>
|
||||||
<option value="energy_mwh">Energymeter (Value: MWh, Rate: MW)</option>
|
<option value="energy_mwh">Energymeter (Value: MWh, Rate: MW)</option>
|
||||||
<option value="energy_gj">Energymeter (Value: GJ, Rate: GJ/h) *Not officially supported by Homeassistant!*</option>
|
<option value="energy_gj">Energymeter (Value: GJ, Rate: GJ/h) ⚠️ Not supported by Homeassistant 2025.7 and later! ⚠️</option>
|
||||||
<option value="temperature_c">Thermometer (Value: °C, Rate: °C/min)</option>
|
<option value="temperature_c">Thermometer (Value: °C, Rate: °C/min)</option>
|
||||||
<option value="temperature_c">Thermometer (Value: °F, Rate: °F/min)</option>
|
<option value="temperature_c">Thermometer (Value: °F, Rate: °F/min)</option>
|
||||||
<option value="temperature_c">Thermometer (Value: K, Rate: K/min)</option>
|
<option value="temperature_c">Thermometer (Value: K, Rate: K/min)</option>
|
||||||
@@ -2676,7 +2677,19 @@ function saveTextAsFile() {
|
|||||||
WriteConfigININew();
|
WriteConfigININew();
|
||||||
SaveConfigToServer(domainname);
|
SaveConfigToServer(domainname);
|
||||||
|
|
||||||
|
if(window.location.hash) {
|
||||||
|
var hash = window.location.hash.substring(1); //Puts hash in variable, and removes the # character
|
||||||
|
|
||||||
|
if(hash == 'description') {
|
||||||
|
firework.launch('Configuration saved.', 'success', 2000);
|
||||||
|
}
|
||||||
|
else {
|
||||||
firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
|
firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
firework.launch('Configuration saved. It will get applied after the next reboot!<br><br>\n<a id="reboot_button" onclick="doReboot()">reboot now</a>', 'success', 5000);
|
||||||
|
}
|
||||||
|
|
||||||
if (changeCamValue == 1) {
|
if (changeCamValue == 1) {
|
||||||
camSettingsSet();
|
camSettingsSet();
|
||||||
@@ -3331,7 +3344,7 @@ function openDescription() {
|
|||||||
|
|
||||||
if(hash == 'description') {
|
if(hash == 'description') {
|
||||||
document.getElementById("desc_details").open = true;
|
document.getElementById("desc_details").open = true;
|
||||||
document.getElementById("reboot").style.display = "none";
|
// document.getElementById("reboot").style.display = "none";
|
||||||
document.getElementById("reboot_text").style.display = "none";
|
document.getElementById("reboot_text").style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
webinstaller/Readme.md
Normal file
3
webinstaller/Readme.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Webinstaller
|
||||||
|
This folder is used to provide the required files to generate the [Web-Installer](https://jomjol.github.io/AI-on-the-edge-device/).
|
||||||
|
The Webinstaller gets automatically updated on a release using the Github actions.
|
||||||
2
webinstaller/binary/Readme.md
Normal file
2
webinstaller/binary/Readme.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Binary folder of the webinstaller
|
||||||
|
The firmware itself (`firmware.bin`) gets copied to this folder through the Github action.
|
||||||
@@ -67,21 +67,19 @@
|
|||||||
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<span>Support & Contact Us</span>
|
<span>Support & Contact</span>
|
||||||
<a href="https://github.com/jomjol/AI-on-the-edge-device" target="_blank" title="GitHub">
|
<a href="https://github.com/jomjol/AI-on-the-edge-device" target="_blank" title="GitHub">
|
||||||
<img src="https://github.com/jomjol/AI-on-the-edge-device/images/github-logo.png" alt="GitHub">
|
<img src="https://raw.githubusercontent.com/jomjol/AI-on-the-edge-device/refs/heads/main/images/github-logo.png" alt="GitHub">
|
||||||
</a>
|
|
||||||
<img src="https://github.com/jomjol/AI-on-the-edge-device/images/gmail-logo.png" alt="Email">
|
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/jomjol/AI-on-the-edge-device/discussions" target="_blank" title="GitHub">
|
<a href="https://github.com/jomjol/AI-on-the-edge-device/discussions" target="_blank" title="GitHub">
|
||||||
<img src="https://github.com/jomjol/AI-on-the-edge-device/images/discussion-logo" alt="GitHub">
|
<img src="https://raw.githubusercontent.com/jomjol/AI-on-the-edge-device/refs/heads/main/images/discussion-logo.png" alt="GitHub">
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer-section">
|
<div class="footer-section">
|
||||||
<span>Donations</span>
|
<span>Donations</span>
|
||||||
<a href="https://www.paypal.com/donate?hosted_button_id=8TRSVYNYKDSWL" target="_blank" title="Donate via PayPal">
|
<a href="https://www.paypal.com/donate?hosted_button_id=8TRSVYNYKDSWL" target="_blank" title="Donate via PayPal">
|
||||||
<img src="https://github.com/jomjol/AI-on-the-edge-device/images/paypal.png" alt="PayPal" style="width: 60px; height: auto;">
|
<img src="https://raw.githubusercontent.com/jomjol/AI-on-the-edge-device/refs/heads/main/images/paypal.png" alt="PayPal" style="width: 60px; height: auto;">
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Reference in New Issue
Block a user