Daten von seriell - wie finde ich den Beginn einer 4 Byte Sequenz

Code-Stücke können hier veröffentlicht werden.
Antworten
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

Hallo,
ich habe einen Sensor, der kontinuierlich Messwerte über die serielle Schnittstelle an den Raspberry liefert, alle 100ms jeweils 4 Byte.
Diese 4 Byte bestehen aus (dezimal) 255, Messwert1, Messwert2, Prüfsumme. Habe den PI so umkonfiguriert, dass die serielle Schnittstelle funktioniert.
Mein Problem ist nun, den Anfang der Sequenz zu finden. Mein Code:

#!/usr/bin/env python
import serial
ser = serial.Serial(
port='/dev/ttyAMA0',
baudrate = 9600,
parity=serial.PARITY_NONE,
stopbits=serial.STOPBITS_ONE,
bytesize=serial.EIGHTBITS,
timeout=1
)

while 1:

x = ser.read()
if len(x) > 0:
y = str(ord(x))
print (y)

Es werden kontinuierlich die richtigen Daten ausgegeben. Wie kann ich die Messwerte, also Bytes 2 und 3 isolieren ?
Die 255 als Start können leider auch in den 3 Folgebytes vorkommen.

Vielen Dank schon mal
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@helmuel: Dafür ist die Prüfsumme da. Wenn die nicht stimmt, dann war's kein gültiges Datenpaket.

Ich verstehe den `timeout` hier nicht wirklich. Was soll der, ausser den Code komplizierter zu machen als er sein müsste?

`ser` ist kein guter Name, `x` und `y` sind noch schlechter.

Man sollte keine literalen Zahlen als Wahrheitswerte missbrauchen. Python hat da einen eigenen Datentyp für.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import serial


def main():
    connection = serial.Serial("/dev/ttyAMA0")
    while True:
        connection.read_until(b"\xff")
        first_value, second_value, checksum = connection.read(3)
        if ... == checksum:
            print(first_value, second_value)


if __name__ == "__main__":
    main()
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

@__blackjack__: Vielen Dank für die schnelle und sehr gute Lösung.
Die Funktion soll eine bestehende ín einem grösseren Programm ersetzen, deshalb nur zum testen die einfachen Namen.

Das getestete Programm schaut so aus:

#!/usr/bin/env python3
import serial
connection = serial.Serial("/dev/ttyAMA0",baudrate = 9600)
distance = 0
while distance == 0:
connection.read_until(b"\xff")
dist_high, dist_low, checksum = connection.read(3)
checksum2 = dist_high + dist_low - 1
distance = dist_high * 256 + dist_low
if checksum == checksum2:
print("Debug : ",dist_high, dist_low, checksum,checksum2)
print("Entfernung : ",distance)

Ist es so besser ? Wie kann ich denn abfangen, wenn gar nichts über die serial kommt ?
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@helmuel: Die 9600 Baud ist der Defaultwert. Ich hatte alle Argumente aus dem ersten Code von Dir rausgenommen der sowieso nur den jeweiligen Defaultwert setzt.

Das initialisieren von `distance` vor der Schleife ist unschön. Da es in Python keine Syntax für eine nachprüfende Schleife gibt, nimmt man in solchen Fällen eine ”Endlosschleife” und bricht die bei entsprechender Bedingung innerhalb der Schleife mit ``break`` ab.

Der Code hat auch noch das Problem das die ``while``-Schleife auch bei falscher Prüfsumme verlassen wird wenn `distance` ungleich null ist. In dem Fall ist der `distance`-Wert ja aber gar nicht aussagekräftig.

Bei der Berechnung der Prüfsumme dürfte noch ein modulo 256 fehlen, weil die ja sonst grösser als 255 werden kann, was sie ja nicht werden darf/kann.

Code: Alles auswählen

#!/usr/bin/env python3
import serial


