Archiv der Kategorie: Elektronikbastler

Beiträge über Bastelprojekte zum Thema Elektronik
zum Beispiel: Ein Radio mit Arduino, Geigerzähler…

LAN für Pylontech PV-Akkustatus, OpenDTU und mehr im HomeAssistant

75 total views , 1 views today

In dem Beitrag Pylontech PV-Akkustatus im HomeAssistant hatte ich das Projekt „Pylontech-Battery-Monitoring“ der folgenden GitHub Links etwas aufgehübscht und eine Platine gezeichnet, um das ganze Konstrukt etwas kompakter und professioneller aufzubereiten. 
https://github.com/irekzielinski/Pylontech-Battery-Monitoring
https://github.com/hidaba/PylontechMonitoring
Im Homeassistant wurden damit, bzw. werden, sämtliche Batteriedaten der Pylontech Akku Module angezeigt. Super! Wenn ich aber einen Blick in die Geräteliste der, in meinen Wifi – Netzen angemeldeten Geräte ansehe, wird mir fast übel – es sind mittlerweile viel zu viele Funkgeräte, vor allem aus dem SmartHome Bereich, die sich die Kanalbandbreite teilen.  So ist es mein aktueller Plan, einige der Smarthome Geräte ins verkabelte LAN-Netzwerk zu bringen.

edit 06.Mar.2025: das Projekt ist jetzt auch unter:
https://github.com/ingmarsretro/pylontech_Lan_Interface

zu finden.

Die selber gebastelten Geräte auf Basis der ESPs bieten sich dafür an. Das sind dann Geräte, wie das OpenDTU Interface, das EVU-Smarthomeinterface oder wie hier, die Schnittstelle von der seriellen Console der Pylontechakkus zum MQTT Server im Homeassistant.

Dazu habe ich einige Versuche mit Boards wie dem OLIMEX ESP32-PoE und dem WT32-ETH01 gemacht. Die Olimexplatine hätte den großen Vorteil, auch über PoE mit Energie versorgt werden zu können. Allerdings ist die Spannungsversorgung bei PoE Betrieb so „schlecht“, dass die benötigten Standards externer Boards nicht erfüllt werden. Hier kann ich zum Beispiel das Funkmodul NRF24L01 erwähnen. Damit habe ich einige Versuche gemacht und beschlossen, die PoE Funktionalität vorerst einmal außer Acht zu lassen. So entstand dann der Plan mit dem WT32-ETH01 auf dem ein ESP32 arbeitet, ein universelles Board zu designen, auf dem mehrere Schnittstellen vorhanden sind. Es sollte folgendes können:

  • mit OpenDTU und NRF24L01 mit den PV-Invertern kommunizieren
  • über die Pylontech Console via MQTT mit dem Smarthome System kommunizieren
  • eine optionale CAN Schnittstelle besitzen
  • neben der RS232 Schnittstelle auch über RS422/RS485 kommunizieren zu können
  • die Spannungsversorgung über 5V USB zu erhalten
  • und alles schön klein und kompakt in einem Gehäuse verpackt zu haben

So habe ich dazu eine Schaltung entworfen und eine Platine gezeichnet. Bei einer Fernost PCB Manufaktur ließ ich die Boards fertigen. Das Bestücken ist auch schnell erledigt.

Schaltplan des Universal Lan Interface

Das nachstehende Bild zeigt das Platinenlayout vor der Fertigung.

Das WT32-ETH01 Board besitzt keinen USB-Port zum Programmieren des Controllers. Es wird über einen externen USB-UART Adapter programmiert. Um den Programmiermodus zu aktivieren, muss auch ein IO Pin gegen GND geschaltet werden. Um das etwas zu vereinfachen, gibt es jetzt auf dem Board einen Jumper „PROG“. Wenn diese Brücke gesteckt ist, kann der der WT32 die Firmwarefiles empfangen. Einen Pinheader Steckplatz „TO-FTDI“ habe ich als Anschlussmöglichkeit für den USB-UART Adapter vorgesehen.
Die Platine ist nun so konzipiert, dass man damit unterschiedliche Geräte bedienen kann. Schliesst man ein NRF24L01 Modul an den Pinheader „NRF24L01+“ an und flasht das ESP32-OpenDTU Image auf den Controller, dann können damit die Wechselrichterdaten empfangen und über das LAN Netzwerk übertragen werden. Ein geeignetes IO-config jason-file für die Benutzung des WT32 habe ich erstellt.

Eine weitere Anwendung ist die Verwendung der Platine mit der seriellen Ausgabe der Batteriedaten der Pylontech PV Akkus. Die Akkus stellen einen „Console“ Port zur Verfügung der eine RS232 Schnittstelle darstellt. Über diese werden die Daten zum WT32 Controller übertragen und stehen dann über LAN im lokalen Netzwerk zur Verfügung. 

Dazu habe ich das ESP8266 script von hidaba und irekzielinski für die ESP32 Controller angepasst. (siehe Code am Ende des Beitrags)

Ist der Code kompiliert und hochgeladen, dann sollte nach Anschluss aller Verbindungen unter der eingestellten IP-Adresse der Status der Pylontec Batterien zu sehen sein.

Ausführung der Platine mit Pylontech Setup
Ausführung mit OpenDTU und NRF24L01 Setup

Die im Bild dargestellten Gerätesetups sind mit einem Gehäuse ausgestattet. Die „.stl“ Dateien dazu habe ich mit FreeCad erstellt auf thingiverse veröffentlicht.

Link zu den thingiverse Dateien

Hier der angepasste Code für die Verwendung mit dem WT32-ETH01 Board:

Die Datei „TimerConfig.h“ ist zu erstellen. Folgender Inhalt muss in der Datei stehen:

// TimerConfig.h
#ifndef TIMERCONFIG_H
#define TIMERCONFIG_H
#define TIMER_BASE_CLK 80000000
#endif // TIMERCONFIG_H

