Я опустил кучу подробностей и историю развития проекта, которые мне показались несущественными для данной проблемы, но в этом секрета нет. На данный момент имеем следующее…
- Нарисованной схемы не существует, но суть заключается в подключении электронных компонентов к нужным пинам на контроллере.
Система состоит из:
Arduino Leonardo R3
Сетевой платы W5500 SPI, запитанной от 5В
Датчика температуры DS18B20 от 3.3В
Четырёхзнакового дисплея 74HC595
Двух ультразвуковых датчиков RCWL-1670 (по задумке должно быть три), от 5В
Логика следующая:
Измеряется температура
Последовательно снимаются показания со всех УЗ сенсоров
Определяется среднее усечённое значения для каждого датчика.
Усреднённые измерения сравниваются друг с другом и если показания близки (расходятся не более, чем на 3%), то они признаются правильными
Правильные показания ещё раз усредняются между собой, и результат скидывается REST-запросом на сервер.
Результаты фактических измерений температуры и каждого сенсора скидываются на сервер в Prometheus. Собственно, там видны все сырые показания по каждому сенсору и термометру.
Измеренный уровень воды (отклонение от нормы) выводится на экран. Также на экран ненадолго выводятся некоторые коды ошибок, например, сетевого обмена.
В случае конфигурации с тремя сенсорами, при сбое одного, в работу брались бы значения с двух других. Сейчас сенсора два. Если показания расходятся, то на данной итерации, результат не сбрасываем на сервер. Третий сонар заглушен программно.
-
Сфотографировать не могу, ибо сейчас нахожусь далеко. Когда менялись HC-SR04, то покойные выглядели как живые: чистые, без мусора и паутины.
-
HC-SR04 врали и в плюс и в минус. Подробностей уже не помню – это было два года назад.
Сейчас RCWL-1670 врут в большую сторону, причём тремя вариантами 4300, 5500, и 6600. мм Несколько дней назад эти выбросы били миллиметров на 300 поменьше, но их было также три таких варианта.
Поскольку сейчас датчики врут не такими уж и случайными значениями, то, иногда, когда они врут одинаково, на сервер прилетают левые значения. Приходится чистить руками.
- Кода мне не жалко, но тут почти 450 строк.
#include <Ethernet2.h>
#include “OneWire.h”
#include “DallasTemperature.h”
#include <AceSorting.h>
#include <GyverSegment.h>
// Пин, к которому подключен датчик температуры ds18b20
#define ONE_WIRE_PIN 4
// пины эхолота 0
#define HC_TRIG_0 A1
#define HC_ECHO_0 A0
// пины эхолота 1
#define HC_TRIG_1 A3
#define HC_ECHO_1 A2
// пины эхолота 2
#define HC_TRIG_2 A5
#define HC_ECHO_2 A4
//пины дисплея
#define SDI_PIN 5
#define SCLK_PIN 6
#define LOAD_PIN 7
//Количество разрядов на дисплее
#define DISP_DIGITS_COUNT 4
//Яркость дисплея. 0…15. (15 - max)
#define DISP_BRIGHTNESS 15
//Критическое расхождение в процентах между показаниями двух датчиков,
//при котором считается, что один из них врёт. Считаем от того, у которого минимальное значение.
#define CRITICAL_DIFFERENCE 3
//Время в миллисекундах между снятием показаний. Запись данных в БД будет выполняться с интервалом DELAY * MEASUREMENTS_COUNT
#define MEASUREMENT_DELAY 120000
//Расстояни в миллиметрах до реки при нормальном уровне воды
#define ZERO_RIVER_LEVEL 3410
//Время в течение которого старое измерение считается действительным
#define MEASUREMET_VALID_WHILE 1800000
//Втечение какого времени высвечивать сообщение об ошибке
#define ERROR_VISUALISATION_DURATION 5000
const String errorDhcp(“E01”);
const String errorThermometer(“E02”);
const String errorAllSensors(“E03”);
const String errorPostTimeout(“E04”);
const String errorPostConnectionFailed(“E05”);
byte mac
= { 0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED }; // MAC-адрес сетевой платы
// Адрес сервера
#define SERVER “1.1.1.1”
// Порт HTTP
#define PORT 80
//Путь для запроса.
#define ENDPOINT_DATA “/meteo/0”
#define ENDPOINT_ERROR “/meteo/error/0”
#define ENDPOINT_PUSHGATEWAY “/pushgateway/metrics/job/pushgateway”
//С какким идентификатором типа данных будем отправлять измеренное расстояние в БД
#define DISTANCE_TYPE 0
// Максимальное количество попыток отправить данные
#define SENDING_ATTEMPTS 2
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////// Дальше идёт код //////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Количество измерений каждым датчиком. За расстояние будем брать среднее усечённое от ряда из MEASUREMENTS_COUNT измерений.
#define MEASUREMENTS_COUNT 9
//Количество эхолотов
#define SENSORS_COUNT 3
#define POST_OK 0
#define POST_TIMEOUT 1
#define POST_CONNECTION_FAILED 2
// Настройка объекта OneWire
OneWire oneWire(ONE_WIRE_PIN);
//Дисплей
Disp595Static<DISP_DIGITS_COUNT> disp(SDI_PIN, SCLK_PIN, LOAD_PIN);
// Передаем ссылку на OneWire объект DallasTemperature
DallasTemperature thermometer(&oneWire);
boolean initializationError = false;
//Время последнего измерения
unsigned long lastShotTime = 0;
//Время последнего успешного снятия измерения
unsigned long lastResultTime = 0;
//Признак того, что измерения ещё не проводились и сейчас первый раз.
bool firstShot = true;
String lastResultStr = “”;
struct Sensor {
char trigPin = 0;
char echoPin = 0;
float measurements[MEASUREMENTS_COUNT];
float distance = 0;
char differenes = 0;
Sensor(char _trigPin, char _echoPin) {
trigPin = _trigPin;
echoPin = _echoPin;
}
};
Sensor sensors
= {
Sensor(HC_TRIG_0, HC_ECHO_0),
Sensor(HC_TRIG_1, HC_ECHO_1),
Sensor(HC_TRIG_2, HC_ECHO_2)
};
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void setup() {
Serial.begin(9600); // открытие порта для связиc:\programs\arduino\distance\config.h
Serial.println(F(“Погнали!”));
//!!! Для использования статического IP раскоментировать и закоментировать инициализацию по DHCP
//Ethernet.begin(mac, ip);
// //Инициализиркем пины эхолотов
for (int i = 0; i < SENSORS_COUNT; ++i) {
pinMode(sensors[i].trigPin, OUTPUT); // эхолот trig выход
pinMode(sensors[i].echoPin, INPUT); // эхолот echo вход
}
// Инициализация датчика температуры
thermometer.begin();
thermometer.setResolution(12);
disp.brightness(DISP_BRIGHTNESS);
pinMode(LED_BUILTIN, OUTPUT);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void loop() {
if (millis() - lastShotTime >= MEASUREMENT_DELAY || firstShot) {
lastShotTime = millis();
firstShot = false;
if (Ethernet.begin(mac)) { // Инициализируем Ethernet по DHCP. Если ОК, едем дальше
printNetworkInfo(); // Выведем инфу о сети
} else { //Если не договорились с DHCP-сервером
Serial.println(F("Не договорились с DHCP-сервером! Работы не будет!"));
showError(errorDhcp);
return;
}
// Запрос температуры
thermometer.requestTemperatures();
//На получение температуры у него уходит до 750 мс. Надо подождать.
//Иначе вернёт старое значение.
delay(1000);
// Чтение температуры в градусах Цельсия
float temperature = thermometer.getTempCByIndex(0);
if (temperature == DEVICE_DISCONNECTED_C) {
Serial.println(F("Ошибка: датчик температуры отвалился! Всё идёт по бороде."));
showError(errorThermometer);
return;
}
EthernetClient ethClient;
String temperatureMetrics = String("# TYPE core_temp gauge\nvodomer_temperature") + " " + String(temperature) + "\n";
sendPostRequest(ethClient, SERVER, PORT, ENDPOINT_PUSHGATEWAY, temperatureMetrics.c_str());
//Делаем измерения всеми датчиками
for (int measurementNo = 0; measurementNo < MEASUREMENTS_COUNT; ++measurementNo) {
for (char sensorIndex = 0; sensorIndex < SENSORS_COUNT; ++sensorIndex) {
doMeasurement(sensors[sensorIndex], temperature, measurementNo);
if (measurementNo == MEASUREMENTS_COUNT - 1) {
calcAvgDistance(sensors[sensorIndex]);
sensors[sensorIndex].differenes = 0;
}
delay(1000);
}
}
//Выведем показания с каждого сонара и отправим их метрики куда следует
for (int i = 0; i < SENSORS_COUNT; ++i) {
String str = "Sensor_" + String(i) + " " + String(sensors[i].distance);
Serial.println(str);
String metrics = String("# TYPE core_temp gauge\nvodomer_sensor_") + String(i) + " " + String(sensors[i].distance) + "\n";
sendPostRequest(ethClient, SERVER, PORT, ENDPOINT_PUSHGATEWAY, metrics.c_str());
}
//Сравниваем полученные значения
for (char first = 0; first < SENSORS_COUNT - 1; ++first) {
for (char second = first + 1; second < SENSORS_COUNT; ++second) {
compareDistance(sensors[first], sensors[second]);
}
}
float sum = 0;
char validMeasurements = 0;
for (int i = 0; i < SENSORS_COUNT; ++i) {
if (sensors[i].differenes != SENSORS_COUNT - 1) {
sum += sensors[i].distance;
++validMeasurements;
}
}
if (validMeasurements > 0) {
float distance = sum / validMeasurements;
String data = String(distance);
Serial.print(data.c_str());
// Отправляем POST-запрос
byte postRes;
for (int i = 0; i < SENDING_ATTEMPTS; ++i) {
postRes = sendPostRequest(ethClient, SERVER, PORT, ENDPOINT_DATA, data.c_str());
if (postRes == POST_OK)
break;
}
switch (postRes) {
case POST_OK:
break;
case POST_TIMEOUT:
showError(errorPostTimeout);
break;
case POST_CONNECTION_FAILED:
showError(errorPostConnectionFailed);
break;
}
lastResultTime = millis();
lastResultStr = String(round((ZERO_RIVER_LEVEL - distance) / 10));
showLastMeasurement();
printMeteo(temperature, distance); //Пишем инфу в COM-порт
}
if (validMeasurements != SENSORS_COUNT) {
//Значит были ошибки. Пошлём на сервер номера сбоивших сенсоров.
for (int i = 0; i < SENSORS_COUNT; ++i) {
if (sensors[i].differenes == SENSORS_COUNT - 1) {
String json = "{\"sensorId\":" + String(DISTANCE_TYPE) + ", \"sonarNumber\": " + String(i) + "}";
for (int i = 0; i < SENDING_ATTEMPTS; ++i) {
if (sendPostRequest(ethClient, SERVER, PORT, ENDPOINT_ERROR, json.c_str()))
break;
}
}
}
Serial.println(F("Были ошибки эхолотов"));
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void doMeasurement(Sensor &sensor, float temperature, char measurementNo) {
//led_light(100);
// импульс 10 мкс
digitalWrite(sensor.trigPin, HIGH);
delayMicroseconds(10);
digitalWrite(sensor.trigPin, LOW);
// измеряем время ответного импульса в микросекундах
//PulseIn возвращает unsigned long, но мы приводим к double,
//чтобы дальнейшие вычисления отработали, с повышенной точностью
double echoTime = pulseIn(sensor.echoPin, HIGH);
//считаем расстояние и возвращаем
//Скорость звука в воздухе при 0℃ равняется 331 м/с. С повышением температуры на 1° увеличивается скорость на 0,6 м/с.
sensor.measurements[measurementNo] = echoTime * (331. + temperature * .6) / 2000.;
Serial.println(sensor.measurements[measurementNo]);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Считаем distance, как среднее усечённое (отбрасываем минимальные и максимальные значения, а средние усредняем).
void calcAvgDistance(Sensor &sensor) {
ace_sorting::shellSortKnuth(sensor.measurements, MEASUREMENTS_COUNT);
float sum = 0;
for (int i = MEASUREMENTS_COUNT / 3; i < MEASUREMENTS_COUNT - MEASUREMENTS_COUNT / 3; ++i) {
sum += sensor.measurements[i];
}
sensor.distance = sum / (MEASUREMENTS_COUNT / 3);
// Serial.print(F("dist - "));
// Serial.print(sensor.distance);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Входит ли разница между значениями в заданный диапазон?
void compareDistance(Sensor &first, Sensor &second) {
float minDistance = min(first.distance, second.distance);
float difference = fabs(first.distance - second.distance) / (minDistance / 100.);
if (difference > CRITICAL_DIFFERENCE) {
++first.differenes;
++second.differenes;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void showError(const String &str) {
showOnDisplay(str);
delay(ERROR_VISUALISATION_DURATION);
showLastMeasurement();
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void showLastMeasurement() {
if (millis() - lastResultTime < MEASUREMET_VALID_WHILE)
showOnDisplay(lastResultStr);
else {
// String dbg = "delta " + String (millis() - lastResultTime) + " lstResTm " + String(lastResultTime);
// Serial.println(dbg);
String str = “”;
for (int i = 0; i < DISP_DIGITS_COUNT; i++)
str += ‘-’;
showOnDisplay(str);
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void showOnDisplay(const String &str) {
Serial.println(F(“draw on display”));
disp.clear();
disp.setCursor(DISP_DIGITS_COUNT - str.length());
disp.print(str);
disp.update();
Serial.println(F(“end draw”));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Вывод данных в COM-порт
void printMeteo(float temperature, float distance) {
Serial.println("Расстояние до воды: " + String(distance));
Serial.println("Температура: " + String(temperature));
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Отправка запроса POST
byte sendPostRequest(EthernetClient &client, const char* server, uint16_t port, const char* endpoint, const char* data) {
Serial.println(F(“Connecting to server”));
//Serial.println(server);
// Подключаемся к серверу
if (client.connect(server, port)) {
Serial.println(F(“Connected to server”));
// Формируем HTTP-заголовки
client.print("POST ");
client.print(endpoint);
client.println(" HTTP/1.1");
client.print("Host: ");
client.println(server);
client.println("Content-Type: application/json");
client.print("Content-Length: ");
client.println(strlen(data));
client.println("Connection: close");
client.println();
client.println(data);
// Serial.println(F(“Request sent”));
// Ждём ответа (таймаут 5 секунд)
unsigned long timeout = millis();
while (client.available() == 0) {
if (millis() - timeout > 5000) {
Serial.println(F("Timeout, no response!"));
client.stop();
return POST_TIMEOUT;
}
}
// Читаем ответ сервера (если нужно)
while (client.available()) {
char c = client.read();
Serial.write(c);
}
client.stop();
return POST_OK;
} else {
Serial.println(F(“Connection failed”));
return POST_CONNECTION_FAILED;
}
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//Вывод нашего айпишника, маски подсети, шлюза и DNS-сервера
void printNetworkInfo() {
Serial.print(F("IP-адрес: "));
Serial.println(Ethernet.localIP());
Serial.print(F("Маска подсети: "));
Serial.println(Ethernet.subnetMask());
Serial.print(F("Шлюз: "));
Serial.println(Ethernet.gatewayIP());
Serial.print(F(“DNS-сервер: “));
Serial.println(Ethernet.dnsServerIP());
Serial.println(””);
}
#endif