def main():
    connection = serial.Serial("/dev/ttyAMA0", baudrate=9600)
    while True:
        connection.read_until(b"\xff")
        distance_high, distance_low, expected_checksum = connection.read(3)
        checksum = (distance_high + distance_low - 1) % 256
        if checksum == expected_checksum:
            distance = distance_high * 256 + distance_low
            print(
                "Debug:      ",
                distance_high,
                distance_low,
                expected_checksum,
                checksum,
            )
            print("Entfernung: ", distance)
            if distance != 0:
                break


if __name__ == "__main__":
    main()
Ob gar nichts über die serielle Verbindung kommt, kann man dann doch wieder nur über einen timeout ”prüfen”. Man muss die Rückgabewerte von den Leseoperationen dann auf Vollständigkeit prüfen. Also ob die mit `read_until()` gelesenen Daten mit einem b"\xff" enden und ob bei dem anderen `read()` tatsächlich drei Bytes gelesen wurden.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

@__Blackjack__: Vielen Dank für den neuen Code.
Den habe ich jetzt in mein Programm eingefügt und nur noch geringfügig abgeändert:
- timeout ist drin, damit die Funktion auf jeden Fall zu Ende kommt.
- Wenn die Checksum nicht stimmt, wird -1 zurückgegeben.
Die Entfernung wird im Hauptprogramm auf Plausibilität geprüft, und, wenn ungültig wird die Messung wiederholt.
Die Messung ist nicht lebenswichtig sondern eher Statistik. Sie soll meine Zisterne überwachen, in dem sie jede Stunde den Wasserstand ermittelt und eine Warnung rausgibt, bevor die Zisterne leer ist. Dann kann Wasser nachgefüllt werden. Sollte die Messung nicht funktionieren, so wird 70 Minuten nach der letzten erfolgreichen Messung der Raspberry über den eingebauten Hardware Watchdog neu gestarted.

Die neue Funktion schaut jetzt so aus:

def measure():

print ("Measure_Connect")
connection = serial.Serial("/dev/ttyAMA0", timeout=1)
while True:
print(" Read_FF")
connection.read_until(b"\xff")
print(" Read Data")
distance_high, distance_low, expected_checksum = connection.read(3)
checksum = (distance_high + distance_low - 1) % 256
print(
" Debug: ",
distance_high,
distance_low,
expected_checksum,
checksum,
)

if checksum == expected_checksum:
distance = distance_high * 256 + distance_low
print("Entfernung: ", distance, " mm")
else:
distance = -1

break

return distance

Das werde ich jetzt mal eine Zeit so laufen lassen und dann die Debug prints rausnehmen.
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@helmuel: Einfach nur einen `timeout`-Wert zu setzen ist aber nicht genug, man muss dann natürlich auch jeweils prüfen ob eine Zeitüberschreitung aufgetreten ist. Sonst wird es beim ersten Lesen einfach ignoriert und beim zweiten Lesen kommt es zu einem Folgefehler in der Weiterverarbeitung der unvollständigen Daten. Man sollte da besser explizit eine passende Ausnahme auslösen, so das der Aufrufer eine Zeitüberschreitung auch erkennen und behandeln kann wenn er möchte.

Die ``while``-Schleife ist jetzt keine Schleife mehr weil da am Ende bedingungslos ``break`` ausgeführt wird.

Was ich im letzten Beitrag vergessen hatte: Die Verbindung wird geöffnet, aber nicht wieder geschlossen. Man sollte solche Ressourcen immer ordentlich hinter sich aufräumen.

Statt irgendwelcher mehr oder weniger willkürlichen Sonderwerte zurück zu geben nimmt man dafür besser eine Ausnahme.

Statt später `print()`-Anweisungen raus zu nehmen bietet es sich an Logging in irgendeiner Form zu verwenden und die Ausgaben oder eben auch Nicht-Ausgaben über das Loglevel zu regeln. Die Standardbibliothek hat ein `logging`-Modul. Ich persönlich nehme für Anwendungen ganz gerne das externe `loguru`-Modul.

Code: Alles auswählen

#!/usr/bin/env python3
import serial

from loguru import logger