Ist die Datei angelegt, sollte sie im Verzeichnis des Hauptprogramms liegen. Hier das Hauptprogramm:

 //Pylontec2MQTT interface  
 //code by https://github.com/irekzielinski/Pylontech-Battery-Monitoring  
 //  and https://github.com/hidaba/PylontechMonitoring  
 // the original code used WEMOS Boards with ESP8266  
 // code changed to use with WT32-ETH01 Board for use with LAN connection  
 // changes by ingmarsretro 01/2025  
 #include <ETH.h>  
 #include <WiFi.h>  
 #include <ESPmDNS.h>  
 #include <ArduinoOTA.h>  
 #include <WebServer.h>  
 #include <circular_log.h>  
 #include <ArduinoJson.h>  
 #include <NTPClient.h>  
 #include "TimerConfig.h"  
 #include <ESP32TimerInterrupt.h>  
 //+++ START CONFIGURATION +++  
 #define LED 12  
 #define RXD2 5  
 #define TXD2 17  
 #define HOSTNAME "mppsolar-pylontec"  
 #define STATIC_IP  
 IPAddress local_IP(192, 168, xx, yy);  
 IPAddress subnet(255, 255, 255, 0);  
 IPAddress gateway(192, 168, yy, zz);  
 IPAddress primaryDNS(192, 168, yy, ww);  
 //Uncomment for authentication page  
 //#define AUTHENTICATION  
 const char* www_username = "admin";  
 const char* www_password = "password";  
 //IMPORTANT: Uncomment this line if you want to enable MQTT (and fill correct MQTT_ values below):  
 #define ENABLE_MQTT  
 // Set offset time in seconds to adjust for your timezone, for example:  
 // GMT +1 = 3600  
 // GMT 0 = 0  
 #define GMT 3600  
 //NOTE 1: if you want to change what is pushed via MQTT - edit function: pushBatteryDataToMqtt.  
 //NOTE 2: MQTT_TOPIC_ROOT is where battery will push MQTT topics. For example "soc" will be pushed to: "home/grid_battery/soc"  
 #define MQTT_SERVER    "192.168.aa.bb"  
 #define MQTT_PORT     1883  
 #define MQTT_USER     ""  
 #define MQTT_PASSWORD   ""  
 #define MQTT_TOPIC_ROOT  "ingmarsretro/pylontec/" //this is where mqtt data will be pushed  
 #define MQTT_PUSH_FREQ_SEC 2 //maximum mqtt update frequency in seconds  
 //+++  END CONFIGURATION +++  
 #ifdef ENABLE_MQTT  
 #include <PubSubClient.h>  
 WiFiClient ethClient;  
 PubSubClient mqttClient(ethClient);  
 #endif //ENABLE_MQTT  
 //text response  
 char g_szRecvBuff[7000];  
 const long utcOffsetInSeconds = GMT;  
 char daysOfTheWeek[7][12] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};  
 // Define NTP Client to get time  
 WiFiUDP ntpUDP;  
 NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);  
 //ESP8266WebServer server(80);  
 WebServer server(80);  
 circular_log<7000> g_log;  
 bool ntpTimeReceived = false;  
 int g_baudRate = 0;  
 void Log(const char* msg)  
 {  
  g_log.Log(msg);  
 }  
 //Define Interrupt Timer to Calculate Power meter every second (kWh)  
 #define USING_TIM_DIV1 true                       // for shortest and most accurate timer  
 //ESP8266Timer ITimer;  
 ESP32Timer ITimer(0); // Use timer 0  
 bool setInterval(unsigned long interval, timer_callback callback);   // interval (in microseconds)  
 #define TIMER_INTERVAL_MS 1000  
 //Global Variables for the Power Meter - accessible from the calculating interrupt und from main  
 unsigned long powerIN = 0;    //WS gone in to the BAttery  
 unsigned long powerOUT = 0;   //WS gone out of the Battery  
 //Global Variables for the Power Meter - Überlauf  
 unsigned long powerINWh = 0;    //WS gone in to the BAttery  
 unsigned long powerOUTWh = 0;   //WS gone out of the Battery  
 void setup() {  
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff)); //clean variable  
  pinMode(LED, OUTPUT);   
  //digitalWrite(LED, HIGH);//high is off  
  digitalWrite(LED, LOW);//low is off  
  // put your setup code here, to run once:  
  //WiFi.mode(WIFI_STA);  
  //WiFi.persistent(false); //our credentialss are hardcoded, so we don't need ESP saving those each boot (will save on flash wear)  
  //WiFi.hostname(HOSTNAME);  
  ETH.begin();  
  ETH.setHostname(HOSTNAME);  
   #ifdef STATIC_IP  
    ETH.config(local_IP, gateway, subnet, primaryDNS);  
  #endif  
  for(int ix=0; ix<10; ix++)  
  {  
   Log("Wait for LAN Connection");  
   if (ETH.linkUp()) {  
     Serial2.println("Ethernet connected");  
     Serial2.print("IP Address: ");  
     Serial2.println(ETH.localIP());  
   } else {  
     Serial2.println("Failed to connect to Ethernet");  
   }  
   delay(1000);  
  }  
  ArduinoOTA.setHostname(HOSTNAME);  
  ArduinoOTA.begin();  
  server.on("/", handleRoot);  
  server.on("/log", handleLog);  
  server.on("/req", handleReq);  
  server.on("/jsonOut", handleJsonOut);  
  server.on("/reboot", [](){  
   #ifdef AUTHENTICATION  
   if (!server.authenticate(www_username, www_password)) {  
    return server.requestAuthentication();  
   }  
   #endif  
   ESP.restart();  
  });  
  server.begin();   
  timeClient.begin();  
 #ifdef ENABLE_MQTT  
  mqttClient.setServer(MQTT_SERVER, MQTT_PORT);  
 #endif  
  Log("Boot event");  
 }  
 void handleLog()  
 {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }   
  #endif  
  server.send(200, "text/html", g_log.c_str());  
 }  
 void switchBaud(int newRate)  
 {  
  if(g_baudRate == newRate)  
  {  
   return;  
  }  
  if(g_baudRate != 0)  
  {  
   Serial2.flush();  
   delay(20);  
   Serial2.end();  
   delay(20);  
  }  
  char szMsg[50];  
  snprintf(szMsg, sizeof(szMsg)-1, "New baud: %d", newRate);  
  Log(szMsg);  
  Serial2.begin(newRate,SERIAL_8N1, RXD2, TXD2);  
  g_baudRate = newRate;  
  delay(20);  
 }  
 void waitForSerial()  
 {  
  for(int ix=0; ix<150;ix++)  
  {  
   if(Serial2.available()) break;  
   delay(10);  
  }  
 }  
 int readFromSerial()  
 {  
  memset(g_szRecvBuff, 0, sizeof(g_szRecvBuff));  
  int recvBuffLen = 0;  
  bool foundTerminator = true;  
  waitForSerial();  
  while(Serial2.available())  
  {  
   char szResponse[256] = "";  
   const int readNow = Serial2.readBytesUntil('>', szResponse, sizeof(szResponse)-1); //all commands terminate with "$$\r\n\rpylon>" (no new line at the end)  
   if(readNow > 0 &&   
     szResponse[0] != '\0')  
   {  
    if(readNow + recvBuffLen + 1 >= (int)(sizeof(g_szRecvBuff)))  
    {  
     Log("WARNING: Read too much data on the console!");  
     break;  
    }  
    strcat(g_szRecvBuff, szResponse);  
    recvBuffLen += readNow;  
    if(strstr(g_szRecvBuff, "$$\r\n\rpylon"))  
    {  
     strcat(g_szRecvBuff, ">"); //readBytesUntil will skip this, so re-add  
     foundTerminator = true;  
     break; //found end of the string  
    }  
    if(strstr(g_szRecvBuff, "Press [Enter] to be continued,other key to exit"))  
    {  
     //we need to send new line character so battery continues the output  
     Serial2.write("\r");  
    }  
    waitForSerial();  
   }  
  }  
  if(recvBuffLen > 0 )  
  {  
   if(foundTerminator == false)  
   {  
    Log("Failed to find pylon> terminator");  
   }  
  }  
  return recvBuffLen;  
 }  
 bool readFromSerialAndSendResponse()  
 {  
  const int recvBuffLen = readFromSerial();  
  if(recvBuffLen > 0)  
  {  
   server.sendContent(g_szRecvBuff);  
   return true;  
  }  
  return false;  
 }  
 bool sendCommandAndReadSerialResponse(const char* pszCommand)  
 {  
  switchBaud(115200);  
  if(pszCommand[0] != '\0')  
  {  
   Serial2.write(pszCommand);  
  }  
  Serial2.write("\n");  
  const int recvBuffLen = readFromSerial();  
  if(recvBuffLen > 0)  
  {  
   return true;  
  }  
  //wake up console and try again:  
  wakeUpConsole();  
  if(pszCommand[0] != '\0')  
  {  
   Serial2.write(pszCommand);  
  }  
  Serial2.write("\n");  
  return readFromSerial() > 0;  
 }  
 void handleReq()  
 {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }  
  #endif  
  bool respOK;  
  if(server.hasArg("code") == false)  
  {  
   respOK = sendCommandAndReadSerialResponse("");  
  }  
  else  
  {  
   respOK = sendCommandAndReadSerialResponse(server.arg("code").c_str());  
  }  
  handleRoot();  
 }  
 void handleJsonOut()  
 {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }  
  #endif  
  if(sendCommandAndReadSerialResponse("pwr") == false)  
  {  
   server.send(500, "text/plain", "Failed to get response to 'pwr' command");  
   return;  
  }  
  parsePwrResponse(g_szRecvBuff);  
  prepareJsonOutput(g_szRecvBuff, sizeof(g_szRecvBuff));  
  server.send(200, "application/json", g_szRecvBuff);  
 }  
 void handleRoot() {  
  #ifdef AUTHENTICATION  
  if (!server.authenticate(www_username, www_password)) {  
   return server.requestAuthentication();  
  }  
  #endif  
  timeClient.update(); //get ntp datetime  
  unsigned long days = 0, hours = 0, minutes = 0;  
  unsigned long val = os_getCurrentTimeSec();  
  days = val / (3600*24);  
  val -= days * (3600*24);  
  hours = val / 3600;  
  val -= hours * 3600;  
  minutes = val / 60;  
  val -= minutes*60;  
  time_t epochTime = timeClient.getEpochTime();  
  String formattedTime = timeClient.getFormattedTime();  
  //Get a time structure  
  struct tm *ptm = gmtime ((time_t *)&epochTime);   
  int currentMonth = ptm->tm_mon+1;  
  static char szTmp[9500] = "";   
  long timezone= GMT / 3600;  
  snprintf(szTmp, sizeof(szTmp)-1, "<html><b>Pylontech Battery LAN Interface</b><br>Time GMT: %s (%s %d)<br>Uptime: %02d:%02d:%02d.%02d<br><br>free heap: %u<br>MQTT-Server: %s<br>MQTT-root topic: %s",   
       formattedTime, "GMT ", timezone,  
       (int)days, (int)hours, (int)minutes, (int)val,   
       ESP.getFreeHeap(),MQTT_SERVER,MQTT_TOPIC_ROOT);  
  strncat(szTmp, "<BR><a href='/log'>Runtime log</a><HR>", sizeof(szTmp)-1);  
  strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'> <a href='/req?code=pwr'>PWR</a> | <a href='/req?code=pwr%201'>Power 1</a> | <a href='/req?code=pwr%202'>Power 2</a> | <a href='/req?code=pwr%203'>Power 3</a> | <a href='/req?code=pwr%204'>Power 4</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);  
  //strncat(szTmp, "<form action='/req' method='get'>Command:<input type='text' name='code'/><input type='submit'><a href='/req?code=pwr'>Power</a> | <a href='/req?code=help'>Help</a> | <a href='/req?code=log'>Event Log</a> | <a href='/req?code=time'>Time</a><br>", sizeof(szTmp)-1);  
  strncat(szTmp, "<textarea rows='80' cols='180'>", sizeof(szTmp)-1);  
  //strncat(szTmp, "<textarea rows='45' cols='180'>", sizeof(szTmp)-1);  
  strncat(szTmp, g_szRecvBuff, sizeof(szTmp)-1);  
  strncat(szTmp, "</textarea></form>", sizeof(szTmp)-1);  
  strncat(szTmp, "</html>", sizeof(szTmp)-1);  
  //send page  
  server.send(200, "text/html", szTmp);  
 }  
 unsigned long os_getCurrentTimeSec()  
 {  
  static unsigned int wrapCnt = 0;  
  static unsigned long lastVal = 0;  
  unsigned long currentVal = millis();  
  if(currentVal < lastVal)  
  {  
   wrapCnt++;  
  }  
  lastVal = currentVal;  
  unsigned long seconds = currentVal/1000;  
  //millis will wrap each 50 days, as we are interested only in seconds, let's keep the wrap counter  
  return (wrapCnt*4294967) + seconds;  
 }  
 void wakeUpConsole()  
 {  
  switchBaud(1200);  
  //byte wakeUpBuff[] = {0x7E, 0x32, 0x30, 0x30, 0x31, 0x34, 0x36, 0x38, 0x32, 0x43, 0x30, 0x30, 0x34, 0x38, 0x35, 0x32, 0x30, 0x46, 0x43, 0x43, 0x33, 0x0D};  
  //Serial.write(wakeUpBuff, sizeof(wakeUpBuff));  
  Serial.write("~20014682C0048520FCC3\r");  
  delay(1000);  
  byte newLineBuff[] = {0x0E, 0x0A};  
  switchBaud(115200);  
  for(int ix=0; ix<10; ix++)  
  {  
   Serial.write(newLineBuff, sizeof(newLineBuff));  
   delay(1000);  
   if(Serial.available())  
   {  
    while(Serial.available())  
    {  
     Serial.read();  
    }  
    break;  
   }  
  }  
 }  
 #define MAX_PYLON_BATTERIES 8  
 struct pylonBattery  
 {  
  bool isPresent;  
  long soc;   //Coulomb in %  
  long voltage; //in mW  
  long current; //in mA, negative value is discharge  
  long tempr;  //temp of case or BMS?  
  long cellTempLow;  
  long cellTempHigh;  
  long cellVoltLow;  
  long cellVoltHigh;  
  char baseState[9];  //Charge | Dischg | Idle  
  char voltageState[9]; //Normal  
  char currentState[9]; //Normal  
  char tempState[9];  //Normal  
  char time[20];    //2019-06-08 04:00:29  
  char b_v_st[9];    //Normal (battery voltage?)  
  char b_t_st[9];    //Normal (battery temperature?)  
  bool isCharging()  const { return strcmp(baseState, "Charge")  == 0; }  
  bool isDischarging() const { return strcmp(baseState, "Dischg")  == 0; }  
  bool isIdle()    const { return strcmp(baseState, "Idle")   == 0; }  
  bool isBalancing()  const { return strcmp(baseState, "Balance") == 0; }  
  bool isNormal() const  
  {  
   if(isCharging()  == false &&  
     isDischarging() == false &&  
     isIdle()    == false &&  
     isBalancing()  == false)  
   {  
    return false; //base state looks wrong!  
   }  
   return strcmp(voltageState, "Normal") == 0 &&  
       strcmp(currentState, "Normal") == 0 &&  
       strcmp(tempState,  "Normal") == 0 &&  
       strcmp(b_v_st,    "Normal") == 0 &&  
       strcmp(b_t_st,    "Normal") == 0 ;  
  }  
 };  
 struct batteryStack  
 {  
  int batteryCount;  
  int soc; //in %, if charging: average SOC, otherwise: lowest SOC  
  int temp; //in mC, if highest temp is > 15C, this will show the highest temp, otherwise the lowest  
  long currentDC;  //mAh current going in or out of the battery  
  long avgVoltage;  //in mV  
  char baseState[9]; //Charge | Dischg | Idle | Balance | Alarm!  
  pylonBattery batts[MAX_PYLON_BATTERIES];  
  bool isNormal() const  
  {  
   for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)  
   {  
    if(batts[ix].isPresent &&   
      batts[ix].isNormal() == false)  
    {  
     return false;  
    }  
   }  
   return true;  
  }  
  //in Wh  
  long getPowerDC() const  
  {  
   return (long)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));  
  }  
  // power in Wh in charge  
  float powerIN() const  
  {  
   if (currentDC > 0) {  
     return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0));  
   } else {  
     return (float)(0);  
   }  
  }  
  // power in Wh in discharge  
  float powerOUT() const  
  {  
   if (currentDC < 0) {  
     return (float)(((double)currentDC/1000.0)*((double)avgVoltage/1000.0)*-1);  
   } else {  
     return (float)(0);  
   }  
  }  
  //Wh estimated current on AC side (taking into account Sofar ME3000SP losses)  
  long getEstPowerAc() const  
  {  
   double powerDC = (double)getPowerDC();  
   if(powerDC == 0)  
   {  
    return 0;  
   }  
   else if(powerDC < 0)  
   {  
    //we are discharging, on AC side we will see less power due to losses  
    if(powerDC < -1000)  
    {  
     return (long)(powerDC*0.94);  
    }  
    else if(powerDC < -600)  
    {  
     return (long)(powerDC*0.90);  
    }  
    else  
    {  
     return (long)(powerDC*0.87);  
    }  
   }  
   else  
   {  
    //we are charging, on AC side we will have more power due to losses  
    if(powerDC > 1000)  
    {  
     return (long)(powerDC*1.06);  
    }  
    else if(powerDC > 600)  
    {  
     return (long)(powerDC*1.1);  
    }  
    else  
    {  
     return (long)(powerDC*1.13);  
    }  
   }  
  }  
 };  
 batteryStack g_stack;  
 long extractInt(const char* pStr, int pos)  
 {  
  return atol(pStr+pos);  
 }  
 void extractStr(const char* pStr, int pos, char* strOut, int strOutSize)  
 {  
  strOut[strOutSize-1] = '\0';  
  strncpy(strOut, pStr+pos, strOutSize-1);  
  strOutSize--;  
  //trim right  
  while(strOutSize > 0)  
  {  
   if(isspace(strOut[strOutSize-1]))  
   {  
    strOut[strOutSize-1] = '\0';  
   }  
   else  
   {  
    break;  
   }  
   strOutSize--;  
  }  
 }  
 /* Output has mixed \r and \r\n  
 pwr  
 @  
 Power Volt  Curr  Tempr Tlow  Thigh Vlow  Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time         B.V.St  B.T.St   
 1   49735 -1440 22000 19000 19000 3315  3317  Dischg  Normal  Normal  Normal  93%   2019-06-08 04:00:30 Normal  Normal   
 ....    
 8   -   -   -   -   -   -   -   Absent  -    -    -    -    -          -    -      
 Command completed successfully  
 $$  
 pylon  
 */  
 bool parsePwrResponse(const char* pStr)  
 {  
  if(strstr(pStr, "Command completed successfully") == NULL)  
  {  
   return false;  
  }  
  int chargeCnt  = 0;  
  int dischargeCnt = 0;  
  int idleCnt   = 0;  
  int alarmCnt   = 0;  
  int socAvg    = 0;  
  int socLow    = 0;  
  int tempHigh   = 0;  
  int tempLow   = 0;  
  memset(&g_stack, 0, sizeof(g_stack));  
  for(int ix=0; ix<MAX_PYLON_BATTERIES; ix++)  
  {  
   char szToFind[32] = "";  
   snprintf(szToFind, sizeof(szToFind)-1, "\r\r\n%d   ", ix+1);  
   const char* pLineStart = strstr(pStr, szToFind);  
   if(pLineStart == NULL)  
   {  
    return false;  
   }  
   pLineStart += 3; //move past \r\r\n  
   extractStr(pLineStart, 55, g_stack.batts[ix].baseState, sizeof(g_stack.batts[ix].baseState));  
   if(strcmp(g_stack.batts[ix].baseState, "Absent") == 0)  
   {  
    g_stack.batts[ix].isPresent = false;  
   }  
   else  
   {  
    g_stack.batts[ix].isPresent = true;  
    extractStr(pLineStart, 64, g_stack.batts[ix].voltageState, sizeof(g_stack.batts[ix].voltageState));  
    extractStr(pLineStart, 73, g_stack.batts[ix].currentState, sizeof(g_stack.batts[ix].currentState));  
    extractStr(pLineStart, 82, g_stack.batts[ix].tempState, sizeof(g_stack.batts[ix].tempState));  
    extractStr(pLineStart, 100, g_stack.batts[ix].time, sizeof(g_stack.batts[ix].time));  
    extractStr(pLineStart, 121, g_stack.batts[ix].b_v_st, sizeof(g_stack.batts[ix].b_v_st));  
    extractStr(pLineStart, 130, g_stack.batts[ix].b_t_st, sizeof(g_stack.batts[ix].b_t_st));  
    g_stack.batts[ix].voltage = extractInt(pLineStart, 6);  
    g_stack.batts[ix].current = extractInt(pLineStart, 13);  
    g_stack.batts[ix].tempr  = extractInt(pLineStart, 20);  
    g_stack.batts[ix].cellTempLow  = extractInt(pLineStart, 27);  
    g_stack.batts[ix].cellTempHigh  = extractInt(pLineStart, 34);  
    g_stack.batts[ix].cellVoltLow  = extractInt(pLineStart, 41);  
    g_stack.batts[ix].cellVoltHigh  = extractInt(pLineStart, 48);  
    g_stack.batts[ix].soc      = extractInt(pLineStart, 91);  
    //////////////////////////////// Post-process ////////////////////////  
    g_stack.batteryCount++;  
    g_stack.currentDC += g_stack.batts[ix].current;  
    g_stack.avgVoltage += g_stack.batts[ix].voltage;  
    socAvg += g_stack.batts[ix].soc;  
    if(g_stack.batts[ix].isNormal() == false){ alarmCnt++; }  
    else if(g_stack.batts[ix].isCharging()){chargeCnt++;}  
    else if(g_stack.batts[ix].isDischarging()){dischargeCnt++;}  
    else if(g_stack.batts[ix].isIdle()){idleCnt++;}  
    else{ alarmCnt++; } //should not really happen!  
    if(g_stack.batteryCount == 1)  
    {  
     socLow = g_stack.batts[ix].soc;  
     tempLow = g_stack.batts[ix].cellTempLow;  
     tempHigh = g_stack.batts[ix].cellTempHigh;  
    }  
    else  
    {  
     if(socLow > g_stack.batts[ix].soc){socLow = g_stack.batts[ix].soc;}  
     if(tempHigh < g_stack.batts[ix].cellTempHigh){tempHigh = g_stack.batts[ix].cellTempHigh;}  
     if(tempLow > g_stack.batts[ix].cellTempLow){tempLow = g_stack.batts[ix].cellTempLow;}  
    }  
   }  
  }  
  //now update stack state:  
  g_stack.avgVoltage /= g_stack.batteryCount;  
  g_stack.soc = socLow;  
  if(tempHigh > 15000) //15C  
  {  
   g_stack.temp = tempHigh; //in the summer we highlight the warmest cell  
  }  
  else  
  {  
   g_stack.temp = tempLow; //in the winter we focus on coldest cell  
  }  
  if(alarmCnt > 0)  
  {  
   strcpy(g_stack.baseState, "Alarm!");  
  }  
  else if(chargeCnt == g_stack.batteryCount)  
  {  
   strcpy(g_stack.baseState, "Charge");  
   g_stack.soc = (int)(socAvg / g_stack.batteryCount);  
  }  
  else if(dischargeCnt == g_stack.batteryCount)  
  {  
   strcpy(g_stack.baseState, "Dischg");  
  }  
  else if(idleCnt == g_stack.batteryCount)  
  {  
   strcpy(g_stack.baseState, "Idle");  
  }  
  else  
  {  
   strcpy(g_stack.baseState, "Balance");  
  }  
  return true;  
 }  
 void prepareJsonOutput(char* pBuff, int buffSize)  
 {  
  memset(pBuff, 0, buffSize);  
  snprintf(pBuff, buffSize-1, "{\"soc\": %d, \"temp\": %d, \"currentDC\": %ld, \"avgVoltage\": %ld, \"baseState\": \"%s\", \"batteryCount\": %d, \"powerDC\": %ld, \"estPowerAC\": %ld, \"isNormal\": %s}", g_stack.soc,   
                                                                                                       g_stack.temp,   
                                                                                                       g_stack.currentDC,   
                                                                                                       g_stack.avgVoltage,   
                                                                                                       g_stack.baseState,   
                                                                                                       g_stack.batteryCount,   
                                                                                                       g_stack.getPowerDC(),   
                                                                                                       g_stack.getEstPowerAc(),  
                                                                                                       g_stack.isNormal() ? "true" : "false");  
 }  
 void loop() {  
 #ifdef ENABLE_MQTT  
  mqttLoop();  
 #endif  
  ArduinoOTA.handle();  
  server.handleClient();  
  //if there are bytes availbe on serial here - it's unexpected  
  //when we send a command to battery, we read whole response  
  //if we get anything here anyways - we will log it  
  int bytesAv = Serial.available();  
  if(bytesAv > 0)  
  {  
   if(bytesAv > 63)  
   {  
    bytesAv = 63;  
   }  
   char buff[64+4] = "RCV:";  
   if(Serial.readBytes(buff+4, bytesAv) > 0)  
   {  
    //digitalWrite(LED, LOW);  
    digitalWrite(LED, HIGH);  
    delay(5);  
    //digitalWrite(LED, HIGH);//high is off  
    digitalWrite(LED, LOW);  
    Log(buff);  
   }  
  }  
 }  
 #ifdef ENABLE_MQTT  
 #define ABS_DIFF(a, b) (a > b ? a-b : b-a)  
 void mqtt_publish_f(const char* topic, float newValue, float oldValue, float minDiff, bool force)  
 {  
  char szTmp[16] = "";  
  snprintf(szTmp, 15, "%.2f", newValue);  
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)  
  {  
   mqttClient.publish(topic, szTmp, false);  
  }  
 }  
 void mqtt_publish_i(const char* topic, int newValue, int oldValue, int minDiff, bool force)  
 {  
  char szTmp[16] = "";  
  snprintf(szTmp, 15, "%d", newValue);  
  if(force || ABS_DIFF(newValue, oldValue) > minDiff)  
  {  
   mqttClient.publish(topic, szTmp, false);  
  }  
 }  
 void mqtt_publish_s(const char* topic, const char* newValue, const char* oldValue, bool force)  
 {  
  if(force || strcmp(newValue, oldValue) != 0)  
  {  
   mqttClient.publish(topic, newValue, false);  
  }  
 }  
 void pushBatteryDataToMqtt(const batteryStack& lastSentData, bool forceUpdate /* if true - we will send all data regardless if it's the same */)  
 {  
  mqtt_publish_f(MQTT_TOPIC_ROOT "soc",     g_stack.soc,        lastSentData.soc,        0, forceUpdate);  
  mqtt_publish_f(MQTT_TOPIC_ROOT "temp",     (float)g_stack.temp/1000.0, (float)lastSentData.temp/1000.0, 0.1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "currentDC",  g_stack.currentDC,     lastSentData.currentDC,     1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "estPowerAC",  g_stack.getEstPowerAc(),  lastSentData.getEstPowerAc(),  10, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "battery_count",g_stack.batteryCount,    lastSentData.batteryCount,    0, forceUpdate);  
  mqtt_publish_s(MQTT_TOPIC_ROOT "base_state",  g_stack.baseState,     lastSentData.baseState      , forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "is_normal",  g_stack.isNormal() ? 1:0,  lastSentData.isNormal() ? 1:0,  0, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "getPowerDC",  g_stack.getPowerDC(),    lastSentData.getPowerDC(),    1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerIN",   g_stack.powerIN(),     lastSentData.powerIN(),     1, forceUpdate);  
  mqtt_publish_i(MQTT_TOPIC_ROOT "powerOUT",   g_stack.powerOUT(),     lastSentData.powerOUT(),     1, forceUpdate);  
  // publishing details  
  for (int ix = 0; ix < g_stack.batteryCount; ix++) {  
   char ixBuff[50];  
   String ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/voltage";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_f(ixBuff, g_stack.batts[ix].voltage / 1000.0, lastSentData.batts[ix].voltage / 1000.0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/current";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_f(ixBuff, g_stack.batts[ix].current / 1000.0, lastSentData.batts[ix].current / 1000.0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/soc";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].soc, lastSentData.batts[ix].soc, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/charging";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].isCharging()?1:0, lastSentData.batts[ix].isCharging()?1:0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/discharging";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].isDischarging()?1:0, lastSentData.batts[ix].isDischarging()?1:0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/idle";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_i(ixBuff, g_stack.batts[ix].isIdle()?1:0, lastSentData.batts[ix].isIdle()?1:0, 0, forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/state";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_s(ixBuff, g_stack.batts[ix].isIdle()?"Idle":g_stack.batts[ix].isCharging()?"Charging":g_stack.batts[ix].isDischarging()?"Discharging":"", lastSentData.batts[ix].isIdle()?"Idle":lastSentData.batts[ix].isCharging()?"Charging":lastSentData.batts[ix].isDischarging()?"Discharging":"", forceUpdate);  
   ixBattStr = MQTT_TOPIC_ROOT + String(ix) + "/temp";  
   ixBattStr.toCharArray(ixBuff, 50);  
   mqtt_publish_f(ixBuff, (float)g_stack.batts[ix].tempr/1000.0, (float)lastSentData.batts[ix].tempr/1000.0, 0.1, forceUpdate);  
  }  
 }   
 void mqttLoop()  
 {  
  //if we have problems with connecting to mqtt server, we will attempt to re-estabish connection each 1minute (not more than that)  
  static unsigned long g_lastConnectionAttempt = 0;  
  //first: let's make sure we are connected to mqtt  
  const char* topicLastWill = MQTT_TOPIC_ROOT "availability";  
  if (!mqttClient.connected() && (g_lastConnectionAttempt == 0 || os_getCurrentTimeSec() - g_lastConnectionAttempt > 60)) {  
   if(mqttClient.connect(HOSTNAME, MQTT_USER, MQTT_PASSWORD, topicLastWill, 1, true, "offline"))  
   {  
    Log("Connected to MQTT server: " MQTT_SERVER);  
    mqttClient.publish(topicLastWill, "online", true);  
   }  
   else  
   {  
    Log("Failed to connect to MQTT server.");  
   }  
   g_lastConnectionAttempt = os_getCurrentTimeSec();  
  }  
  //next: read data from battery and send via MQTT (but only once per MQTT_PUSH_FREQ_SEC seconds)  
  static unsigned long g_lastDataSent = 0;  
  if(mqttClient.connected() &&   
    os_getCurrentTimeSec() - g_lastDataSent > MQTT_PUSH_FREQ_SEC &&  
    sendCommandAndReadSerialResponse("pwr") == true)  
  {  
   static batteryStack lastSentData; //this is the last state we sent to MQTT, used to prevent sending the same data over and over again  
   static unsigned int callCnt = 0;  
   parsePwrResponse(g_szRecvBuff);  
   bool forceUpdate = (callCnt % 20 == 0); //push all the data every 20th call  
   pushBatteryDataToMqtt(lastSentData, forceUpdate);  
   callCnt++;  
   g_lastDataSent = os_getCurrentTimeSec();  
   memcpy(&lastSentData, &g_stack, sizeof(batteryStack));  
  }  
  mqttClient.loop();  
 }  
 #endif //ENABLE_MQTT  

TV-Deckenhalterung motorisiert über Homeassistant steuern

281 total views , 1 views today

Seit ich mich mit Home Automatisierungen beschäftige soll natürlich so viel wie möglich optimiert, vereinfacht und unter den Aspekten der neuen Schlagworte „Green Electronics“, „Nachhaltigkeit“, „Energiesparend“ … usw. angepasst und realisiert werden. So schalten bei mir Geräte bei Nichtbenutzung oder Nichtbeachtung ab, Stand-by Energieverbrauch wird weitgehend vermieden und auch die menschliche Vergesslichkeit (Fenster offen gelassen im Winter, oder vergessen Licht aus zu schalten) verhindert die IOT – Technologie. Wie die Leser des Blogs mittlerweile schon wissen habe ich hier Systeme wie HomeMatic, NodeRed und seit einiger Zeit Homeassistant mit ESPHome, Zigbee2Mqtt usw. in Verwendung. Das Ziel ist natürlich auch, alles Systeme Cloudfrei zu halten. Ich will nicht, dass die Daten den Umweg über irgendwelche Server in Fernost nehmen um bei mir ein Licht ein und aus zu schalten. Also soll möglichst alles im Kreis meines eigenen Netzwerks stattfinden und nicht nach außen „telefonieren“ und auch funktionieren wenn ich die Datenleitung kappe.

Bei diversen Lieferanten gibts es seit langem, ein, für die Bequemlichkeit im elterlichen Ruheraum, äußert praktisches Gerät. Ich spreche da von einer platzsparenden Möglichkeit, die Flimmerkiste (heute auch Flat-TV genannt) im Raum unterzubringen. Ich nenne hier nur Bezeichnungen wie:

Speaka Professional TV-Deckenhalterung elektrisch motorisiert (1439178) oder MyWall HL46ML … etc. Manche von diesen Gräten sind mit einer Funkfernbedienung steuerbar, andere wiederum über die CloudApp von Tuya. Man kann die Tuya App zwar über die Tuya IOT Entwicklungsumgebung umgehen und diese Geräte über die Integration „TuyaLocal“ in seinen Homeassistant bringen – geht zwar – ist aber eher eine „NUR“- Lösung. Die ideale Lösung ist aus meiner Sicht, die Integration dieser Geräte ins ESPHome System.  Am Beispiel der Speaka Professional TV Deckenhalterung zeige ich, wie diese mit einer kleinen Erweiterung ins ESPHome Netz und somit im Homeassistant integriert werden kann. Diese Ausführung des SpeaKa Teils hat keine Internet Anbindung und wird nur über eine Funkfernbedienung gesteuert.

TV Deckenhalter mit geöffneter Abdeckung

Mit ein wenig reverse Engineering haben wir (Kollege Werner und meiner einer) das bestehende Gerätewerk analysiert. Das System ist in etwa so aufgebaut:

Platine in der Deckenhalterung

 

Systemdiagramm

Das Systemdiagramm oben zeigt wie die Platine aufgebaut ist. Die Stromversorgung kommt von einem Steckernetzteil mit DC 24V Ausgang bei 1,5A. Auf der Platine erkennt man noch einen unbestückten Bereich, dessen Lötpads mit +3V3, GND und RX, TX Leitungen passend für einen ESP8266 beschaltet sind. Ebenso ist eine USB Buchse zu erkennen. Diese beiden Schnittstellen sind im Diagramm nicht berücksichtigt. Untersucht haben wir die RX/TX Leitungen, die von den unbestückten Lötpads (ESP8266) zum Microcontroller (1301 X 016B) geroutet sind. Doch hier waren keinerlei Signale zu messen. (Vermutlich ist die Schnittstelle in der geflashten Programmversion nicht aktiviert).

„Debug“ Drähte an den RX/TX und am RF-Chip

Dieser Weg bringt uns also nicht weiter. Im nächsten Schritt haben wir uns angesehen wo die Steuersignale der Funkfernbedienung herkommen, bzw. wie sie in weiterer Folge umgesetzt werden. Der RF-Empfänger Chip hat 16 Pins und leider keinerlei Beschriftung. Oder wurde sie entfernt. Die Versorgungsspannung des RF-Chips liegt an Pin1 und Pin16 an, Pin2 und Pin3 ist mit einem Quarz beschaltet und von Pin9 ist eine Leitung zum Microcontroller geroutet. Das muss also der Datenausgang sein. Mit Hilfe der Software „PulseView“ von Sigrok und einem Fernost Logicanalyzer haben wir diesen Ausgang mitgesnifft. Und siehe da, hier offenbarten sich Datenpakete mit einer Dauer von 10.3ms. Die Software PulseView konnte das Protokoll nach einigen Versuchen mit unterschiedlichen analysierten Datenraten als RS232 Protokoll erkennen. So war es dann ein leichtes die empfangenen und dekodierten Steuerbefehle zum Microcontroller zu protokollieren.

RF-Chip mit angeschlossener „Sniffer“ Leitung

Die Baudrate des RS232 Ports am RF-Chip Ausgangs beträgt 9600 bei 8N1. Es werden bei jedem gesendeten Befehl 10 Bytes in HEX empfangen. Hier die Liste der Kommandos: (fehlende Byte folgen…)

Befehl Byte0 Byte1 Byte2 Byte3 Byte4 Byte5 Byte6 Byte7 Byte8 Byte9
UP 0xAA 0x06 0x04 0x25 0x03 0xD5 0x01 0x00 0x02 0x55
DOWN 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x10 0x11 0x55
LEFT 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
RIGHT 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
BUTTON1 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
BUTTON2 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x08 0x09 0x55
MEM1 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
MEM2 0xAA 0x06 0x04 0x25 0x03 0xD5       0x55
OK 0xAA 0x06 0x04 0x25 0x03 0xD5 0x00 0x40 0x41 0x55
SET xx xx xx xx xx xx xx xx xx xx

Nachdem mit dem Logikanalyzer das Datenprotokoll gefunden war, versuchten wir über ein Terminalprogramm und einen USB zu TTL232 Converter die Daten an den Microcontroller zu senden. Dazu wurde der RF-Chip entfernt. Er zog den Pegel im Ruhezustand auf VCC und verhinderte ein paralleles Betreiben der „RS232 Transmitter“.

RF-Chip entfernt
Board ohne Chip mit Debugleitung

 

USB UART zum Senden der Befehle

Die Steuerbefehle aus oben dargestellter Tabelle konnten per Terminal Programm erfolgreich gesendet werden. Jetzt musste nur noch ein ESP32 Board diese Aufgabe übernehmen. Ein ESP32 NodeMCU Board aus dem Fundus wurde mit einem Basis ESPHome-Image bestückt und ins Homeassistant Netzwerk integriert. Dem ESPHome Knoten war jetzt nur noch beizubringen, über den TX Pin des ESP32 die Bytefolge bei entsprechendem Trigger im Homeassistant zu senden. Dazu wurde das ESP32 Board in auf der Platine befestigt und die VCC3V3, GND und TX Leitung zum PIN9 des ehemaligen RF Chip gelötet.

ESP32 am Board des Speaka Deckenhalters

 

Wieder im Deckenhalter eingebaut

In der ESPHome Webumgebung ist nun das folgende yaml Script hinzuzufügen.

 esphome:  
  name: tvhalterung  
  friendly_name: TVHalterung  
   
 esp32:  
  board: esp32dev  
  framework:  
   type: arduino  
   
 # Enable logging  
 logger:  
   
 # Enable Home Assistant API  
 api:  
  encryption:  
   key: "hier dein key beim Anlegen des device"  
   
 ota:  
  password: "hier dein ota password"  
   
 wifi:  
  ssid: !secret wifi_ssid  
  password: !secret wifi_password  
   
  # Enable fallback hotspot (captive portal) in case wifi connection fails  
  ap:  
   ssid: "Tvhalterung Fallback Hotspot"  
   password: "hier wieder deins"  
   
 captive_portal:  
   
 uart:  
  tx_pin: 4  
  rx_pin: 5  
  baud_rate: 9600  
   
 # Example button configuration  
 button:  
  - platform: template  
   name: TV Halterung UP  
   id: tv_up  
   icon: "mdi:arrow-up-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Up"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x01,0x00,0x02,0x55]  
    
  - platform: template  
   name: TV Halterung OK  
   id: tv_ok  
   icon: "mdi:stop-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV OK"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x40,0x41,0x55]  
   
  - platform: template  
   name: TV Halterung DOWN  
   id: tv_down  
   icon: "mdi:arrow-down-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Down"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x10,0x11,0x55]  
   
  - platform: template  
   name: TV Halterung Button1  
   id: tv_button1  
   icon: "mdi:numeric-1-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV Button1"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
   
  - platform: template  
   name: TV Halterung Button2  
   id: tv_button2  
   icon: "mdi:numeric-2-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV Button2"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x08,0x09,0x55]  
    
  - platform: template  
   name: TV Halterung Left  
   id: tv_left  
   icon: "mdi:arrow-left-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Left"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
   
  - platform: template  
   name: TV Halterung Right  
   id: tv_right  
   icon: "mdi:arrow-right-bold-outline"  
   on_press:  
    - logger.log: "Button pressed TV Right"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x20,0x21,0x55]  
    
  - platform: template  
   name: TV Halterung MEM1  
   id: tv_mem1  
   icon: "mdi:alpha-m-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV MEM1"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]  
   
  - platform: template  
   name: TV Halterung MEM2  
   id: tv_mem2  
   icon: "mdi:alpha-m-circle-outline"  
   on_press:  
    - logger.log: "Button pressed TV MEM2"  
    - uart.write: [0xAA,0x06,0x04,0x25,0x03,0xD5,0x00,0x01,0x02,0x55]  

