/* ====== Lack Fuellstandsmesser =================================================================
* Arduino Nano Software zur Messung eines Lack-Fuellstandes.
* Verwendet wird ein UART Laser-Distanzmesser, ein Bewegungsmelder zur Aktivierung,
* ein LC-Display, eine Neopixel LED-Leiste, ein Bluetooth-Modul zum Senden an eine App.
*
* Author: vtx.engineering
* Date: 21.01.2023
* ==============================================================================================
*/
// === LIBRARIES ===
#include "LiquidCrystal_I2C.h" // LC-Display
#include "SoftwareSerial.h" // Software Serial für Bluetooth
#include "Adafruit_NeoPixel.h" // Neopixel LEDs als Füllstandsanzeige
#include "string.h"
// === DEFINITIONEN ===
// Fass-Parameter (bei Bedarf für genauere Werte auf float ändern)
constexpr uint16_t FULL_TO_SENSOR = 61; // Abstand vom Sensor zum Lack bei einem vollen Fass in Millimeter
constexpr uint16_t BARREL_FILL_HEIGHT = 764; // Füllhöhe für 100
constexpr uint16_t BARREL_FULL_WEIGHT = 125; // Gewicht vom vollen Fass
// LCD
constexpr uint8_t LCD_ADDRESS = 0x27; // I2C-Adresse des LC-Displays
// NeoPixel
constexpr uint8_t LED_PIN = 7; // Pin des NeoPixel LED-Streifens
constexpr uint8_t LED_COUNT = 8; // Anzahl der LEDs auf dem Streifen
constexpr uint8_t LED_FULL_BRIGHTNESS = 255; // LED Helligkeit im ungedimmten Zustand (0 - 255)
constexpr uint8_t LED_DIMMED_BRIGHTNESS = 50; // LED Helligkeit im gedimmten Zustand (0 - 255)
// Abstandssensor
constexpr uint8_t DISTANCE_MESSAGE_LENGTH = 34; // Länge einer Sensornachricht || Format: State:xxxxxxxxRange Valid: 1878 mm
constexpr uint32_t DISTANCE_BAUD = 115200; // Abstandssensor Baud-Rate
constexpr uint8_t DISTANCE_TIMEOUT = 2; // 2ms Timeout beim Lesen
constexpr uint8_t DISTANCE_TX = 10; // Abstandssensor TX Pin
constexpr uint8_t DISTANCE_RX = 11; // Abstandssensor RX Pin
// Bluetooth
constexpr uint32_t BLT_BAUD = 9600; // Bluetooth Baud-Rate (muss mit der des Moduls übereinstimmen!)
constexpr uint8_t BLT_RX_PIN = 12; // Bluetooth RX Pin
constexpr uint8_t BLT_TX_PIN = 13; // Bluetooth TX Pin
// Bewegungsmelder
constexpr uint8_t MOTION_PIN = 2; // Bewegungsmelder Pin !NUR INTERRUPT PINS VERWENDEN! https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/
// Piezo Buzzer
constexpr uint8_t PIEZO_PIN = 8; // Piezo Buzzer Pin
// TIMER
constexpr uint16_t DIS_MEASURE_CYCLE = 2000; // Zykluszeit der Abstandsmessungen in ms
constexpr uint16_t LED_REFRESH_CYCLE = 500; // Zykluszeit der LED Aktualisierung
// === GLOBALE VARIABLEN ===
uint8_t sensorBuf[DISTANCE_MESSAGE_LENGTH]; // Buffer, in den eine Sensornachricht geschrieben wird
uint16_t currentDistance = 0; // Aktuell gemessener Abstandswert in mm
float currentPercentage = 0; // Aktueller Füllstand (0.0 = leer, 100.0 = voll)
bool ledFullBrightness = false; // LED Helligkeit, bei false wird die Helligkeit von 100% auf LED_DIMMED_BRIGHTNESS reduziert
bool motionDetectedFlag = true; // Wird gesetzt wenn der Bewegungsmelder seinen PinState ändert
bool errorMeasurement = false; // Wird bei Fehlmessung (d = 0mm) gesetzt
uint32_t piezoInterval = UINT32_MAX; // Intervall, in dem der Piezo piept. Da er zu Beginn nicht piepen soll wird er maximal initialisiert
// === GLOBALE TIMER VARIABLEN ===
uint32_t lastDistanceMeasurement = 0; // Speichert den Zeitpunkt der letzten Abstandssensorauswertung
uint32_t lastLEDRefresh = 0; // Speichert den Zeitpunkt der letzten LED Aktualisierung
uint32_t lastPiezoBeep = 0; // Speichert den Zeitpunkt des letzten Piezo Beeps
// === OBJEKTE ===
Adafruit_NeoPixel Pixels(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);
LiquidCrystal_I2C LCD(LCD_ADDRESS, 16, 2);
SoftwareSerial Bluetooth(BLT_RX_PIN, BLT_TX_PIN);
void setup()
{
// Setzen der PinModes INPUT/OUTPUT
pinMode(PIEZO_PIN, OUTPUT);
// Der Bewegungsmelder Pin wird über einen Interrupt ausgewertet, mehr dazu in der Doku
pinMode(MOTION_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(MOTION_PIN), movementStateChanged, CHANGE);
// Distanzsensor Serial Object
Serial.begin(DISTANCE_BAUD);
Serial.setTimeout(DISTANCE_TIMEOUT);
// Bluetooth Serial Object
Bluetooth.begin(BLT_BAUD);
LCD.init(); // LCD Initialisierung
Pixels.begin(); // LED-Streifen Initialisierung
startupAnimation(); // Startsequenz mit LEDs, LCD, Buzzer
}
void loop()
{
// Neuen Distanzmesswert abfragen wenn Zyklus abgelaufen
if(millis() - lastDistanceMeasurement > DIS_MEASURE_CYCLE)
{
// Auswerten der nächsten Sensornachricht
//if(readSensorData(¤tDistance))
{
currentDistance = floatMap(analogRead(A7), 0, 1023, 0, 825);
// Setzen der Error Flag wenn der gemessene Abstand 0mm ist
errorMeasurement = currentDistance == 0;
// Nur Zeit aktualisieren, wenn Auswertung erfolgreich war. Wenn nicht, wird es im nächsten Durchlauf direkt erneut versucht
lastDistanceMeasurement = millis();
// Wertebereich der Distanz d einschränken sodass d nur zwischen FULL_TO_SENSOR und FULL_TO_SENSOR + BARREL_FILL_HEIGHT liegen kann. Größe und kleinere Werte werden abgeschnitten
currentDistance = constrain(currentDistance, FULL_TO_SENSOR, FULL_TO_SENSOR + BARREL_FILL_HEIGHT);
// Berechnung der prozentualen Füllhöhe zwischen 0.0 (leer) und 100.0 (voll)
currentPercentage = floatMap(currentDistance, FULL_TO_SENSOR, FULL_TO_SENSOR + BARREL_FILL_HEIGHT, 100, 0);
// Piezo Intervall je nach Füllstand setzen
setPiezoInterval(currentPercentage, errorMeasurement);
// Anzeigen des neuen Messwerts auf dem LCD
setLCDLevel(currentPercentage, errorMeasurement);
// Senden des Messwerts über Bluetooth
Bluetooth.print(currentDistance);
}
}
// Aktualisierung der LEDs
if(millis() - lastLEDRefresh > LED_REFRESH_CYCLE)
{
setLEDLevel(currentPercentage, errorMeasurement);
lastLEDRefresh = millis();
}
if(millis() - lastPiezoBeep > piezoInterval)
{
piezoBeep(10);
lastPiezoBeep = millis();
}
// Reaktion wenn der PinState des Bewegungsmelders wechselt
if(motionDetectedFlag)
{
// Kurze Verzögerung zum Entprellen
delay(1);
motionDetectedFlag = false;
// Pin auswerten, wenn HIGH (Bewegung) LCD an und LEDs dunkler
if(digitalRead(MOTION_PIN))
{
LCD.backlight();
ledFullBrightness = false;
return;
}
LCD.noBacklight();
ledFullBrightness = true;
}
}
void setPiezoInterval(float percentage, bool error)
{
if(error)
{
piezoInterval = 2000;
return;
}
// Wenn über 8% kein Piepen
if(percentage > 8)
{
piezoInterval = UINT32_MAX;
return;
}
if(percentage > 5)
{
piezoInterval = 4000;
return;
}
if(percentage > 3)
{
piezoInterval = 2000;
return;
}
// Ansonsten ( < 3 )
piezoInterval = 600;
return;
}
void movementStateChanged()
{
motionDetectedFlag = true;
}
bool readSensorData(uint16_t* distance)
{
bool readSuccess = false;
// Schauen, ob mindestens eine Nachricht im Buffer ist
if(Serial.available() >= DISTANCE_MESSAGE_LENGTH)
{
// Einlesen der ersten 34 Byte (eine volle Nachrichten Länge)
Serial.readBytes(sensorBuf, DISTANCE_MESSAGE_LENGTH);
// Checken, ob das Format der Nachricht stimmt. Dazu muss die Nachricht am Anfang des Buffers stehen. Sollte sie das nicht, wird sie verworfen und der Buffer wird geleert
if(sensorBuf[0] == 'S' && sensorBuf[1] == 't' && sensorBuf[2] == 'a' && sensorBuf[DISTANCE_MESSAGE_LENGTH - 2] == 'm' && sensorBuf[DISTANCE_MESSAGE_LENGTH - 1] == 'm')
{
// Distanzmesswert aktualisieren (nur wenn der Check in der Funktion stimmt, ansonsten bleibt der alte Wert)
// Außerdem wird eine Flag gesetzt, wenn der Wert erfolgreich gesetzt wurde
readSuccess = getDistance(sensorBuf, distance);
}
// Leeren des restlichen Buffers (falls noch etwas drin ist), damit die neue Nachricht wieder am Anfang steht
clearSerialBuffer();
}
return readSuccess;
}
void clearSerialBuffer()
{
// So lange Bytes im Buffer sind...
while(Serial.available() > 0)
{
//... wird ein Byte gelesen (und damit aus dem Buffer gelöscht)
Serial.read();
}
}
bool getDistance(uint8_t *buf, uint16_t* distance)
{
// Keyword, nach dem gefiltert wird und das vor der Zahl steht
const char* keyword = "Range Valid: ";
// strstr liefert Pointer auf erste Stelle wo "Range Valid: " im Buffer vorkommt, ansonsten NULL
char *p = strstr(buf, keyword);
// Wenn "Range Valid: " im Buffer vorkam, ist der Wert gültig
if (p != NULL)
{
// Konvertieren der Zahlen hinter "Range Valid" zu integer
// p zeigt auf das 'R' von Range Valid im Buffer
// Durch Addition von der Länge von "Range Valid: " landet man bei der ersten Ziffer
// ! Wichtig: Es muss sichergestellt sein, dass buf lang genug ist. Dies geschieht beim Einlesen der Nachricht !
*distance = (uint16_t)atoi(p + strlen(keyword));
return true;
}
return false;
}
void piezoBeep(uint16_t duration)
{
digitalWrite(PIEZO_PIN, HIGH);
delay(duration);
digitalWrite(PIEZO_PIN, LOW);
}
void startupAnimation()
{
// LCD Hintergrundbeleuchtung aktivieren
LCD.backlight();
Pixels.fill(Pixels.Color(0,0,100));
Pixels.show();
// Piezo Beep
piezoBeep(10);
// Firmentext anzeigen
LCD.setCursor(0, 0);
LCD.print("PRINTEX AG");
LCD.setCursor(0, 1);
LCD.print("printex.ch");
delay(2500);
LCD.clear();
// Namen und Version anzeigen
LCD.setCursor(0, 0);
LCD.print("NEW Version 2.0");
LCD.setCursor(0, 1);
LCD.print("Samuel Baer 2023");
delay(1000);
LCD.clear();
// Messungstext anzeigen
LCD.setCursor(0, 0);
LCD.print("Lackfuellstand");
LCD.setCursor(0, 1);
LCD.print("messen");
// Drei Punkte zeitlich versetzt einblenden
for(uint8_t i = 0; i < 3; i++)
{
delay(300);
LCD.print(".");
}
delay(300);
}
float floatMap(float x, float in_min, float in_max, float out_min, float out_max)
{
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
}
void setLCDLevel(float percentage, bool error)
{
// Berechnung des Restgewichts über bekanntes Gewicht bei 100% Füllung
float remainingWeight = BARREL_FULL_WEIGHT * percentage / 100.0;
LCD.clear();
LCD.setCursor(0, 0);
// Fehlmessung
if(error)
{
LCD.print("Fehlmessung!");
return;
}
// Niederstand
if(percentage <= 10)
{
LCD.print("Niedriger Stand!");
LCD.setCursor(0, 1);
LCD.print("noch ");
LCD.print(round(percentage), 0);
LCD.print("%");
return;
}
// Normale Füllstandsanzeige
LCD.print("Fuellstand: ");
LCD.print(percentage, 0);
LCD.print("%");
LCD.setCursor(0, 1);
LCD.print("noch ");
LCD.print(remainingWeight, 2); // 2 bedeutet hier zwei Nachkommastellen anzeigen
LCD.print(" kg");
}
void setLEDLevel(float percentage, bool errorMeasurement)
{
/*
// Farbwerte werden nur hier benötigt, können also lokal angelegt werden
uint8_t r = 0, g = 0, b = 0;
// Die Anzahl an angeschalteten LEDs wird berechnet anstatt explizit gesetzt
// Wenn Ihnen das nicht gefällt, können Sie natürlich auch direkt in jedem Case die Anzahl manuell angeben
uint8_t ledsToTurnOn = LED_COUNT;//(uint8_t)round(LED_COUNT * percentage / 100.0);
// Ein Switch-Case Statement ist übersichtlicher und bei vielen Bedingungen auch schneller
// Die Nuller können weggelassen werden da wir in der Zeile drüber initial auf 0 setzen
switch(currentPercentageInt)
{
case 89 ... 100:
g = 115;
b = 20;
break;
case 76 ... 88:
r = 42;
g = 112;
b = 40;
break;
case 64 ... 75:
r = 62;
g = 112;
b = 36;
break;
case 50 ... 63:
r = 78;
g = 114;
b = 36;
break;
case 39 ... 49:
r = 104;
g = 118;
b = 36;
break;
case 26 ... 36:
r = 114;
g = 119;
b = 36;
break;
case 8 ... 25:
r = 125;
g = 104;
b = 28;
break;
case 0 ... 7:
r = 255;
ledsToTurnOn = LED_COUNT;
break;
}
*/
// Die Anzahl der anzuschaltenden LEDs entspricht dem Füllstand
uint8_t ledsToTurnOn = round(LED_COUNT * percentage / 100.0);
// Bei Fehlmessung oder geringem Füllstand wird der Wert überschrieben, alle LEDs an
if(errorMeasurement || percentage < 8)
{
ledsToTurnOn = LED_COUNT;
}
// Berechnung des Farbwerts im HSV-Farbraum basierend auf dem Füllstand
// Fall 1: Fehlmessung: Wert wird fest auf Blau gesetzt (H = 65536 * 2/3)
// Fall 2: Keine Fehlmessung: Wert wird kontinuierlich zwischen Rot (H = 0) und Grün (H = 65536/3) gesetzt
uint16_t hue = round(errorMeasurement ? 65536 * 2 / 3.0 : 65536 / 3.0 * percentage / 100.0);
// Wahl der Helligkeit, entweder die eingestellte maximale oder die gedimmte Helligkeit
uint8_t brightness = ledFullBrightness ? LED_FULL_BRIGHTNESS : LED_DIMMED_BRIGHTNESS;
// Setzen der Farbe im HSV-Farbraum
uint32_t color = Pixels.ColorHSV(hue, 255, brightness);
// Update der LEDs
// Zuerst alle LEDs aus, weil wir nur die angeschalteten LEDs setzen
Pixels.clear();
// Schleife über alle LEDs die angeschaltet werden sollen
for(uint8_t i = 0; i < ledsToTurnOn; i++)
{
Pixels.setPixelColor(i, color);
}
Pixels.show();
}