def measure():
    logger.trace("Measure_Connect")
    with serial.Serial("/dev/ttyAMA0", timeout=1) as connection:
        logger.trace("Read_FF")
        skipped_data = connection.read_until(b"\xff")
        if not skipped_data.endswith(b"\ff"):
            raise TimeoutError()

        logger.trace("Read Data")
        try:
            distance_high, distance_low, expected_checksum = connection.read(3)
        except ValueError:
            raise TimeoutError()

        checksum = (distance_high + distance_low - 1) % 256
        logger.debug(
            "{} {} {} {}",
            distance_high,
            distance_low,
            expected_checksum,
            checksum,
        )
        if checksum != expected_checksum:
            raise ValueError(
                f"expected checksum {expected_checksum}, got {checksum}"
            )

        distance = distance_high * 256 + distance_low
        logger.debug("Entfernung: {} mm", distance)
        return distance
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

@__blackjack__: Puh, ist das mühsam ... oder war das alles früher einfach mit PL1, der einzigen Sprache, die ich richtig gelernt habe.
Deinen neuen Code bekomme ich leider nicht zum laufen. Erstens lässt sich loguru nicht installieren (neuestes Raspberry OS , findet schon mal PIP nicht), und ohne das Logging läuft es auch nicht, er bringt gleich den ersten Timeout Error.
Das Beenden der Verbindung habe ich mir auch überlegt. Allerdings wird das Programm nur einmal pro Stunde per Cron aufgerufen, misst hoffentlich nur 1 mal und beendet sich dann wieder . Beim Ende des Programms sollten die Ressourcen doch automatisch freigegeben werden, oder liege ich da auch falsch ?
Da ich bis Ende Juli weg bin, möchte ich morgen eine Version aktivieren, die dann bis dahin laufen sollte, einfach um den Dauereinsatz zu testen.
Vielen Dank für Deine Hilfe, Helmut
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@helmuel: Ich kenne PL/1 nicht aber Python ist eigentlich relativ einfach.

Wenn ``pip3`` nicht gefunden wird, beziehungsweise ``python3 -m pip`` nicht funktioniert, muss man es halt installieren.

Wie sieht denn `skipped_data` in dem Fall aus?

Ob da am Programmende von jemand anderem Ressoucen freigegeben werden ist IMHO eher nicht die Frage. Es ist einfach „best practice“ sich darum zu kümmern und mit ``with`` ja auch nicht wirklich schwer.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Hab mir mal PL/1 angeschaut. Das sieht so nach COBOL, FORTRAN, & Co aus. Es mag sein, dass das ”einfacher” ist, aber dafür auch irgendwo viel kleinteiliger und ”geschwätziger”. Hier wäre so etwas mal in einer Sprache die etwas näher an der Maschine ist (Vala) und einer eingebundenen C-Bibliothek (libserialport) die ähnlich wie das `serial`-Modul in Python eine plattformunabhängige API für serielle Ports zur Verfügung stellt (ungetestet):

Code: Alles auswählen

using LibSerialPort;

const uint TIMEOUT = 1000;  // in ms.

private errordomain SerialError {
    INVALID_ARGUMENT,
    FAILED,
    NOT_SUPPORTED,
    OUT_OF_MEMORY
}

private errordomain MeasureError {
    TIMEOUT,
    CHECKSUM
}

private static uint check(Return result) throws SerialError {
    switch (result) {
        case Return.ERR_ARG:
            throw new SerialError.INVALID_ARGUMENT ("Invalid argument.");
        case Return.ERR_FAIL:
            throw new SerialError.FAILED ("Failed.");
        case Return.ERR_SUPP:
            throw new SerialError.NOT_SUPPORTED ("Not supported.");
        case Return.ERR_MEM:
            throw new SerialError.OUT_OF_MEMORY ("Out of memory.");
        default:
            return result;
    }
}


