In diesem Teil geht es um den Aufbau der Schaltung auf einem Breadboard und Strommessungen der Ventile. Um mal zu schauen, wie denn der Strombedarf der Ventile ist, habe ich kurzerhand noch einen INA219 Spannungs- und Strommesser reinkonfiguriert. Der kann über I2C die aktuelle Spannung und den Strom darstellen.
Für die im ersten Teil genannten Komponenten habe ich den folgenden Aufbau mit Kicad geplant.
Nach der Planung erfolgt dann auch gleich der Aufbau. Ich habe mir vor einiger Zeit mal ein Raspberry Testbrett aus Holz gebaut, welches ich jetzt mal hier einsetzten werden.
Auf dem Bild oben ist erstmal nur das Magnetventil angeschlossen (das Ventil auf der rechten Seite), weil ich mit diesem zuerst gearbeitet habe.
Stromaufnahme: Im Datenblatt des Magnetventils ist von 1,6A Stromaufnahme die Rede und davon, dass das Ventil nicht dauergeschaltet, also offen, sein sollte. Hintergrund ist die starke Wärmeentwicklung. Um beides mal zu prüfen, habe ich noch einen INA219 eingebaut und werde mal den Strom messen.
Freilaufdiode: Nach den ersten Tests habe ich festgestellt, dass nach einige Schaltvorgängen der INA219 Sensor keine Ampere Daten mehr liefert. Ich hatte den Hinweis bzgl. der Spannungsspitzen bei induktiven Lasten auf der Adafruit Seite wohl gesehen, doch erstmal ignoriert. Lief ja alles. Da ich mir aber gut vorstellen kann, dass die Spule nach dem Abschalten eine schöne Spannungsspitze liefert habe ich sicherheitshalber eine Freilaufdiode (1N4007) parallel zum Ventil eingebaut. Und siehe da, keine Störungen mehr. Also die Hinweise in Zukunft lesen und umsetzen. Spart Zeit.
Software
Die beiden ESP MCU können mit verschiedenen Frameworks programmiert werden. Das native Framework ist das von Espressif erstellte ESP-IDF, welches auf FreeRTOS basiert. Die Programme werden in C erstellt. Eine gute Alternative ist das ESP-Arduino Framework welches die Programmierung des MCU mit Arduino Code ermöglicht. So können dann auch Sensor Bibliotheken von Herstellern genutzt werden. Es gibt noch mindestens ein weiteres Framework wie z.B. MicroPython, doch dies ist für mich nicht relevant.
Ich habe mich erstmal für ESP-IDF entschieden, obwohl ich glaube dass der Entwicklungsaufwand größter als beim Arduino Code ist. Arduino ist dann doch noch etwas einfacher und die Nutzung der Bibliotheken ein wirklicher Vorteil.
Ich habe mich weitestgehend an den Code Beispielen des esp-idf orientiert und die mal ein bisschen was zusammenkopiert.
Die Struktur des Code ist grob wie folgt.
- In app_main
- Init GPIO: GPIO Parameter setzen und den GPIO auf aus setzen
- Init NVS:
- Init WLAN: WLAN Verbindung herstellen -> wifi_init_sta();
- Init I2C: I2C initialisieren
- Init MQTT: Setup MQTT
- Start Measuring task:
- Erstellen einer Tasks die welche den INA219 abfragt und das Ergebnis via MQTT sendet. -> task();
- tasks()
- Messung der INA219 Daten und versandt über MQTT (Topic: garden/mainvalve/current, bzw. voltage)
- Ausgabe der Werte über printf für serial monitor
Um zu prüfen, ob das INA219 Sensor am I2C Bus korrekt erkannt wird, habe ich einen I2C Scanner eingebaut.
/* ESP32 WaterValve Control program Author: Karl@skat-foundation.de Purpose: - Toggle an external relay connected via GPIO by MQTT telegrams. - Measure voltage of wire for valve control and measure current consumption. Version: 0.1 Version Control: - 0.1 Initial Version */ // setup logging static const char *TAG = "ValveControl"; #define LOG_LOCAL_LEVEL ESP_LOG_DEBUG // ESP_LOG_ERROR #include "esp_log.h" // important to place the include after the logging definitions #include <string.h> #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "freertos/event_groups.h" #include "esp_system.h" #include "esp_wifi.h" #include "esp_event.h" #include "nvs_flash.h" #include "driver/gpio.h" #include <stdio.h> #include "lwip/err.h" #include "lwip/sys.h" #include "mqtt_client.h" #include "sdkconfig.h" #include <ina219.h> // INA219 Header taken from https://github.com/UncleRus/esp-idf-lib #include "driver/i2c.h" // Parameter for I2C #define I2C_MASTER_SCL_IO 22 /*!< gpio number for I2C master clock */ #define I2C_MASTER_SDA_IO 21 /*!< gpio number for I2C master data */ #define I2C_MASTER_NUM I2C_NUM_0 /*!< I2C port number for master dev */ #define I2C_MASTER_FREQ_HZ 100000 /*!< I2C master clock frequency */ #define I2C_MASTER_TX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */ #define I2C_MASTER_RX_BUF_DISABLE 0 /*!< I2C master doesn't need buffer */ // Parameter for WiFi access #define ACCESSPOINT_SSID "AP" // WiFi Access Point #define ACCESSPOINT_PASS "password" // WiFi Access Point password #define EXAMPLE_ESP_MAXIMUM_RETRY 5 static EventGroupHandle_t s_wifi_event_group; // FreeRTOS event group to signal when we are connected /* The event group allows multiple bits for each event, but we only care about two events: * - we are connected to the AP with an IP * - we failed to connect after the maximum amount of retries */ #define WIFI_CONNECTED_BIT BIT0 #define WIFI_FAIL_BIT BIT1 static int s_retry_num = 0; // Parameter for MQTT #define MQTT_BROKER "mqtt://192.168.0.2" #define MQTT_TOPIC "garden/mainvalve" #define MQTT_TOPIC_SET MQTT_TOPIC "/set" #define MQTT_TOPIC_CONTROLLER MQTT_TOPIC "/controller" #define MQTT_TOPIC_CURRENT MQTT_TOPIC "/current" #define MQTT_TOPIC_VOLTAGE MQTT_TOPIC "/voltage" #define Valve0_GPIO 16 // GPIO Pin connected to external board for Relay 1 static void wlan_event_handler(void* arg, esp_event_base_t event_base, int32_t event_id, void* event_data) { if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) { if (s_retry_num < EXAMPLE_ESP_MAXIMUM_RETRY) { esp_wifi_connect(); s_retry_num++; ESP_LOGI(TAG, "retry to connect to the AP"); } else { xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); } ESP_LOGI(TAG,"connect to the AP fail"); } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t* event = (ip_event_got_ip_t*) event_data; ESP_LOGI(TAG, "got ip:%s", ip4addr_ntoa(&event->ip_info.ip)); s_retry_num = 0; xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); } } void wifi_init_sta() { s_wifi_event_group = xEventGroupCreate(); tcpip_adapter_init(); ESP_ERROR_CHECK(esp_event_loop_create_default()); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wlan_event_handler, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wlan_event_handler, NULL)); wifi_config_t wifi_config = { .sta = { .ssid = ACCESSPOINT_SSID, .password = ACCESSPOINT_PASS, .pmf_cfg = { .capable = true, .required = false }, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) ); ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &wifi_config) ); ESP_ERROR_CHECK(esp_wifi_start() ); ESP_LOGI(TAG, "wifi_init_sta finished."); /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */ EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, pdFALSE, pdFALSE, portMAX_DELAY); /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually * happened. */ if (bits & WIFI_CONNECTED_BIT) { ESP_LOGI(TAG, "connected to ap SSID:%s password:%s", ACCESSPOINT_SSID, ACCESSPOINT_PASS); } else if (bits & WIFI_FAIL_BIT) { ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s", ACCESSPOINT_SSID, ACCESSPOINT_PASS); } else { ESP_LOGE(TAG, "UNEXPECTED EVENT"); } ESP_ERROR_CHECK(esp_event_handler_unregister(IP_EVENT, IP_EVENT_STA_GOT_IP, &wlan_event_handler)); ESP_ERROR_CHECK(esp_event_handler_unregister(WIFI_EVENT, ESP_EVENT_ANY_ID, &wlan_event_handler)); vEventGroupDelete(s_wifi_event_group); } static esp_err_t mqtt_event_handler_cb(esp_mqtt_event_handle_t event) { esp_mqtt_client_handle_t client = event->client; char datatopic[20] = ""; int msg_id; switch (event->event_id) { case MQTT_EVENT_CONNECTED: ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); msg_id = esp_mqtt_client_publish(client, MQTT_TOPIC_CONTROLLER, "MQTT-connected", 0, 1, 0); ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); msg_id = esp_mqtt_client_subscribe(client, MQTT_TOPIC_SET, 0); ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); break; case MQTT_EVENT_DISCONNECTED: ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); break; case MQTT_EVENT_SUBSCRIBED: ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED"); // ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); // msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0); // ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); break; case MQTT_EVENT_UNSUBSCRIBED: // ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); break; case MQTT_EVENT_PUBLISHED: // ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); break; case MQTT_EVENT_DATA: ESP_LOGI(TAG, "MQTT_EVENT_DATA"); ESP_LOGI(TAG, "TOPIC :%.*s", event->topic_len, event->topic); ESP_LOGI(TAG, "Incoming Data :%.*s", event->data_len, event->data); // printf(" TOPIC=%.*s\r\n", event->topic_len, event->topic); // printf(" Incoming Date=%.*s\r\n", event->data_len, event->data); strncpy (datatopic, event->data, event->data_len); if (strcmp (datatopic, "on") == 0) { gpio_set_level (Valve0_GPIO,1); ESP_LOGI(TAG, "Valve open"); ESP_LOGI(TAG, "MQTT-DATA: %s", datatopic); } else { gpio_set_level (Valve0_GPIO,0); ESP_LOGI(TAG, "Valve closed"); ESP_LOGI(TAG, "MQTT-DATA: %s", datatopic); } break; case MQTT_EVENT_ERROR: ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); break; default: ESP_LOGI(TAG, "Other event id:%d", event->event_id); break; } return ESP_OK; } static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%d", base, event_id); mqtt_event_handler_cb(event_data); } static esp_err_t i2c_master_init(void) { int i2c_master_port = I2C_MASTER_NUM; i2c_config_t conf; conf.mode = I2C_MODE_MASTER; conf.sda_io_num = I2C_MASTER_SDA_IO; conf.sda_pullup_en = GPIO_PULLUP_DISABLE; // Disable Pullup-Resistor because INA219 Board already has one conf.scl_io_num = I2C_MASTER_SCL_IO; conf.scl_pullup_en = GPIO_PULLUP_DISABLE; // Disable Pullup-Resistor because INA219 Board already has one conf.master.clk_speed = I2C_MASTER_FREQ_HZ; i2c_param_config(i2c_master_port, &conf); return i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0); } void task(void *pvParameters) { int msg_id; char current_buf[318]; // buffer for conversion of float to char - current char voltage_buf[318]; // buffer for conversion of float to char - bus_voltage ina219_t dev; memset(&dev, 0, sizeof(ina219_t)); ESP_ERROR_CHECK(ina219_init_desc(&dev, 64, 0 ,I2C_MASTER_SDA_IO, I2C_MASTER_SCL_IO)); ESP_LOGI(TAG, "Initializing INA219"); ESP_ERROR_CHECK(ina219_init(&dev)); ESP_LOGI(TAG, "Configuring INA219"); ESP_ERROR_CHECK(ina219_configure(&dev, INA219_BUS_RANGE_16V, INA219_GAIN_0_125, INA219_RES_12BIT_1S, INA219_RES_12BIT_1S, INA219_MODE_CONT_SHUNT_BUS)); ESP_LOGI(TAG, "Calibrating INA219"); ESP_ERROR_CHECK(ina219_calibrate(&dev, 2.0, 0.1)); // Set max current 2.0 and 0.1 Ohm shunt resistance float bus_voltage, shunt_voltage, current, power; ESP_LOGI(TAG, "Starting the loop"); while (1) { ESP_ERROR_CHECK(ina219_get_bus_voltage(&dev, &bus_voltage)); ESP_ERROR_CHECK(ina219_get_shunt_voltage(&dev, &shunt_voltage)); ESP_ERROR_CHECK(ina219_get_current(&dev, ¤t)); ESP_ERROR_CHECK(ina219_get_power(&dev, &power)); // printf("VBUS: %.04f V | VSHUNT: %.04f mV | IBUS: %.04f mA | PBUS: %.04f mW\n", bus_voltage, shunt_voltage * 1000, current * 1000, power * 1000); printf("DATA: %04f, %.04f\n", bus_voltage, current * 1000); sprintf(current_buf,"%.1f", current * 1000); // Convert value to string msg_id = esp_mqtt_client_publish(pvParameters, MQTT_TOPIC_CURRENT, current_buf, 0, 1, 0); sprintf(voltage_buf,"%.2f", bus_voltage); // Convert value to string msg_id = esp_mqtt_client_publish(pvParameters, MQTT_TOPIC_VOLTAGE, voltage_buf, 0, 1, 0); vTaskDelay(500 / portTICK_PERIOD_MS); } } void i2c_scanner (void) { // Scanner from nkolban https://github.com/nkolban/esp32-snippets printf("Starting main program...\n"); printf(" Initialising i2c master controller and driver\n"); ESP_ERROR_CHECK (i2c_master_init()); printf(" Scanning for i2c devices on the bus\n"); int i; esp_err_t espRc; printf(" 0 1 2 3 4 5 6 7 8 9 a b c d e f\n"); printf("00: "); for (i=3; i< 0x78; i++) { i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_master_start(cmd); i2c_master_write_byte(cmd, (i << 1) | I2C_MASTER_WRITE, 1 /* expect ack */); i2c_master_stop(cmd); espRc = i2c_master_cmd_begin(I2C_NUM_0, cmd, 10/portTICK_PERIOD_MS); if (i%16 == 0) { printf("\n%.2x:", i); } if (espRc == 0) { printf(" %.2x", i); } else { printf(" --"); } //ESP_LOGD(tag, "i=%d, rc=%d (0x%x)", i, espRc, espRc); i2c_cmd_link_delete(cmd); } printf("\n"); } void app_main() { // Set component loglevel during runtime. esp_log_level_set ("*", ESP_LOG_ERROR); // esp_log_level_set ("MQTT", ESP_LOG_VERBOSE); ESP_LOGI(TAG, "Starting main ..."); // i2c_scanner(); // Init GPIO gpio_config_t io_conf; io_conf.intr_type = GPIO_PIN_INTR_DISABLE; //disable interrupt io_conf.mode = GPIO_MODE_OUTPUT; //set as output mode io_conf.pin_bit_mask = (1ULL<<Valve0_GPIO); //bit mask of the pins that you want to set,e.g.GPIO18 io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE; //disable pull-down mode io_conf.pull_up_en = GPIO_PULLUP_DISABLE; //disable pull-up mode esp_err_t error=gpio_config(&io_conf); //configure GPIO with the given settings if(error!=ESP_OK){ printf("error configuring outputs \n"); } gpio_set_level (Valve0_GPIO,0); // Init NVS ESP_LOGI(TAG, "Init NVS ..."); esp_err_t ret = nvs_flash_init(); if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { ESP_ERROR_CHECK(nvs_flash_erase()); ret = nvs_flash_init(); } ESP_ERROR_CHECK(ret); // Init WLAN ESP_LOGI(TAG, "Init WLAN ..."); wifi_init_sta(); // Init I2C ESP_ERROR_CHECK(i2cdev_init()); // Init MQTT ESP_LOGI(TAG, "Init MQTT ..."); const esp_mqtt_client_config_t mqtt_cfg = { .uri = MQTT_BROKER, // .user_context = (void *)your_context }; esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, client); esp_mqtt_client_start(client); // Start measuring tasks ESP_LOGI(TAG, "Start measuring task ..."); xTaskCreate(task, "test", configMINIMAL_STACK_SIZE * 8, (void *) client, 5, NULL); ESP_LOGI(TAG, "Ending main ..."); }
Testlauf mit Magnetventil
Der erste Testlauf mit dem Magnetventil verlief erfolgreich. Das Relais hat nach dem entsprechende MQTT Telegram das Relais geschaltet und die Werte für Stromspannung und -stärke wurden über MQTT übertragen.
Hier ein Screenshot vom MQTT Explorer für MacOS.
Messung und Übermittlung der INA219 Sensordaten
Innerhalb der erstellten Tasks werden die Sensordaten über MQTT verschickt und auf der Console über Printf ausgegeben. Wenn ich also mit idf.py monitor über die UART Schnittstelle auf das Board gehe, werden die Daten auch dort angezeigt. Um aber auch eine schöne Grafik mit den zeitlichen Verlauf der Stromstärke darzustellen, müssen diese Daten irgendwie in eine Form zur weiteren Verarbeitung gebracht werden.
In einem anderen Projekt hatte ich mir mal ein kleines Python Script geschrieben, welches über die UART Schnittstelle auf das Kommando „Data“ horcht und die nachfolgenden Werte abgreift und in eine CSV Datei schreibt.
DATA: 12.036000, 0.7000 DATA: 12.032000, 0.5000 DATA: 12.040000, 0.7000 DATA: 12.032000, 0.5000 DATA: 12.032000, 0.7000 DATA: 12.036000, 0.7000 DATA: 12.032000, 0.4000 DATA: 12.036000, 0.5000 DATA: 12.028000, 0.7000 DATA: 12.024000, 0.7000 DATA: 12.036000, 0.7000 DATA: 12.028000, 0.5000 DATA: 12.028000, 0.7000 DATA: 12.036000, 0.4000 DATA: 12.036000, 0.5000 DATA: 11.188000, 1439.9999 DATA: 11.184000, 1439.3999 DATA: 11.184000, 1438.3000 DATA: 11.184000, 1437.2000 DATA: 11.188000, 1435.4999 DATA: 11.184000, 1435.2000 DATA: 11.184000, 1434.0000 DATA: 11.184000, 1432.7000 DATA: 11.184000, 1431.3999 DATA: 11.184000, 1430.9000 DATA: 11.184000, 1430.4000 DATA: 11.180000, 1429.8999
Ich hätte natürlich auch mit Hilfe von NodeRed einen schicken Graphen zeichnen können, doch wo ich das Script schon hatte und auch gerne Charts mit Excel male…
Hier noch kurz der Python Code, mit dem ich die Daten von der UART Schnittstelle hole und in eine Datei schreibe.
import serial import time import csv ser = serial.Serial('/dev/tty.usbserial-AK05ZWJU', 115200, timeout=1) ser.flushInput() while True: try: ser_bytes = ser.readline() # print(ser_bytes) if ser_bytes[0:4].decode('utf-8') == "DATA": ser_bytes = ser_bytes[6:] ser_voltage_raw = ser_bytes[:5] ser_current_raw = ser_bytes[11:18] voltage = float(ser_voltage_raw[0:len(ser_voltage_raw)].decode("utf-8")) current = float(ser_current_raw[0:len(ser_current_raw)].decode("utf-8")) t = time.localtime() current_time = time.strftime("%H:%M:%S", t) print (current_time, voltage, current) with open ("serial-data.csv","a") as f: writer = csv.writer(f,delimiter=",") writer.writerow ([current_time,voltage,current]) except: print("Keyboard interrupt"); break
Nach dem Import in Excel sieht man auf dem folgenden Chart den Verlauf der Stromstärke bei der Nutzung des Magnetventils über einen Zeitraum von ca. 30 Minuten. Am Anfang sind es 1.460mA und nach 30 Minuten ca. 1.088mA. Also eine Differenz von 25%.
Ventiltemperatur: Zudem ist mir aufgefallen, dass das Ventil sehr heiß wird. Und da ich ja, wie man unschwer erkennen kann, gerne Dinge messe, muss ich mal schauen wie ich die Temperatur noch über den Zeitraum erfassen kann. Das Ventil wird im operativen Betrieb mit Sicherheit auch ein wenige durch das durchfließende Wasser gekühlt. Wie stark sich das auswirkt, kann man wohl nur bei einem angeschlossenen Ventil messen.
Fazit bleibt aber, dass der Strombedarf doch recht hoch ist und die Temperatur auf jeden Fall ausreicht um sich Verbrennungen zuzuziehen. Das Ventil scheint mir jetzt aktuell nur die zweitbeste Wahl zu sein.
Im nächsten Beitrag werde ich das Kugelhahn Ventil anschließen und vermessen.