Ist das yaml Script dann kompiliert und zum ESP hochgeladen, gibt es ein neues ESPHome Device mit dem Namen TV-Halter in der Homeassistant Umgebung. Hier sind nun die Tasten für die Steuerung als Entitäten gelistet. Hat alles gekplappt, sollte sich die TV-Halterung über den Homeassistant jetzt steuern lassen.

(Es sind noch nicht alles Steuerkommandos richtig implementiert – die korrekten Codes werden in der Tabelle noch ergänzt)

EVU Smartmeter mit ESP32 auslesen und Daten per MQTT senden

2912 total views , 6 views today

So nach und nach bringe ich viele meiner Smarthome Komponenten auf einen gemeinsamen Standard. Dabei habe ich mich entschieden, sämtliche Geräte über einen NodeRed Server zusammen zu führen. Auch das HomeMatic System kommuniziert mit NodeRed. Hier übergebe ich unter anderen auch die Messwerte des EVU-Zählers (bei mir ist ein Siemens IM350 Smartmeter verbaut) an die HomeMatic CCU. Dies geschieht wie schon einem früheren Beitrag erwähnt, über die LED-Impulsschnittstelle (1000 Impulse/kWh). Hierzu wird einfach ein Fototransistor über der LED am Zähler angebracht, der die Blinkimpulse der LED erkennt und in der Zählersensor-Sendeeinheit HM-ES-TX-WM in die Momentanleistung umrechnet und über die Zeit integriert und die Daten dann an die CCU weitersendet. Das funktioniert an sich ganz gut. Nur die Aktualisierungsrate (im Minutenbereich) ist mir zu lange. Auch scheint der Fototransistor immer wieder auf das Streulicht der benachbarten LED (diese zeigt die Blindleistung in 1000 Impulse/kvarh an) zu reagieren. So entstehen Abweichungen zwischen der Zählung über den HomeMatic Sensor und den direkt am Zähler abgelesenen Werten.