private uint measure () throws SerialError, MeasureError {
    var port_name = "/dev/ttyAMA0";
    
    Port port;
    debug ("Getting port named %s.\n", port_name);
    check (Port.new_by_name (port_name, out port));
    debug ("Open port.\n");
    check (port.open (OpenMode.READ));
    
    uint8 buffer[3];
    
    try {
        debug ("Setting options.\n");
        check (port.set_baudrate (9600));
        check (port.set_bits (8));
        check (port.set_parity (Parity.NONE));
        check (port.set_stopbits (1));
        check (port.set_flowcontrol (FlowControl.NONE));
        
        debug ("Skipping bytes until 0xFF\n");
        uint8 skip_buffer[1];
        do {
            if (
                (check (port.blocking_read (skip_buffer, TIMEOUT)))
                != skip_buffer.length
            ) {
                throw new MeasureError.TIMEOUT ("Timeout while skipping bytes.");
            }
        } while (skip_buffer[0] != 0xff);
        
        debug ("Reading data\n");
        if (
            (check (port.blocking_read (buffer, TIMEOUT))) != buffer.length
        ) {
            throw new MeasureError.TIMEOUT ("Timeout while reading data.");
        }
    } finally {
        port.close ();
    }
    
    uint8 checksum = buffer[0] + buffer[1] - 1;
    if (checksum != buffer[2]) {
        throw new MeasureError.CHECKSUM (
            "Wrong checksum: Expected %02x, got %02x".printf (
                buffer[2], checksum
            )
        );
    }
    
    return buffer[0] << 8 | buffer[1];
}


public static int main () {
    try {
        print ("Distance: %u mm\n", measure ());
    } catch (SerialError error) {
        print ("Serial error: %s\n", error.message);
        return 1;
    } catch (MeasureError error) {
        print ("Error: %s\n", error.message);
        if (error is MeasureError.TIMEOUT) {
            return 2;
        } else if (error is MeasureError.CHECKSUM) {
            return 3;
        }
    }
    return 0;
}
Hier sieht man schon ein bisschen ein noch tieferes Level durchscheinen weil die Vala-Anbindung an die C-Bibliothek das mit den Fehlercoderückgabewerten einfach übernommen hat statt da noch ein bisschen ”glue code” zu schreiben, der das in Ausnahmen umwandelt. Den habe ich dann mit der `check()`-Funktion selbst geschrieben.

Der Code sieht jetzt nicht einfacher aus als der in Python. Wenn man hier jetzt eine noch einfachere, primitivere Programmiersprache nimmt, wird das ja nicht besser, sondern eher schlechter. In C käme da ohne Ausnahmen mehr Code zur Fehlerbehandlung dazu und um die Speicherverwaltung müsste man sich auch noch selbst kümmern.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

Hallo Blackjack,
Das mit Pl1 war eher so allgemein gemeint, und ja, es war damals als Ablösung für Cobol und Fortran gedacht. Zurück möchte ich nicht. Ich habe im Haus einige jahrzehntealte Steuerungen, die bereits teilweise auf Raspberry sind. Bei Änderungen möchte ich auf Python umstellen, weil es interpretativ ist und damit einfacher zu pflegen ist als das bisherige VB ode VBA.
Hochkomplexe Anwendungen habe ich nicht vor, eigentlich nur GPIO und Mails
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

BASIC, da sagst'e was. Ist ja syntaktisch ähnlich zu den Dinosaurier-Programmiersprachen und die meisten alten Microsoft-BASIC-Dialekte haben auch Untertsützung für die serielle Schnittstelle. In QBasic könnte das wohl ungefähr so aussehen:

Code: Alles auswählen

DECLARE FUNCTION readByte% ()
ON ERROR GOTO ErrorHandler

OPEN "COM1:9600,N,8,1,BIN,CD1000" FOR INPUT AS #1
DO: LOOP UNTIL readByte = 255
h% = readByte: l% = readByte: ecs% = readByte
CLOSE #1

cs% = (h% + l% - 1) AND 255
IF cs% <> ecs% THEN
  PRINT "Expected checksum"; ecs%; "got"; cs%; "instead."
ELSE
  PRINT "Distance:"; h% * 256! + l%; "mm."
END IF
END

ErrorHandler:
  IF ERR = 24 THEN
    PRINT "Timeout.": END
  ELSE
    ON ERROR GOTO 0: ERROR ERR
  END IF

