Ви не увійшли.
Li-po аккумулятор обеспечит часы автономной работы, длительные отключения случаются достаточно редко (хотя это ситуативно). Ну а остановиться в середине между цифрами может любой разряд и это тоже приведет к сбою.
Действительно использование аккумулятора позволяет обеспечить автономность и защитить от сбоев, несмотря на значительное потребление самой платы.
Если проследить за работой счетчика, то плавно "переключается" только младший разряд, хотя сбои бываю и в других разрядах.
Вот пример некорректного распознавания:
дата время м3*100 минут разница, м3
04.03.2021 04:00:01 2818796 1 2
04.03.2021 04:02:01 2818899 1 103
04.03.2021 04:03:01 2818801 1 -98
04.03.2021 04:04:01 2818803 1 2
т.к. у меня распознается вся шкала, то происходит само "коррекция".
Действительно можно построить систему и на том методе, что Вы предлагаете и запоминать значение во внутренней памяти, передавая его по необходимости.
Объясните мне тугому пожалуйста, зачем распознавать все цифры, если можно ввести начальное значение и отслеживать изменение младшего разряда?
На первой странице я пояснил, что "... данный метод предполагает установку первоначальных значений и не обеспечивает возврата к текущим показаниям в случае сбоя."
Это газовый счетчик, а он может работать даже при отсутствии напряжения. После восстановления которого каждый раз нужно вводить новые начальные значения.
Кроме того отслеживание только одного значения, последнего разряда, который постоянно меняется может приводить к сбоям в распознавании. Например остановиться в середине между цифрами
Это старый интерфейс Гугл Скрипта. Тогда действительно можно было подавать анонимный доступ к скрипту. А теперь ни то, что невозможно анонимно обращаться, а интерфейс совсем другой, нежели тот что показан в данном ролике.
Интерфейс изменился, но я давно не работал с этим.
Только что попробовал создать новый проект (пустой).
Вверху выбрал Начать развертывание
Веб-приложение
В разделе У кого есть доступ - доступно:
- Только у меня
- Любой пользователь с аккаунтом Google
- Все
Вот я вижу, у вас код тоже обходится анонимным входом на Гугл (char *GScriptId = "****************"; //ID что есть адрес скрипта, но оно не предполагает, что Вы авторизованы этим при входе). Это по данным 20-го года. А как обстоит дело в наши дни?
Я не уверен, что это ответ на Ваш вопрос, но в этом видео на 0:57 выбирается доступ без авторизации
https://www.youtube.com/watch?v=f46VBqWwUuI
Вы в начале темы, в прошлом году, писали, что проект находиться в стадии улучщения и доработки. Может есть на него более актуальная, посвежее версия?
Если я не ошибаюсь то это последняя https://drive.google.com/drive/folders/ … sp=sharing
Еще 9 дней и будет год как работает и сохраняет данные в Google таблице.
Удачи
Вот эти-"fonts/FreeSansBold24pt7b.h"б "time.h", "VirtuinoCM.h" откуда взяты?
FreeSansBold24pt7b.h взято с библиотеки Adafruit_GFX_Library
time.h с библиотеки Time
Библиотека VirtuinoCM.h можно взять с https://virtuino.com/index.php/virtuino/arduino-libraries
или загрузить стандартную библиотеку в Arduino IDE VirtuinoCM
Надеюсь хоть как-то помог
Пожалуйста, за каждую библиотеку извне дайте ссылку для скачивания. Например для дисплея их просто несчетное количество на Github. Я хотел попробовать проект в деле, но отсутствие библиотек встали непреодолимым препятствием.
У меня система работает и я давно туда не заглядывал - сейчас быстро сложно будет дать ссылки.
Так, например, библиотека дисплея TFT_22_ILI9225
[v1.4.3](https://github.com/Nkawu/TFT_22_ILI9225/releases/tag/v1.4.3) | 2019-11-25 |
но это стандартная библиотека загруженная в Arduino IDE
Сделал архив всех библиотек и скинул на Google Disk
https://drive.google.com/file/d/13AVonp … sp=sharing
Надеюсь это поможет
На работе возникла задача считывания показаний электросчетчика.
Но камера должна стоять не вплотную к счетчику, а на расстоянии.
Вначале хотел повторить эту систему увеличив разрешение камеры и вырезая нужный кусок изображения - практически все собрал и запустил, но потом решил, что нужно двигаться дальше
Думаю здесь вполне можно реализовать распознавание с помощью нейронной сети. Задача обсосаная и решений полно. На одном шрифте обучение должно быть простым.
На основании этого примера https://github.com/surya-veer/RealTime-DigitRecognition на Python OpenCV (настольный компьютер) получается довольно быстро (на мое обучение Python и первые наброски программы ушло неполных 2 дня).
Изображение, которое можно получить практически от любого источника (файл, Web камера, USB камера), преобразуется в градации серого и после фильтра переводится в черно-белое. Найденные контуры позволяют определить положение цифр шкалы, которые вырезаются из общего изображения.
В начале (май - середина июня 2020) распознавание цифр проводилось уже обученной нейронной сетью, предназначенной для рукописных цифрах. Однако анализ показал, что некоторые цифры распознавались не точно.
Поэтому была построена и обучена новая нейронная сеть на изображениях цифр реальной шкалы. Для увеличения ряда обучающей последовательности был использован ImageDataGenerator, который каждую цифру смещал по осям и производил вращение в произвольном порядке. Всего было создано 10000 изображений.
Нейронная сеть обучалась 8 эпох.
Не думаю, что все это можно реализовать на ESP32 Tensofflow lite, хотя заманчиво.
Основные идеи программы приведены ниже.
#!/usr/bin/env python3
import os
from datetime import datetime, timedelta
import numpy as np
import openpyxl
from PIL import Image
import cv2
import sys
import imutils
from keras.models import load_model
import glob
import time
from collections import Counter
import urllib
# поиск и выделенее цифр шкалы
def letters_extract(image_pic, image_size_W, image_size_H):
add_x_y = 10
min_contourArea = 2000 # минимальне площадь контура, которая отбрасывается
min_h = 150 # минимлаьная высоты цифры
max_h = 210 # максимальная высота цифры
min_w = 60 # миниалная ширина цифры
# преобразовать в градации серого
gray = cv2.cvtColor(image_pic, cv2.COLOR_BGR2GRAY)
# cv2.imshow("gray", gray)
W = image_pic.shape[1]
H = image_pic.shape[0]
# Apply bilateral filter with d = 15,
# sigmaColor = sigmaSpace = 75.
gray = cv2.bilateralFilter(gray, 4, 75, 75)
# cv2.imshow("gray1", gray)
# поиск значения уровня бинаризации
ret, thresh = cv2.threshold(
gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
# cv2.imshow("thresh", thresh)
# бинаризация изображения
_, binary = cv2.threshold(gray, ret + 5, 255, cv2.THRESH_BINARY_INV)
cv2.imshow("binary", binary)
img_erode = binary.copy()
# выделенее контуров
contours, hierarchy = cv2.findContours(
img_erode, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
output = image_pic.copy()
letters = []
for idx, contour in enumerate(contours): # индекс и элемент
(x, y, w, h) = cv2.boundingRect(contour)
# если площадь меньше отбрасываем
if cv2.contourArea(contour) < min_contourArea:
continue
# строим рамку вокруг контура
cv2.rectangle(output, (x, y), (x + w, y + h), (255, 0, 255), 1)
# подписываем высоту, ширину и площадь контура
cv2.putText(output, str(h)+" "+str(w)+" " + str(cv2.contourArea(contour)), (x+30, y+40), cv2.FONT_HERSHEY_SIMPLEX,
0.4, (255, 0, 255), 1, cv2.LINE_AA)
# отбираем по высоте
if h < min_h or h > max_h:
continue
if hierarchy[0][idx][3] > -1:
# расширим размер области для вырезания цифры
if (w < min_w):
x -= (min_w - w) // 2
w = min_w
x -= add_x_y
y -= add_x_y
# подписываем высоту и ширину контура
cv2.putText(output, str(h)+" "+str(w)+" "+str(cv2.contourArea(contour)), (x, y), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (0, 255, 0), 1, cv2.LINE_AA)
w += 2 * add_x_y
h += 2 * add_x_y
# строим расширенную рамку вокруг контуров
cv2.rectangle(output, (x, y), (x + w, y + h), (0, 255, 255), 1)
# вырезаем область контура - считем это цифой
letter_crop = gray[y:y + h, x:x + w]
# cv2.putText(output, str(idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX,
# 1, (0, 255, 0), 1, cv2.LINE_AA)
# print(str(len(letters)), letter_crop.shape, idx)
# изменяем размер
letter_crop = cv2.resize(letter_crop, (image_size_W, image_size_H))
letters.append((x, y, h, letter_crop))
# сортируем значения по X-координате
letters.sort(key=lambda a: (a[0], a[2]), reverse=False)
return letters, output
if __name__ == "__main__":
print("Start loading model...")
model_32_16 = load_model('my_model.h5')
print("Model loaded")
old_time = int(time.time())
# файл сохранения значений
WORKBOOK_NAME = 'Counter.xlsx'
ONE_HOUR = timedelta(hours=1)
old_shot_hour = datetime.now().hour
if os.path.isfile(WORKBOOK_NAME):
wb = openpyxl.load_workbook(WORKBOOK_NAME)
else:
wb = openpyxl.Workbook()
ws = wb.active
# параметры изображения для распознавания
image_size_H = 32
image_size_W = 16
num_channels = 1
old_count = 0
counter_volue = []
counter_res = 0
d = 0
while (1):
# видео берем от телефона с программой IP WebCam
cap = cv2.VideoCapture("rtsp://10.0.1.251:8080/h264_ulaw.sdp")
ret, start_pic = cap.read()
if ret is not True:
continue
# вырезаем область шкалы с запасом
image_pic = start_pic[100:700, 0:1920]
H = image_pic.shape[0]
W = image_pic.shape[1]
# понизим яркость в районе красных цифр
image_pic[0:H, W-230:W] = cv2.add(
image_pic[0:H, W - 230:W], np.array([-40.0]))
# повысим яркость в районе первой цифры
image_pic[0:H, 90:300] = cv2.add(
image_pic[0:H, 90:300], np.array([30.0]))
# надем все цифры шкалы
letters, output = letters_extract(
image_pic, image_size_W, image_size_H)
i = 0
count_metter = 0
while i < len(letters):
# обработаем найденные цыфры
pic = letters[i][3]
pic_data = np.reshape(
pic, (1, image_size_H, image_size_W, num_channels))
# опознать цифру
predictions = model_32_16.predict(pic_data)
dig = np.argmax(predictions)
ver = predictions[0][dig]
y_dig = letters[i][1]
# если цифры находятся на одной оси Х - то выберем максимальную по высоте
if i < len(letters) - 1:
if abs(letters[i][0] - letters[i + 1][0]) < 50: # x
print("цифра {} на одной оси x = {} {}".format(
i, letters[i][0], letters[i + 1][0]))
if letters[i][2] < letters[i + 1][2]: # h
pic = letters[i + 1][3]
pic_data = np.reshape(
pic, (1, image_size_H, image_size_W, num_channels))
predictions = model_32_16.predict(pic_data)
dig = np.argmax(predictions)
ver = predictions[0][dig]
print("next -------- ", end=" ")
print(i, (dig, ver),
(letters[i + 1][0], letters[i + 1][2]))
y_dig = letters[i + 1][1]
else:
print("old -------- ", end=" ")
print(i, (dig, ver), (letters[i][0], letters[i][2]))
i += 1
# преобразуем значения в число
count_metter = count_metter * 10 + dig
# вывод значений опознанных цифр
cv2.putText(output, str(dig), (letters[i][0]+35, y_dig-20), cv2.FONT_HERSHEY_SIMPLEX,
1, (255, 255, 0), 1, cv2.LINE_AA)
i += 1
now_time = int(time.time())
now_date = datetime.now()
# сохранение показаний в таблице каждый час
if (now_date.minute == 0) and (now_date.hour != old_shot_hour):
cv2.imwrite(now_date.strftime("%Y%m%d-%H%M%S.jpg"), start_pic)
delta = counter_res - old_count
time_delta = timedelta(seconds=(now_time-old_time))
ws.append([now_date, old_count, time_delta, delta])
wb.save(WORKBOOK_NAME)
old_shot_hour = now_date.hour
# сохранение показаний в таблице при изменении
if (now_time - old_time) >= 60:
if (len(letters) == 7):
counter_volue.append(count_metter)
# накопленее значений для усреднения 17 раз
if len(counter_volue) == 17:
c = Counter(counter_volue)
counter_res = c.most_common(1)[0][0]
counter_res_n = c.most_common(1)[0][1]
print("c = {} counter_res = {} counter_res_n = {}".format(
c, counter_res, counter_res_n))
counter_volue = []
if old_count != counter_res:
delta = counter_res - old_count
time_delta = timedelta(seconds=(now_time-old_time))
ws.append([now_date, counter_res, time_delta, delta])
ws.column_dimensions['A'].width = 20
wb.save(WORKBOOK_NAME)
old_time = now_time
old_count = counter_res
else:
print("recognized " + str(len(letters)), end=" digits ")
print("data_time = {} counter_res = {} count_metter = {} old_count = {}".format(
datetime.now(), counter_res, count_metter, old_count))
# print("-------")
cv2.imshow("finish", output)
k = cv2.waitKey(5) & 0xFF
if k == 27:
print("CLOSING WEBCAM")
break
cap.release()
cv2.destroyAllWindows()
Эксплуатация программы в течении месяца (середина июня - июль 2020) показала ее полную работоспособность. Может нужно будет перевести потом на Raspberry Pi
Большая работа - выходные проходят не зря.
На картинке там где нули засветы они могут немного затруднять распознавание. Это видно и на найденной ширине цифр - она разная.
Как у Вас выполнена подсветка?
Вы линзу ставили?
я правильно понял что окно с индикатором счетчика ищется по всему кадру?
На первых этапах при разработке так и было. Потом я ограничил поиск в процедуре find_digits_y
//поиск положения цифр по высоте Y
#define Y_FIRST_LOOK 10 //ограничим начало поиска 10 пикселями в строке
#define Y_LAST_LOOK 100 //ограничим конец поиска 100 пикселями в строке
но тестовый счетчик был первоначально "затемнен" черной бумагой (см. фото 2 первого сообщения) и сторонних засветов, которые могли быть восприняты как шкала не было.
Когда стал применять программные "шторки"
#define old_level_Y_up 22 //Положение шторки сверху Y_up
#define old_level_Y_down 48 //Положение шторки снизу X_down
то изображение выше и ниже шторок в программе зачерняется и затемнение счетчика потеряло смысл.
Сейчас, когда эта часть отработана и нет перемещения счетчика, то поиск положения шкалы по оси Y фактически не имеет значения, т.к. шкала уже находится в нужном месте сразу, а я могу подстроить окно из приложения, изменив значение "V2 - смещение по оси Y при суммировании кадров и отображении на дисплее" .
Кроме того идет постоянная автоматическая подстройка положения цифр, о которой я писал выше.
Как мне кажется, для стационарных объектов проще сразу определиться с местом шкалы на изображении и подстраиваться только в небольших пределах, связанных с возможной неточной установкой камеры и индикатора.
Единственное требование - оно должно быть более-менее горизонтально расположено на кадре?
Это требование вызвано тем, что у меня нет процедуры вращения изображения.
Я пока думаю что для понимания шагов обработки и промежуточных результатов нужно сделать подмену TFT библиотеки - заглушку чтобы она генерировала TFT вывод в jpeg картинки для браузера, чтобы в исходном проекте графический вывод не менять сильно.
Мне как-то было проще делать это вначале физически - подклеивая изоленту и картонки , а затем выводить изображение именно на индикатор.
Были случаи, когда изображение "терялось" из-за значительного перемещения счетчика и камеры поэтому и ввел режим вывода полного изображения на индикатор в градациях серого (фото 5 первое сообщение).
Я запускал программу без подключенного физически индикатора TFT и ошибок не выдавало.
Изображение, которое "видит" камера, сохраненное в DropBox сегодня утром в 7:13
Экран дисплея
Шикарно, я думал там сплошная магия в библиотеке ) Гуглотаблицы кажется идеальное хранилище таких данных.
А я пока немного застрял на сбросе картинки на Google Disk по запросу на основе этого примера https://github.com/executer-uno/esp32cam-gdrive.
Изображение взял такое же как и у меня 320х240 PIXFORMAT_GRAYSCALE. После получения от камеры преобразовываю в JPEG (frame2jpg()).
Это работает, а вот после добавления "рабочего" куска в общий проект нет ответа от сервера Google после отправки туда картинки.
Фактически срабатывает этот кусочек кода
Serial.println("Waiting for response.");
long int StartTime=millis();
while (!client.available()) {
Serial.print(".");
delay(100);
if ((StartTime+waitingTime) < millis()) {
Serial.println();
Serial.println("No response.");
//If you have no response, maybe need a greater value of waitingTime
break;
}
waitingTime - 30 секунд, хотя, когда работает полный цикл (считывание, кодировка и отправка) занимает не более 5 секунд, т.е. не в этом дело .
Так и не смог за несколько дней добиться стабильной работы записи изображения на Google Disk, поэтому переделал сегодня (1 мая) сохранение не на Google Disk, а на DropBox на основе https://github.com/duthienkt/ESP32DropboxCam
Этот код работает нормально
//https://github.com/duthienkt/ESP32DropboxCam
//http://developer.alexanderklimov.ru/android/dropbox.php
#include "dropbox.h"
// Это ключ приложения, каждое приложение будет иметь свой собственный ключ
#define DROPBOX_ACESS_KEY "*************************"
DropboxMan dropboxMan;
void capture_DropBox(){
size_t _jpg_buf_len = 0;
uint8_t * _jpg_buf = NULL;
fb = NULL;
fb = esp_camera_fb_get();
if (!fb) {
Serial.println("Camera capture failed");
return;
}
if (fb->format != PIXFORMAT_JPEG) {
bool jpeg_converted = frame2jpg(fb, 80, &_jpg_buf, &_jpg_buf_len);
if (!jpeg_converted) {
esp_camera_fb_return(fb);
fb = NULL;
Serial.println("JPEG compression failed");
return;
}
}
else {
_jpg_buf_len = fb->len;
_jpg_buf = fb->buf;
}
String path = "/picture/";
// Если есть возможность получить время из интернета, используйте его для названия, если нет то время запуска
struct tm timeinfo;
if (!getLocalTime(&timeinfo)) {
path += String(millis());
}
else {
char buf[50];
sprintf(buf, "%4d%02d%02d_%02d%02d%02d\0", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
path += buf;
// Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}
path += ".jpg";
dropboxMan.bufferUpload(_jpg_buf, _jpg_buf_len, path, 1); // save to DropBox
esp_camera_fb_return(fb);
}
По GoogleSheets можете подсмотреть у меня в предыдущем проекте https://github.com/executer-uno/ESP32_DustTracker - запись пары сотни символов раз в 30 секунд - работает отлично на ESP32. Лимит - миллион ячеек на книгу, кажется. Делал по мануалам от автора библиотеки https://github.com/electronicsguy/ESP8266/tree/master/HTTPSRedirect, ну только запись в книгу использовал, без календаря и вычитки параметров.
Спасибо за ссылку на Ваш предыдущий проект, но как в описании Вы написали
"HTTPS Redirect библиотека Таблицы Google использовал этот протокол для загрузки данных. Самая сложная библиотека как по мне)"
Немного упростил код из других примеров и сделал запись в таблицу значений м3 при изменении. Эти же значения сохраняются в кольцевом буфере.
//запись данных счетчика в таблицу
//https://github.com/mrmdudek/ESP32-send-data-to-google-sheet
//https://github.com/ExploreLab/IoT_ggSpreadSheet
//https://voltiq.ru/post-data-to-google-sheets-with-esp8266/ - скрипт за основу брал этот
//https://developers.google.com/apps-script/reference/spreadsheet/sheet
#include <WiFiClientSecure.h>
WiFiClientSecure client;
char *server_sheet = "script.google.com"; // Server URL
char *GScriptId = "****************"; //ID
const int httpsPort = 443;
//---------------------------------------------------- GG_ScriptSheet
void GG_ScriptSheet(float m3_sheet, float minutes_sheet){
String Data = String("&m3=")+(int)m3_sheet+String("&minutes=") + (int)minutes_sheet;
String url32 = String("/macros/s/") + GScriptId + "/exec?"+Data;
Serial.print("Client.Connecting...");
// Serial.println(url32);
if (client.connect(server_sheet, httpsPort)) {
client.println("GET " + String(url32) + " HTTP/1.1"); //HTTP/1.0 OK sent LINEnotify and GoogleSheet
client.println("Host: " + String(server_sheet));
client.println("User-Agent: ESP32_counter");
client.println("Connection: close\r\n\r\n");
// Serial.println("Response...OK !!!");
}
else Serial.println("Connection failed!");
/*
while (client.connected())
{
String line = client.readStringUntil('\n');
if (line == "\r")
{
// Serial.println("headers received");
break;
}
}
// if there are incoming bytes available
// from the server, read them and print them:
while (client.available())
{
char c = client.read();
// Serial.write(c);
}
*/
client.stop();
}
//---------------------------------------------------- GG_ScriptSheet
Поставил на прогон - работает.
Таблица состоит из двух листов на первом - последнее время записи, число строк и последнее значение счетчика.
На вторую страницу записываются сами данные виде ID | DataTime | m3*100 | minutes
За трое суток, при неработающем отоплении, у меня в кольцевом буфере сохранилось около 150 значений. Потребление при использовании горячей воды (как правило не более 0,01 м3/мин).
Приехал экранчик на ST7789, почти как у вас исходный вариант был. Вы библиотеку Adafruit_ST7735.h использовали? Не осталось бекапа с маленьким экраном подсмотреть?
У Вас экранчик с выведеным пином CS был? У меня - без, я так понимаю он на шине параллельно с SD карточкой сидит и тогда что-то одно получится использовать или подпаиваться надо.
У меня был экран ST7735. Я использовал две библиотеки Adafruit_ST7735.h и UTFT.h
Бакапы этих двух библиотек скинул на Google Disk
https://drive.google.com/open?id=1NJuxV … 2Apie_qK7C
Может поможет, но у библиотек разные команды и сами бекапы старые.
Библиотека UTFT более универсальная - подходит для разных дисплеев, на "тяжелая" и мне больше понравилась Adafruit, т.к. она как бы встроена в IDE.
Если судить из этого описания http://arduino.ua/prod466-1-8-128h160-t … reen-modyl то наверное можно и без него.
Я SD карточкой не пользовался, тем более, что карта есть и на плате процессора.
Попробовал записывать данные в GoogleSheets - что-то работает на ESP8266, а что-то на ESP32, но нужно разбираться слета не вышло.
Не поделитесь проектом для Virtuino и пару слов как его развернуть на телефоне у себя?
Скинул последние версии файлов на goodle диск
https://drive.google.com/drive/folders/ … sp=sharing
Из Play Маркета устанавливаете Virtuino (именное Virtuino).
Файл gas1-0.vrt копируете в папку virtuino/projects. Загружаете и запускаете Virtuino.
Устанавливаете правильный адреса сервера - тот, который выводится на экран монитора при запуске скетча (у меня это):
"Camera Stream Ready! Go to: http://172.20.0.35"
Выбирайте настройка сервера
Далее gas counter
Смените адрес сервера на Ваш.
При необходимости работать не только внутри локальной домашней сети, но и извне можно добавить второй сервер, например, no-ip.
Для этого нужно, чтобы соответствующий адрес и порт были видны извне - т.е. открыть этот порт в роутере.
Второй сервер NodeMCU termo у Вас думаю должен быть просто отключен - это из моего предыдущего проекта по управлению котлом я беру данные для построения графика режима работы котла (страница 1 в приложении Virtuino).
У меня версия Virtuino платная поэтому нет ограничений на количество виджетов. Если загрузите мой gas1-0.vrt, то работать должен, но редактировать, наверное не даст.
В бесплатной версии можно добавлять, кажется, до 10 виджетов, думаю этого достаточно для тестирования.
Значение виджета - это элемент массива V[] в скетче описание которого в файле virtuino_pins.h.
Если в приложении Virtuino Вы строите график, например м3, то потом накопленные значения можно сохранить в формате Excel.
Кроме того, как я писал выше после добавления модуля с Bluetooth накопленные данные можно легко вывести на экран монитора и также сохранить в в формате Excel.даже без физического подключения.
Добавил сбор статистики по использованию эталонов. Стало интересно все ли используются.
Данные выводится на экран монитора при нажатии клавиши "м3 на монитор".
В скетче нашел пару ошибок поэтому нужно работать с последней версией.
В приложении Virtuino действительно много данных, которые наверное после окончательной отладки не нужны и можно будет значительно упростить интерфейс.
Смещение изображения, о котором я писал в предыдущем сообщении происходит если используется два буфера для камеры config.fb_count = 2. т.к. скорости достаточно, то использую config.fb_count = 1.
Готов ответить на Ваши вопросы.
Буду признателен Вам за совместную доработку.
Сегодня вечером добавил модуль HC-05 Bluetooth (был именно такой). Использую только для передачи данных от устройства на телефон или компьютер вместо того, что бы тянуть кабель.
С левой стороны "коробочки" есть два разъема: питание и вход/выход для программирования через USB-TTL преобразователь - он же Serial. Модуль HC-05 включил параллельно USB-TTL преобразователю на скорости 115200.
Теперь все, что передается обычно на Serial также теперь поступает по каналу Bluetooth.
В программе ничего менять не нужно.
Еще хочу сделать меленький резервный источник на одном 18650.
Сейчас на прогоне стоят два варианта: на модуле J5019 и на модуле TP4056+MT3608.
При нагрузке 0,5 А ни один из модулей не показывает полного заряда аккумулятора, хотя на нем 4,2 В.
От источника идет потребление по 0,7 А. Ну и как известно эти модули греются при зарядке.
При работе устройство потребляет 0,2-0,4 А. Поэтому сопротивление R3 модуля TP4056 заменил на 1,5 кОм уменьшив ток зарядки и модуль будет соответственно меньше греться.
Почему-то несмотря на то, что "коробка" закреплена и не движется по отношению к счетчику иногда происходит смещение изображения по оси Y. Причем это смещение может быть как вверх так и вниз. Объяснить это я не могу. Поэтому добавил еще автоматическую подстройку по оси Y. Каждые 10 измерений происходит смещение по оси Y на +/- 1 или 0 пикселей. Выбирается то значение смещения, которое дает минимальное значение суммы расстояний Хемминга для 7 цифр шкалы (без самой последней). На следующем шаге процесс повторяется.
Добавил возможность программного сброса с помощью команды ESP.restart() из приложения.
Сброс дисплея пришлось также сделать программным подключив контакт RST к GPIO13 заменив в скетче строчку
#define TFT_RST 13 // -1
NickVectra пише:Это газ и кто разрешит его установить?
А кто разрешит навешивать на счетчик эту приблуду?
В теории она (приблуда) как бы рядом и даже не касается счетчика
Главное в его работу не вмешивается.
Естественно проверяющим лучше не показывать и перед их приходом снимать.
Вот есть электронный за 5,6 тыс. грн. с радио-модулем для передачи данных https://www.samgas.com.ua/sites/default/files/rse_instr_user_ru.pdf
но тогда будут вопрос по расшифровке отсылаемых данных.
Да ESP32-CAM, как специально, для этого сделана. И все на одном чипе, не считая памяти. Из чего можно такое собрать, за $5?
https://aliexpress.ru/item/32963016501.html
На этой плате уже есть внешняя PSRAM 4 MB, которая используется, в том числе, для организации 2-х кадровых буферов.
Еще можно отказаться от дисплея и установленного на постоянно преобразователя USB-TTL.
А я вот думаю, не легче ли было сделать счетчики самому.. И нафига это распознавание
Это газ и кто разрешит его установить?
Насколько я читал, все даже современные счетчики (кроме электрических и тепловых) считают импульсы.
И нафига это распознавание
А просто интересная задача - что делать когда карантин
А что за процессор?
Процессор ESP32-CAM
"На показанных фотографиях [ширина цифр] равна 45 пикселям (Ymid), высота цифр 21 пикселям (Y_d)", и вы можете добавить порядка двух внутренних уровней по 16-20 узлов, а так же иметь один 10 разрядный уровень на выходе. Итого 45*21+2*20+10 = 995 байтов займет нейронная сеть. Плюс несколько десятков байтов на другие структуры. Само собой, веса тренированной сети прописаны в ПЗУ.
Суммирование операция простая, скорость тут тоже не требуется. Даже если цифра будет проворачиваться в момент получения фотографии и ее нельзя будет распознать, то через 5 секунд это уже станет возможным. С дополнительным контролем результата (число должно все время увеличиваться, цифры в старших разрядах не должны хаотично менять свои значения) можно избежать случайных ошибок. Кроме того, можно даже вести карту вероятностей появления каждой цифры - счетчик ведь работает предсказуемым образом. Она тоже займет порядка 10*N байтов, где N - количество разрядов на счетчике. Зато вероятность ошибки может быть получится свести чуть ли не до нуля.
Ymid - это середина цифры по оси Y
Высота (height_letter) и ширина цифры (width_letter) принята по 26 пикселей.
Ширина цифры после определения занимает от 16 до 3 пикселей в зависимости от того какая цифра.
В высоте также участвует возможный сдвиг цифры, когда она занимает промежуточное положение.
У меня так просто, к сожалению, не получилось разобраться в нейронной сети даже самой простейшей.
А какие потребители газа висят на этом счётчика?
Это квартира. Из потребителей двухконтурный газовый котел 24 кВт и газовая плита.
Но думаю, что алгоритм ещё потребует доработки. Он может не сгодиться для счетчиков с другими шрифтами.
Набор эталонов создается очень легко. При нажатии на кнопку HEX на монитор выводятся наборы из текущих цифр, которые нужно просто вставить в программу. При изменении показаний и появлении новых цифр - расширяем набор эталонов и т. д. Есть отдельная программка, которая проверяет наборы и дает готовый текст программы.
У меня распознавание тестировалось для двух счетчиков тех, которые были под рукой.
Полностью согласен, что алгоритм требует доработки. Если кто-то возьмет за основу идею может совместно и доработаем
Думаю здесь вполне можно реализовать распознавание с помощью нейронной сети. Задача обсосаная и решений полно. На одном шрифте обучение должно быть простым.
Думал про распознавание с помощью нейронной сети, но не хватило тямы как это просто реализовать на данном процессоре. Решений много для обычного компьютера включая готовые библиотеки, но задействовать еще и его как-то не хотелось.
Время покажет может нужно будет вернуться к этому.
Всем спасибо
Учитывая актуальную задачу «экономии» ресурсов возникла идея считывания показаний счетчика с возможностью в дальнейшем проведения анализа и построения графиков потребелния. Работу электросчетчика можно контролировать с помощью измерителя мощности, например, такого как PZEM-004. Для счетчика воды можно врезать в водопровод дополнительный водомер. Для учета потребления газа это сделать самостоятельно невозможно - поэтому и решено было попробовать создать устройство считывания показаний. Поиск в интернете показал, что задача выполнима, хотя и довольно сложна в виду отсутствия у меня соответствующих в том числе и базовых знаний.
Как правило в квартирах установлены счетчика старого образца, которое не имеют устройства встроенного модуля считывания данных и не формирует импульсов, из-за этого единственный способ снятия показаний — оптический.
На первых этапах проработки идеи рассматривался вариант считывания показаний только последней цифры счетчика с помощью фото и светодиода (датчик TCRT5000), например, подсчет белых делений (https://www.ab-log.ru/forum/viewtopic.php?t=8&start=160). На эту идею натолкнула также разработка, в которой предложено считывать с помощью фотоэлемента импульсы светодиода электросчетчика (https://habr.com/ru/post/234679/, https://forum.arduino.ua/viewtopic.php?id=857, https://mysensors.ru/build/pulse_water/). Однако данный метод предполагает установку первоначальных значений и не обеспечивает возврата к текущим показаниям в случае сбоя.
В результате было принято решение построить систему способную распознать цифры на шкале счетчика. Как известно, сейчас доступен, в том числе и на бесплатной основе, сервис по распознаванию текста на картинках в интернете. Естественно был рассмотрен вариант передачи файла с изображением счетчика и получением результата распознавания. На https://www.youtube.com/watch?v=Upcr_rXnMQY было выложено видео в котором тестировалось Google Vision. В ходе экспериментов несколько фотографий шкалы без предварительной обработки были «скормлены» таким бесплатным сайтам. В большинстве случаем либо они отказывались распознавать цифры и сообщали, что это капча или в виду плохого качества фотографии распознавали их неправильно. Причем текст на лицевой панели самом счетчика распознавался лучше чем цифры шкалы. Можно было бы установить на компьютере одну из бесплатных программ распознавания, однако, как мне показалось такое решение выглядит несколько громоздким. Кроме того в любом случае требовалась предварительная обработка фотография.
В качестве основного модуля использован ESP32-CAM. Данный модуль обладает достаточными ресурсами, компактен и имеет встроенную камеру. Для вывода служебной и визуальной информации был вначале использован TFT дисплея 1,8“ работающий по шине SPI. В окончательном варианте был применен 2,0“ TFT дисплей с разрешающей способностью 220х176 пикселей. Данный дисплей позволил вывести больше необходимой визуальной информации и показать всю шкалу счетчика практически полностью. Все используемые компоненты могут питаться от 5 В, но при этом хорошо стыкуются по 3,3 В.
Вначале все делалось на «коленках» в виде конструктора из досточек и изоленты. Любое неосторожное задевание стола или неудачный жест рукой приводил к необходимости по новой строить макет. В качестве дисплея был использован счетный механизм от старого счетчика, который в свое время не прошел поверку.
Когда надоело мучатся и подошло время натуральных живых испытаний для удобства отладки устройства камера с процессором и дисплей были собраны на единой плате. Возможно из-за того, что встроенная антенна ESP32-CAM была обращена в строну платы связь по WiFi была неустойчивая. Пришлось выпаивать модуль с процессором/камерой и подключать внешнюю антенну. Из аппаратной части также был установлен преобразователь USB-TTL для связи с компьютером и возможностью прошивки модуля. Повышающий преобразователь МТ3806 был использован для формирования напряжения питания светодиодов подсветки в пределах 7-9 В. Включение модулей стандартное и не имеет каких либо особенностей. В библиотеки доработки не вносились.
Более подробно об алгоритме обработки изображения:
Камера ESP32-CAM работает в режиме градаций серого с разрешением 320х240 пикселей. Каждый пиксель имеет значение от 0 до 255 и занимает один байт.
Из-за того, что фокусное расстояние штатного объектива линзы камеры около одного метра перед камерой установлена дополнительная линза 5Х (недорогая линза найденная в ближайшем магазине). Это позволило получить боле-менее не замыленное изображение шкалы счетчика на расстоянии около 10 сантиметров при захвате всей шкалы сразу. Установка линзы с большим увеличением привело бы к тому, что расстояние между камерой и счетчиком сократилось, но вся шкала тогда не помещалась. Эксперименты показали, что подходит любая линза от 4Х до 8Х.
Для устранения шумов камеры полученные изображения (кадры) в градациях серого усредняются (sum_frames) и интегрированный результат помещается в отдельный буфер с разрешением 320х240 пикселей (camera_capture). С помощью Web интерфейса число кадров суммирования можно выбрать от 1 до 10. По умолчанию установлено 5 кадров.
В функции find_middle_level_image предварительно автоматически определяется уровень яркости изображения методом Отцу. Данный метод дает более стабильный результат по сравнению со среднеарифметическим и наиболее точно приближен к «идеальному» уровню бинаризации, который обеспечивает более качественное выделение цифр шкалы.
Процедура find_digits_y позволяет определить границы расположения строки с изображением шкалы счетчика в общем изображении получаемым от камеры. При проведении бинаризации, для определенного на предварительном шаге уровня яркости, строчки изображения которые соответствуют шкале счетчика будут иметь максимальное суммарное значение пикселей, т. к. в этом месте общего изображения будет самое большое количество засвеченных пикселей. Определенные на этом этапе границы отображаются на индикаторе в виде трех горизонтальных линий (верх, середина и низ цифр) для визуального анализа и последующей коррекции при необходимости.
Для того, чтобы хота как-то можно было избавиться от посторонних засветов линзу были установлены «шторки». В дальнейшем учитывая то, что камера и индикатор расположены неподвижно друг по отношению к другу и после первоначальной установки не перемещаются - на последующих этапах можно применить так называемый программный метод «шторок», с помощью которого ограничивалась область индикатора с точностью до нескольких пикселей и тем самым обрезать засветка его границ. Дополнительную корректировку значений границ цифр шкалы счетчика и метоположения самого индикатора, также можно выполнить через Web интерфейс.
В процедуре find_max_letter_X осуществляется поиск середины цифр. Уровень бинаризации повышается и по оси Х строится гистограммы суммы пикселей по вертикали. Дополнительно не учитываются еще два пикселя младших пикселя. После нахождения границ первой цифры осуществляется поиск следующей с шагом равным ширине цифры - 26 пикселей. Для каждой найденной цифры строятся вертикальные линии: левый край, середина, правый край показывающие точность автоматического нахождения положения цифры на изображении. Варианты построения гистограмм яркостных уровней по осям Х и Y для каждой цифры показаны на фотографиях.
На дисплей также вывена информация об уровне середины цифр, который на показанных фотографиях равен 45 пикселям (Ymid), высота цифр 21 пикселm (Y_d) и средний уровень яркости (mid_lev) изображения 39 единиц. В дальнейшем после отладки данного шага алгоритма от вывода гистограмм на экран дисплея я отказался. В связи стем, что горизонт камеры и счетчика выставлены точно и не перемещаются в дальнейшем уточнении границ расположения шкалы счетчика нет необходимости. Поэтому гистограмма по оси Y для каждой цифры не используется в алгоритме распознавания. Следует обратить внимание, что если при первоначальном нахождении положения окна шкалы счетчика по оси Y пороговая яркость для бинаризации не изменялась по сравнению с определенной по всему изображению, то для определения границ отдельных цифр по оси Х уровень яркости был повышен на 60 единиц. Если этого не делать, то не произойдет четкого разделения яркостной гистограммы на отдельные цифры. Т.е. на каждом из выше приведенных этапов обработки был применен свой уровень яркости при проведении бинаризации.
Качество получаемого от камеры изображения в том числе зависит и от освещенности. К сожалению, даже применение отельных линеек светодиодов для подсветки шкалы счетчика и закрытие всей конструкции от внешнего света не позволяет добиться идеальной и равномерной яркости для всех цифр одновременно. Поэтому на следующем этапе определив места расположения середины цифры по оси Х в пределах предполагаемой ширины цифры в границах расположения цифр по оси Y производиться расчет среднего уровня яркости для каждой отдельной цифры шкалы счетчика (процедура find_middle_britnes_digital).
Для поиска совпадения полученного изображения цифр с эталоном в начале был опробован метод свертки https://ru.m.wikipedia.org/wiki/Свёртка … й_анализ)) . Однако этот метод не давал приемлемого результата и предпочтение было отдано подсчету расстояния Хемминга https://ru.wikipedia.org/wiki//Расстояние_Хэмминга.
Для удобства расчета расстояния Хемминга при наложении эталона на изображение осуществляется бинаризация и преобразование изображения в 32 битное число в процедуре convert_to_32. Уровень бинаризации для каждой отдельной цифры выбирается в этом случае индивидуальный (процедура find_middle_britnes_digital).
Расчет и поиск минимального расстояния Хемминга выполняется в функции image_recognition, которая возвращает значение распознанной цифры. При изменении значения счетчика в процессе своей работы цифры могут занять положение, которое смещено по отношению к середине шкалы счетчика по высоте (промежуточные положения цифр не учитываются). Кроме того, при определении границ каждой цифры возможны отклонения на несколько пикселей как по оси X так и по Y. Поэтому, сравнение эталона с распознаваемой цифрой производиться с учетом возможного смещения на 4 пикселя вверх, 4 пикселя вниз, 1 пиксель право и 1 пиксель влево. Всего возможно 27 вариантов различных смещений. Из всех рассчитанных вариантов сдвигов и всех имеющихся эталонов выбирается тот, который имеет меньшее значение расстояния Хемминга. В качестве порогового, в дальнейшем, применяется значение расстояния Хемминга менее 50 единиц.
Для последующего усреднения и накопления результатов распознавания цифр в процедуре frequency происходит подсчет числа совпадения распознанной цифры. По результатам выбирается цифра имеющая максимальное значение. Всего таким образом учитывается 10 выборок. В качестве порогового применяется значение равное 7. Таким образом, если из 10 выборок не менее 7 раз распознана одна и та же цифра и при этом расстояние Хеминга было менее порогового уровня 60 — считается, что цифра распознана правильно и она выводиться на индикатор желтым цветом. Если данные условия не выполняются, то цифра имеет красный цвет. В случае, когда на предыдущем цикле распознавания цифра уже имела такое же значение, то она становиться зеленой.
Для визуального контроля на дисплей может выводятся изображение шкалы в следующих режимах:
- черно-белое изображение, которое прошло бинаризацию согласно описанного выше алгоритма. Данный режим позволяет подкорректировать уровень бинаризации и определить засветы.
- шкала выведенная в градациях серого. Распознавание при этом осуществляется также согласно алгоритма. Применяется для визуального считывания данных шкалы и более точного нахождения границ цифр;
- изображение выводиться в градациях серого на весь экран. Распознавание не осуществляется. Данный режим необходим при первоначальных установках положения камеры и позволяет определить где находиться шкала на изображении.
На 1,8“ дисплее одновременно помещается первых 5 цифр счетчика, которое собственно и нужны при передаче показаний (остальные три цифры после запятой). На 2,0“ помещается все цифры. Через Web интерфейс при необходимости можно осуществить сдвиг выводимой информации для визуального просмотра и анализа.
На первых этапах разработки программы в качестве эталонного использовался шрифт получаемый от программного знакогенератора. Однако, как выяснилось позже каждый счетчик может иметь свой шрифт на табло. Кроме того, формируемый шрифт знакогенератором имеет фиксированные размеры 22, 24 и т.д. пикселя по высоте, что не всегда удобно. Кроме того трудно подобрать шрифт, которые будет максимально совпадать со шрифтом шкалы счетчика.
В данной реализации высота цифр принята 26 пикселя. В качестве эталона использовано полученное от камеры бинаризированное изображение. Для упрощения этой процедуры в программе предусмотрена возможность вывода в порт в шестнадцатеричном формате цифр шкалы. Данные могут быть скопированы сразу в программу для последующего использования как эталонные. Эталонны хранятся в виде массива uint32_t sample[number_of_samples][sample_height] файл sample.h. Массив эталонов может быть расширен различными версиями начертания цифр в том числе и занимающие половинное положение.
Некоторые этапы разработки приведены на фото ниже
На последней фотографии видно, что даже самая последняя цифра успела определиться корректно. На этой же фотографии в нижней строчке выведены значения индивидуальной яркости для каждой цифры (минимальное 62 максимальное 103).
Оперативное управление осуществляется через Web (файл myserver.h), интерфейс которого показан на рисунке и имеет следующие органы управления:
number_of_sum_frames — число кадров суммирования изображения;
offset_y — смещение по оси Y при выводе на дисплей и интегрировании кадров. Позволяет подогнать высоту расположения индикатора разных моделей;
level_dispalay_ttf_B_W — дополнительный уровень яркости добавляемый к автоматически определенному при выводе на дисплей в черно-белом режиме;
level_find_digits_y — дополнительный уровень яркости добавляемый к автоматически определенному при нахождении границ цифр по высоте — ось Y;
level_find_max_letter_X — дополнительный уровень яркости добавляемый к автоматически определенному при нахождении границ цифр по ширене — ось Х;
level_convert_to_32 — дополнительный уровень яркости добавляемый к автоматически определенному при бинаризации изображения;
level_Y_up — уровень программной «шторки» выше которой изображение ограничивается — позволяет убрать засветы на индикаторе;
level_Y_down — уровень программной «шторки» ниже которой изображение ограничивается — позволяет убрать засветы на индикаторе;
show_digital — выводит на экран монитора процесс результатов расчета расстояния Хеминга в виде двоичного представления выбранной цифры и эталонов с учетом всех сдвигов. Занимает много времени;
offset_x — смещение по оси Х для вывода на дисплей цифр, которое не помещаются на экране дисплея (6-8).
Gray or BW? - позволяет выбрать режим индикации изображения шкалы в режиме черно-белом, шкала в режиме градаций серого, все изображение в режиме градаций серого, которое помещается на экран;
Show 0/1? - выводит на экран монитора цифры после бинаризации в виде 1 и пробелов или 1 и 0 для возможности визуального анализа работы программы;
Show HEX? - выводит на экран монитора цифры после бинаризации в шестнадцатеричном виде для использования их в качестве эталонна;
Found digitals : - результаты текущего распознавания и долгосрочного распознавания. Значение 10 показывает, что цифра ни разу не была определена корректно по указанным выше критериям
Сам интерфейс написан некорректно и не оптимально, но позволяет оперативно осуществлять предварительную настройку программы.
Из-за перехода на 2,0“ дисплей пришлось полностью переписать программу под другую библиотеку TFT_22_ILI9225, т. к. в нем используется контроллер ILI9225, а в версии 1,8“ ST7735.
После «жесткой» сборки всей конструкции, несмотря на то, что алгоритм поиска положения границ цифр по высоте (ось Y), работал исправно, применение программных шторок сразу уменьшает зону поиска до нужных границ. Кроме того, так как фокусное расстояние не изменяется размер цифр также не может поменяться. В процедуре find_digits_y фактически находится середина цифр, а границы определяются как ½ высоты цифр, т. е. +/- 13 пикселей.
Для накопления результатов объема потребленного газа м3 в программе реализован кольцевой буфер размерностью 2048 процедура m3_calculate(). Показания сохраняются каждую минуту. Если изменений в показаниях не произошло или нет правильного результата происходит увеличение текущего времени простоя. Так как буфер кольцевой, то после его заполнения запись начинается сначала. Выводом накопленных результатов занимается процедура print_m3(). При выводе значений из текущего времени получаемого от NMT сервера отнимаются накопленные минуты, что позволяет узнать время начала записи. Результаты выводятся в виде таблицы порядковый номер, значение m3, время простоя в минутах. За сутки заполняется приблизительно 70-80 ячеек кольцевого буфера. Для корректного сравнения float все значения показаний счетчика были умножены на 100 и тем самым переведены в целое.
Оригинальным решением подсказанным мне стало фиксация конструкции вот такими креплениями за трубы счетчика. В качестве корпус решил применить пластиковый пищевой контейнер.
Итоговый вариант
Интерфейс управления был переписан под библиотеку Virtuino
Управление аналогично выше описанному Web интерфейсу:
режим дисплея - позволяет выбрать режим индикации изображения шкалы в режиме черно-белом, шкала в режиме градаций серого, все изображение в режиме градаций серого, которое помещается на экран;
сдвиг для анализа
- смещение по оси Х при выводе на дисплей и интегрировании кадров.
Позволяет подогнать расположения для индикаторов разных моделей по горизонтали;
- смещение по оси Y при выводе на дисплей и интегрировании кадров.
Позволяет подогнать высоту расположения индикатора разных моделей;
дисплей — дополнительный уровень яркости добавляемый к автоматически определенному при выводе на дисплей в черно-белом режиме;
яркость Y — дополнительный уровень яркости добавляемый к автоматически определенному при нахождении границ цифр по высоте — ось Y;
яркость X — дополнительный уровень яркости добавляемый к автоматически определенному при нахождении границ цифр по ширене — ось Х;
conv 32 — дополнительный уровень яркости добавляемый к автоматически определенному при бинаризации изображения;
Y_up — уровень программной «шторки» выше которой изображение ограничивается — позволяет убрать засветы на индикаторе;
Y_down — уровень программной «шторки» ниже которой изображение ограничивается — позволяет убрать засветы на индикаторе;
сдвиг для просмотра на дисплее — смещение по оси Х для вывода на дисплей цифр, которое не помещаются на экране дисплея.
HEX - выводит на экран монитора цифры после бинаризации в шестнадцатеричном виде для использования их в качестве эталонна;
м3 на монитор - выводит результаты сохраненных данных в кольцевом буфере на монитор.
Также выводится информация о текущих распознанных цифрах, расстояние Хемминга и число совпадений распознанной цифры за 10 циклов в правой нижней части экрана. Если значения расстояние Хемминга или числа совпадений находится вне пределов, то цифра соответствующая этим значениям будут высвечиваться как символ знака вопроса, красного цвета.
В верхней части показаны текущие значения шкалы счетчика.
Слева зеленым мы видим распознанные показания. Именно эти показания и сохраняются в кольцевом буфере и по ним строится график на другой странице приложения Virtuino работающего на смартфоне.
Спасибо всем, кто помогал мне в реализации данного проекта
Разработка устройства еще в стадии изготовления и доработки, не оптимальная и содержит много неточностей.
Код программы представлен ниже
/*********
Rui Santos
Complete project details at https://RandomNerdTutorials.com/esp32-cam-video-streaming-web-server-camera-home-assistant/
*********/
#include "esp_camera.h"
#include <WiFi.h>
#include "esp_timer.h"
#include "img_converters.h"
#include "Arduino.h"
#include "fb_gfx.h"
#include "soc/soc.h" //disable brownout problems
#include "soc/rtc_cntl_reg.h" //disable brownout problems
#include "esp_http_server.h"
#include "TFT_22_ILI9225.h"
#include <SPI.h>
// Include font definition files
// NOTE: These files may not have all characters defined! Check the GFXfont def
// params 3 + 4, e.g. 0x20 = 32 = space to 0x7E = 126 = ~
#include "fonts/FreeSansBold24pt7b.h"
#define TFT_CS 15
#define TFT_RST -1 //
#define TFT_RS 2 //RS
#define TFT_SDI 12 // Data out SDA MOSI SDI
#define TFT_CLK 14 // Clock out CLK
TFT_22_ILI9225 tft = TFT_22_ILI9225(TFT_RST, TFT_RS, TFT_CS, TFT_SDI, TFT_CLK, 0, 200);
#include "virtuino_pins.h"
#include "time.h"
const char* ntpServer = "pool.ntp.org";
const long gmtOffset_sec = 7200;
const int daylightOffset_sec = 3600; //летнее время 3600;
struct tm timeinfo; //структура времени записи кольцевого буфера
#define info_first 2 ////строка вывода результатов ширины и высоты цифр
#define info_result 58 //строка вывода результатов распознавания
#define info_gistorgamm 105 //строка вывода гистограммы
#define info_Hemming 130 //строка вывода информационнии о расстоянии Хемминга
#define info_frequency 145 //строка вывода информационнии о частоте совпадения
#define info_britnes 115 //строка вывода информационнии об уровнях яркости
#define info_time 160 //строка вывода на дисплей м3/мин сек и времени
//++++++++++++++++++++++++++++++++++++++++++ снизить до 50
#define Hemming_level 80 //Значенее расстояния Хемминга которое считается максимально допустимым при распознавании 50
#define width_letter 26 //ширина цифр в пикселях
#define number_letter 8 //число цифр в шкале
#define height_letter 26 //высота по y - совпадает с высотой эталона
#define average_count 10 //количество усреднений результатов распознавания
#define average_count_level average_count-3 //число усреднений, которое принимается за положительное при распознавании
#define F_HEIGHT 176 //высота обработки изображения совпадает с высотой дисплея 2.0
#define F_WIDTH 320 //ширина обработки изображения совпадает с шириной изображения камеры
#include "sample.h" //образцы эталонов
uint16_t Y_first, Y_last; //положение окна расположения цифр в буфере камеры
uint16_t pixel_level = 0; //пороговый уровень пикселей на изображении определеный методом Отцу
uint16_t max_letter_x[number_letter]; //массив середины цифры по оси Х
uint32_t l_32[number_letter][F_HEIGHT]; //массив после перевода распознаваемых цифр в 32 битное число. Запас по высоте равен высоте экрана
uint8_t result[average_count][number_letter]; //накопление результатов распознавания цифр со шкалы
uint16_t *frame_buf; //указатель на буфер для накопления кадров камеры
#define max_shift 9*3 //число вариантов сдвига перемещения эталона
int shift_XY[max_shift][2] = { //содержит сдвиг по оси X Y
{0, 0},
{0, 1}, //up
{0, 2}, //up
{0, 3}, //up
{0, 4}, //up
{0, -1}, //down
{0, -2}, //down
{0, -3}, //down
{0, -4}, //down
{1, 0}, //right
{1, 1}, //right up
{1, 2}, //right up
{1, 3}, //right up
{1, 4}, //right up
{1, -1}, //right down
{1, -2}, //right down
{1, -3}, //right down
{1, -4}, //right down
{ -1, 0}, //left
{ -1, 1}, //left up
{ -1, 2}, //left up
{ -1, 3}, //left up
{ -1, 4}, //left up
{ -1, -1},//left down
{ -1, -2},//left down
{ -1, -3},//left down
{ -1, -4},//left down
};
struct Hemming_struct { //структура расстояний Хемминга для всех цифр шкалы
uint8_t result; // опознанная цифра
uint16_t min_Hemming; // расстояния Хемминга для опознанной цифры
uint8_t etalon_number; // номер эталона в массиве эталонов
uint8_t frequency; //число совпадений при опознании
uint8_t next_result; // значенее следующей цифры
uint16_t next_min_Hemming; // расстояния Хемминга для следующей цифры
uint8_t dig_defined; //набор символов, который определяется автоматически после распознавания
uint8_t britnes_digital; //яркость для каждой буквы
uint16_t x_width; //определенная ширина цифры
} Hemming[number_letter];
uint8_t frequency[number_of_samples][number_letter]; //подсчет максимального числа совпадений результатов распознавания
camera_fb_t * fb; //для работы камеры указатель на структуру буфер
sensor_t * s; //для работы камеры указаитель на структуру сенсора
// Replace with your network credentials
const char* ssid = "********";
const char* password = "*********";
#define PART_BOUNDARY "123456789000000000000987654321"
// This project was tested with the AI Thinker Model, M5STACK PSRAM Model and M5STACK WITHOUT PSRAM
#define CAMERA_MODEL_AI_THINKER
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
static const char* _STREAM_CONTENT_TYPE = "multipart/x-mixed-replace;boundary=" PART_BOUNDARY;
static const char* _STREAM_BOUNDARY = "\n--" PART_BOUNDARY "\n";
static const char* _STREAM_PART = "Content-Type: image/jpeg\nContent-Length: %u\n\n";
httpd_handle_t stream_httpd = NULL;
#include <Ticker.h> //esp32 library that calls functions periodically
Ticker Gas_minute_Ticker; //используется для расчета объма газа каждую минуту
#define size_m3 2048 //размер кольцевого буфера для хранения данных каждую минуту должен быть 256, 512, 1024 ...
//структура для сохранения информации о расчете объема газа
struct Gas_struct {
uint32_t m3; //значенее объма газа умноженное на 100
uint32_t minutes; //разница в минутах между предыдущим и текущим измерением
} Gas[size_m3];
uint16_t position_m3 = 0; //позиция сохранения данных
//---------------------------------------------------- m3_calculate
void m3_calculate() {
uint32_t k = 1000000; //коэффициент перевода цифр шкалы в число
uint32_t current_m3 = 0; //текущее значенее
bool flag = true; //флаг рапознаввания
uint16_t pos_1 = (position_m3 - 1) & (size_m3 - 1); //позиция в буфере места записи минус 1 - педыдущее значение
for (uint8_t dig = 0; dig < number_letter - 1; dig++) { //проверка на все кроме последней цифры -1
if ((Hemming[dig].frequency < average_count_level) || (Hemming[dig].min_Hemming > Hemming_level) || Hemming[dig].dig_defined == 10) {
//нет правильного результата увеличим пропущенное время если это не начало - предыдущее значение минут равно 0
if (Gas[pos_1].m3 == 0) Gas[position_m3].minutes++;
current_m3 = 0; //обнуляем, т.к. нет правильного результата
flag = false; //сбросим флаг - не распознали
break; //выйти из цикла
}
current_m3 += Hemming[dig].dig_defined * k; //берем опознанное значенее и переводим в число
k = k / 10; //уменьшаем коэффициент для следующего числа
}
Serial.printf("flag=%d current_m3= %d minutes=%d position_m3=%d pos_1=%d\n", flag, current_m3, Gas[position_m3].minutes, position_m3, pos_1);
if (flag) { //распознали сохраняем
if (((current_m3 - Gas[pos_1].m3 < 0) || (current_m3 - Gas[pos_1].m3 > 6)) && (Gas[pos_1].m3 != 0)) { //ошибка распознавания вне пределов
//если разница между предыдущем и текущим значением меньше нуля - текущее определили не верно
//если разница больше 0,06 - ошибка определения - текущее значение определили не верно - за 1 минуту не может быть больше 0,05
//при начальном заполнении буфера первый раз будет давать ошибку
V[V_26_error_recognition]++; //увеличим счетчик ошибок неправльно распознанных
Serial.printf("Значение %d м3 вне пределов на минуте %d\n", current_m3, Gas[pos_1].minutes);
}
else { //значения совпадают или нормально измененные
V[V_26_error_recognition] = 0.0; //сбросим счетчик ошибок неверно распознанных
}
Serial.printf("Предыдущее %d текущее %d м3 позиция %d минут %d\tразница=%d\n", Gas[pos_1].m3, current_m3, position_m3, Gas[pos_1].minutes, current_m3 - Gas[pos_1].m3);
if (Gas[pos_1].m3 == current_m3) {
//если совпало с предыдущим просто увеличим время простоя на 1 минуту или на время пропущенных минут
//если ранее были нераспознанные минуты, то прибавим к пропущенному времени
// Serial.printf("Совпадение значений current_m3 %.2f м3 увеличим предыдущее время Gas[pos_1].minutes=%d разница %f\n",current_m3, Gas[pos_1].minutes,current_m3 - Gas[pos_1].m3);
Gas[pos_1].minutes += Gas[position_m3].minutes;
Gas[position_m3].minutes = 1; //возобновим подсчет времени сначала для следующего элемента
V[V_m3] = current_m3;
V[V_m3_minutes] = Gas[pos_1].minutes;
}
else { //не совпали значения - есть изменения сохраним не обращая внимание на пределы и переход к следующему элементу
Gas[position_m3].m3 = current_m3;
V[V_m3] = current_m3;
V[V_m3_minutes] = Gas[position_m3].minutes;
position_m3 = (position_m3 + 1) & (size_m3 - 1); //переход к следующему элементу с учетом конца буффера;
}
Gas[position_m3].minutes = 1; //подсчет времени сначала для текущего элемента
Gas[position_m3].m3 = 0;
} //распознали сохраняем
pos_1 = (position_m3 - 1) & (size_m3 - 1); //позиция в буфере места записи минус 1 - педыдущее значенее
uint16_t pos_2 = (position_m3 - 2) & (size_m3 - 1); //позиция в буфере места записи минус 2
if (Gas[pos_2].minutes != 0) { //если уже сохранено не менее 2-х элементов
Serial.printf("Gas[pos_2].m3= %d Gas[pos_1].m3= %d minutes=%d difference_m3=%d position-1=%d position-2=%d\n",
Gas[pos_2].m3, Gas[pos_1].m3, Gas[pos_1].minutes, (Gas[pos_1].m3 - Gas[pos_2].m3) / Gas[pos_1].minutes, pos_1, pos_2);
}
else
Serial.printf("current_m3= %d minutes=%d\n", Gas[pos_1].m3, Gas[pos_1].minutes);
}
//---------------------------------------------------- m3_calculate
//---------------------------------------------------- print_m3
void print_m3() {
//вывести на монитор накопленные результаты m3
time_t now;
uint32_t all_minutes = 0; //для подсчета времени с начала отсчета
uint16_t pos_1 = (position_m3 - 1) & (size_m3 - 1); //позиция в буфере места записи минус 1 - педыдущее значенее
if (Gas[pos_1].minutes == 0) return; //не выводить если начало - нет предыдущего значения
Serial.printf("i\tm3*100\tminutes\t\tmax number=%d\n", position_m3);
for (uint32_t i = 0; i < size_m3; i++) {
uint16_t pos = (position_m3 + i) & (size_m3 - 1); //позиция в буфере после места записи
if (Gas[pos].m3 == 0) continue; //если обнаружили конец буфера продолжим
// Serial.printf("i=%d pos=%d position_m3+i=%d Gas[pos].minutes=%d Gas[pos].m3=%.2f\n",i, pos , position_m3+i,Gas[pos].minutes,Gas[pos].m3);
all_minutes += Gas[i].minutes; //подсчет общего времени суммируем все
Serial.printf("%d\t%d\t%d\n", pos, Gas[pos].m3, Gas[pos].minutes);
}
V[V_SH_M3] = 0; //выведем один раз
if (!getLocalTime(&timeinfo)) { //получим время записи сохранения м3
Serial.printf("Failed to obtain time\n");
}
time(&now);
now -= all_minutes * 60; //отнимим прошедшие минуты о получим начало отсчета
localtime_r(&now, &timeinfo);
Serial.printf("\nНачало записи %02d.%02d.%4d %02d:%02d:%02d\n", timeinfo.tm_mday, timeinfo.tm_mon + 1, timeinfo.tm_year + 1900,
timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
}
//---------------------------------------------------- print_m3
//---------------------------------------------------- printBinary
#include <limits.h>
template<typename T>
void printBinary(T value, String s) {
for ( size_t mask = 1 << ((sizeof(value) * CHAR_BIT) - 1); mask; mask >>= 1 ) {
Serial.printf("%c", value & mask ? '1' : ' ');
}
Serial.printf("%s", s);
}
//---------------------------------------------------- printBinary
//---------------------------------------------------- britnes_digital
uint16_t find_middle_britnes_digital(uint16_t *fr_buf, bool show) {
//расчет средней яркости пикселей для каждой цифры после того как определили их место
for (uint8_t dig = 0; dig < number_letter; dig++) { //поочередно обрабатываем каждое знакоместо отельно
float britnes = 0;
uint16_t w_l = width_letter;
int x1 = max_letter_x[dig] - width_letter / 2; //для первой цифры размер может быть меньше установленной ширины
if (x1 < 0) {
w_l += x1; //x1 имеет отрицательное значенее поэтому суммируем
if (show) Serial.printf("x1=%d max_letter_x[dig]=%d w_l=%d\n", x1, max_letter_x[dig], w_l);
x1 = 0;
}
for (uint16_t y = Y_first; y < Y_last; y++) { //перебор по столбцу в пределах высоты буквы
for (uint16_t x = x1; x < max_letter_x[dig] + width_letter / 2; x++) { //перебор по строке в пределах ширины одной цифры
uint32_t i = (y * F_WIDTH + x);
britnes += fr_buf[i]; //суммируем все значения
}
}
Hemming[dig].britnes_digital = (int)(britnes / (w_l * (Y_last - Y_first))); //для первой цифры ширина может быть меньше
if (show)
Serial.printf("dig=%d britnes=%d pixel_level=%d\n", dig, (int)(britnes / (width_letter * (Y_last - Y_first))), pixel_level);
}
}
//---------------------------------------------------- britnes_digital
//---------------------------------------------------- find_middle_level_image
uint16_t find_middle_level_image(uint16_t *fr_buf, bool show) {
//найти уровень яркости изображения по Отцу
float av = 0;
uint16_t min1 = fr_buf[0];
uint16_t max1 = fr_buf[0];
uint32_t f_size = F_WIDTH * F_HEIGHT;
//найти средний уровень пикселей окно табло - засвечено
// Посчитаем минимальную и максимальную яркость всех пикселей
for (uint32_t i = 0; i < f_size; i++) {
av += fr_buf[i];
if (fr_buf[i] < min1) min1 = fr_buf[i];
if (fr_buf[i] > max1) max1 = fr_buf[i];
}
av = av / f_size;
// Гистограмма будет ограничена снизу и сверху значениями min и max,
// поэтому нет смысла создавать гистограмму размером 256 бинов
int histSize = max1 - min1 + 1;
int *hist = (int *) heap_caps_calloc(histSize * sizeof(int), 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (hist == NULL) {
Serial.printf("Problem with heap_caps_malloc find_middle_level_y\n");
return 0;
}
// Заполним гистограмму нулями
for (int t = 0; t < histSize; t++)
hist[t] = 0;
// И вычислим высоту бинов
for (int i = 0; i < f_size; i++)
hist[fr_buf[i] - min1]++;
// Введем два вспомогательных числа:
int m = 0; // m - сумма высот всех бинов, домноженных на положение их середины
int n = 0; // n - сумма высот всех бинов
for (int t = 0; t <= max1 - min1; t++)
{
m += t * hist[t];
n += hist[t];
}
float maxSigma = -1; // Максимальное значение межклассовой дисперсии
int threshold = 0; // Порог, соответствующий maxSigma
int alpha1 = 0; // Сумма высот всех бинов для класса 1
int beta1 = 0; // Сумма высот всех бинов для класса 1, домноженных на положение их середины
// Переменная alpha2 не нужна, т.к. она равна m - alpha1
// Переменная beta2 не нужна, т.к. она равна n - alpha1
// t пробегается по всем возможным значениям порога
for (int t = 0; t < max1 - min1; t++)
{
alpha1 += t * hist[t];
beta1 += hist[t];
// Считаем вероятность класса 1.
float w1 = (float)beta1 / n;
// Нетрудно догадаться, что w2 тоже не нужна, т.к. она равна 1 - w1
// a = a1 - a2, где a1, a2 - средние арифметические для классов 1 и 2
float a = (float)alpha1 / beta1 - (float)(m - alpha1) / (n - beta1);
// Наконец, считаем sigma
float sigma = w1 * (1 - w1) * a * a;
// Если sigma больше текущей максимальной, то обновляем maxSigma и порог
if (sigma > maxSigma)
{
maxSigma = sigma;
threshold = t;
}
}
// Не забудем, что порог отсчитывался от min, а не от нуля
threshold += min1;
heap_caps_free(hist); //освободить буфер
if (show)
Serial.printf("min =%d max=%d cреднее по уровню=%.0f threshold= %d\n", min1, max1, av, threshold);
// Все, порог посчитан, возвращаем его наверх :)
return (uint16_t)(threshold);
}
//---------------------------------------------------- find_middle_level_image
//---------------------------------------------------- find_digits_y
void find_digits_y (uint16_t *fr_buf, uint16_t mid_level, uint8_t add_mid_level, bool show) {
//fr_buf буфер с изображением формата uint16_t
//mid_level средний уровень яркости изображения
//add_mid_level повышение уровня для устранения засветки при анализе
//show вывести информацию на экран
//поиск положения цифр по высоте Y
#define Y_FIRST_LOOK 10 //ограничим начало поиска 10 пикселями в строке
#define Y_LAST_LOOK 100 //ограничим конец поиска 100 пикселями в строке
Y_first = 0;
Y_last = 0;
float av = 0;
char buf[50]; //буфер для перевода значений строку и печати на дисплеи
/*
//проверка на максимум уровня не должен быть больше 255
if (mid_level + add_mid_level > 255)
add_mid_level = (255 - mid_level);
*/
//поиск среднего уровня
for (uint8_t y = Y_FIRST_LOOK; y < Y_LAST_LOOK; y++) { //только в пределах экрана по высоте 10-100 строки
for (uint16_t x = 0; x < tft.maxX(); x++) { //ограничим шириной экрана, а не всем изображением F_WIDTH
uint32_t i = (y * F_WIDTH + x);
if (fr_buf[i] > mid_level + add_mid_level) av++;
}
}
av = (uint16_t) (av / (Y_LAST_LOOK - Y_FIRST_LOOK));
for (uint8_t y = Y_FIRST_LOOK; y < Y_LAST_LOOK; y++) { //только в пределах экрана по высоте 10-100 строки
float av1 = 0;
for (uint16_t x = 0; x < tft.maxX(); x++) { //ограничим шириной экрана, а не всем изображением F_WIDTH
uint32_t i = (y * F_WIDTH + x);
if (fr_buf[i] > mid_level + add_mid_level) av1++;
}
if (av < av1) {
if (show) {
Serial.printf("av=%.0f av1=%.0f Y = %d\n", av, av1, y);
}
if (Y_first == 0) Y_first = y;
Y_last = y;
}
}
uint8_t Y_mid = Y_first + ((Y_last - Y_first) >> 1);
if(Y_last - Y_first != height_letter) {
Y_last = Y_mid + (height_letter >> 1);
Y_first = Y_mid - (height_letter >> 1);
}
tft.drawLine(0, Y_first, tft.maxX() - 1, Y_first, COLOR_YELLOW);
tft.drawLine(0, Y_last, tft.maxX() - 1, Y_last, COLOR_YELLOW);
tft.drawLine(0, Y_mid, tft.maxX() - 1, Y_mid, COLOR_CYAN);
/*
//вывод на дисплей индивидуальных значений яркости
tft.setFont(Terminal6x8 ); //10 pixel
tft.fillRectangle (0, info_britnes, tft.maxX(), 20, COLOR_BLACK); //очистить часть экрана
tft.setColor(COLOR_WHITE);
tft.setCursor(0, info_britnes);
for (uint8_t dig = 0; dig < number_letter; dig++) {//вывести значения яркости
tft.drawTextf("%d ", Hemming[dig].britnes_digital);
}
*/
//вывод на дисплей результатов сохраненных значений
tft.setFont(Terminal6x8); //10 pixel
tft.fillRectangle (0, info_time - 2, tft.maxX(), info_time + 10, COLOR_BLACK); //очистить часть экрана
uint8_t pos_1 = (position_m3 - 1) & (size_m3 - 1); //позиция в буфере после места записи -1
uint8_t pos_2 = (position_m3 - 2) & (size_m3 - 1); //позиция в буфере после места записи -2
if (Gas[pos_2].minutes != 0) { //если уже сохранено не менее 2-х элементов
sprintf(buf, "%4d mins %4.2f m3/h\0", Gas[pos_1].minutes, (Gas[pos_1].m3 - Gas[pos_2].m3) / (Gas[pos_1].minutes * 100.0));
tft.drawText(0, info_time, buf, COLOR_WHITE);
}
else {
sprintf(buf, "%4d mins %4.2f m3\0", Gas[pos_1].minutes, Gas[pos_1].m3 / 100.0);
tft.drawText(0, info_time, buf, COLOR_WHITE);
}
sprintf(buf, "%02d:%02d:%02d\0", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
if (!getLocalTime(&timeinfo)) {
Serial.printf("Failed to obtain time\n");
tft.drawText(tft.maxX() - tft.getTextWidth(buf), info_time, buf, COLOR_RED); //9 * tft.getCharWidth(48)
}
else {
sprintf(buf, "%02d:%02d:%02d\0", timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
tft.drawText(tft.maxX() - tft.getTextWidth(buf), info_time, buf, COLOR_YELLOW);
}
uint16_t x_width_min = F_WIDTH; //ширина экрана
uint16_t x_width_max = 0; //минимальное значенее
for (uint8_t dig = 0; dig < number_letter; dig++) {
//поиск максимальной и минимальной ширины определнной цифры
if (x_width_min > Hemming[dig].x_width) x_width_min = Hemming[dig].x_width;
if (x_width_max < Hemming[dig].x_width) x_width_max = Hemming[dig].x_width;
}
uint16_t next_x = 0;
sprintf(buf, "Y_m=%2d", Y_mid);
if ((Y_last - Y_first) != sample_height)
tft.drawText(next_x, info_first, buf, COLOR_YELLOW);
else
tft.drawText(next_x, info_first, buf, COLOR_GREEN);
next_x += tft.getTextWidth(buf);
sprintf(buf, " X_dmax=%d", x_width_max);
if ((x_width_max > width_letter) || (x_width_max <= x_width_min)) //ширина не должна быть больше ширины буквы
tft.drawText(next_x, info_first, buf, COLOR_YELLOW);
else
tft.drawText(next_x, info_first, buf, COLOR_GREEN);
next_x += tft.getTextWidth(buf);
sprintf(buf, " X_dmin=%d", x_width_min);
if ((x_width_min < 3) || (x_width_min > width_letter)) //ширина не должна быть меньше 3 пикселей
tft.drawText(next_x, info_first, buf, COLOR_YELLOW);
else
tft.drawText(next_x, info_first, buf, COLOR_GREEN);
if (show) {
Serial.printf(" Y_first = %d Y_last = %d\n", Y_first, Y_last);
}
}
//---------------------------------------------------- find_digits_y
//---------------------------------------------------- find_max_digital_X
void find_max_digital_X(uint16_t *fr_buf, uint16_t mid_level, uint8_t add_mid_level, bool show) {
//fr_buf буфер с изображением формата uint16_t
//mid_level средний уровень изображения
//add_mid_level повышение следующего уровня для устранения засветки при отображении
//show вывести информацию на экран
//поиск границ цифр в найденной строке по X
uint8_t letter[F_WIDTH]; //массив для поиска максимума по оси Х
/*
//проверка на максимум уровня
if (mid_level + add_mid_level > 255)
add_mid_level = (255 - mid_level);
*/
//строим гистограмму подсчет количества единиц по столбцу
for (uint16_t x = 0; x < F_WIDTH; x++) { //перебор по строке
letter[x] = 0; //обнуляем начальное значение
for (uint16_t y = Y_first; y < Y_last + 1; y++) { //ищем только в пределах обнаруженных цифр
uint16_t i = (y * F_WIDTH + x);
if (fr_buf[i] > mid_level + add_mid_level) {
letter[x]++;
}
}
// if(show) Serial.printf("x=%3d letter=%d\n",x,letter[x]);
}
if (show) {
//вывод гистограммы на дисплей с учетом смещения по оси Х
tft.fillRectangle (0, info_Hemming - 30, tft.maxX(), info_Hemming - 2, COLOR_BLACK); //очистить часть экрана
for (uint16_t x = V[V_offset_x_digital]; x < F_WIDTH; x++) { //перебор по строке V[V_offset_x]
if (letter[x] != 0)
tft.drawLine(x - V[V_offset_x_digital], info_Hemming - 30, x - V[V_offset_x_digital], 100 + letter[x], COLOR_CYAN); //Y_last + 10 V[V_offset_x]
}
}
//уточним центры цифр по гистограмме
uint16_t x1 = 0, x2 = 0; //начало и конец цифры
uint16_t dig = 0; //номер найденной цифры от 0 до number_letter-1
for (uint16_t x = 0; x < F_WIDTH; x++) { //перебор по строке
if (letter[x] > 2) { //если есть пиксели найти начало больше 2 пикселей
if (x1 != 0) x2 = x; //если было найдено начало установить конец
else x1 = x; //установить начало
}
else { //нет пикселей
if (x1 != 0 && x2 != 0) { //если были ранее определены пределы показать границы
if (show) Serial.printf("x1=%4d x2=%4d x_mid=%4d d_x=%d dig=%d\n", x1, x2, ((x2 - x1) >> 1) + x1, (x2 - x1), dig);
if (dig > number_letter - 1) { //усли найдено больше чем цифр в шкале 8 number_letter - 1
if (show) Serial.printf("Найдено больще цифр чем в шкале по оси Х!!! %d\n", dig);
return;
}
max_letter_x[dig] = ((x2 - x1) >> 1) + x1;
Hemming[dig].x_width = (x2 - x1); //сохраним значенее ширины буквы
dig++;
//линия на +/- 5 пикселей от Y_last и Y_first
tft.drawLine(((x2 - x1) >> 1) + x1 - V[V_offset_x_digital], Y_first - 5, ((x2 - x1) >> 1) + x1 - V[V_offset_x_digital], Y_last + 5, COLOR_BLUE);
tft.drawLine(x1 - V[V_offset_x_digital], Y_first - 5, x1 - V[V_offset_x_digital], Y_last + 5, COLOR_OLIVE);
tft.drawLine(x2 - V[V_offset_x_digital], Y_first - 5, x2 - V[V_offset_x_digital], Y_last + 5, COLOR_OLIVE);
/*
//построение диаграммы по оси Y между x1 и x2
for (uint16_t y = Y_first; y < Y_last; y++) {
uint8_t y_l = 0;
for (uint16_t x = x1; x < x2; x++) {
uint16_t i = (y * F_WIDTH + x);
if (fr_buf[i] > mid_level + add_mid_level) {
y_l++;
}
}
//вывести гистограмму по высоте на экран
// if (SH_0_1 == 1) {
// for (uint8_t i = 0; i < y_l; i++)
// Serial.printf("1");
// }
//// tft.drawFastHLine(x1 - V[V_offset_x_digital], Y_first - 65 + y, y_l, COLOR_YELLOW);
// if (SH_0_1 == 1) Serial.printf("\n");
}
// if (SH_0_1 == 1) Serial.printf("\n");
//построение диаграммы по оси Y между x1 и x2
*/
}
//обнулим значение для следующей цифры
x1 = 0;
x2 = 0;
}
}
if (dig != number_letter)
if (show) Serial.printf("Не все цифры шкалы найдены по оси Х!!! %d\n", dig);
}
//---------------------------------------------------- find_max_digital_X
//---------------------------------------------------- sum_one
uint8_t sum_one(uint32_t d) { //суммирование всех 1 в 32 битном числе
uint8_t r = 0;
for (uint8_t i = 0; i < 32; i++) {
r += d & 0x1;
d = d >> 1;
}
return r;
}
//---------------------------------------------------- sum_one
//---------------------------------------------------- compare
uint8_t compare(uint8_t y, uint8_t samp_dig, uint8_t dig, int X_shift, int Y_shift, bool show) {
//y положенее по оси Y
//samp_dig номер эталона
//dig - какую цифру обрабатываем
//X_shift сдвиг по оси +/- Х
//Y_shift сдвиг по оси +/- Y
//show выводить на экран
uint32_t samp = sample[samp_dig][y];//центры эталона и сравниваемая цифра совпадают
//<< 5; нужно подготовить центры середина эталонна полученная из знакогенератора 8, середина цифры от камеры 13
uint32_t samp1;
if ((y + Y_shift < F_HEIGHT) || (y + Y_shift > 0)) {
if (X_shift < 0)
samp1 = l_32[dig][y + Y_shift] >> abs(X_shift); //образец который сравниваем может быть отрицательное значенее
else
samp1 = l_32[dig][y + Y_shift] << X_shift; //образец который сравниваем
}
else samp1 = 0;
if (show)
printBinary(samp1 ^ samp, "\t");
return sum_one(samp1 ^ samp);
}
//---------------------------------------------------- compare
//---------------------------------------------------- image_recognition
uint8_t image_recognition(uint8_t dig, uint8_t dig_show) {
//dig - какую цифру обрабатываем
//dig_show какую цифру отображаем если > 8 = не выводим
//распознавание цифр
//сравнить с эталоном - расчет расстояния Хемминга
uint8_t min_dig_number = number_of_samples; //присвоим максимальное значенее
//вывод цифры со шкалы в двоичном виде
if (dig == dig_show) {
Serial.printf("------------------------------\n");
for (uint8_t y = 0; y < Y_last - Y_first; y++) { //перебор по Y
printBinary(l_32[dig][y], "\n");
}
Serial.printf("------------------------------\n");
}
//вывод цифры со шкалы в HEX формате
if (V[V_SH_HEX] == 1) {
Serial.printf("{//%d\n", dig);
for (uint8_t y = 0; y < Y_last - Y_first; y++) { //перебор по Y
Serial.printf("0x%010lx,\t//", l_32[dig][y]);
printBinary(l_32[dig][y], "\n");
}
Serial.printf("},\n");
}
uint32_t min_Hemming[number_of_samples]; //массив минимальных расстояний Хемминга для всех эталонов
for (uint8_t samp_dig = 0; samp_dig < number_of_samples; samp_dig++) { //перебор по все эталлонам
uint16_t shift[max_shift]; //хранение результатов расчетов расстояния Хеминга после всех вариантов сдвигов
for (uint8_t i = 0; i < max_shift; i++)
shift[i] = 0; //обнулим для накопления результатов расчета расстояния Хеминга
for (uint8_t y = 0; y < sample_height; y++) { //перебор по Y - высоте эталона и образца
for (uint8_t i = 0; i < max_shift; i++) //перебор по всем сдвигам
shift[i] += compare(y, samp_dig, dig, shift_XY[i][0], shift_XY[i][1], dig == dig_show);
if (dig == dig_show) Serial.printf("\n");
}
if (dig == dig_show) {
Serial.printf("Etalon Digit=%d", samp_dig);
for (uint8_t i = 0; i < max_shift; i++)
Serial.printf("\tshift=%3d %d %d\t\t\t", shift[i], shift_XY[i][0], shift_XY[i][1]);
Serial.printf("\n");
}
//поиск минималного значения после сдвигов
min_Hemming[samp_dig] = shift[0];
for (uint8_t i = 0; i < max_shift; i++) {
if (min_Hemming[samp_dig] > shift[i])
min_Hemming[samp_dig] = shift[i];
}
}
uint32_t min_dig = 1024; //будет содержать минимальное значение расстояния Хемминга
for (uint8_t samp_dig = 0; samp_dig < number_of_samples; samp_dig++) { //перебор по все эталлонам
if (min_dig >= min_Hemming[samp_dig]) {
min_dig = min_Hemming[samp_dig];
min_dig_number = sample_equation[samp_dig]; //получить цифру соответсвия
Hemming[dig].etalon_number = samp_dig; //номер эталона в массиве
}
if (dig == dig_show)
Serial.printf("Etalon=%d\tmin_Hemming=%d\n", sample_equation[samp_dig], min_Hemming[samp_dig]);
}
if (dig == dig_show)
Serial.printf("\n***** Found Digit=%d\tmin_Hemming= %d *****\n", min_dig_number, min_dig);
Hemming[dig].min_Hemming = min_dig; //сохранить значениее расстояния Хемминга
//поиск следующего минимума
min_dig = 1024; //будет содержать минимальное значение расстояния Хемминга
Hemming[dig].next_result = 10; //всего цифры от 0 до 9
for (uint8_t samp_dig = 0; samp_dig < number_of_samples; samp_dig++) { //перебор по все эталлонам
// Serial.printf("samp_dig=%d min_dig_number=%d min_Hemming=%d min_dig=%d\n",samp_dig,min_dig_number,min_Hemming[samp_dig], min_dig);
if (sample_equation[samp_dig] == min_dig_number) continue; //если уже найденный минимум пропустить
if (min_dig >= min_Hemming[samp_dig]) {
min_dig = min_Hemming[samp_dig];
Hemming[dig].next_result = sample_equation[samp_dig]; //значенее опознанной цифры
}
}
Hemming[dig].next_min_Hemming = min_dig; //сохранить значение расстояния Хемминга
return min_dig_number;
}
//---------------------------------------------------- image_recognition
//---------------------------------------------------- convert_to_32
void convert_to_32(uint16_t *fr_buf, uint16_t mid_level, uint8_t add_mid_level, bool show) {
/*
//проверка на максимум уровня
if (mid_level + add_mid_level > 255)
add_mid_level = (255 - mid_level);
*/
for (uint8_t dig = 0; dig < number_letter; dig++) { //всего 8 цифр в шкале последовательно обрабатываем каждую
for (uint16_t y = Y_first; y < Y_last; y++) { //перебор по столбцу
l_32[dig][y - Y_first] = 0;
int x1 = max_letter_x[dig] - width_letter / 2; //для первой цифры размер может быть меньше установленной ширины
if (x1 < 0) x1 = 0;
for (uint16_t x = x1; x < max_letter_x[dig] + width_letter / 2; x++) { //перебор по строке в пределах одной цифры
l_32[dig][y - Y_first] = l_32[dig][y - Y_first] << 1; //сдвиг на 1 позицию
uint32_t i = (y * F_WIDTH + x);
if (fr_buf[i] > Hemming[dig].britnes_digital + add_mid_level) { //индивидуальный уровень яркости для каждой цифры
// if (fr_buf[i] > mid_level + add_mid_level) { //средний уровень
if (show || V[V_SH_0_1] != 0) Serial.printf("1");
l_32[dig][y - Y_first]++;
}
else {
if (show) Serial.printf(" ");
if (V[V_SH_0_1] == 1) Serial.printf(" ");
if (V[V_SH_0_1] == 2) Serial.printf("0");
}
}
if (show) Serial.printf("|0x%010lx\n", l_32[dig][y - Y_first]);
if (V[V_SH_0_1] != 0) Serial.printf("\n");
}
if (show) Serial.printf("Letter box middel = %d d_x = %d d_y =%d mid_line_y=%d\n", max_letter_x[dig], width_letter, Y_last - Y_first, Y_first + (Y_last - Y_first) / 2);
if (V[V_SH_0_1] != 0) Serial.printf("\n");
}
}
//---------------------------------------------------- convert_to_32
//---------------------------------------------------- dispalay_ttf_B_W
esp_err_t dispalay_ttf_B_W(uint16_t *fr_buf, uint16_t mid_level, uint8_t add_mid_level) {
//fr_buf буфер с изображением формата uint16_t
//X0 начальная кордината вывода по оси Х
//Y0 начальная кордината вывода по оси Y
//mid_level средний уровень изображения применятся индивидуальный для каждой цифры
//add_mid_level повышение следующего уровня для устранения засветки при отображении
//зарезервировтать паять для буфера дисплея
uint16_t W = tft.maxX();
uint16_t H = tft.maxY();
uint16_t *disp_buf = (uint16_t *)heap_caps_calloc(W * H * 2, 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (disp_buf == NULL) {
Serial.printf("malloc failed f_b\n");
return ESP_FAIL;
}
for (int y = 0; y < H; y++) { //выводим только по высоте экрана //tft.maxX() tft.maxY()
for (int x = 0; x < W; x++) {
uint8_t dig = 0; //номер цифры табло
uint32_t i = (y * F_WIDTH + x + V[V_offset_x_digital]);
uint32_t j = (y * W + x);
uint16_t color;
if (s->pixformat == PIXFORMAT_GRAYSCALE) { //если GRAYSCALE преобразовать в RGB565 каждый байт буфера
color = (((uint16_t)(fr_buf[i]) & 0xF8) << 8) | (((uint16_t)(fr_buf[i]) & 0xFC) << 3) | ((uint16_t)(fr_buf[i]) >> 3);
}
else
color = fr_buf[i];
if (V[V_GBW] == 0) {
if (x > max_letter_x[dig] + width_letter / 2) { //если текущее значенее в пределах цифры, то использовать соответствующее значенее яркости
dig++; //перейти к следующей цифре
if (dig > number_letter) dig = number_letter - 1; //если больше цифр то принимать яркость последней
}
if (fr_buf[i] < Hemming[dig].britnes_digital + add_mid_level) //индивидуальный уровень яркости для каждой цифры
*(disp_buf + j) = COLOR_BLACK;
else
*(disp_buf + j) = COLOR_WHITE;
}
else *(disp_buf + j) = color;
}
}
if (V[V_GBW] == 2) //если выводить полный экран
tft.drawBitmap(0, 0, disp_buf, W, H); //отобразить на дисплеи
else
tft.drawBitmap(0, 0, disp_buf, W, V[V_level_Y_down] + 10); //отобразить на дисплеи часть изображения с запасом на 10 пикселей
heap_caps_free(disp_buf); //освободить буфер
return ESP_OK;
}
//---------------------------------------------------- dispalay_ttf_B_W
//---------------------------------------------------- sum_frames
esp_err_t sum_frames(uint16_t *fr_buf, bool show, uint8_t Y_up, uint8_t Y_down) {
uint32_t tstart;
fb = NULL;
//накопление кадров - проинтегрировать несколько кадров для устранения шумов
tstart = clock();
memset(fr_buf, 0, F_WIDTH * F_HEIGHT * 2); //выделить память и очистить размер в байтих
uint8_t frame_c = V[V_number_of_sum_frames];
if (s->pixformat != PIXFORMAT_GRAYSCALE)
frame_c = 1; //если цветное то не суммировать по кадрам
for (uint8_t frames = 0; frames < frame_c; frames++) { //усредненее по кадрам frame_count
if (fb) { //освободить буфер
esp_camera_fb_return(fb);
fb = NULL;
}
fb = esp_camera_fb_get(); //получить данные от камеры
if (!fb) {
Serial.printf("Camera capture failed to display\n");
return ESP_FAIL;
}
uint32_t i_max = fb->height * fb->width; //мксимальное значенее массива для данного экрана
for (uint16_t y = 0; y < F_HEIGHT; y++) { //работаем только с верхней частью кадра
for (uint16_t x = 0; x < fb->width; x++) {
if (s->pixformat == PIXFORMAT_GRAYSCALE) {
uint32_t i = ((y + V[V_offset_y]) * fb->width + x + V[V_offset_x]); //смещенее по оси Y и Х
uint32_t j = (y * fb->width + x); //GRAYSCALE
if ((y < Y_up) || (y > Y_down)) {
fr_buf[j] = 0; //обнулим значения ниже и выше Y - эммитация шторки
// Serial.printf("\n");
}
else {
if (i < i_max) //размер экрана (176)+смещенее может быть больше размера изображения 240
fr_buf[j] += fb->buf[i];
else
fr_buf[j] = 0;
}
}
else { //если RGB565 берем 8 битный буфер и преобразуем в 16 битный
uint32_t i = (y * fb->width + x) << 1; //если RGB565
uint32_t j = (y * fb->width + x); //если RGB565
//https://github.com/techtoys/SSD2805/blob/master/Microchip/Include/Graphics/gfxcolors.h
fr_buf[j] += (uint16_t)(fb->buf[i]) << 8 | (uint16_t)(fb->buf[i + 1]); //преобразуем в 16 битное
}
}
}
} //суммирование по кадрам
//усредним все пиксели
for (uint16_t i = 0; i < F_WIDTH * F_HEIGHT; i++) {
fr_buf[i] = (uint16_t)(fr_buf[i] / frame_c);
}
if (fb) { //освободить буфер
esp_camera_fb_return(fb);
fb = NULL;
}
if (show) {
Serial.printf("Capture camera time: %u ms of %d frames\n", clock() - tstart, frame_c);
}
return ESP_OK;
}
//---------------------------------------------------- sum_frames
//---------------------------------------------------- camera_capture
esp_err_t camera_capture(uint16_t *fr_buf, bool show, uint8_t Y_up, uint8_t Y_down) {
//сумировать кадры
if (sum_frames(fr_buf, show, Y_up, Y_down) != ESP_OK) return ESP_FAIL;
uint32_t tstart = clock();
if (show) {
dispalay_ttf_B_W(fr_buf, 0, 0);
Serial.printf("Send buffer time: %u ms\n", clock() - tstart);
}
}
//---------------------------------------------------- camera_capture
//---------------------------------------------------- setup
void setup() {
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); //disable brownout detector
Serial.begin(115200);
Serial.setDebugOutput(false);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
/*
config.pixel_format = PIXFORMAT_GRAYSCALE; //PIXFORMAT_JPEG;
//init with high specs to pre-allocate larger buffers
if (psramFound()) {
// config.frame_size = FRAMESIZE_UXGA;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 10;
config.fb_count = 1;
} else {
config.frame_size = FRAMESIZE_SCOLOR;
config.jpeg_quality = 12;
config.fb_count = 1;
}
// config.frame_size = FRAMESIZE_QVGA;
// Camera init
*/
// for display
config.frame_size = FRAMESIZE_QVGA;
config.pixel_format = PIXFORMAT_GRAYSCALE; //PIXFORMAT_GRAYSCALE; //PIXFORMAT_RGB565;
config.fb_count = 2;
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("Camera init failed with error 0x%x", err);
delay(1000);
ESP.restart();
}
//drop down frame size for higher initial frame rate
s = esp_camera_sensor_get();
s->set_framesize(s, FRAMESIZE_QVGA);
/*
#ifdef display_2_0
s->set_hmirror(s, 1);
s->set_vflip(s, 1);
#endif
*/
// s->set_special_effect(s, 1); //Negative
//s->set_brightness(s, Value);
// s->set_saturation(s, Value);
// s->set_contrast(s, Value);
// Wi-Fi connection
uint32_t timeout = millis();
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.printf(".");
if (millis() - timeout > 5000) break;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.printf("\nWiFi connected.\nCamera Stream Ready! Go to: http://");
Serial.printf("%s\n", WiFi.localIP().toString().c_str());
}
else { //create AP
WiFi.softAP("ESP32", "87654321");
Serial.printf("\nWiFi %s not found create AP Name - 'ESP32' Password - '87654321'\n", ssid);
Serial.printf("Camera Stream Ready! Go to: http://");
Serial.printf("%s\n", WiFi.softAPIP().toString());
}
tft.begin();
tft.setOrientation(3);
tft.clear(); //черным
uint32_t f8 = heap_caps_get_free_size(MALLOC_CAP_8BIT);
frame_buf = (uint16_t *)heap_caps_calloc(F_WIDTH * F_HEIGHT * 2, 1, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
if (frame_buf == NULL) {
Serial.printf("malloc failed frame_buf\n");
}
else {
Serial.printf("malloc succeeded frame_buf\n");
}
Serial.printf("8BIT = %d\n", f8 - heap_caps_get_free_size(MALLOC_CAP_8BIT));
for (uint8_t dig = 0; dig < number_letter; dig++) {
Hemming[dig].dig_defined = 10; //заносим первоначально максимальное число вне диапазона 0-9
}
// Start web server
// startCameraServer();
virtuino.begin(onReceived, onRequested, 512); //Start Virtuino. Set the buffer to 256. With this buffer Virtuino can control about 28 pins (1 command = 9bytes) The T(text) commands with 20 characters need 20+6 bytes
//virtuino.key="1234"; //This is the Virtuino password. Only requests the start with this key are accepted from the library
// avoid special characters like ! $ = @ # % & * on your password. Use only numbers or text characters
server.begin();
/*
for (uint8_t dig = 0; dig < number_letter; dig++) {
for (uint8_t y = 0; y < height_letter; y++) { //перебор по Y
printBinary(original[dig][y], "\n");
}
}
*/
for (uint16_t i = 0; i < size_m3; i++) { //обнулим буфер сохранения значений
Gas[i].m3 = 0;
Gas[i].minutes = 0;
}
Gas[0].minutes = 1; //подсчет времени сначала для текущего элемента
Gas_minute_Ticker.attach(60, m3_calculate); //вызывать расчета объма газа каждую минуту 60
//init and get the time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
if (!getLocalTime(&timeinfo)) { //получим время начала записи сохранения м3
Serial.printf("Failed to obtain time\n");
return;
}
EEPROM.begin(16);//Установить размер внутренней памяти для хранения первоначальных значений
init_V();
}
//---------------------------------------------------- setup
//---------------------------------------------------- find_max_number
uint8_t find_max_number(uint8_t d) {
//возврат максимального значения из всех найденых
uint8_t n = 0; //первое значенее 0
uint8_t m1 = frequency[0][d]; //присваиваем первое значение
for (uint8_t i = 1; i < number_of_samples; i++) {
if (m1 < frequency[i][d]) {
m1 = frequency[i][d];
n = i;
}
}
return n;
}
//---------------------------------------------------- find_max_number
//---------------------------------------------------- show_result
void show_result(bool show) {
uint8_t defined;
uint16_t next_x = 0;
char buf[10]; //буфер для формирования строки расстояния Хемминга и частоты повторения цифр
T_0 = ""; //обновим строчку вывода результатов
int16_t w, h;
tft.setGFXFont(&FreeSansBold24pt7b); // Set current font
tft.getGFXTextExtent("0", 0, info_result, &w, &h); // Get string extents
h += info_result;
// Serial.printf("info_result=%d w=%d h=%d\n",info_result,w,h);
// tft.setFont(Trebuchet_MS16x21); //22 pixel for size 3 Trebuchet_MS16x21
tft.fillRectangle (0, info_result - 2, tft.maxX(), h + 7, COLOR_BLACK); //очистить часть экрана GFXFont привязан верхней точкой
//найти максимальную частоту вхождения цифр после опознавания
for (uint8_t dig = 0; dig < number_letter; dig++) { //number_letter
defined = find_max_number(dig);
sprintf(buf, "%d\0", defined);
//обновленее первоначально предопределенного набора символов если частота определения символа более 7 и рассояние Хемминга менее Hemming_level
if ((frequency[defined][dig] > average_count_level) && (Hemming[dig].min_Hemming < Hemming_level)) {
if (defined != Hemming[dig].dig_defined) { //корректно обнаружили первый раз
// Serial.printf("Change defined digital in position=%d from=%d to %d\n", dig, Hemming[dig].dig_defined, defined);
Hemming[dig].dig_defined = defined;
tft.drawGFXText(next_x, h, buf, COLOR_YELLOW); // Print string
// tft.drawText(next_x, info_result,buf,COLOR_YELLOW); //цифра опознана с большой вероятностью
}
else tft.drawGFXText(next_x, h, buf, COLOR_GREEN); //цифра распознана корректно уже неоднократно
}
else tft.drawGFXText(next_x, h, buf, COLOR_RED);
T_0 += defined;
next_x += w; //шаг между цифрами
if (dig == 4) {
T_0 += ".";
tft.drawGFXText(next_x, h, ".", COLOR_GREEN);
next_x += (w >> 1); //шаг между цифрами
}
Hemming[dig].result = defined;
Hemming[dig].frequency = frequency[defined][dig];
if ((Hemming[dig].frequency < average_count_level) && (Hemming[dig].min_Hemming > Hemming_level)) {
Hemming[dig].dig_defined = 10; //не распознали с большой вероятностью
}
if (show)
Serial.printf("found number=%2d frequency=%2d Hemming_min=%4d Hemming_defined =%d position=%2d Hemming_next=%4d next_dig=%2d delta=%3d x_width=%d\n",
Hemming[dig].result, Hemming[dig].frequency, Hemming[dig].min_Hemming, Hemming[dig].dig_defined, Hemming[dig].etalon_number,
Hemming[dig].next_min_Hemming, Hemming[dig].next_result, Hemming[dig].next_min_Hemming - Hemming[dig].min_Hemming, Hemming[dig].x_width);
}
if (show) Serial.printf("\n");
//сохраненее данных для вывода на экран Virtuino
V[V_D0] = Hemming[0].dig_defined;
V[V_D1] = Hemming[1].dig_defined;
V[V_D2] = Hemming[2].dig_defined;
V[V_D3] = Hemming[3].dig_defined;
V[V_D4] = Hemming[4].dig_defined;
V[V_D5] = Hemming[5].dig_defined;
V[V_D6] = Hemming[6].dig_defined;
V[V_D7] = Hemming[7].dig_defined;
//вывод растояния Хемминга
T_1 = "";
T_2 = "";
tft.setFont(Terminal6x8); //10 pixel
tft.fillRectangle (0, info_Hemming - 2, tft.maxX(), info_Hemming + 24, COLOR_BLACK); //очистить часть экрана для расстояния Хеминга и частоты
for (uint8_t dig = 0; dig < number_letter; dig++) {
sprintf(buf, "|%3d \0", Hemming[dig].min_Hemming);
next_x = max(tft.getTextWidth(T_1), tft.getTextWidth(T_2));
if (next_x != 0) next_x -= tft.getTextWidth(" ");
if (Hemming[dig].min_Hemming < Hemming_level)
tft.drawText(next_x, info_Hemming, buf, COLOR_GREEN); //печатать каждую цифру со смещенеем на экране дисплея
else
tft.drawText(next_x, info_Hemming, buf, COLOR_RED); //печатать каждую цифру со смещенеем на экране дисплея
// Serial.printf("%3d %3d '%s'\n",next_x,tft.getTextWidth(T_1),T_1.c_str());
T_1 += buf;
sprintf(buf, "|%3d \0", Hemming[dig].frequency);
if (Hemming[dig].frequency > average_count_level)
tft.drawText(next_x, info_frequency, buf, COLOR_GREEN); //печатать каждую цифру со смещенеем на экране дисплея
else
tft.drawText(next_x, info_frequency, buf, COLOR_RED); //печатать каждую цифру со смещенеем на экране дисплея
// Serial.printf("%3d %3d %3d '%s'\n",next_x,tft.getTextWidth(T_2),tft.getTextWidth(T_2)-5,T_2.c_str());
T_2 += buf;
}
T_1 += "|";
T_2 += "|";
// Serial.printf("T_1 = %s\n",T_2.c_str());
}
//---------------------------------------------------- show_result
uint32_t free_heap;
bool V_GBW_old = false;
//---------------------------------------------------- loop
void loop() {
for (uint8_t dig = 0; dig < number_letter; dig++) { //обнулить массив для поиска частоты повторения цифр
for (uint8_t i = 0; i < number_of_samples; i++) { //перебор по всем значения образцов
frequency[i][dig] = 0;
}
}
for (uint8_t count = 0; count < average_count; count++) { //повторим результат и найдем опознаные числа
//обработка запросов web сервера
virtuinoRun(); // Necessary function to communicate with Virtuino. Client handler
if (V[V_SH_M3] == 1) print_m3(); //вывести накопленные даные на экран монитора
if (V[V_GBW] == 2) { //Вывод полного экрана без анализа
camera_capture(frame_buf, false, 0, F_HEIGHT); //получить кадры с камеры и усреднить их
dispalay_ttf_B_W(frame_buf, pixel_level, V[V_level_dispalay_ttf_B_W]); //повысим на 5-20 единиц, чтобы убрать засветку
V_GBW_old = true;
}
else {
if (V_GBW_old) tft.clear(); //очистка после вывода полного экрана без анализа
V_GBW_old = false;
if(V[V_GBW] == 1)
camera_capture(frame_buf, false, V[V_level_Y_up]-10, V[V_level_Y_down]+10); //получить кадры с камеры и усреднить их
else
camera_capture(frame_buf, false, V[V_level_Y_up], V[V_level_Y_down]); //получить кадры с камеры и усреднить их
//найти средний уровень пикселей окна табло
pixel_level = find_middle_level_image(frame_buf, false);
//отображение на дисплеи
dispalay_ttf_B_W(frame_buf, pixel_level, V[V_level_dispalay_ttf_B_W]); //повысим на 5-20 единиц, чтобы убрать засветку
// free_heap = heap_caps_get_free_size(MALLOC_CAP_8BIT);
//поиск положения окна цифр - при найденом уровне по оси y
find_digits_y(frame_buf, pixel_level, V[V_level_find_digital_Y], false); //уровень повысим на 15 единиц, чтобы убрать засветку
// Serial.printf("heap = %d\n",free_heap-heap_caps_get_free_size(MALLOC_CAP_8BIT));
//поиск максимума - предположительно середины цифр
find_max_digital_X(frame_buf, pixel_level, V[V_level_find_digital_X], false); //уровень повысим на 7 единиц, чтобы убрать засветку
//найти средний уровень для каждой цифры
find_middle_britnes_digital(frame_buf, false);
//преобразование в 32 битное числа
convert_to_32(frame_buf, pixel_level, V[V_level_convert_to_32], false); //уровень повысим на 20 единиц, чтобы убрать засветку
//сравнить с эталонном - рассчет расстояния Хемминга
for (uint8_t dig = 0; dig < number_letter; dig++) { //проврека по всем цифрам шкалы
result[count][dig] = image_recognition(dig, V[V_show_digital]);
frequency[result[count][dig]][dig]++; //посчет числа совпадения цифра определенной цифры
}
// if ((int)(millis() - 20000) > 0) //ждать стабилизации камеры 20 секунд
// show_result(false); //вывести результат, чтобы результат всегда был на экране
}
}
if (V[V_SH_M3] == 1) print_m3(); //вывести накопленные даные на экран монитора
if (V[V_GBW] != 2) {
// if ((int)(millis() - 20000) > 0) //ждать стабилизации камеры 20 секунд
show_result(true);
}
change_variables(false); //если были изменения коэффициентов записать
}
//---------------------------------------------------- loop
файл sample.h с эталонами
#define sample_height height_letter
#define number_of_samples 41//число эталонов
//массив соответсвий образцов и цифр
uint8_t sample_equation[] = { 0,0,0,1,1,1,2,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,6,7,7,7,8,8,8,9,9,9,9,9,0,1,
0,1,2,4,6,}; //старый счетчик
static uint32_t sample[][sample_height] = {
{//0 (0)
0x0000000000, //
0x0000003c00, // 1111
0x000000fe00, // 1111111
0x000000ff00, // 11111111
0x000000ff80, // 111111111
0x000000e780, // 111 1111
0x000001e780, // 1111 1111
0x000001e380, // 1111 111
0x000001e380, // 1111 111
0x000001e380, // 1111 111
0x000001c3c0, // 111 1111
0x000001c3c0, // 111 1111
0x000001c3c0, // 111 1111
0x000001c3c0, // 111 1111
0x000001e3c0, // 1111 1111
0x000001e380, // 1111 111
0x000001e380, // 1111 111
0x000001e380, // 1111 111
0x000000e780, // 111 1111
0x000000e380, // 111 111
0x000000ff80, // 111111111
0x000000ff00, // 11111111
0x0000003c00, // 1111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//1 (0)
0x0000000000, //
0x0000004200, // 1 1
0x000001c380, // 111 111
0x000001c380, // 111 111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000001e780, // 1111 1111
0x000001ff80, // 1111111111
0x000001ff80, // 1111111111
0x000000ff00, // 11111111
0x0000007e00, // 111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
},
{//2 (0)
0x0000000000, //
0x0000010380, // 1 111
0x0000010380, // 1 111
0x0000018380, // 11 111
0x0000038380, // 111 111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000001c380, // 111 111
0x000001c780, // 111 1111
0x000001c780, // 111 1111
0x000001ff80, // 1111111111
0x000000ff00, // 11111111
0x000000ff00, // 11111111
0x0000007e00, // 111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000001800, // 11
0x0000003800, // 111
0x0000003800, // 111
0x0000003800, // 111
0x0000003800, // 111
0x0000000000, //
},
{//3 (1)
0x0000000000, //
0x0000000000, //
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000000000, //
0x0000000000, //
},
{//4 (1)
0x0000007f00, // 1111111
0x0000003e00, // 11111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000001000, // 1
0x0000001c00, // 111
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000000000, //
},
{//5 (1)
0x0000000000, //
0x0000000000, //
0x0000001800, // 11
0x0000001c00, // 111
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//6 (2)
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000003c00, // 1111
0x0000007f00, // 1111111
0x000000ff80, // 111111111
0x000001ffc0, // 11111111111
0x000001e3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000001c3c0, // 111 1111
0x00000007c0, // 11111
0x0000000780, // 1111
0x0000000f80, // 11111
0x0000001f00, // 11111
0x0000001e00, // 1111
0x0000003e00, // 11111
0x0000003c00, // 1111
0x0000007800, // 1111
0x0000007800, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000ffc0, // 1111111111
0x000000ffe0, // 11111111111
0x000001ffe0, // 111111111111
0x0000000000, //
},
{//7 (2)
0x0000000000, //
0x0000000000, //
0x0000003e00, // 11111
0x000000ff00, // 11111111
0x000001ff80, // 1111111111
0x000003ffc0, // 111111111111
0x000007c7e0, // 11111 111111
0x000007c3e0, // 11111 11111
0x000007c3e0, // 11111 11111
0x00000387e0, // 111 111111
0x0000000fc0, // 111111
0x0000000f80, // 11111
0x0000001f80, // 111111
0x0000003f00, // 111111
0x0000007e00, // 111111
0x0000007c00, // 11111
0x000000fc00, // 111111
0x000000f800, // 11111
0x000001f800, // 111111
0x000001f000, // 11111
0x000001e000, // 1111
0x000003e000, // 11111
0x000003ff80, // 11111111111
0x000003ffe0, // 1111111111111
0x000003ffe0, // 1111111111111
0x0000000000, //
},
{//8 (2)
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007800, // 1111
0x000001fe00, // 11111111
0x000003ff00, // 1111111111
0x000007ff80, // 111111111111
0x0000078780, // 1111 1111
0x00000f8780, // 11111 1111
0x0000070780, // 111 1111
0x0000020780, // 1 1111
0x0000000f80, // 11111
0x0000001f00, // 11111
0x0000003f00, // 111111
0x0000007e00, // 111111
0x000000fc00, // 111111
0x000000fc00, // 111111
0x000000f800, // 11111
0x000001f000, // 11111
0x000003f000, // 111111
0x000003e000, // 11111
0x000003e000, // 11111
0x000003e000, // 11111
0x000007ffc0, // 1111111111111
0x000007ffc0, // 1111111111111
},
{//9 (2)
0x0000000000, //
0x0000000c00, // 11
0x0000000800, // 1
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007f00, // 1111111
0x000000ff80, // 111111111
0x000001ff80, // 1111111111
0x000001c380, // 111 111
0x000001c380, // 111 111
0x000003c380, // 1111 111
0x0000000780, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000001e00, // 1111
0x0000003e00, // 11111
0x0000003c00, // 1111
0x0000007800, // 1111
0x0000007000, // 111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000e000, // 111
0x000001e000, // 1111
0x000001e000, // 1111
},
{//10 (3)
0x0000000000, //
0x0000000000, //
0x000003fe00, // 111111111
0x000003fe00, // 111111111
0x000003ff00, // 1111111111
0x0000001f00, // 11111
0x0000001f00, // 11111
0x0000001e00, // 1111
0x0000003e00, // 11111
0x0000003c00, // 1111
0x0000007c00, // 11111
0x000000fc00, // 111111
0x000000fe00, // 1111111
0x000000ff00, // 11111111
0x0000007f80, // 11111111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x000003ff80, // 11111111111
0x000003ff00, // 1111111111
0x000003fe00, // 111111111
0x0000000000, //
0x0000000000, //
},
{//11 (3)
0x0000000000, //
0x000001fc00, // 1111111
0x000001fe00, // 11111111
0x000001fe00, // 11111111
0x0000001e00, // 1111
0x0000001e00, // 1111
0x0000001e00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000007800, // 1111
0x000000f800, // 11111
0x000000fc00, // 111111
0x000000fe00, // 1111111
0x000000ff00, // 11111111
0x0000000f00, // 1111
0x0000000700, // 111
0x0000000700, // 111
0x0000000700, // 111
0x0000000700, // 111
0x0000000700, // 111
0x0000000f00, // 1111
0x000003ff00, // 1111111111
0x000003fe00, // 111111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//12 (3)
0x0000000200, // 1
0x0000000300, // 11
0x0000000380, // 111
0x0000000380, // 111
0x0000000380, // 111
0x0000000380, // 111
0x0000000380, // 111
0x0000000380, // 111
0x0000000780, // 1111
0x000003ff00, // 1111111111
0x000003fe00, // 111111111
0x000003fc01, // 11111111 1
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000301, // 11 1
0x0000000700, // 111
0x0000000f00, // 1111
0x0000001e00, // 1111
0x0000001c00, // 111
0x0000007800, // 1111
0x0000007000, // 111
0x0000007000, // 111
0x000000e000, // 111
0x0000000000, //
},
{//13 (4)
0x0000000000, //
0x0000000000, //
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003800, // 111
0x0000007800, // 1111
0x0000007800, // 1111
0x0000007800, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000e000, // 111
0x000001e600, // 1111 11
0x000001ef00, // 1111 1111
0x000001ef00, // 1111 1111
0x000003ff80, // 11111111111
0x000003ff80, // 11111111111
0x000003ff80, // 11111111111
0x000001ff00, // 111111111
0x0000000f00, // 1111
0x0000000e00, // 111
0x0000000e00, // 111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//14 (4)
0x0000000000, //
0x0000000000, //
0x0000001c00, // 111
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000007800, // 1111
0x0000007800, // 1111
0x0000007000, // 111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000001e200, // 1111 1
0x000001ef00, // 1111 1111
0x000001ef00, // 1111 1111
0x000003ef00, // 11111 1111
0x000003ff80, // 11111111111
0x000003ff80, // 11111111111
0x000003ff80, // 11111111111
0x000003ff80, // 11111111111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//15 (4)
0x0000000000, //
0x0000008000, // 1
0x0000008000, // 1
0x000000c700, // 11 111
0x000001e700, // 1111 111
0x000001ff80, // 1111111111
0x000001ff80, // 1111111111
0x000001ff80, // 1111111111
0x0000000700, // 111
0x0000000700, // 111
0x0000000700, // 111
0x0000000700, // 111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x000001ff01, // 111111111 1
0x000001ff80, // 1111111111
0x000001ff00, // 111111111
0x000001c000, // 111
0x000001c000, // 111
0x000001c000, // 111
0x0000018000, // 11
0x000001c000, // 111
0x000001e000, // 1111
0x0000000000, //
},
{//16 (5)
0x0000000000, //
0x0000010100, // 1 1
0x0000011100, // 1 1 1
0x0000000000, //
0x0000000000, //
0x0000000600, // 11
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x000001ff00, // 111111111
0x000001ff00, // 111111111
0x000001ff00, // 111111111
0x000001c000, // 111
0x000001c000, // 111
0x000001c001, // 111 1
0x000001c000, // 111
0x000001c000, // 111
0x000001f800, // 111111
0x000001fe00, // 11111111
0x000001ff0f, // 111111111 1111
0x000000070e, // 111 111
0x0000000700, // 111
0x0000000380, // 111
0x0000000000, //
},
{//17 (5)
0x0000000000, //
0x0000000000, //
0x000001ff80, // 1111111111
0x000001ff80, // 1111111111
0x000001ff00, // 111111111
0x000001e000, // 1111
0x000001c000, // 111
0x000001c000, // 111
0x000001e000, // 1111
0x000001fc00, // 1111111
0x000001fe00, // 11111111
0x000001ff00, // 111111111
0x000000ff80, // 111111111
0x0000000780, // 1111
0x0000000380, // 111
0x0000000380, // 111
0x0000000380, // 111
0x0000000380, // 111
0x0000000780, // 1111
0x0000000780, // 1111
0x000001ff00, // 111111111
0x000001fe00, // 11111111
0x000001fc00, // 1111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//18 (5)
0x0000000000, //
0x0000000000, //
0x000000ff80, // 111111111
0x000001ff80, // 1111111111
0x000001ff00, // 111111111
0x000001c000, // 111
0x000001c000, // 111
0x000001c000, // 111
0x000001c000, // 111
0x000001e000, // 1111
0x000001fc00, // 1111111
0x000001fe00, // 11111111
0x000001ff00, // 111111111
0x000000ff80, // 111111111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x0000000780, // 1111
0x000001ff00, // 111111111
0x000001ff00, // 111111111
0x000001fe00, // 11111111
0x0000000000, //
0x0000000000, //
},
{//19 (6)
0x0000000000, //
0x0000000000, //
0x0000000700, // 111
0x0000000f00, // 1111
0x0000001e00, // 1111
0x0000003c00, // 1111
0x0000007c00, // 11111
0x0000007800, // 1111
0x0000007000, // 111
0x000000f000, // 1111
0x000000fc00, // 111111
0x000001fe00, // 11111111
0x000001ff00, // 111111111
0x000001ff80, // 1111111111
0x000001e780, // 1111 1111
0x000001c380, // 111 111
0x000001c380, // 111 111
0x000001c380, // 111 111
0x000001c380, // 111 111
0x000001e780, // 1111 1111
0x000001ff00, // 111111111
0x000000fe00, // 1111111
0x0000007c00, // 11111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//20 (6)
0x0000000000, //
0x0000000000, //
0x0000000700, // 111
0x0000000f00, // 1111
0x0000001e00, // 1111
0x0000003c00, // 1111
0x0000003800, // 111
0x0000007800, // 1111
0x0000007000, // 111
0x000000f000, // 1111
0x000000f000, // 1111
0x000001fe00, // 11111111
0x000001ff00, // 111111111
0x000001ff00, // 111111111
0x000003f780, // 111111 1111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000001e780, // 1111 1111
0x000001ff00, // 111111111
0x000000fe00, // 1111111
0x0000007c00, // 11111
0x0000000000, //
0x0000000000, //
},
{//21 (6)
0x0000000000, //
0x0000000000, //
0x0000000700, // 111
0x0000000f00, // 1111
0x0000001e00, // 1111
0x0000003c00, // 1111
0x0000003800, // 111
0x0000007800, // 1111
0x0000007000, // 111
0x000000f000, // 1111
0x000000f800, // 11111
0x000001fe00, // 11111111
0x000001ff00, // 111111111
0x000001ff00, // 111111111
0x000003ff80, // 11111111111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000001e780, // 1111 1111
0x000001ff00, // 111111111
0x000000fe00, // 1111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//22 (6)
0x0000000000, //
0x0000004000, // 1
0x0000004000, // 1
0x0000008000, // 1
0x000000f000, // 1111
0x000001fe00, // 11111111
0x000001ff80, // 1111111111
0x000001e780, // 1111 1111
0x000001c380, // 111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000003c380, // 1111 111
0x000001e780, // 1111 1111
0x000001ff00, // 111111111
0x000000fe00, // 1111111
0x0000007e01, // 111111 1
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x000001ff00, // 111111111
0x000001ff80, // 1111111111
0x000001ff00, // 111111111
0x0000000100, // 1
0x0000000000, //
},
{//23 (7)
0x0000000000, //
0x000003ff00, // 1111111111
0x000007ff80, // 111111111111
0x000007ff80, // 111111111111
0x000003ff00, // 1111111111
0x0000001e00, // 1111
0x0000001e00, // 1111
0x0000003e00, // 11111
0x0000003c00, // 1111
0x0000003800, // 111
0x0000007800, // 1111
0x0000007800, // 1111
0x000000f800, // 11111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000001e000, // 1111
0x000001e000, // 1111
0x000000e000, // 111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//24 (7)
0x0000000000, //
0x000000ff80, // 111111111
0x000003ff80, // 11111111111
0x000003ff80, // 11111111111
0x0000000f00, // 1111
0x0000000e00, // 111
0x0000001e00, // 1111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000003800, // 111
0x0000003800, // 111
0x0000007800, // 1111
0x0000007800, // 1111
0x0000007000, // 111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000e000, // 111
0x000000e000, // 111
0x000000e000, // 111
0x000000e000, // 111
0x000000e000, // 111
0x000000e000, // 111
0x0000000000, //
0x0000000000, //
},
{//25 (7)
0x0000000f00, // 1111
0x0000000e00, // 111
0x0000001e00, // 1111
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000003800, // 111
0x0000007800, // 1111
0x0000007800, // 1111
0x0000007000, // 111
0x0000007000, // 111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000f000, // 1111
0x000000e000, // 111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007e00, // 111111
0x000000ff00, // 11111111
0x000001ff00, // 111111111
0x000001ff00, // 111111111
},
{//26 (8)
0x0000004000, // 1
0x0000004000, // 1
0x0000006000, // 11
0x0000006000, // 11
0x0000006000, // 11
0x0000006000, // 11
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007e00, // 111111
0x0000007f00, // 1111111
0x000000ff80, // 111111111
0x000000e780, // 111 1111
0x000001e780, // 1111 1111
0x000001e781, // 1111 1111 1
0x000001e380, // 1111 111
0x000001e700, // 1111 111
0x000000ff00, // 11111111
0x000000ff00, // 11111111
0x000000ff00, // 11111111
0x000001ff80, // 1111111111
0x000001c380, // 111 111
0x0000018300, // 11 11
0x0000000000, //
},
{//27 (8)
0x0000000000, //
0x0000007c00, // 11111
0x000000fe00, // 1111111
0x000001fe00, // 11111111
0x000001cf00, // 111 1111
0x000001cf00, // 111 1111
0x000001cf00, // 111 1111
0x000001ff00, // 111111111
0x000001fe00, // 11111111
0x000001fe00, // 11111111
0x000001ff00, // 111111111
0x000001ff00, // 111111111
0x000001ef80, // 1111 11111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003e780, // 11111 1111
0x000001ff00, // 111111111
0x000000ff00, // 11111111
0x0000007e00, // 111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//28 (8)
0x0000000000, //
0x0000000000, //
0x0000001e00, // 1111
0x0000003f00, // 111111
0x0000007f00, // 1111111
0x000000f780, // 1111 1111
0x000000e780, // 111 1111
0x000000e780, // 111 1111
0x000000e780, // 111 1111
0x000000ff80, // 111111111
0x000000ff00, // 11111111
0x000000ff00, // 11111111
0x000000ff80, // 111111111
0x000000ff80, // 111111111
0x000001e7c0, // 1111 11111
0x000001e3c0, // 1111 1111
0x000001c3c0, // 111 1111
0x000001c3c0, // 111 1111
0x000001c3c0, // 111 1111
0x000001e3c0, // 1111 1111
0x000001e780, // 1111 1111
0x000001ff80, // 1111111111
0x000000ff00, // 11111111
0x0000007e00, // 111111
0x0000000000, //
0x0000000000, //
},
{//29 (9)
0x0000000000, //
0x0000010080, // 1 1
0x0000008080, // 1 1
0x0000014080, // 1 1 1
0x000000e380, // 111 111
0x000000ff80, // 111111111
0x000000ff00, // 11111111
0x0000001e00, // 1111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x000000fc00, // 111111
0x000000ff00, // 11111111
0x000001ff80, // 1111111111
0x000001c780, // 111 1111
0x0000038380, // 111 111
0x0000038381, // 111 111 1
0x0000038380, // 111 111
0x0000038380, // 111 111
0x0000038380, // 111 111
0x000001cf00, // 111 1111
0x000001ff00, // 111111111
0x000000ff00, // 11111111
},
{//30 (9)
0x0000000000, //
0x0000000000, //
0x0000007c00, // 11111
0x000000ff00, // 11111111
0x000001ff00, // 111111111
0x000001ef80, // 1111 11111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000003c780, // 1111 1111
0x000001ff80, // 1111111111
0x000001ff00, // 111111111
0x000000ff00, // 11111111
0x0000007f00, // 1111111
0x0000001e00, // 1111
0x0000001e00, // 1111
0x0000001e00, // 1111
0x0000003c00, // 1111
0x0000007c00, // 11111
0x0000007800, // 1111
0x000000f000, // 1111
0x000000e000, // 111
0x000001c000, // 111
0x0000000000, //
0x0000000000, //
},
{//31 (9)
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007e00, // 111111
0x0000007f80, // 11111111
0x000000ff80, // 111111111
0x000001e7c0, // 1111 11111
0x000001c3c0, // 111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000003c3c0, // 1111 1111
0x000001e7c0, // 1111 11111
0x000001ffc0, // 11111111111
0x000001ff80, // 1111111111
0x000000ff80, // 111111111
0x0000007f80, // 11111111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000001e00, // 1111
0x0000001e00, // 1111
0x0000003c00, // 1111
0x0000003c00, // 1111
0x0000007800, // 1111
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//32 (9)
0x0000000000, //
0x0000010100, // 1 1
0x0000010000, // 1
0x0000018100, // 11 1
0x0000018380, // 11 111
0x000001c700, // 111 111
0x000001ff80, // 1111111111
0x000001ff0f, // 111111111 1111
0x0000007f00, // 1111111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000e00, // 111
0x0000001e01, // 1111 1
0x0000003e00, // 11111
0x0000003c00, // 1111
0x0000007800, // 1111
0x000000f000, // 1111
0x000001e000, // 1111
0x000001e001, // 1111 1
0x0000008000, // 1
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007c01, // 11111 1
0x000001fe00, // 11111111
},
{//33 (9)
0x0000000000, //
0x0000010000, // 1
0x0000010100, // 1 1
0x000001e700, // 1111 111
0x000000ff00, // 11111111
0x0000007f00, // 1111111
0x0000000f00, // 1111
0x0000000700, // 111
0x0000000e00, // 111
0x0000001c00, // 111
0x0000001c00, // 111
0x0000003c00, // 1111
0x0000007800, // 1111
0x000000f000, // 1111
0x000000e000, // 111
0x000001c001, // 111 1
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000007c00, // 11111
0x000000fe00, // 1111111
0x0000018600, // 11 11
0x0000010100, // 1 1
0x0000000000, //
},
//старый счетчик
{//34 (0)
0x0000000000, //
0x0000000000, //
0x000000ff00, // 11111111
0x000003ff80, // 11111111111
0x000007ffc0, // 1111111111111
0x00000781c0, // 1111 111
0x00000701e0, // 111 1111
0x00000701e0, // 111 1111
0x00000f00e0, // 1111 111
0x00000f00e0, // 1111 111
0x00000f00e0, // 1111 111
0x00000f00e0, // 1111 111
0x00000f00e0, // 1111 111
0x00000700e0, // 111 111
0x00000700e0, // 111 111
0x00000700e0, // 111 111
0x00000700e0, // 111 111
0x00000700e0, // 111 111
0x00000700e0, // 111 111
0x00000781e0, // 1111 1111
0x00000781e0, // 1111 1111
0x000007ffe0, // 11111111111111
0x000001ffc0, // 11111111111
0x000001ff00, // 111111111
0x0000000000, //
0x0000000000, //
},
{//35 (1)
0x0000000000, //
0x0000001e00, // 1111
0x0000003f00, // 111111
0x0000007f00, // 1111111
0x0000007f00, // 1111111
0x000000ff00, // 11111111
0x000000ff00, // 11111111
0x000000ff00, // 11111111
0x000000cf00, // 11 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000f00, // 1111
0x0000000700, // 111
0x0000000600, // 11
0x0000000000, //
0x0000000000, //
},
{//36 (2)
0x0000000000, //
0x0000000000, //
0x000000fe00, // 1111111
0x000001ff80, // 1111111111
0x000007ffc0, // 1111111111111
0x00000783c0, // 1111 1111
0x00000f03c0, // 1111 1111
0x00000e01c0, // 111 111
0x00000e01c0, // 111 111
0x00000e01c0, // 111 111
0x00000003c0, // 1111
0x00000003c0, // 1111
0x00000007c0, // 11111
0x0000001f80, // 111111
0x0000003f00, // 111111
0x000000fe00, // 1111111
0x000001fc00, // 1111111
0x000003f000, // 111111
0x000007e000, // 111111
0x000007c000, // 11111
0x00000f8000, // 11111
0x00000f8000, // 11111
0x00000fffc0, // 11111111111111
0x000007ffc0, // 1111111111111
0x0000000000, //
0x0000000000, //
},
{//37 (4)
0x0000000000, //
0x0000000000, //
0x0000000780, // 1111
0x0000000f80, // 11111
0x0000001f80, // 111111
0x0000001f80, // 111111
0x0000001f80, // 111111
0x0000003fe0, // 111111111
0x0000007fc0, // 111111111
0x000000ffe0, // 11111111111
0x000001f7c0, // 11111 11111
0x000001e600, // 1111 11
0x000003c700, // 1111 111
0x000007cf00, // 11111 1111
0x0000078f80, // 1111 11111
0x00000fffc0, // 11111111111111
0x00000fffc0, // 11111111111111
0x00000fffc0, // 11111111111111
0x00000fffc0, // 11111111111111
0x00000fff80, // 1111111111111
0x0000000f80, // 11111
0x0000000780, // 1111
0x0000000700, // 111
0x0000000700, // 111
0x0000000000, //
0x0000000000, //
},
{//38 (6)
0x0000000000, //
0x000000ff00, // 11111111
0x000001ff80, // 1111111111
0x000003ffc0, // 111111111111
0x00000381c0, // 111 111
0x0000078000, // 1111
0x0000078000, // 1111
0x000007fe00, // 1111111111
0x000007ff80, // 111111111111
0x000007ffc0, // 1111111111111
0x000007efc0, // 111111 111111
0x00000781e0, // 1111 1111
0x00000781e0, // 1111 1111
0x00000780e0, // 1111 111
0x00000780e0, // 1111 111
0x00000780e0, // 1111 111
0x00000700e0, // 111 111
0x00000781e0, // 1111 1111
0x00000381c0, // 111 111
0x000003c3c0, // 1111 1111
0x000001ff00, // 111111111
0x000000ff00, // 11111111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//39 (6)
0x0000000000, //
0x0000000300, // 11
0x0000003c00, // 1111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000003c00, // 1111
0x000000ff00, // 11111111
0x000001cf80, // 111 11111
0x0000038180, // 111 11
0x0000038000, // 111
0x0000038000, // 111
0x000003b800, // 111 111
0x000003ff00, // 1111111111
0x000003ff80, // 11111111111
0x000003e780, // 11111 1111
0x00000381c0, // 111 111
0x00000381c0, // 111 111
0x0000030080, // 11 1
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
{//40 (6)
0x0000000000, //
0x0000001c00, // 111
0x000000f000, // 1111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x000000f800, // 11111
0x000003fc00, // 11111111
0x000007be00, // 1111 11111
0x00000f0600, // 1111 11
0x00000e0000, // 111
0x00000e0000, // 111
0x00000fe000, // 1111111
0x00000ffc00, // 1111111111
0x00000ffe00, // 11111111111
0x00000f9e00, // 11111 1111
0x00000e0700, // 111 111
0x00000e0700, // 111 111
0x00000c0700, // 11 111
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
0x0000000000, //
},
};
Файл virtuino_pins.h
//---VirtuinoCM Library settings --------------
#include "VirtuinoCM.h"
VirtuinoCM virtuino;
#define V_memory_count 32 // the size of V memory. You can change it to a number <=255)
float V[V_memory_count]; // This array is synchronized with Virtuino V memory. You can change the type to int, long etc.
//---
boolean debug = false; // set this variable to false on the finale code to decrease the request time.
WiFiServer server(80); // Default Virtuino Server port
//Опеределенее ячеек памяти на сервере virtuino
#define V_lastCommTime 0 //V0 время соединения используется для первоначального сброса видимости кнопок в приложении
#define V_number_of_sum_frames 1 //V1 - число кадров суммирования
#define V_offset_y 2 //V2 - смещенее по оси Y при суммировании кадров и отображении на дисплее 75 85 87
#define V_offset_x 3 //V3 - смещенее по оси X при суммировании кадров и отображении на дисплее
#define V_level_dispalay_ttf_B_W 4 //V4 - Доп. уровень бинаризации для дисплея 15
#define V_level_find_digital_Y 5 //V5 - Доп. уровень бинаризации для поиска цифр по y
#define V_level_find_digital_X 6 //V6 - Доп. уровень бинаризации для поиска цифр по X
#define V_level_convert_to_32 7 //V7 - Доп. уровень бинаризации при конвертации в 32 бита
#define V_level_Y_up 8 //V8 - Положенее шторки сверху Y_up
#define V_level_Y_down 9 //V9 - Положенее шторки снизу Y_down
#define V_show_digital 10 //V10 - номер цифры какую выводим на экран для сравнения 8 - нет вывода
#define V_offset_x_digital 11 //V11 - смещенее по оси X при отображении на дисплеи для анализа 50 100 150
#define V_GBW 12 //V12 - 0 = gray 1 - b/w
#define V_SH_0_1 13 //V13 - вывод на монитор значения гистограммы в двоичном виде
#define V_SH_HEX 14 //V14 - вывод на монитор в HEX цифр шкалы
#define V_SH_M3 15 //V15 - вывод на монитор результатов накопленного газа
#define V_m3 16 //V16 - текущее значенее объема газа умноженное на 100
#define V_m3_minutes 17 //V17 - значенее минут при текущем объеме газа
#define V_D0 18 //V18 - опознанная цифра 1
#define V_D1 19 //V19 - опознанная цифра 2
#define V_D2 20 //V20 - опознанная цифра 3
#define V_D3 21 //V21 - опознанная цифра 4
#define V_D4 22 //V22 - опознанная цифра 5
#define V_D5 23 //V23 - опознанная цифра 6
#define V_D6 24 //V24 - опознанная цифра 7
#define V_D7 25 //V25 - опознанная цифра 8
#define V_26_error_recognition 26 //V26 - ошибки распознавания по сравнению с предыдущим
#define V_27 27 //V27 -
#define V_28 28 //V28 -
#define V_29 29 //V29 -
#define V_30 30 //V30 -
#define V_Total_run 31 //V31 -
String T_0 = ""; // результаты распознавания
String T_1 = ""; // расстояние Хемминга
String T_2 = ""; // частоты повторения цифр
//предопределенные значения
#define old_number_of_sum_frames 5 //число кадров суммирования
#define old_offset_y 83 //смещенее по оси Y при суммировании кадров и отображении на дисплее 54 86
#define old_offset_x 0 //смещенее по оси X при суммировании кадров и отображении на дисплее
#define old_level_dispalay_ttf_B_W 30 //Доп. уровень бинаризации для дисплея 30
#define old_level_find_digital_Y 30 //Доп. уровень бинаризации для поиска цифр по y 30
#define old_level_find_digital_X 70 //Доп. уровень бинаризации для поиска цифр по X 60 70
#define old_level_convert_to_32 30 //Доп. уровень бинаризации при конвертации в 32 бита 15 73 25 50
#define old_level_Y_up 22 //Положенее шторки сверху Y_up 23
#define old_level_Y_down 48 //Положенее шторки снизу X_down 48
#include <EEPROM.h>
//адреса хранения предопределеных значений в EEPROM
#define offset_y_addr 0 //смещенее по оси Y при суммировании кадров и отображении на дисплее
#define offset_x_addr offset_y_addr + sizeof(byte) //смещенее по оси X при суммировании кадров и отображении на дисплее
#define level_find_digital_Y_addr offset_x_addr + sizeof(byte) //Доп. уровень бинаризации для поиска цифр по y
#define level_find_digital_X_addr level_find_digital_Y_addr + sizeof(byte) //Доп. уровень бинаризации для поиска цифр по X
#define level_convert_to_32_addr level_find_digital_X_addr + sizeof(byte) //Доп. уровень бинаризации при конвертации в 32 бита
#define level_Y_up_addr level_convert_to_32_addr + sizeof(byte) //Положенее шторки сверху Y_up
#define level_Y_down_addr level_Y_up_addr + sizeof(byte) //Положенее шторки снизу X_down
//================================================================= check_limits
bool check_limits(float &V_test, uint8_t V_max, uint8_t V_min, uint8_t V_set, uint8_t V_MI, uint8_t V_addr)
{
//Проверка на нахождение в допустимых прелах переменной V_test
//V_test - значение, который нужно проверить
//V_max - максимальное значение передела
//V_min - минимальное значение передал
//V_set - установить если вне предлов
//V_MI - ячейка памяти в virtuino
//V_addr - адрес ячейки для сохранения
// printf("На входе: V_set %d\tV_test %f4\tV_old %d\n", V_set, V_test, EEPROM.readByte(V_addr));
if ((uint8_t)(V_test) >= V_max || (uint8_t)(V_test) < V_min || isnan(V_test))
{ //если значение вне пределов или неопределено - nan
// Serial.print("Установка первоначального значения: V_set " + (String)V_set
// + "\tV_test " + (String)V_test + "\tV_MI " + (String)V_MI + "\tV_V_addr " + (String)V_addr);
// Serial.printf("Установка первоначального значения: V_set %f\tV_test %f\tV_MI %d\tV_addr %d\n", V_set, V_test, V_MI, V_addr);
V_test = V_set; //не будет изменения восстанавливаем первоначальное
V[V_MI] = V_test;
if (EEPROM.readByte(V_addr) == (uint8_t)(V_test)) //если значение в памяти совпадает не записывать
return false;
EEPROM.writeByte(V_addr, (uint8_t)(V_test)); //сохранить в памяти EEPROM
return true;
}
else
{
if (EEPROM.readByte(V_addr) != (uint8_t)(V_test)) //если значение в памяти не совпадает записать
{
EEPROM.writeByte(V_addr, (uint8_t)(V_test));
printf("Установить и сохранить значение: V_set %d\tV_test %d\tV_MI %d\tV_addr %d\n", V_set, (uint8_t)(V_test), V_MI, V_addr);
return true;
}
else
return false;
}
}
//================================================================= check_limits
//================================================================= change_variables
void change_variables(bool read_from_memory)
//read_from_memory true восстановить из памяти
{
boolean write_EEPROM_flag = false;
//смещенее по оси Y при суммировании кадров и отображении на дисплее 20 250
if (read_from_memory) V[V_offset_y] = EEPROM.readByte(offset_y_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_offset_y], 250, 20, old_offset_y, V_offset_y, offset_y_addr);
//смещенее по оси X при суммировании кадров и отображении на дисплее 0 - 50
if (read_from_memory) V[V_offset_x] = EEPROM.readByte(offset_x_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_offset_x], 50, 0, old_offset_x, V_offset_x, offset_x_addr);
//Доп. уровень бинаризации для поиска цифр по y 30
if (read_from_memory) V[V_level_find_digital_Y] = EEPROM.readByte(level_find_digital_Y_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_level_find_digital_Y], 100, 0, old_level_find_digital_Y, V_level_find_digital_Y, level_find_digital_Y_addr);
//Доп. уровень бинаризации для поиска цифр по X 60 70 80
if (read_from_memory) V[V_level_find_digital_X] = EEPROM.readByte(level_find_digital_X_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_level_find_digital_X], 150, 0, old_level_find_digital_X, V_level_find_digital_X, level_find_digital_X_addr);
//Доп. уровень бинаризации при конвертации в 32 бита 15 73 25 50
if (read_from_memory) V[V_level_convert_to_32] = EEPROM.readByte(level_convert_to_32_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_level_convert_to_32], 150, 0, old_level_convert_to_32, V_level_convert_to_32, level_convert_to_32_addr);
//Положенее шторки сверху Y_up 23
if (read_from_memory) V[V_level_Y_up] = EEPROM.readByte(level_Y_up_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_level_Y_up], 100, 10, old_level_Y_up, V_level_Y_up, level_Y_up_addr);
//Положенее шторки снизу X_down 44
if (read_from_memory) V[V_level_Y_down] = EEPROM.readByte(level_Y_down_addr); //восстановить значение из памяти
write_EEPROM_flag |= check_limits(V[V_level_Y_down], 150, 10, old_level_Y_down, V_level_Y_down, level_Y_down_addr);
//запись данных в память
if (write_EEPROM_flag)
{ //записать данные в память
EEPROM.commit(); //подтвердить запись в память
printf("Обновлены данные в памяти\n");
}
}
//================================================================= change_variables
//================================================================= init_V
void init_V() {
boolean write_EEPROM_flag = false;
//инициализация переменных
change_variables(true);
V[V_number_of_sum_frames] = old_number_of_sum_frames; //число кадров суммирования
// V[V_level_dispalay_ttf_B_W] = old_level_dispalay_ttf_B_W; //Доп. уровень бинаризации для дисплея 15
V[V_level_dispalay_ttf_B_W] = V[V_level_convert_to_32]; //Доп. уровень бинаризации для дисплея совпдает с уровнем уровень бинаризации при конвертации в 32 бита
V[V_show_digital] = 8; //V10 - номер цифры какую выводим на экран для сравнения 8 - нет вывода
V[V_offset_x_digital] = 0; //V11 - смещенее по оси X при отображении на дисплеи для анализа 50 100 150
V[V_GBW] = 0; //V12 - 0- - b/w 1 - gray 2 - gray full
V[V_SH_0_1] = 0; //V13 - вывод на монитор значения гистограммы в двоичном виде
V[V_SH_HEX] = 0; //V14 - вывод на монитор в HEX цифр шкалы
V[V_SH_M3] = 0; //V15 - вывод на монитор результатов накопленного газа
V[V_26_error_recognition] = 0.0; //ошибки распознавания нет
}
//================================================================= init_V
//================================================================= onReceived
/* This function is called every time Virtuino app sends a request to server to change a Pin value
The 'variableType' can be a character like V, T, O V=Virtual pin T=Text Pin O=PWM Pin
The 'variableIndex' is the pin number index of Virtuino app
The 'valueAsText' is the value that has sent from the app */
void onReceived(char variableType, uint8_t variableIndex, String valueAsText) {
if (variableType == 'V') {
float value = valueAsText.toFloat(); // convert the value to float. The valueAsText have to be numerical
if (variableIndex < V_memory_count) V[variableIndex] = value; // copy the received value to arduino V memory array
}
/*
else if (variableType=='T'){
if (variableIndex==0) T_0=valueAsText; // Store the text to the text variable T0
else if (variableIndex==1) T_1=valueAsText; // Store the text to the text variable T1
else if (variableIndex==2) T_2=valueAsText; // Store the text to the text variable T2
//else if (variableIndex==3) T_3=valueAsText; // Store the text to the text variable T3
}
*/
}
//================================================================= onReceived
//================================================================= onRequested
/* This function is called every time Virtuino app requests to read a pin value*/
String onRequested(char variableType, uint8_t variableIndex) {
char S[30];
if (variableType == 'V') {
if (variableIndex < V_memory_count) {
return String(V[variableIndex]);
// sprintf(S, "%.6f", V[variableIndex]); //увеличенее до 6 знаков после запятой
// return S; // return the value of the arduino V memory array
}
}
else if (variableType == 'T') {
if (variableIndex == 0) return T_0;
else if (variableIndex == 1) return T_1;
else if (variableIndex == 2) return T_2;
//else if (variableIndex==3) return T_3;
}
return "";
}
//================================================================= onRequested
//================================================================= virtuinoRun
void virtuinoRun() {
WiFiClient client = server.available();
if (!client) return;
if (debug) Serial.println("Connected");
unsigned long timeout = millis() + 3000;
while (!client.available() && millis() < timeout) delay(1);
if (millis() > timeout) {
Serial.println("timeout");
client.flush();
client.stop();
return;
}
virtuino.readBuffer = ""; // clear Virtuino input buffer. The inputBuffer stores the incoming characters
while (client.available() > 0) {
char c = client.read(); // read the incoming data
virtuino.readBuffer += c; // add the incoming character to Virtuino input buffer
if (debug) Serial.write(c);
}
client.flush();
if (debug) Serial.println("\nReceived data: " + virtuino.readBuffer);
String* response = virtuino.getResponse(); // get the text that has to be sent to Virtuino as reply. The library will check the inptuBuffer and it will create the response text
if (debug) Serial.println("Response : " + *response);
client.print(*response);
client.flush();
delay(10);
client.stop();
// lastCommTime = millis();
V[V_lastCommTime] = round(millis() / 1000.0); //преобразовать в секунды
if (debug) Serial.println("Disconnected");
}
//================================================================= virtuinoRun
//================================================================= vDelay
void vDelay(int delayInMillis) {
unsigned long t = millis();
while ((unsigned long)(millis() - t) < delayInMillis)
virtuinoRun();
}
//================================================================= vDelay
все забуваю спитати, а які фільтри встановлені у вас перед баком? у мене встановлено один 10-20 мікронний, але забивається досить швидко. міняю кожен місяць.
і ще, не могли б ви поділитися посиланням на кран, що управляється сервоприводом.
Я использую 5 или 10 микрон. Моя первая модель фильтра обратного осмоса была построена самостоятельно на двух колбах. Так как они остались то их и поставил. Фильтры меняю не чаще чем раз в пол года. На фото видно первый фильтр грязный второй практически как новый.
По расчету если я использую баки раз в неделю, в месяц 4 раза, то за 6 месяце будет около 7200 л при ресурсе самого дешёвого 10000 л.
Эту модель крана https://aliexpress.ru/item/32825291411.html?spm=a2g0s.9042311.0.0.264d33ednLZtoD в то время я выбрал т.к она имела возможность ручного управления и была на 220 В.
Потом из-за такой конструкции долго пришлось ломать голову как сделать отслеживание за положением крана (см. Фото выше) на базе датчика Холла. Сейчас бы брал не на 220, а на более низкое напряжение. Ручным управлением сейчас не пользуюсь.
але я вирішив підключити гідрофор на постійно - бо це питання комфорту: в нас багато точок водорозбору і багато користувачів
У меня первоначально так и было - гидрофор был включён на постоянно, а вода набралась с водопровода в баки, но мой гидрофор был без гидроаккумулятора. Из-за этого гидрофор часто срабатывает.
Сейчас я дополнительно установил гидроаккумулятор на 80 л.
Для Вышего случая я думаю гидроаккумулятора тоже нужен большой ёмкости, а не стандартный. Хотя с таким мотором как Вы описали он работает немного по-другому.
Для обогрева баков на чердаке возможно Вам целесообразно использовать саморегулирующий кабель.
Второй вариант бочки установить в небольшом "домике" из пенопласта и обогревать все это теном с термостатом, поддерживая нужную температуру. Тут можно и ардуинку использовать.?
Возможно если Вы их хорошо утеплителем, то обогрев вообще не нужен, т.к будет происходить постоянный обмен воды в банках, которая будет иметь температуру воды в водопровода.
Режим оповещения (индикация и Email) у меня реализован для аварийной ситуации - перелив. Добавить индикацию и отправку по Email сообщения о низком уровне воды в течении заданного времени не сложно - спасибо подумаю. Просто пока такая задача у меня не стояла.
Обновление воды в баках у меня реализовано для того, чтобы вода в них не "испортилась", как это у меня уже было. Когда я долго не использовал воду в баках появился запах и баки пришлось срочно снимать и мыть.
Постоянно использовать гидрофор также нет необходимости - сейчас и у нас, как будто давление стало в основном нормальное. Воду тоже отключают не часто. Запасом в 400 л пользуется 2 человека и перед предполагаемым интенсивным использованием воды можно посмотреть на индикатор и даже визуально оценить остаток в баках по его цвету.
Надеюсь ничего не упустил и по возможности ответил на все Ваши вопросы.
Удачной Вам реализации задуманного.
P.S. То, что к системе можно подключиться с нескольких планшетов или телефонов одновременно и осуществить контроль или управление очень удобно. Единственное если устройства находятся в разных часовых поясах, то таймеры работают по местному времени.
Это второй котле, который установлен у меня. До этого был Юнкерс (проработал около 18 лет). Сейчас Saunier Duval. Также имел опыт работы с Аристоном. Все они имею возможность подключать комнатный термостат. Для этого убирается перемычка и вместо не подключается реле термостата.
Установка комнатного термостата позволила существенно экономить потребление газа и влажность в комнате стало в пределах нормы.
Если у Вас не старое АОГВ, то думаю такая функция должна быть и у Вашего котла.
При включении термостата включается режим отопления. Температурой теплоносителя котле управляет сам. Также котел сам управляет циркулярным насосом. Режим приготовления горячей воды при этом не меняется.
По потребляемой мощности можно проследить режим работы котла: ожидание (4 Вт), подача теплоносителя (60 Вт), горелка включена (110 Вт), горячая вода (120 Вт).
Мой старый термостат был программируемый недельный, но в таком режиме я его практически не использовал. Если я уезжал на долго зимой в отпуск, то выставлял температуру в комнате на термостате 16 градусов. Расход газа меньше и цветам легче.
Немного передела дополнил интерфейс, добавив время работы котла в режиме отопления и ожидания. Также была добавлена кнопка включения котла. Установлена температура 19,7 градуса дельта +/- 0,2.
Добавил еще один таймер - включение перед приходом с работы за 1 час и утром перед подъемом на работу и отключение перед сном.
Этот таймер просто включает или отключает режим отопления в котле, а работа по температуре происходит независимо.