Das geht auf jeden Fall genauer. Wenn man sich den IM350 Smartmeter Zähler im Detail ansieht, bzw. das Manual durchliest, so stellt man schnell fest, dass er eine sog. „Kundenschnittstelle“ besitzt. Diese Kundenschnittstelle stellt einige Messdaten über eine galvanisch getrennte Datenleitung im Sekundentakt zur Verfügung. Dazu gehören unter anderen die momentane Wirkleistung in beiden Richtungen, sowie die Zählerstände von Wirk- und Blindleistung in Bezugs- und Einspeiserichtung. Also perfekte Ausgangbedingungen, um den HomeMatic Zählersensor durch eine eigene Konstruktion zu ersetzen. Nach ein wenig Internetrecherche habe ich schnell erkannt, dass ich nicht der Einzige bin, der sich mit genau dieser Thematik beschäftigt. Die Daten der Kundenschnittstelle purzeln nach Anforderung über eine Daten-Request Leitung mit einer Geschwindigkeit von 115kbaud heraus. Sie sind allerdings verschlüsselt, und nicht direkt lesbar. Um den 16 Byte langen Entschlüsselung Key zu erhalten, muss der Energieversorger konsultiert werden. Der Schlüssel ist an die Seriennummer des Smartmeters gebunden und für jedes Smartmeter individuell. Nach einigem Telefonieren mit meinen Kärntner Energieversorger wurde mir der Key-Code per Mail zugesandt. Im nächsten Schritt testete ich mit einem USB-UART Adapter an einem PC, ob bei korrekter Beschaltung der Schnittstelle auch wirklich Daten aus dem Zähler herauskommen. Dazu habe ich einen RJ11 Stecker auf ein geeignetes 6pol. Kabel gekrimpt und das offene Ende des Kabels entsprechend des Datenblattes des Zählers beschaltet. Dazu ist nicht besonders viel notwendig. Eine 5V Versorgung muss die Schnittstelle aktivieren, ebenso muss auch die Data Request Leitung an 5V geschaltet werden und schon stehen an der Data Out Leitung die Datenpakete an. Es funktioniert übrigens auch mit einer 3V3 Versorgung. Mit einem Terminalprogramm am PC (ich verwende meist putty oder hterm) kann man die verschlüsselten Daten visualisieren.

Jetzt ging es daran, sich zu überlegen, wie die Daten entschlüsselt und aufbereitet werden. Hierzu findet man mit Netz zwei Ansätze:

* über einen RaspberryPi, mit einer Python-Umgebung und einem Python Skript. Die Skripten übernehmen hier den Empfang und die Entschlüsselung der Daten und stellen sie dann auf unterschiedliche Weise zur weiteren Verarbeitung zur Verfügung

* über einen ESP32. Der ESP ist ebenfalls in der Lage eine 128Bit AES Verschlüsselung zu dekodieren und hat noch reichlich Ressourcen um die Daten aufzubereiten und über WiFi zu versenden. Außerdem ist ein ESP für wenig Geld in ausreichender Stückzahl verfügbar. Also habe ich mich für diese Lösung entschieden. Dazu gibt es auf GitHub ein open source Projekt vom User https://github.com/Andre-Schuiki/esphome_im350  in dem er einen ESP32 IM350 Decoder als Basis für eigene Projekte zur Verfügung stellt. Mit seinen Sourcen erhält man einen Decoder der die Zählerdaten im Sekundentakt ausliest und über die USB UART Programmierschnittstelle und auch via Telnet über WiFi ausgibt. Dieses Sourcen habe ich als Basis verwendet. 

Mein Ziel ist es, die aus dem Smartmeter gewonnenen Daten in MQTT Messages zu verpacken und an meinen MQTT Broker zu senden. Ab da ist es dann ein Einfaches, sie in NodeRed und die HomeMatic CCU zu bekommen und dort zu speichern. Also habe ich den Code angepasst.  Dazu wurde die Wifi Verbindung zum Router auf eine statische IP gesetzt. (sind in settings.h zu definieren). Die ausgelesenen Messwerte, sowie die RSSI der WiFi Verbindung, werden jetzt über MQTT Topics zur Verfügung gestellt. (die IP-Adresse zum Broker ist auch in settings.h zu definieren). Wenn man den Code jetzt kompiliert und auf den ESP jagt, dann sollte er sich in das jeweilige Netzwerk einbuchen. Solange der ESP noch auf einem PC-hängt, kann man über die Programmierschnittstelle und ein Terminal auch gleich überprüfen was er tut. Verbindet man jetzt den RJ11 Stecker mit der Kundenschnittstelle des Zählers, dann solle im Display des Zählers im Sekundentakt das Dreieck über der Beschriftung „KU“ blinken. Passiert das, dann sollten auch schon im Terminal die Messwerte stehen (vorausgesetzt man hat den KEY vom EVU nicht vergessen in secrets.h einzugeben). Klappt auch das, dann stellt ein Blick auf den MQTT-Broker (mit z. Bsp.: MQTT-Explorer) sicher, dass die Messages auch ankommen. Jetzt kann der ESP vom PC entfernt werden.

Anschlussbelegung
ESP32 im „freifliegenden“ Testaufbau

Ich habe eine sehr einfache Lösung gewählt und des ESP auf einer Lochrasterplatine befestigt. Das 6polige Kabel zum Smartmeter dort angelötet. Auf der Lochrasterplatte finden dann noch die Pull-Up Widerstände und ein NPN Transistor (BC547 etc.) zum Invertieren der Datenpulse Platz. Die Platine habe ich in einem kleinen Kunststoffkästchen untergebracht, dass jetzt lediglich mit einem Kabel an der Kundenschnittstelle und mit einem USB Kabel an einem USB Steckernetzteil angeschlossen ist.

Der fertige Aufbau sieht dann (bzw. zurzeit) so aus. Die Daten landen im MQTT Broker und NodeRed visualisiert sie und schickt sie auch zur HomeMatic CCU.

so kommen die Daten im MQTT Broker an
und können zum Beispiel so in NodeRed verarbeitet werden

wenn jemand an den angepassten Skripten interessiert ist, kann ich die gerne zusenden. Betreffend einer Veröffentlichung auf  GitHub muss ich mich erst informieren welche Lizenzbedingungen betreffend des ursprünglichen Repositorys zu erfüllen sind. Es wird dann hier (public) verfügbar sein:

https://github.com/ingmarsretro/esphome_im350/tree/main/standalone_version_mqtt

Ein Nachbauprojekt für die Vectrex

58 total views

Es ist wieder einige Zeit vergangen, dass ich es schaffe, in den späteren Abendstunden Zeit und Energie zu finden, hier im Blog über eines meiner kleinen Projektchen zu schreiben. Ich habe mir in den letzten Jahren angewöhnt, bei Autofahrten und nächtens, Podcasts zu hören. Dazu gehören in erster Linie Podcasts zu technischen Themen. Darunter ist auch ein Podcast, der sich „Retrokompott“ nennt und sich mit Homecomputern und Technik aus unserer Jugendzeit beschäftigt. Deren Slogan lautet:

Retrokompott, eine Zeitreise in die Vergangenheit alter Homecomputer, Spielekonsolen und Games