FUNCTION readByte%
  readByte% = ASC(INPUT$(1, #1))
END FUNCTION
Das lässt sich zwar mit dem FreeBasic-Compiler auch auf modernen Rechnern kompilieren, aber leider gibt es da den Fehlercode 24 („device timeout“) nicht mehr.
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

Hallo __blackjack__.
vielen Dank für Deine ganzen Code-Beispiele. So wie es jetzt ausschaut, werde ich wohl bei Python3 für Raspberry-Aufgaben bleiben.
Insgesamt möchte ich Programme mit ca. 1000 Zeilen Code(die teilweise noch auf Windows laufen) auf Raspberry mit Python bringen.
Das Auslesen des Zisternen Füllstand (ca. 100 Zeilen) ist der erste Versuch. Gerade läuft alles, es kommt jede Stunde eine Messung an.
Inzwischen habe ich einen zweiten Sensor bekommen. Ich werde erst mal die Schnittstelle zum RPI zusammen bauen und kann dann bequem am Schreibtisch das Programm verbessern. Wenn dann das alles gut ausschaut, und ich mich Python angefreundet habe, kann ich mich an die restlichen Programme machen.
Aber jetzt bin ich erst mal unterwegs, vor Anfang Juli geht es nicht weiter.
Ich melde mich dann mit meinen nächsten Versuchen wieder.
Viele Grüsse aus München, Helmut
Benutzeravatar
__blackjack__
User
Beiträge: 13004
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Einen (ungetesteten) Code hätte ich noch, weil Du ja VB/VBA erwähnt hast, in VB.NET was mit Mono auch unter Linux laufen sollte (natürlich nicht mit "COM1"):

Code: Alles auswählen

Option Explicit
Imports System
Imports System.IO.Ports

Module Distance
    Function Main() As Integer
        Dim port As SerialPort
        Dim lowByte As Byte, highByte As Byte, expectedChecksum As Byte
        Dim checkSum as Byte

        port = New SerialPort("COM2", 9600, Parity.None, 8, StopBits.One)
        port.ReadTimeout = 1000
        port.Open
        Try
            Do: Loop Until port.ReadByte = 255
            highByte = CType(port.ReadByte, Byte)
            lowByte = CType(port.ReadByte, Byte)
            checkSum = CType(port.ReadByte, Byte)
        Catch ex As TimeoutException
            Console.WriteLine("Timeout.")
            Return 1
        Finally
            port.Close
        End Try
        expectedCheckSum = CType((highByte + lowByte - 1) And 255, Byte)
        If checkSum <> expectedCheckSum Then
            Console.WriteLine("Expected checksum ", expectedCheckSum, " got ",
                    checkSum, " instead.")
            Return 2
        Else
            Console.WriteLine("Distance: ", highByte * 256 + lowByte, " mm.")
        End If
        
        Return 0
  End Function
End Module
“Most people find the concept of programming obvious, but the doing impossible.” — Alan J. Perlis
helmuel
User
Beiträge: 9
Registriert: Dienstag 9. Juni 2020, 17:11

Hallo __blackjack__
bin jetzt wieder zurück und habe 14 Tage Test ausgewertet. Mit meinem noch nicht perfekten Python Code gabe es keine Probleme, Alle >330 Auswertungen sind gelaufen, die Checksum war immer richtig. Ein automatischer(Watchdog-)Boot war nicht nötig.
Allerdings ist der Sensor nicht stabil. Mal gibt es 3 Tage richtige Werte, dann einen Tag total schwankend zwischen 10 cm und 9 meter Abstand. Da muss ich erst wieder an die Hardware ran, dass die Werte konstant werden.
Viele Dank für den VB Code. Allerdings läuft meine Heizungssteuerung mit VB.NET und Mono (die habe ich mir so bauen lassen) und mache nur kleinere Änderungen. Nach 5 Jahren Erfahrung damit habe ich dann beschlossen, ein Re-Design mit Python zu machen. Die Abstandsmessung ist die Übung dazu.
Ich melde mich wieder, wenn mein Code läuft und präsentabel ausschaut.
Viele Grüsse, Helmut
Antworten