Meine eigene Version von LogoControl

Willkommen Foren LogoControl Meine eigene Version von LogoControl

Verschlagwortet: 

4 Beiträge anzeigen - 1 bis 4 (von insgesamt 4)
  • Autor
    Beiträge
  • #4017
    Keden92
    Teilnehmer

    Vorweg: ich habe vor ca. einem Jahr angefangen meine Bude auf Smart-Home mittels Logos umzubauen (gelernter Elektroniker, mit einer Vorliebe für‘s Programmieren).
    Aktueller Stand:
    – 8x Logo8 mit jeweils 2 Erweiterungsgruppen
    – VM mit Linux sh 4.19.0-13-amd64 #1 SMP Debian 4.19.160-2 (2020-11-28) x86_64 (64bit) als LogoControl-Server
    – 8 vCPU’s (@2.4Ghz), 8GB Ram

    Habe „LogoControl“ von einem Kollegen empfohlen bekommen war (und bin es auch heute noch) sehr von diesem Projekt begeistert. Meinen größten Respekt davor so etwas als „Freeware“ anzubieten. Für die Arbeit die da drin steckt, Hut ab!
    Zu Anfang (Damals nur 4 Logos, alles Digital) funktionierte auch alles einwandfrei. Mit zunehmendem Ausmaß der Logos und Verarbeitung von Analogwerten kamen jedoch die ersten Fehler hoch. Diese äußerten sich in Form von:
    über die rest-API übermittelte daten (value?set=AnalogWert), kamen an der Logo nicht an.
    Das Serialisieren der API-Aufrufe hatte erst einmal weitestgehend Abhilfe geschaffen.

    Threadsafe?

    Dies ging so lange gut bis ich vor kurzem über die Leistungsgrenze der API hinaus kam. Die Grenze ist bei ca. 5 API-Requests/Sekunde. Dies äußerte sich mit dem Verhalten, dass LogoControl als Prozess einfach „Freez“te. (deckt sich mit der Problembeschreibung aus logo control freez)
    Nach mehreren Anläufen, hatte ich die Schnauze voll und habe mir den SourceCode Runtergeladen. Mit der Unterstützung eines Kasten’s Bier habe ich angefangen diesen zu Analysieren und habe folgende Bugs gefunden: (und mit ein paar kleinen Änderungen auch explicit nachstellen können).

    Im File: RemoteControlService.cs

    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]

    Ist zwar schön, wenn man hier dem Service via ConcurrencyMode Multiple Threads zuweist, bringt aber relativ wenig, wenn dem Konstruktor eine bereits initialisierte Klasse übergeben wird.

    File: Controller.cs

    _httpWebservice = new WebServiceHost(_webservice, new Uri(String.Format("URL", Config.Settings.HttpWebservicePort)));

    Dies hat zur Folge: dass die initialisierte Klasse zwischen zwar verschiedenen Threads übergeben wird, diese jedoch rein Seriell arbeiten. Solange ein API-Aufruf abgearbeitet wird, stehen die anderen Schlange. Die Class _webservice wird mit einer Art AccessLock versehen. Bei einer Prozess-Zeit von ca. 200ms/Request, ist ab der 6 Anfrage/Sekunde irgendwann Schluss. Nämlich dann, wenn die Warteschlange überläuft -> Application Freez.

    Nach dem ich den Kompletten Webservice kurzerhand umcodiert habe, sodass Anfragen mittels Multipler Threads gleichzeitig abgearbeitet werden, betreibe ich diesen nun mit ca. 30 API-Request‘s/Sekunde und es ist noch einiges an Luft nach oben. Quellcode hierfür kann ich gerne zur Verfügung stellen.

    Seit dem der Web Service nun wirklich Multi-Request fähig ist, habe ich mit Daten Kollisionen zu kämpfen:

    Fehlerbild:
    API-Aufruf für value?set=XXX bekommt Responscode 200, der Wert kommt jedoch auf der Logo nicht an.
    Nach weiterer Quellcode Analyse kam ich bis in die LogoConnection.cs. In der Methode

    public void SetBit(int byteNumber, int bitNumber, bool state)

    ist es vorbildlich mit

    lock (_deviceAccessLock)
                {

    Threadsafe Programmiert. In der Methode

    public void SetBytes(int startOffset, byte[] data)

    Fehlt der AccessLock hingegen. Verfolgt man nun den Quellcode weiter bis in die Tiefen der LibNoDave, kommt irgendwann die Zeile:

    res = send((SOCKET)(dc->iface->fd.wfd), dc->msgOut+dc->partPos, size, 0);

    Heißt im Umkehrschluss: die LibNoDave ist NICHT THREADSAFE. (Hatte ich jetzt auch nicht wirklich erwartet.) Beim Schreiben von Daten auf die „Socket Connection“ kommt es zu Daten-Kollisionen, die, im harmlosesten Fall, dafür sorgen, dass „nur“ die Daten auf der Logo nicht ankommen.

    Ob die Lösung mittels lock() hier sinnvoll ist, kann man jetzt drüber streiten. Ich sage mal „Besser als nichts“ auf jeden Fall, würde es jedoch für sinnvoller erachten hier mittels Access List (Allein wegen FiFo-Multithreading) den Zugriff zu realisieren. Werde mich bei Gelegenheit mal mit einer alternativen ThreadSafe’n AccessLogik beschäftigen. Hier kann man im Bereich „Latenz“ gegenüber der API bestimmt noch einiges rausholen. Zumindest beim Schreiben von Daten ich Richtung Logo.

    STAND HEUTE:

    An der Access-Logik habe ich noch keinen Handschlag getan, funktioniert erstmal auch so.

    Stattdessen habe ich noch viele andere Kleinigkeiten angepasst wie zb. Asynchrones Event-Handling im Bereich des Event-CallBacks wenn sich daten auf der Logo geändert haben, das Thread-Management allgemein oder einen Application-Exit wenn die im begrenzung word werte beschrieben Begrenzung überschritten wird.
    Einen Haufen Bug’s behoben und die Stabilität der Application Optimiert.
    Zusätzlich einiges an Debug-Funktionen im Bereich Laufzeitanalyse, Latenzen und Thread-Delay. Wobei es bei den letzten Punkten lediglich um Performance-Optimierung geht.
    Des Weiteren habe ich in dem Projekt eine MySQL-Schnittstelle Implementiert, sodass die ganze Variablen der Logo’s gleichzeitig in eine DB weggeschrieben werden können. Im Anschluss kann man dann mit Hilfe eines einfachen LAMP-Servers das Lesen der Daten mittels Multipoint-API um ein vielfaches beschleunigen.

    #4048
    msigma
    Teilnehmer

    Hallo Keden92,

    ich denke das meiste deines Posts übersteigt mein Verständnis bei weitem, allerdings bin ich vorletzten Satz hängengeblieben.

    Ich habe eine einfache Poolsteuerung auf einer Logo! 0BA7 realisiert und kann die Stati und Werte über LogoControl abrufen.
    Ich suche jetzt nach einem Weg die Daten in einer Datenbank (MariaDB) zu speichern damit ich z.B. die Temperaturverläufe mit Grafana visualisieren kann.

    Hast Du die MySQL-Schnittstelle in den Code integriert oder kann man das in eine bestehende Installation „nachrüsten“?

    Gruß msigma

    #4049
    Keden92
    Teilnehmer

    Hallo @msigma,

    Ich habe diese im Projekt implementiert, sodass die Werte direkt nach dem diese von der Logo geliefert werden, weggeschrieben werden.

    Die Umsetzung ist nicht ganz einfach, vorallem weil die DB halt auch zum Quellcode passen muss.

    Hier ein Auszug der Klasse:

    using MySql.Data.MySqlClient;
    using System;
    using System.Data;
    
    namespace LogoControl.DataModel
    {
        public class MySQLCon
        {
            private MySqlConnection _Connection;
            private String _TableName;
            public MySQLCon(DataModel.Settings.Mysql_Data mysql)
            {
                _TableName = mysql.TableName;
    
                string connStr = CreateConnStr(mysql.server, mysql.DB, mysql.user, mysql.pass);
    
                //create a MySQL connection with a query string
                _Connection = new MySqlConnection(connStr);
    
                Reconnect();
            }
    
            public void Stop()
            {
                try
                {
                    if (_Connection.State == ConnectionState.Open)
                    {
                        _Connection.Close();
                    }
                }
                catch (MySqlException ex)
                {
                    Console.Out.WriteLine("Stop MySQL: " + ex.Message);
                }
            }
    
            private bool Reconnect()
            {
                if (_Connection.State == ConnectionState.Open)
                {
                    return true;
                }
                else
                {
                    try
                    {
                        _Connection.Open();
                        return true;
                    }
                    catch (MySqlException ex)
                    {
                        Console.Out.WriteLine("Start MySQL: " + ex.Message);
                        return false;
                    }
                }    
            }
    
            public bool StoreData(int id, int attribute, string plcName, string group, string device, double value, string textvalue)
            {
                if (Reconnect())
                {
                    try
                    {
                        using (MySqlCommand cmd = new MySqlCommand())
                        {
                            cmd.CommandText = @"INSERT INTO " + this._TableName +
                                                   "(ID, Attribute, Logo, Group, Device, Val, TextVal, LastChange, LastUpdate)" +
                                                   "VALUES" +
                                                   "(@ID,@ATTRIBUTE,@LOGO,@GROUP,@DEVICE,@VALUE,@TEXTVALUE,NOW(),NOW())" +
                                                   "ON DUPLICATE KEY UPDATE" +
                                                   "LastChange = IF(Val <> VALUES(Val), NOW(), LastChange)," +
                                                   "Logo = VALUES(Logo)," +
                                                   "Group = VALUES(Group)," +
                                                   "Device = VALUES(Device)," +
                                                   "Val = VALUES(Val)," +
                                                   "TextVal = VALUES(TextVal)," +
                                                   "LastUpdate = NOW();";
    
                            cmd.Connection = this._Connection;
    
                            cmd.Parameters.AddWithValue("@ID", SqlDbType.SmallInt);
                            cmd.Parameters.AddWithValue("@ATTRIBUTE", SqlDbType.SmallInt);
                            cmd.Parameters.AddWithValue("@VALUE", SqlDbType.Int);
                            
                            cmd.Parameters["@ID"].Value = id;
                            cmd.Parameters["@ATTRIBUTE"].Value = attribute;
                            cmd.Parameters["@VALUE"].Value = value;
    
                            cmd.Parameters.AddWithValue("@LOGO", plcName);
                            cmd.Parameters.AddWithValue("@GROUP", group);
                            cmd.Parameters.AddWithValue("@DEVICE", device);
                            cmd.Parameters.AddWithValue("@TEXTVALUE", textvalue);
    
                            cmd.ExecuteNonQuery();
                        }
                       
                    }
                    catch (MySql.Data.MySqlClient.MySqlException ex)
                    {
                        Console.Out.WriteLine("Error " + ex.Number + " has occurred: " + ex.Message);
                    }
                }
    
                return true;
            }
    
            /// <summary>
            /// Generates a connection string
            /// </summary>
            /// <param name="server">The name or IP of the machine where the MySQL server is running</param>
            /// <param name="databaseName">The name of the database (catalog)</param>
            /// <param name="user">The user id - root if there are no new users which have been created</param>
            /// <param name="pass">The user's password</param>
            /// <returns></returns>
            private static string CreateConnStr(string server, string databaseName, string user, string pass)
            {
                //build the connection string
                string connStr = "server=" + server + ";database=" + databaseName + ";uid=" +
                    user + ";password=" + pass + ";charset=utf8;";
    
                //return the connection string
                return connStr;
            }
    
        }
    }

    Zum „nachrüsten“ würde ich eher folgendes vorschlagen: (Für die RasPi-Version)

    1. php-cli (inkl. php-curl) mit mysql/mariaDB client & Server nachinstallieren (falls nicht bereits vorhanden). – Gibt es ggf. etliche Anleitungen im Netz, einfach mal nach „LAMP“ googeln.

    2. via CronJob (CronTab) z.b. minütlich folgenden Script zum loggen ausführen lassen:

    <?php
    
    $url = "die_URL_bzw_IP_zu_deinem_LogoControl_Server_oder_RasPI:8088/rest/attributes";
    
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    $json = curl_exec($ch);
    curl_close($ch); 
    
    $dataarray = json_decode($json, true)["attributeUpdates"];
    
    $servername = "localhost";
    $username = "LogoControl";
    $password = "SuperSicher123";
    $dbname = "LogoControl";
    
    // Create connection
    $conn = new mysqli($servername, $username, $password, $dbname);
    // Check connection
    if ($conn->connect_error)
    {
        die("Connection failed: " . $conn->connect_error);
    }
    
    $SQL_values = array();
    foreach ($dataarray as $value)
    {
    	array_push($SQL_values, "(" . $value["D"] . "," . $value["A"] . "," . $value["V"] . ")");
    }
    
    $sql = "CREATE TEMPORARY TABLE DataAkt(
       Device INT(11) NOT NULL,
       Attribute INT(11) NOT NULL,
       Value varchar(10) NOT NULL,
       constraint pk_ primary key (Device, Attribute) USING BTREE
    );
    INSERT INTO DataAkt (Device, Attribute, Value) VALUES " . implode(",",$SQL_values) . ";
    INSERT INTO DataAkt (Device, Attribute, Value) VALUES (0,0, DATE_FORMAT(NOW(),'%H:%i:%s'));";
    
    if ($conn -> multi_query($sql))
    {
    	do
    	{
    		if ($result = $conn -> store_result())
    		{
    			while ($row = $result -> fetch_row())
    			{
    				var_dump($row);
    				echo "<br>";
    			}
    			$result -> free_result();
    		}
    		else
    		{
    			//echo "0 results<br>";
    		}
    		if ($conn -> more_results())
    		{
    			//echo "-------------<br>";
    		}
    	} while ($conn -> next_result());
    }
    
    $conn->close();
    die("OK");
    
    ?>

    Die SQL’s & die Tabellen müssten halt noch entsprechend angepasst / erweitert werden, sodass eine „Verlauf-Log“ entsteht.

    VG Keden92

    #4050
    msigma
    Teilnehmer

    Hallo Keden92,

    vielen Dank für die Anregung mit dem Script, das werd‘ ich mir am WE mal genauer ansehen und ausprobieren.

    Ich werde berichten ob ich es zum Laufen gebracht habe.

    Gruß
    msigma

4 Beiträge anzeigen - 1 bis 4 (von insgesamt 4)
  • Du musst angemeldet sein, um zu diesem Thema eine Antwort verfassen zu können.