[http://blog.retrokompott.de/]

In einem der Beiträge von Retrokompott diskutierte man einige Folgen lang (172-177) über die Vectrex, den Heim – Vectorspieleautomaten von MBE. Hier wurden unter anderen auch Homebrewprojekte, also Software-Eigenentwicklungen der Anwender vorgestellt.  „Vectorblade“ ist dabei ein Spieletitel, der von Malban [http://vide.malban.de/] entwickelt wurde. Das Projekt wurde dabei mit dem ebenfalls von Malban entwickelten Vectrexcompiler (vide) erstellt. Die Sourcen sind öffentlich auf der Website verfügbar. In dem „Kompott“-Beitrag hat man so begeistert über Vectorblade berichtet, dass mein Interesse dafür geweckt war. Das Spielemodul war auch eine Zeit lang über Malban zu erwerben. Ich habe aber keine Quelle gefunden, über die ich das Modul auf einfache Weise erwerben kann. So dachte ich mir, baue ich mir das halt einfach nach. Das Besondere an diesem Gamerom ist die Größe des Games. Es hat stolze 192 kB. Um diesen Speicher adressieren zu können, hat sich Malban der Bank-Switching Technologie bedient. Er verwendet in seinem Design einen Flash-Speicher von SST, den SST39SF020. Das Bankswitching wird über einen Vierfach-2-Eingang NAND Schmitt Trigger (74AC132) gesteuert. Malban hat auf git das Layout veröffentlicht. Dort verwendet er den Speicher im DIL-Package und ebenso auch den AC132. Eine detaillierte Anleitung findet man hier.

Da ich von meinem alten Selbstbau-Rom Modul Projekt noch einige Platinen über habe, konnte ich schnell einen Versuchsaufbau zusammenstoppeln.  Flashspeicher hatte ich zwar keinen zur Verfügung – sehr wohl aber eine ausreichend großes EPROM. Der Vide-Compiler und die Source-Files sind auf Malbans GIT ebenfalls veröffentlicht. Nach kurzem Studium seines Vide-Compilers ist es mir gelungen das Projekt zu kompilieren und eine ROM – Datei zu erstellen. Mit meinem „Fernostprogrammer“ konnte ich dann das EPROM „brennen“.  Mit ein paar Drahtbrücken und einem AC132 wurde aus meinem alten ROM-Platinen Projekt dann der Vectorblade Versuchsaufbau.

Versuchsaufbau Vectorblade

Mit der Ausnahme, dass keine Settings gespeichert werden können, funktioniert der Testaufbau und das Game lässt sich spielen :). Der nächste Schritt des Nachbaus war dann die Platine zu zeichnen. Hier wollte ich den Schmitt-Trigger Baustein in SMD Ausführung einbauen und den SST weiterhin in DIL. Ich habe diese Ausführung auch realisiert und erfolgreich getestet. Es gibt aber einen kleinen Haken – keiner meiner Lieferanten hat den SST39SF020 Flashspeicher in DIL Ausführung auf Lager. Ich habe jetzt zwar einige Platinen mit DIL – Layout aber eben keine Chips… Also noch einmal zum PC und das Design auf PLCC Sockel umzeichnen. Gedacht – getan und einen Satz Platinen beim Fernostproduzenten bestellt.

Ein passendes Gehäuse lässt sich mit dem 3D-Drucker selbst erstellen. Genauer gesagt wurde ich auf Thingiverse fündig und konnte  aus einer Vielzahl an geeigneten Designs wählen.

Es fehlt zwar das Overlay –  aber auch ohne das macht das Spiel Spass. Hier ist Malban ein tolles Game gelungen.

MIDI DB50XG – ein Interface für das Daughter Board

358 total views

Beim Stöbern in einer Kiste mit meinen alten Bastelarbeiten ist das folgende Kästchen zum Vorschein gekommen.  Es stammt aus der Zeit als ich noch mit Amgia, aber auch schon mit PCs zu tun hatte – ich schätze so ca.  um 1996. Das Kästchen beschriftete ich mit „DB50XG MIDI – Wavetableprozessor“.

Das Fundstück aus der Kiste

Darin befindet sich eine Platine von Yamaha, die sich eben DB50XG nennt. Diese Platine war als Tochterplatine für PC-Soundkarten mit „Waveblaster“ Erweiterungsport konzipiert. Sie erweiterte die Soundkarten um einem polyphonen MIDI – Wavetable – Sampler. So konnte der General Midi Standard und der Yamaha XG Standard wiedergegen werden.  Heute macht sich darüber niemand mehr Gedanken. Wenn man damals mit einem PC aus Midi – Daten Sounds erzeugen wollte, dann war entweder eine externe Hardware notwendig, oder eben eine Soundkarte mit einem OnBoard Midi Synthesizer oder Wavetable Chipsatz. Der PC übernahm dann die Steuerung, das Senden und Empfangen der Midi Daten über eine Sequenzer Software. Heute werden die Midi Sounds direkt am PC generiert und die Samples und Tonmodelle in die Software eingebunden. Damals reichte die Leistung der PC-Hardware dazu nicht aus. Wenn sich jetzt jemand gerade fragt, worüber ich hier palavere – was ist Midi und wofür benötigt man das? – dann sei hier kurz gesagt: Midi ist die Abkürzung für „Musical Instrument Digital Interface“ – also eine digitale Schnittstelle – ein Datenprotokoll für Musikinstrumente. Es dient – grob erklärt – dazu, elektronische Musikinstrumente untereinander zu vernetzen und zu steuern. So kann zum Beispiel über ein einziges Keyboard eine Vielzahl von klangerzeugenden Geräten gesteuert werden. Wie der Midi Standard funktioniert, wie die Datenpakete aussehen und das elektrisch aussieht, werde ich hier nicht erläutern. Dazu findet man, wie immer, reichlich Informationen im Netz.

Im Inneren des Kästchens

Zurück zum selber gebastelten Kästchen. In die Plastikbox habe ich damals das DB50XG gepackt und vom „Waveblaster“-Port, einer 26poligen Buchsenleiste, die notwendigen Leitungen zur Inbetriebnahme der Midi Platine nach Außen geführt. Und das war ziemlich simpel. Die Platine benötigt eine Spannungsversorgung von +/-12V und +5V. Es gibt einen Midi-IN und einen Midi-OUT (Through) Pin, einen Reset-Pin und zwei Analog Audio Out Pins – je einen pro Kanal. Die untenstehende Tabelle zeigt die Pinzuordnung des Steckers:

Pin Nummer Zuordnung
1 Digital Masse
2 nicht verbunden
3 Digital Masse
4 nicht verbunden
5 Digital Masse
6 Versorgung +5V
7 Digital Masse
8 nicht verbunden
9 Digital Masse
10 Versorgung +5V
11 Digital Masse
12 nicht verbunden
13 nicht verbunden
14 Versorgung +5V
15 Analog Masse
16 nicht verbunden
17 Analog Masse
18 Versorgung + 12V
19 Analog Masse
20 Audio out rechts
21 Analog Masse
22 Versorgung -12V
23 Analog Masse
24 Audio out links
25 Analog Masse
26 Reset

Der ganze Aufbau war damals eher sehr spartanisch gestaltet. Die Stromversorgung musste über ein, oder mehrere externe Netzteile hergestellt werden. Es gab keine galvanische Signaltrennung mittels Optokoppler. Da musste ich mich auf den ordentlichen Aufbau des Midi-IO-Controller verlassen, den ich an den Amiga angeschlossen hatte. So durfte das natürlich nicht bleiben. Und das schöne DB50XG Board nicht mehr zu verwenden, oder dem Elektronikschrott zuzuführen, bringe ich nicht über´s Herz. Der Plan der daraus entstand, war, ein neues Interfaceboard zu entwickeln – oder basteln, das möglichst universell einsetzbar werden sollte. 

DB50XG

Diese Idee ist nun schon wieder einige Jahre her und immer wieder einmal habe ich ein wenig daran gearbeitet. Folgende Punkte, so habe ich mir ausgedacht, sollte das Interfaceboard erfüllen:

  • eine einfache Spannungsversorgung soll das Yamaha Board mit Energie versorgen. Idealer Weise soll ein USB-Port und optional ein Anschluss für ein Universalnetzteil vorhanden sein. Alle benötigten Spannungen sollen auf dem Interfaceboard aus den 5VDC generiert werden.
  • Das DB50XG soll, wie seinerzeit, auch als „Huckepack“ Platine aufgesteckt werden können
  • Das Midi-in Signal soll über die 5polige DIN Buchse und auch über einen Pinheader eingespeist werden können – natürlich schön entkoppelt (Damit kann auch ein Microcontroller wie Arduino und co. ganz ohne Aufwand angeschlossen werden)
  • Der Ton, also das Audiosignal soll pro Kanal über je eine Chinch-Buchse und auch als 3.5mm Klinkenbuchse und über einen Pinheader zur Abnahme bereitstehen.
  • Wortwiederholungen SOLLTEN vermieden werden, ist mir aber egal 🙂

Daraus entstand schlussendlich der folgende Schaltplan. Die 5VDC Versorgung der USB Quelle wird direkt zur 5V Versorgung des Midi Boards geführt. Die ebenfalls benötigten +12V/-12V erzeugt ein DC/DC Converter (TMR0522). Dieser wird eingangsseitig vom 5V Netz versorgt. Der optionale „Externe“ Spannungseingang gelangt an einen LM2596ADJ. Das ist ein Step-Down Voltage-Regulator der mit Eingangsspannungen bis zu 40V arbeiten kann. Die geregelte Ausgangsseite ist in vielen Bereichen verfügbar. Ich habe hier den ADJ (Adjustable) Typ in die Schaltung integriert, da ich davon einige Stück im Sortiment Kasten habe. Durch einen Jumper am Board ist, die Spannungsquelle wählbar.

Auf Basis dieses Schaltplanes habe ich ein Layout erstellt und es vorerst einmal im eigenen Ätzbad hergestellt. Heraus kam die folgende Platine, die als Testaufbau diente. Technisch funktionierte das Board einwandfrei, jedoch die Anordnung der Komponenten hat mir nicht gefallen. Den Step-Down Converter samt Spule hatte ich auf der Rückseite platziert. Auch war mir der Abstand zwischen den Anschlussbuchsen zu eng beieinander. Und wie man das als PCB Layouter so macht – man macht immer ein zweites Design. So auch dieses Mal.

Der Testaufbau mit bestücktem Midiboard ist im nachfolgenden Bild zu sehen. Das Midi-Signal als Testquelle kommt vom PC und wird durch einen USB-Midi Adapter aus Fernost generiert.

Also noch einmal vor den Rechner gesetzt und das Layout umgezeichnet. Heraus gekommen ist dann die folgende Version. Diese Ausführung habe ich dann bei einem Leiterplattenhersteller bestellt.

Die schlussendlich gefertigte Platine in bestücktem Zustand sieht dann so aus. Darunter ist sie  mit dem aufgesteckten DB50XG Board zu sehen.

 

UV-Sensor Logger selbstgebastelt

163 total views

Kommt der Sommer, kommen neue Ideen. In den Sommermonaten ist ja bekanntlich die Sonnenscheindauer länger und auch die Intensität der Sonnenstrahlen höher. Viele nutzen diese Eigenschaft der Sonne, um ihre Vitamin-D Produktion des Körpers anzutreiben, andere wiederum legen sich unter die Strahlenquelle um durch den hohen UV Anteil ihrer Hautfarbe abzudunkeln. Dies wiederum steigert vermeintlich deren Attraktivität und regt die Hormonproduktion und die Paarungsbereitschaft an… Leider hat der nicht sichtbare UV Bereich im Spektrum des Sonnenlichts bekanntlich auch negative Auswirkungen auf den menschlichen Körper. Auch technisch kann das Sonnenlicht genutzt werden. Durchschnittlich wird die Leistung der Sonne pro Flächeneinheit mit 1000W pro m² angenommen. Großflächige P-N Übergänge in Halbleitermaterialien schaffen mittlerweile mit einem Wirkungsgrad von bis zu 22% daraus elektrische Energie zu erzeugen.

Man kann die Energie aber auch noch anders nutzen, bzw. den UV-Anteil. Vielen Retrosammlern ist sicherlich das Problem mit den vergilbten alten Kunststoffgehäusen bekannt. Um das in den Griff, bzw. wieder in den Ursprungszustand von vor 30, 40 Jahren zu bekommen, verwendet man H2O2 also Wasserstoffperoxid und UV Licht um so einen Bleichprozess in Gang zu bekommen. Und so kam ich zur Idee für folgendes Projekt.

Bei einem online-Elektronik-Laden fand ich im Abverkaufs Angebot ein UV-Sensor Board des Herstellers Waveshare. Darauf befindet sich ein LITEON OPTOELECTRONICS LTR390 Chip samt Levelshifter-Schaltung. Als Interface steht ein I²C Bus zur Verfügung. Ein Blick ins Datenblatt verriet mir, dass der Sensor zwei Wellenlängenbereiche erfasst und separat ausgibt.  Der ALS (Ambient Light Sensor von 500-600nm) und der UV (Ultra Violett Bereich von 300-350nm).  Damit kann man doch schnell ein einfaches Logging Board basteln – dachte ich mir.  So habe ich mir gedacht, das Board sollte folgendes können:

  • Spannungsversorgung von einer 18650er Zelle oder USB
  • USB soll den Akku auch laden können
  • einen Micro-SD Slot zum Aufzeichnen der Sensordaten
  • einen RS-232 Port, zum direkten Loggen am PC
  • ein cooles OLED Display
  • zwei Taster zum Bedienen des Loggers (Intervall, Start/Stop etc.)

Die Steuerung soll natürlich wieder einmal ein Chip von Atmega – der 328er übernehmen. Davon befinden sich einfach noch genügend Stück in meinen Sortiment Kästchen. Damit man sich schneller einen Überblick über den Aufbau verschaffen kann, habe ich das folgende Blockdiagramm gezeichnet:

Im nächsten Schritt habe ich aus dem Blockschaltbild einen Schaltplan erstellt, um aus dem dann wiederum ein Layout erstellen zu können.  Parallel zur Schaltplanerstellung habe ich einzelne Bereiche per „Luftverkabelung“ auch gleich probeweise zusammengeschaltet und getestet, ob das alles auch so funktioniert, wie ich mir das vorstelle. Und vor allem sollte auch alles im Flashspeicher des Microcontrollers Platz haben.

Im Bild oben ist der „luftige“ Aufbau bestehend aus fertigen Komponenten zu erkennen. Für die ersten Tests mit dem Sensor und dem OLED Display reichte ein Arduino vollkommen aus. Damit war es mir möglich, die gewünschten Funktionen zu testen. Somit stand der Erstellung des Schaltplanes nichts mehr im Weg. Eine 18650er Lithiumzelle soll als primäre Energiequelle dienen. Alternativ wird auch ein USB-Port vorhanden sein, der die Zelle laden kann bzw. den Sensor betreiben kann. Dafür, weil ich faul bin und auch ziemliche Bauteil Lieferengpässe ein großes Problem sind, verwende ich zum Laden des Akkus eine fertiges Wemos-D1-Mini Board. Das wird genauso wie das OLED Displayboard und das Sensorboard als fertige Komponente auf dem Design der Platine Platz finden.  Als Controller kommt wieder, wie schon erwähnt, ein Atmega328 im TQFP Gehäuse zum Einsatz. Dieser wird über die I²C Schnittstelle mit dem OLED Display (SBC-OLED01 mit SSD1306 Controller) und dem LTR390 UV-Sensorboard kommunizieren. OLED und Sensor sind 5V kompatibel. Die SD-Karte wird aber mit 3.3V betrieben. Dafür benötigt die Schaltung noch einen Spannungswandler von 5V auf 3.3V für die Versorgung und einen Levelshifter für den SPI-Datenbus, über den die SD-Karte mit dem Atmega die Daten austauscht. Da der Atmega dann auch mit seiner Firmware programmiert werden möchte, habe ich einen 2×4 Pinheader für den Anschluss eines Programmers vorgesehen. Sechs Pins davon (GND,5V, MOSI, MISO, SCK und RESET) benötigt der Programmer und die zwei verbleibenden Pins sind für die serielle Schnittstelle vorgesehen. Die beiden Interrupt-Eingänge des Atmega werden mit je einem Taster beschalten, der dann die Software bedienbar macht. Die Batteriespannung wird über einen Teiler an einem der ADC-Eingänge gemessen bzw. auch mitgeloggt. Das Ergebnis dieser Gedanken ist der folgende Schaltplan:

Ein Layout ist danach der nächste Schritt.  Bei einer Größe von 12 x 4,5 cm ist die Platine einigermaßen „handlich“. Die Leiterbahnführung findet auf beiden Seiten statt und die Module (Ladeschaltung, Display und UV-Sensor) sind über Pinheader steckbar ausgeführt.

Die beiden Bilder oben zeigen die Vorschau der „Top-“ bzw. „Bottom-“ Seite des Layouts. Aus den so erstellten Produktionsdaten konnte eine Platine erstellt werden.

Nach einiger Lötarbeit war die Hardware dann soweit fertig. Um diesem „Lötwerk“  letztendlich auch Leben einzuhauchen, bedurfte es einer Software, die auf dem Microcontroller ihre Arbeit verrichtet.

Beim Basteln der Software bediente ich mich der kostenlosen „Arduino IDE“ Entwicklungsumgebung.  Die Dokumentation des LTR390 beschreibt genau über welche Register welche Funktionen des Sensors zu bedienen sind. Es gibt aber auch schon für ganz Bequeme eine fertige Library – so wie für fast alle Sensoren und Aktoren, die an Microcontroller angeschlossen werden sollen. In der Arduino IDE findet man über den Boardmanager die „Adafruit LTR390 Library“ über die man einfach mit dem Sensor kommunizieren kann.  Die Ansteuerung des OLED Displays übernimmt in meinem Fall die SSD1306Ascii Library. Die Buskommunikation übernehmen die „Wire“ und “ SPI“ Library und die „SD“ spricht mit der SD – Karte.  Die Includes sehen dann so aus:

#include <LTR390.h>
#include <SD.h>
#include <SPI.h>
#include <Wire.h>
#include „SSD1306Ascii.h“
#include „SSD1306AsciiWire.h“

Den gesamten Code kann ich bei Bedarf gerne hier veröffentlichen. Er ist allerdings kein Hexenwerk, sondern simples und sicher nicht optimiertes Codezeilen Geschreibe 🙂 In der derzeitigen Code- (Firmware) Version 1.3d gibt es ein kleines Auswahlmenü, das es ermöglicht, das Logintervall der SD-Karten-Aufzeichnung einzustellen und natürlich auch die Aufzeichnung zu starten bzw. zu stoppen. Geloggt wird in ein Textfile. Die aufgezeichneten Daten sind UV-Index, Umgebungshelligkeit und die Akkuspannung.

Einen Auszug aus dem Datalog habe ich unten eingefügt:

 Ambient[lx], UV-indx, Batt[V], Loggingintervall[s]  
 691.60,0.01,3.77,20  
 691.60,0.03,3.76,20  
 1184.00,0.03,3.77,20  
 1184.00,0.03,3.75,20  
 1191.00,0.03,3.77,20  
 1191.00,0.03,3.75,20  
 1198.60,0.03,3.76,20  
 1198.60,0.03,3.73,20  
 1211.60,0.03,3.76,20  
 1211.60,0.04,3.75,20  
 1223.00,0.04,3.75,20  
 1223.00,0.04,3.76,20  
 1234.20,0.04,3.76,20  
 1234.20,0.04,3.74,20  
 1243.60,0.04,3.76,20  
 1243.60,0.04,3.76,20  
 1252.00,0.04,3.75,20  
 1252.00,0.04,3.73,20  
 1261.20,0.04,3.74,20  
 1261.20,0.04,3.72,20  
 1269.60,0.04,3.76,20  
 1269.60,0.04,3.76,20  
 1278.40,0.04,3.76,20  
 1278.40,0.04,3.75,20  
 1288.40,0.04,3.76,20  
 1288.40,0.04,3.75,20  
 1298.20,0.04,3.76,20  
 1298.20,0.04,3.74,20  
 1305.80,0.04,3.73,20  
 1305.80,0.04,3.73,20  
 1313.20,0.04,3.73,20  
 1313.20,0.04,3.75,20  
 1321.60,0.04,3.74,20  
 1321.60,0.04,3.75,20  
 1331.80,0.04,3.75,20  
 1331.80,0.04,3.75,20  
 1341.60,0.04,3.74,20  
 1341.60,0.04,3.76,20  
 1349.40,0.04,3.76,20  
 1349.40,0.04,3.76,20  
 1358.20,0.04,3.72,20  
 1358.20,0.04,3.76,20  
 1365.60,0.04,3.74,20  
 1365.60,0.04,3.73,20  
 1374.20,0.04,3.72,20  
 1374.20,0.04,3.75,20  
 1380.60,0.04,3.75,20  
 1380.60,0.04,3.76,20  
 1386.60,0.04,3.75,20  
 1386.60,0.04,3.76,20  
 1394.80,0.04,3.75,20  
 1394.80,0.04,3.75,20  
 1401.40,0.04,3.73,20  
 1401.40,0.04,3.74,20  
 1408.60,0.04,3.75,20  
 1408.60,0.04,3.74,20  
 1414.20,0.04,3.73,20  

 

Diese Daten lassen sich jetzt sehr einfach weiterverarbeiten und grafisch darstellen. Als Office-Nutzer kann man zum Beispiel auf Excel zurückgreifen und die Daten dort importieren und als Graphen darstellen. Es geht aber noch einfacher und auch sehr schnell mit Tools wie Matlab. Mit einem Script wie dem nachfolgenden kann man die Logdatei dann visualisieren.

 

 %% Setup the Import Options and import the data  
 opts = delimitedTextImportOptions("NumVariables", 4);  
 opts.DataLines = [3, inf];  
 opts.Delimiter = ",";  
 opts.VariableNames = ["Ambientlx", "UVindx", "BattV", "Loggingintervalls"];  
 opts.VariableTypes = ["double", "double", "double", "double"];  
 opts.ExtraColumnsRule = "ignore";  
 opts.EmptyLineRule = "read";  
 opts = setvaropts(opts, ["Ambientlx", "UVindx", "BattV"], "TrimNonNumeric", true);  
 opts = setvaropts(opts, ["Ambientlx", "UVindx", "BattV", "Loggingintervalls"], "DecimalSeparator", ",");  
 opts = setvaropts(opts, ["Ambientlx", "UVindx", "BattV"], "ThousandsSeparator", ".");  
 datalog = readtable("F:\ingmarsretro\datalog.txt", opts);  
 clear opts  
 x=size(datalog); % groesse der tabelle  
 measurement=x(1); % anzahl messungen   
 uvi=datalog{1:measurement,2};  
 ambient=datalog{1:measurement,1};  
 messzeit = linspace(0,(measurement*datalog{1,4}),measurement); %zeitvektor von 0 bis zeitintervall aus datalog spalte4 * messungen  
 figure(1);  
 title('UV - Index');  
 subplot(2,1,1);  
 plot(messzeit,uvi);  
 title('UV - Index');  
 xlabel('Zeit [s]');ylabel('UV - Index');  
 subplot(2,1,2);  
 plot(messzeit,ambient);  
 title('Beleuchtungsstärke');  
 xlabel('Zeit [s]');ylabel('Beleuchtungsstärke [lux]');  

Wird das Script ausgeführt, dann erhält man einen Plot, der die Messdaten visualisiert.

Die technischen Informationen zum Sensor sind dem Datenblatt des Herstellers zu entnehmen. Hier ein paar kurze Eckdaten:

Der LTR390 besteht aus zwei Fotodioden, einer für das sichtbare Spektrum des Lichtes und einer, die im UV-Bereich empfindlich ist. Der Strom der Photodioden wird in internen ADCs digitalisiert. Eine Interne Logic steuert die ADCs und über eine I²C Schnittstelle wird die Verbindung zur Außenwelt hergestellt. Die Auflösung von ALS und auch UVS ist in 13,16,17,18,19 und 20 Bit konfigurierbar. Der Sensor Chip ist in einem 2x2mm 6pin Gehäuse untergebracht. Die Detektoröffnung hat eine Kantenlänge von 280×280 µm.

Quelle: Datenblatt LTR-390UV https://optoelectronics.liteon.com/en-global/Led/LED-Component/Detail/926
Quelle: Datenblatt LTR-390UV https://optoelectronics.liteon.com/en-global/Led/LED-Component/Detail/926

 

 

Eigenbau Nixiuhr

43 total views

Das in den letzten Jahren das Thema Retro immer mehr zum Trend wurde, ist auch mir nicht entgangen. Auch der „Industrial“- und „Steam“-Style hat in vielen Haushalten Einzug gehalten.  Man(n) stellt sich wieder viele Dinge ins Regal, die die robuste Technik und das Aussehen der vergangen Jahrzehnte repräsentieren. So flackern LED-Leuchtmittel in den Räumen, die optisch den Glühbirnen der Gründerzeit nachempfunden wurden. Die Messing Lampenfassungen werden von einem mit Stoffgeflecht ummanteltem Kabel gehalten. Anstelle der Kohle- oder Wolframglühfäden in den Birnen arbeitet modernes LED-Filament. Thematisch diesem Stil entsprechend, sind beispielsweise auch mechanische Uhren und elektrische Uhren mit Leuchtanzeigen aller Art wieder gefragt. Passend zu diesem Trend, habe ich in älteren Blogbeiträgen schon über die VFD-Uhren berichtet. (VFD = VaccumFLuoreszenzDisplay) Diese Anzeigetechnologie verwendete man zum Beispiel bis Ende der 90iger Jahre noch häufig in Videorecordern, HiFi Geräten und diversen Radioweckern. Danach war die LED und LCD Technologie Standard. Heute halten überall die kleinen OLEDs Einzug. Im Rahmen des Retro Revivals werden VFD´s in Form von Einzelziffer-Anzeigeröhren zu Uhren zusammengebaut. Diese Uhren gibt es als Fertiggeräte oder auch als Bausätze (grother.de).  Da diese Anzeigeröhren mittlerweile nicht mehr hergestellt werden und nur Altbestände (new old stock) verfügbar sind, steigen auch die Preise. Aber es geht preislich noch schlimmer – eine technische Entwicklung aus den 1920er Jahren ist eine Anzeigetechnologie nach dem Prinzip der Glimmlampe.  Hierbei wird in einem, mit Edelgas gefüllten, Glaskolben eine aus Draht gebogene Ziffer als Kathode, vor einem dünnen Metallgitter als Anode angebracht. Legt man eine Spannung an, so beginnt das Edelgas entlang des als Ziffer geformten Drahtes zu glimmen. So entsteht, von außen betrachtet, der Eindruck einer leuchtenden Ziffer. In einer solchen Röhre sind meistens die Ziffern von 0-9 untergebracht und für jede Ziffer ist natürlich auch ein separater Anschluss vorhanden. Viele von den Lesern werden diese Art von Röhre sicherlich kennen. Sie nennt sich NIXIE – Anzeigeröhre (stammt von der der Bezeichnung „Numeric Indicator eXperimental No. 1“

Eine Uhr mit solchen Anzeigeröhren fehlt noch in meiner Sammlung. Also möchte ich eine solche haben. Aber kaufen ist einfach – und außerdem auch sehr teuer. So habe ich mir vorgenommen, eine Nixieuhr selber zu bauen. Begonnen hat alles mit einer langwierigen Suche nach den Röhren, denn auch für diese muss man mittlerweile schon einiges hinlegen. Und ich benötige mindestens sechs Stück, da meine Uhr auch eine Sekundenanzeige haben soll. So habe ich also im Internet auf verschiedensten Plattformen gesucht – und in der Bucht wurde ich fündig. Dort wurde ein Board bestückt mit Nixieröhren angeboten, das aus irgendeinem alten Gerät herausgebrochen wurde. Die Funktion des Boards wurde als „unbekannt“ angegeben – dafür war es sehr günstig. Der Verkäufer hatte zwei davon. Also riskierte ich es und kaufte die beiden Platinen bestückt mit je fünf Nixies.

Die Röhren waren dann auch mit einiger Vorsicht erfolgreich ausgelötet. Die Type der Röhre ist die Z574M, zu der man im Netz auch die Datenblätter findet und somit auch die Sockelbeschaltung hat.

Mit Hilfe der Beschaltung lässt sie sich dann auch einfach kontaktieren und so Ziffer für Ziffer jeder Röhre überprüfen. Die Kenndaten der 574 sind:

  • Anodenzündspannug: 150V
  • Anodenbrennspannung: 140V
  • Anodenlöschspannung: 120V
  • Max Anodenspannung: 170V
  • Kathodenstrom min: 1.5mA
  • Kathodenstrom max: 2.5mA

Mit einem geeigneten Netzgerät konnte ich die notwendigen Versorgungsspannungen für den Funktionstest schnell einstellen.

Man sieht hier, dass die Röhre bei einer Brennspannung von knapp 140V einen Strom von 2.8mA zieht. Das entspricht einer Leistung von 392mW. Wenn ich also hochrechne und alle sechs Ziffern der Uhr dauerbestromt werden, dann muss die Spannungsversorgung für die Röhren ca. 2.3W bringen.

Die Röhren funktionieren also schon mal. Jetzt kann ich mir Gedanken machen wie die Uhr aussehen soll und noch mehr, wie ich sie konstruieren will.

Die Idee ist, dass ein Mikrocontroller alle sechs Röhren ansteuern soll.  Das will ich mit 8-Bit 4094er Schieberegistern realisieren, wovon je vier Bit für eine Röhre verwendet werden. Diese vier Bit aus dem Shift-Register sollen dann über Binary Coded Decimals (also BCD) die Röhren ansteuern. Da die Röhren aber für jede Ziffer einen Anschluss haben, müssen aus den vier BCD-Leitungen zehn separate Zifferansteuerungen generiert werden. Das wird ein CD4028 erledigen. Der IC CD4028 ist ein „BCD to Dezimal Decoder“. Um die relativ hohen Spannungen der Nixies zu schalten, wird der BCD-Dezimal Decoder einen geeigneten Transistor ansteuern. Hier wird der MPSA42 seinen Dienst verrichten. Das ist ein NPN Bipolar Transistor mit einer Kollektor-Emitter Spannungsfestigkeit von 300VDC bei einem maximalen Kollektorstrom von 500mA.  Um die Röhren möglichst flexibel einsetzen zu können, habe ich mir ausgedacht, für jede Röhre eine eigene Platine zu gestalten. Diese einzelnen Anzeigeplatinen sollen dann auf eine Hauptpatine gesteckt werden. So kann man, sollte ein Digit einmal defekt sein, das betreffende Board einfach herausziehen und es reparieren. Dann muss nicht am Mainboard herum gelötet werden.

Am Mainboard soll der Microcontroller Platz finden. Auch die Nieder- und Hochspannungsversorgung und die Schieberegister sollen am Mainboard untergebracht werden. Die Display-Platinen tragen lediglich die Nixieröhre samt deren Treibertransistoren und den BCD-Dezimal Decoder. Mittels Pfostensteckverbindern sollen sie einfach in das Mainboard einsteckbar sein. Um diese Formulierungen ein wenig einfacher darzustellen habe ich diese Skizze angefertigt:

Auf Basis dieser Idee begann ich nun, die Schaltpläne zu zeichnen. Mit dem Displayboard, auf dem sich die Röhre befindet fing es also an. Der Schaltungsaufbau ist sehr einfach. Über zwei gegenüber liegende Pfostensteckverbinder sollte das Board auf dem Mainboard einen stabilen Halt bekommen. Einer der Steckverbinder versorgt den BCD-Dezimaldekoder (CD4028N) mit den vier Dateneingängen und der 5V Versorgungsspannung für die Logik. Auf der anderen Seite des Boards wird die „Hochspannung“ für die Röhre bereitgestellt.

Daraus konnte ich dann einfach ein Layout erstellen und dieses dann als Prototyp als Platine herstellen.

Nach dem Ätzen und Bestücken der ersten Platine und fünf Weiteren war der erste Schritt der Nixieuhr getan:

Um den ersten Teil des Machwerks zu testen, hatte ich an meiner Arbeitsstelle ein DEB100 Digital-Experimentierboard zur Verfügung. Das folgende Kurzvideo zeigt das Testergebnis:

Nachdem dann alle sechs Boards bestückt und getestet waren, hatte ich mich mit der Planung des Mainboards beschäftigt. Zu Beginn stand natürlich wieder die Erstellung eines Schaltplanes. Aus einer externen einer 12VDC Quelle, die idealer Weise ein simples Steckernetzteil sein sollte, mussten die Versorgungsspannungen generiert werden. Zum einen benötigte ich eine 5VDC Versorgung für den Microcontroller, die Schieberegister und die BCD Decoder und zum anderen eine „Hochspannung“ von 140VDC für die Nixieröhren. Die 5V waren schnell erledigt – hier sollte ein 7805 Längsregler seinen Dienst verrichten. Da die Stromaufnahme der digitalen Komponenten relativ gering ist, bedurfte es hier keiner aufwendigen Maßnahmen. Die 7V Differenz am 7805 bei den paar Milliampere packte er ohne großartige Verlustleistungswärmeabgabe. Für die Erzeugung der 140V bastelte ich einen Step-Up – Konverter mit einem MC34062 (Inverting Regulator – Buck, Boost, Switching) Controller, der über einen FET eine 220uH Induktivität schaltet. Über einen Spannungsteiler mit Trimm Poti am Ausgang lässt sich eine Spannungsrückmeldung an den Komparator Ausgang des Controllers senden und somit die Ausgangsspannung einstellen. Als Microcontroller nehme ich für die meisten meiner Projekte (aufgrund des Lagerstandes 🙂 ) immer Atmega328 und Ähnliche. So auch hier. Das Ergebnis ist folgender Schaltplan:

Daraus habe ich wieder ein Layout gebastelt und wieder ein Board geätzt und bestückt. Allerdings wurde dieses Prototypen Testboard nur eine Version mit vier Digits. Der Grund war auch, dass ich keine größere Roh-Platine zur Verfügung hatte 🙂

Daraus habe ich wieder ein Layout gebastelt und wieder ein Board geätzt und bestückt. Allerdings wurde dieses Prototypen Testboard nur eine Version mit vier Digits. Der Grund war auch, dass ich keine größere Roh-Platine zur Verfügung hatte 🙂

Nach diversen erfolgreichen Tests mit dem Prototypen Board, bestellte ich mir beim Platinen Herstellers meines Vertrauens professionell gefertigte Boards. Nach dem Bestücken derselben erstellte ich mir dann ein Testprogramm das alle Digits ansteuern konnte. Ein kurzes Testvideo ist unten verlinkt:

Wie die Uhr dann mit den „schön“ gefertigten Boards aussieht, zeigen die folgenden Fotos. Um das ganze Werk noch etwas nostalgischer zu gestalten, hatte ich die Idee die Boards auf einer gefrästen Holzplatte zu montieren. (Danke an Gebhard für die Holzarbeiten). Um die Uhrenelektronik auch dauerhaft staubfrei zu halten, ließ ich mir eine transparente Plexiglashaube anfertigen.

Skizze für die Arcylglashaube

Die Software habe ich wie so oft mit der Arduino IDE gebastelt. Zum Flashen des Microcontrollers verwende ich den AVRISP mkII Programmer.  Wenn jemand am Code interessiert sein sollte kann ich ihn hier im Blog auch posten.

 

Videostreaming im Auto – Android Auto – die Billiglösung

98 total views

Da in der zurzeit etwas wärmeren Jahreszeit meine Freizeitaktivitäten vermehrt im Outdoor Bereich stattfinden, leidet das Schreiben von den Weblogs ein wenig. Ich arbeite aber trotzdem an einigen Projekten, Reparaturen und Restaurationen. So kommt auch wieder einiges an Material zusammen, um daraus -in der kälteren Jahreszeit- Beiträge zu verfassen. Diesmal habe ich mich nur wieder über die Abzocke und Preisgestaltung im KFZ – Bereich geärgert und nach einer alternativen Lösung gesucht.

Es geht dabei um mein fünf Jahre altes Auto, das mit einem OnBoard Navigationssystem ausgestattet ist. Die Navigationsdaten sind auf einer im Fahrzeug gesteckten SD-Karte gespeichert. Soweit so gut. Die Kartendaten des Fahrzeuges sind mittlerweile allerdings auch in die Jahre gekommen und vieles ist nicht mehr aktuell. Sowas stört besonders wenn man sich auf einer Urlaubsreise befindet und das Navi das Ziel nicht kennt, oder den Weg dorthin nicht verzeichnet hat. Alles kein Problem dachte ich mir, Kartendaten sind ja auf der SD-Karte – da gibt´s doch sicher Updates. Und ja es gibt sie – allerdings kosten die Kartenupdates von 200Euro aufwärts und mehr. Dafür bekomme ich ja ein komplettes Navigationsgerät samt aktuellen Karten mit gratis Onlineaktualisierung.

Also habe ich versucht, mich schlau zu machen und eine aktuelle Karte im Netz zu finden und auf die SD-Karte zu speichern. Aber das klappt natürlich nicht. Hier werden einige Sicherheitsmechanismen angewandt. So ist zum Beispiel die Hardware ID der Speicherkarte im Navigationssystem hinterlegt (kodiert). So scheiterte mein erster Versuch die originale Navikarte als Image auf eine neue SD-Karte zu kopieren. Sie wird als nicht gültige Karte erkannt. Und mit Diagnosegerät VCP und VCDS im Navirechner ohne Anleitung herumzubasteln ist mir dann zu viel Aufwand. Also musste eine andere Möglichkeit her. So ist auf jedem Smartphone ein online Navi installiert – nennt sich Google Maps. Und zudem gibt es auch einige offline Navisystemen die kostenfrei von den Webstores zu laden sind.  So war nun meine Idee das Auto um eine Phone Mirror Funktion zu erweitern. (diese Dinge nennen sich Android Car Play in der Obstabteilung usw.) Da meine alte Kiste nichts davon im Entertainmentsystem bereitstellt gab es für mich folgende Alternativen:

Entweder kaufe ich ein China Navi zum Nachrüsten – und dabei meine ich die, den originalen Boardmonitoren des Autos nachempfundenen Bildschirme, in die dann ein Android Rechner eingebaut ist. Dort können dann die entsprechenden Apps zur Navigation und andere Spielereien installiert werden. Die Daten des originalen Bildes des Auto Infotainment Systems werden natürlich weiterhin dargestellt. Solche Systeme sind in der Größenordnung von 400-600 Euro zu bekommen. Dazu kommen dann noch ein paar Stunden an Einbau- (Bastel-) Arbeiten.

Eine andere Möglichkeit ist ein Retro Fit Umbau. Das bedeutet, ich baue das höherwertige Infotainment System mit dem entsprechenden Funktionsumfang in das Fahrzeug ein. Das wiederum bedeutet: das alte System ausbauen, ein neues System vom Fahrzeughersteller samt allen notwendigen Steuergeräten, Kabelbäumen, Abdeckblenden etc. zu kaufen, dann einzubauen und im Anschluss alles mit viel Aufwand codieren, Parameter einspielen usw. Die Kosten sind immens und rechnen sich auf keinen Fall (>2500,- wenn’s reicht) und dann noch die Arbeit für den Aus- und Einbau. -> alles kann man das vergessen.

MiraScreen Empfänger

Und hier die letzte Möglichkeit für alles zusammen gerade mal 50Euro und mit einem Aufwand von 30 Minuten Einbau bestehend aus folgenden Punkten:

 

 

  • die Video In Motion (VIM) Funktion des Displays bzw. der Radiounit freischalten
  • ein AMI Kabel mit Composite Video In und Audio In erwerben
  • einen MiraScreen WLAN-Receiver um gerade mal 40Euro erwerben, der imstande ist, das Videosignal auch per CVBS auszugeben
  • das ganze Teil (in diesem Fall) in der Ablage der Mittelkonsole einbauen
  • das Kabel für die Energieversorgung der Mirabox durch die Ablage zur 12V Steckdose verlegen und anschliessen.

Diese Arbeiten sind schnell gemacht und das Smartphone kann per „Stream“ (im Android Smartphone unter „Kabellose Übertragung in Bluetooth- & Geräteverbindung“) verbunden werden. Jetzt wird der Bildschirm und der Ton des Smartphones auch über das Infotainmentsystem des Fahrzeugs wiedergegeben.

AMI Videokabel

Das AMI-Videokabel wird an der AMI Buchse des Fahrzeugs angesteckt und die analogen Video- und Audioleitungen mit den Chinch-Steckern des MiraScreen Anschlusskabels verbunden.

Stecker für Versorgung, Video und Audio an den MiraScreen

Die Energieversorgung für den Mira Screen habe ich direkt von der 12V Steckdose hinter der Mittelarmlehne entnommen. Dazu habe ich den Stecker der 12V Dose ausgepinnt, je einen Draht an 12V und GND angelötet und wieder eingepinnt.  An das andere Ende der beiden Drähte habe ich eine 13,5mm Tamiya Kupplung gekrimpt. Zusätzlich hat die 12V Leitung auch noch eine Flugsicherung erhalten. An diese Tamiya Kupplung wird nun das, durch die Ablage gefädelte Mira Screen Anschlusskabel gefädelt und der entsprechende Tamiya Stecker aufgekrimpt. Um das Kabel durch die Ablage zu bekommen, habe ich einfach ein 7mm Loch gebohrt und einen Gummikantenschutz in das Loch gesteckt.

Kabeldurchführung

Ist das Kabel nun angeschlossen, dann kann die Box angesteckt und in der Ablage verstaut werden.

Im Bild oben ist die Box fertig angeschlossen und in der Mittelarmlehnenablage zu erkennen.

Ist die Lehne heruntergeklappt, so ist von der Box nichts mehr zu erkennen. Man kann sie nach Abziehen der Stecker auch wieder einfach und schnell entfernen.

Nachdem die Zündung nun eingeschaltet ist, kann man im Multimediasystem „Media“ auswählen und dort dann auf CVBS-Videoeingang anklicken. Jetzt sollte der Startschirm der Mira Screen Box zu sehen sein. Man kann die Mira Screen Box auch konfigurieren, indem man das Handy per WLAN mit der SSID „MIRAxxxx“ verbindet und die IP Adresse, die auf dem Startbildschirm angegeben ist im Browser des Smartphones eingibt. Das SSID Password steht ebenfalls am Startbildschirm.

Die Fotos oben zeigen das Innenleben der Box. Bei diesem Gerät hatte sich der Pinheader der Stack-Platine aus der Buchsenleiste teilweise gelöst und dies hatte zu Kontaktproblemen der beiden Platinen geführt. Der Messingabstandhalter (im letzten Bild unten links zu erkennen) ist 2mm zu lange, sodass die beiden Platinen nicht ordentlich zusammenhalten. Als Abhilfe habe ich die beiden Abstandhalter um diese 2mm gekürzt und wieder verschraubt. Somit kann ich die Google Maps problemlos im Auto benutzen.

 

 

 

Der Wetterfrosch 2.0 oder Umweltdatenlogger

45 total views

Vor ein paar Jahren habe ich ein Projekt vorgestellt, in dem ein Raspberry Pi als Datenlogger arbeitete. An diesen Raspberry waren ein paar Sensoren angeschlossen, die Umweltdaten, wie Lufttemperatur, relative Luftfeuchte, den Luftdruck und auch die aktuelle GPS Position aufzeichneten. Die Sensoren bestanden größtenteils aus fertigen Breakoutboards, die über die diversen Busse (I²C, Serial, SPI…) an den RaspberryPi angeschlossen waren. Am PI selber liefen Python Skripten, die das Auslesen der Sensoren übernahmen, die Daten zusammenfassten und auf einen USB-Flashspeicher ablegten. Dieses Sammelsurium an Komponenten hatte ich dann in eine Kunststoffbox mit einer Größe von 150x80x50mm eingebaut.

Doch es geht auch um einiges kleiner. Im Rahmen eines kleinen Projektes war es die Aufgabe, diesen Sensor/Datenlogger zu verkleinern. Mein Ansatz, das zu realisieren, war ganz einfach: „Alles neu“. So habe ich das Konzept folgendermaßen geändert:

  • der RaspberryPi wird durch einen Microcontroller ersetzt
  • es wird eine Platine erstellt, auf der sämtliche Komponenten untergebracht sind
  • die erfassten Daten werden auf einer MicroSD Karte gespeichert
  • das Board ist auf die wesentlichsten Komponenten reduziert. Die Sensorelektronik und der SD-Card Reader wird direkt auf dem Board platziert
  • ein GPS-Empfänger (in Form eines Breakoutboards) soll optional aufgesteckt werden können
  • die Programmierung des Controllers wird durch eine ISP Schnittstelle durchgeführt
  • die Spannungsversorgung beträgt 5V DC

Daraus habe ich folgendes Blockschaltbild erstellt:

Blockschaltbild

Das zentrale Element ist, wie so oft, der Microcontroller Atmega328. Er benötigt als externe Beschaltung lediglich einen Quarz als Taktstabilisierung. (genauer gesagt bietet er aber auch die Optionen interne Oszillatoren zu benutzen…) Der Microcontroller kommuniziert über den I²C Bus mit den Sensoren HYT939 und BME280. Über die ausgeklügelte bidirektionale Levelshifter Schaltung mittels BSS138 Mosfet mit integrierter Body Diode wird die Anpassung der Pegel von 5V auf der Controller Seite zu den 3,3V auf der Sensorseite realisiert. Diese Schaltung wird sowohl für die SCL- (Serial Clock), als auch für die SDA-Leitung (Serial Data) angewendet.

Die Datenspeicherung findet auf einer MicroSD-Karte statt. Dafür wird ein Card Slot verbaut, der per SPI (Serial Peripheral Interface) mit dem Controller kommuniziert. Auch hier ist eine Anpassung der Signalamplituden notwendig. Das übernimmt dieses Mal jedoch der Chip TXB0108 von Texas Instruments. Das ist ein 8Bit Bidirektionaler Levelshifter.

Ein Taster wird die Datenaufzeichnung starten und stoppen und eine LED soll diverse Statusmeldungen durch Blinkfolgen darstellen.

Das optional aufsteckbare GPS Modul arbeitet mit 5V Spannungsversorgung und die Pegel der seriellen Datenkommunikation (RS232) sind ebenfalls 5V kompatibel.

Zu guter letzt ist natürlich auch die Spannungsversorgung zu planen. Hier soll lediglich eine externe, stabilisierte 5VDC Quelle angeschlossen werden, um den Logger zu versorgen. Die für die Sensoren und SD-Card benötigten 3,3VDC werden am Board mittels einem LDO (Low Drop Out) Regler erzeugt.

Sind alle Komponenten und deren Zusammenspiel definiert, dann wird daraus der Schaltplan gezeichnet. Für meine Bastelprojekte verwende ich hauptsächlich den Schaltplan- und Layout Editor „Eagle“.  Aus dem Blockschaltbild ergibt sich die unten abgebildete Schaltung.

Aus dem Schaltplan habe ich ein Layout mit zwei Layern erstellt, dessen Grundriss die Abmessungen 55x25mm hat. Bis auf die Steckverbinder befinden ausschließlich SMD Komponenten auf dem Board.

Im Layout Tool gibt es die Funktion, eine optische Vorschau der gefertigten Platine zu betrachten. So kann man vorab überprüfen, ob die Platine den Vorstellungen entspricht und gegebenenfalls die Lage der Bauteile optimieren. Ist das erledigt, wird aus dem Design ein Paket mit Produktionsfiles (Gerberdateien) erzeugt und das dann dem Platinen Hersteller seines Vertrauens gesendet. Da der auch sehr, sehr weit weg angesiedelt ist, dauert die Produktion auch ein paar Tage. Aber schlussendlich kommen die Platinen an und können sich auch sehen lassen.

Die beiden Bilder oben zeigen die Platine von der TOP und der BOTTOM Seite. Der nächste Schritt besteht darin, die Komponenten entsprechend der Planung zu bestellen und danach zu bestücken.

Die Bestückung erledige ich per Hand mit einem, für die SMD-Komponenten geeigneten Lötkolben mit entsprechend kleiner Spitze. Für die ganz kleinen Teile, wie den BME280 Sensor, kommt auch noch ein Mikroskop, bzw. eine Mikroskop Kamera zum Einsatz.

Wie die Platine nach der Bestückung aussieht, zeigen die beiden Bilder oben. Das folgende Foto zeigt den Größenunterschied des fertigen Loggers mit dem aufgesteckten GPS Modul im Vergleich zum alten „Wetterfrosch“Nach der Fertigstellung der Hardware, geht es nun an die Software. Die habe ich praktischer Weise mit dem Arduino IDE Tool gebastelt und per AVRISP mk2 über ISP auf den Controller geflashed. Um den AVRISP auf einem Windows 10 Rechner zum Laufen zu bekommen, muss ein geeigneter Treiber installiert sein. (hier hilft libusb-win32-1.2.6.0)

Programmcode mit der ArduinoIDE erstellt
Controller mit AVRISPmkII geflashed

Auf der SD-Karte wird nach Anlegen der Versorgungsspannung und nach Betätigen des Tasters die Datenaufzeichnung gestartet. Die Messwerte werden im Sekundentakt geschrieben. Wenn, wie in diesem Beispiel der GPS-Sensor gesteckt ist, so werden auch die GPS Daten mit aufgezeichnet. Die Software zeichnet auch auf, wenn der GPS Sensor noch keinen „fix“ hat. (Da in dem Beispiellog unten noch kein GPS-Fix vorhanden war, sind auch keine gültigen GPS Daten enthalten.)

Beispiel des Datenlogs:

Luftdruck962.41
Luftfeuchte37.05
Temperatur26.96
-----------------------------
$PGACK,103*40
$PGACK,105*46
$PMTK011,MTKGPS*08
$PMTK010,001*$GPGGA,235947.799,,,,,0,00,,,M,,M,,*71
$GPGLL,,,,,235947.799,V,N*73
$GPGSA,A,1,,,,,,,,,,,,,,,*1E
$GPGSV,1,1,00*79
$GPRMC,235947.799,V,,,,,0.00,0.00,050180,,,N*48
$GPVTG,0.00,T,,M,0.00,N,0.00,K,N*32
$GPGGA,235948.799,,,,,0,00,,,M,,M

-----------------------------
Luftdruck962.39
Luftfeuchte36.72
Temperatur26.95
-----------------------------
Luftdruck962.43
Luftfeuchte36.66
Temperatur26.97
-----------------------------

CO2-Messung mit SCD30, Arduino und Matlab

762 total views , 2 views today

Dieses Projekt – eigentlich Miniprojekt – könnte vielleicht auch für den einen oder anderen interessant sein. Es handelt sich um den mittlerweile bekannten und häufig verwendeten Kohlendioxid Sensor SCD30 (CO2-Sensor) des Herstellers Sensirion. Es gibt etliche Projekte die man dazu im Netz findet. Im Rahmen eines schnellen Testaufbaues habe ich versucht, die Daten des Sensors mithilfe eines Arduino Uno Boards auszulesen, um sie dann mit der Software Matlab in einem Plot darzustellen. Die Datenübertragung erfolgt über die Serielle Schnittstelle bzw. über serielles Protokoll des USB-UART.

Um den SCD30 am Arduino anzuschließen, benötigt man die Spannungsversorgung und den I²C Datenbus – also in Summe gerade einmal vier Drähte. Damit ist die minimale Konfiguration erfüllt und die Daten können ausgelesen werden.

Der Sensor selbst arbeitet nach dem Prinzip der NDIR Technologie.  (NDIR = non-dispersive-infrared). Das bedeutet, der Sensor ist also ein kleines Spektrometer. Hierbei wird das zu untersuchende Medium in eine Probenkammer geleitet. Die Probenkammer wird von einer Infrarotquelle durchleuchtet und das IR-Licht durchstrahlt das Medium und einen sehr schmalbandigen Wellenlängenfilter und trifft dann auf den IR-Detektor auf. Die Wellenlänge des Filters ist dabei so ausgelegt, dass genau diejenigen Wellenlängen durchgelassen werden, die von den Molekülen des Mediums (Gases) absorbiert werden. Je nach Anzahl der Moleküle, oder Dichte des Gases, werden entsprechend weniger Lichtstrahlen vom Detektor erkannt.  Eine zweite Messkammer, die mit einem Referenzgas gefüllt ist, dient dabei als Referenz. Ein Controller am Sensor wertet diese Informationen aus und gibt sie in Form von ppm über die I²C (oder umschaltbar auch MOD-Bus) Schnittstelle weiter. Zusätzlich befindet sich auch ein Temperatur und Luftfeuchtesensor am Board, deren Daten ebenso über den Bus ausgelesen werden können. Die voreingestellte I²C Adresse des SCD30 ist 0x61. Die genauen Informationen zum Datenprotokoll sind in den Dokumentationen der Firma Sensirion zu finden.

Idealerweise gibt es, wie fast immer, schon eine fertige, studententaugliche Library für die diversen Microcontroller. So braucht man sich keine Gedanken mehr zu machen und kann die Daten des angeschlossenen Sensors direkt auslesen. Die Beispielprogramme findet man unter den Examples der Libraries.

electrical specifications

Für die Versorgungsspannung des Sensors kann der Arduino mit 3.3V oder 5V dienen. Vorsicht ist jedoch geboten, wenn man den I²C Bus, verwendet: Hier ist der Input High-Level mit 1,75-3.0V festgelegt und der Output High Level mit max. 2.4V. An einem Arduino sind die Pegel aber 5V!! Also muss hier ein Levelshifter eingebaut werden – oder zumindest, für einen schnellen Test geeignete Widerstände.

Der hier angeführte Programmcode stammt im Wesentlichen aus dem Beispiel der Library von Nathan Seidle von SparkFun Electronics:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
By: Nathan Seidle SparkFun Electronics  
Library: http://librarymanager/All#SparkFun_SCD30  
*/

#include <Wire.h>
#include "SparkFun_SCD30_Arduino_Library.h" 
SCD30 airSensor;

void setup()  
{
  Wire.begin();
  Serial.begin(9600);
  //Serial.println("SCD30 Example");
  airSensor.begin(); //This will cause readings to occur every two seconds
}

void loop()  
{
  if (airSensor.dataAvailable())
  {
   // Serial.print("co2(ppm):");
   
    Serial.print(airSensor.getCO2());

    //Serial.print(" temp(C):");
    Serial.print(",");
    Serial.print(airSensor.getTemperature(), 1);

   // Serial.print(" humidity(%):");
    Serial.print(",");
    Serial.print(airSensor.getHumidity(), 1);

    Serial.println();
  }
  else
    //Serial.println("No data");

  delay(500);
}

 

Mit diesen Codezeilen im Arduino Uno und der korrekten Verdrahtung (SDA -> an Arduino A4 und SCL -> an Arduino A5 über einen geeigneten Pegelwandler) geht’s dann mit Matlab weiter. Der Arduino sollte jetzt in einem seriellen Terminal folgende Zeilen ausgeben: (Beispiel)

473,28.5,12.9
473,28.5,13.0
470,28.5,13.1
469,28.5,12.9
466,28.5,12.9
465,28.5,12.7
465,28.5,12.5
463,28.6,12.6
461,28.6,12.5
463,28.5,12.4 … und so weiter

Diese gilt es nun in Matlab einzulesen und über einen definierbaren Zeitraum aufzuzeichnen und gleichzeitig in einem Plot darzustellen. Das Matlabskript hier macht es möglich… (pn falls es jemand benötigt)

Als Ergebnis erhält man dann einen Plot, der den CO2 Verlauf im Raum (in dem Fall am Schreibtisch meines Büros) darstellt.