Bitte um Vorschläge, wie man mit Python eine Datei anpassen kann.

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
RIN67630
User
Beiträge: 141
Registriert: Sonntag 29. April 2018, 08:07

Hallo,
Vorwort:
Ich bin kein Python-Programmierer, bin eher bei C++ zu Hause.
Damit habe ich ein ziemlich aufwändiges Programm für den ESP8266 geschrieben.
https://github.com/rin67630/Sound-press ... r/Software

Das Programm hat viele Optionen und ist hoch konfigurierbar.
Problematisch ist es allerdings es "am Manne" zu bringen, denn man muss das IDE installieren, die Bibliotheken laden, die Optionen konfigurieren, est dann kompilieren und ggf. mit kryptografische Fehlermeldungen auskommen.
Damit sind die meisten User komplett überfordert.

Ich möchte das Programm fertig kompiliert liefern, und im Binary sollen nur 5 Strings angepasst werden:
  • String SSID (16 bytes)
  • String WiFiPasswort (16 bytes)
  • String Gerätekennung (16 bytes)
  • String Cloud-User (16 bytes)
  • String Cloud-Passwort (16 bytes)
Dann holt sich das c++ den Rest der Konfiguration aus der Cloud.

Konkretes Thema
Im kompiliertes Programm sollen diese fünf Strings gesucht werden und durch Anwenderangaben ersetzt werden. Aber so, dass die 16 bytes Länge für jede Variable gleich bleibt (d.h. den String mit Null terminieren und die restlichen bytes bis 16 stehen lassen, nicht wie ein Text-Editor es machen würde, in dem die gesamte Dateilänge angepasst wird.

Mit einem Hex-Editor funktioniert es gut, aber meine Anwender haben auch kein Hex-Editor.

Wie könnte man ein Python-skript dafür schreiben?
Python brauchen sie sowieso, um den Binary ins ESP zu laden.

Danke für Ihre Vorschläge.
Laszlo
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@RIN67630: Das ist ja jetzt nicht wirklich verschieden von C++. Man öffnet die Datei im entsprechenden Modus, ”seek”t an die richtige Stelle, und schreibt 16 Bytes. Das ist der `bytes`-Datentyp. Auffüllen mit Nullbytes: Schau einfach mal was für Methoden `bytes` so hat.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
RIN67630
User
Beiträge: 141
Registriert: Sonntag 29. April 2018, 08:07

__blackjack__ hat geschrieben: Sonntag 23. Juni 2024, 21:14 @RIN67630: Das ist ja jetzt nicht wirklich verschieden von C++. Man öffnet die Datei im entsprechenden Modus, ”seek”t an die richtige Stelle, und schreibt 16 Bytes. Das ist der `bytes`-Datentyp. Auffüllen mit Nullbytes: Schau einfach mal was für Methoden `bytes` so hat.
Mir geht es ja um die unbekannte Syntax. Vielleicht gib es irgendwo ein Beispiel, den ich anpassen könnte?
Von Null an sind es zu viele Stolpersteine...
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Was die unbekannte Syntax betrifft, würde ich das Tutorial in der Python-Dokumentation empfehlen.
Benutzeravatar
Kebap
User
Beiträge: 776
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

Üblicherweise speichert man Konfiguration in eigene (Text-)Dateien, oder durch ein Setup-Programm, und nicht indem man eine Binary per Hex-Editor bearbeitet. Also User erwarten das so.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Kebap: Bei Mikrocontrollern gibt es nicht immer ein Dateisystem.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Aber auch hier sollte die Konfiguration in einem Konfigurationsblock im Binary abliegen und nicht verstreut direkt im Code.
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ändert alles nicht wirklich etwas an der Frage.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Benutzeravatar
Kebap
User
Beiträge: 776
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

@__blackjack__: Danke, jetzt habe ich eine Idee, was ESP8266 hier bedeuten sollte. Wie man Software / Konfiguration dorthin ausrollt, da habe ich keine Erfahrung.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1239
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Ich habe das mal ausprobiert, weil es mich interessiert hat.
Mein Beispiel bezieht sich aber auf die Micropython-Firmware, die hinten dran noch eine Partition erstellt, auf der die Programme gespeichert werden. D.h. dort darf man auch nichts überschreiben.
Um mir Zeit zu sparen, habe ich einen ESP32S3 mit 16 MiB SPIFLASH verwendet, aber nur eine Firmware geflasht, die insgesamt 4 MiB belegt. Also 2 MiB Firmware + 2 MiB Partition.

Anstatt die Firmware zu verändern, flasht man einfach die Daten beim entsprechenden Offset. Ich habe z.B. einen Offset von 5 MiB gewählt, was in hexadezimaler Schreibweise folgendes ergibt: 0x500000

Geschrieben habe ich die Datei lokal auf dem PC mit diesem Script:

Code: Alles auswählen

from pathlib import Path


def write_data(file, *str_values, fixed_size=16):
    with file.open("wb") as fd:
        for str_value in str_values:
            data = bytearray(fixed_size)
            raw_value = str_value.encode("utf8")
            data[0: len(raw_value)] = raw_value
            fd.write(data)


data_file = Path.home().joinpath("Desktop", "data.bin")


write_data(
    data_file,
    "123",
    "456",
    "789",
    "abc",
    "def",
)
Befehl zum Flashen der Datei (unter Windows):

Code: Alles auswählen

py -m esptool --port COM3 --baud 460800 write_flash 0x500000 .\Desktop\data.bin
Der Code auf dem ESP32S3 (Micropython):

Code: Alles auswählen

import esp

# muss noch in den SPIFLASH passen,
# darf aber nicht die Firmware oder eine Partition überschreiben

# Ich habe z.B. einen ESP32S3 damit getestet
# Der hat 16 MiB SPIFLASH.
# Als Firmware habe ich eine fertige Variante genommen, die nur eine 2 MiB Partition anlegt.
# Die Firmware selbst befindet sich im ersten 2 MiB-Segment.
# D.h. ich muss die Daten nach einem Offset von 4 MiB schreiben

magic_offset = 0x500000
data = bytearray(80)

esp.flash_read(magic_offset, data)

wifi_ssid = bytes(data[0:16].rstrip(b"\0"))
wifi_password = bytes(data[16:32].rstrip(b"\0"))
cloud_id = bytes(data[32:48].rstrip(b"\0"))
cloud_user = bytes(data[48:64].rstrip(b"\0"))
cloud_password = bytes(data[64:80].rstrip(b"\0"))



print("Data:", data)
print()
print("SSID:", wifi_ssid)
print("Passwort:", wifi_password)
print("ID:", cloud_id)
print("Klaut User:", cloud_user)
print("Klaut Password", cloud_password)
Ich vermute, dass Nullbytes die tatsächliche Länge des Strings bestimmen. Die SSID muss maximal 8 Zeichen lang sein. Eine SSID darf maximal 32 Zeichen lang sein.

Ausgabe:
MPY: soft reboot
Data: bytearray(b'123\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00456\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00789\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00abc\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00def\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')

SSID: b'123'
Passwort: b'456'
ID: b'789'
Klaut User: b'abc'
Klaut Password b'def'
MicroPython v1.24.0-preview.66.gf60c71d13 on 2024-06-25; Generic ESP32S3 module with Octal-SPIRAM with ESP32S3
Type "help()" for more information.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

@DeaD_EyE: man sollte dafür sorgen, dass der Rest des Strings wirklich mit 0en gefüllt wird und dass der String nicht zu lang wird:

Code: Alles auswählen

raw_value = raw_value .ljust(length, b"\0")
assert len(raw_value ) == length and raw_value [-1] == 0
Benutzeravatar
DeaD_EyE
User
Beiträge: 1239
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Wenn es etwas in Python gibt, dass ich bis zum heutigen Zeitpunkt immer noch nicht verstanden habe, ist das Statement assert bzw. dessen falsche Verwendung.
Normalerweise nutzt man assert in unittests und diese Tests werden nicht optimiert, da ansonsten kein Unittest mehr funktionieren würde.

Wenn man Code ausführt und Schlimmeres verhindern möchte, sollte man davon ausgehen, dass die assert Statements aufgrund des Optionsschalters -O deaktiviert sein können.
Sofern man sein Programm selbst ausführt, hat man auch die Kontrolle darüber.

Wenn die Länge der zu schreibenden Daten überprüft werden soll, dann am besten bevor die Datei schreibend geöffnet wird.

So z.B.:

Code: Alles auswählen

from pathlib import Path


def write_data(file, *str_values, fixed_size=16):

    if any(len(s) > fixed_size for s in str_values):
        raise ValueError("Ein Feld ist zu lang")


    with file.open("wb") as fd:
        for str_value in str_values:
            data = bytearray(fixed_size)
            raw_value = str_value.encode("utf8")
            data[0: len(raw_value)] = raw_value
            fd.write(data)


data_file = Path.home().joinpath("Desktop", "data.bin")


write_data(
    data_file,
    "123",
    "456",
    "789",
    "abc",
    "def",
)
Man könnte noch ausgeben, welches Feld betroffen ist, aber der Code diente nur zur Demonstration.
Was z.B. auch nicht behandelt wird, ist einn UnicodeDecodeError. Man könnte ja falsch encodierte bytes übergeben.
Das fällt erst dann auf, wenn man z.B. die print funktion nutzt oder nach bytes encodieren möchte.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
__blackjack__
User
Beiträge: 14047
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Neben Unit-Tests benutze ich ``assert`` für Fälle die nicht auftreten können. Also nicht wenn der Benutzer der API was falsch gemacht hat, sondern wenn intern was nicht stimmt. Beispiel: Code der alle Werte eines Aufzählungstyps prüft und für jeden etwas bestimmtes macht, da packe ich in der Regel ein ``assert False`` in den Code für den Fall, dass der Wert nicht behandelt wurde, weil *ich* dann ja den Fehler gemacht habe nicht an alle Fälle zu denken, und nicht der Aufrufer. Und um Invarianten zu prüfen ist ``assert`` nützlich.
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

@DeaD_EyE: du decodierst in Deinem Code doch gar nichts. Was Du aber machst, ist Encodieren, und das bedeutet, dass die Länge deiner Bytes größer sein kann, als die Länge deines Strings.
Es hat schon seinen Sinn, dass ich in meinem Code die Länge von raw_value geprüft habe, und nicht die Länge von str_value.
RIN67630
User
Beiträge: 141
Registriert: Sonntag 29. April 2018, 08:07

__blackjack__ hat geschrieben: Dienstag 25. Juni 2024, 08:23 @Kebap: Bei Mikrocontrollern gibt es nicht immer ein Dateisystem.
Genau. Man programmiert Bare-Metal. Und es gibt auch keine Console, zumindest nicht solange man nicht im Netz eingebucht ist.
Die Methode ist normalerweise ein eigenes WLAN vorübergehend bereitstellen, und die Daten über HTTP zu holen, dann sie ins geräteeigenes EEPROM zu speichern. Dazu noch ein Test, ob Prozedur schon mal lief
Alles aber sehr mühsam und es frisst meistens mehr Resourcen, als das Nutzprogramm.
Deshalb die Idée direkt vor dem Upload das Programm zu patchen.
Das ist umso sexier, als es ganz für den User ganz einfach wird: statt das Programm mit Python zu uploaden, beantwortet er noch zuvor ein Paar Fragen und danach wird hochgelden und das war's schon.
;-)
RIN67630
User
Beiträge: 141
Registriert: Sonntag 29. April 2018, 08:07

DeaD_EyE hat geschrieben: Dienstag 25. Juni 2024, 15:53 Ich habe das mal ausprobiert, weil es mich interessiert hat.
...

Code: Alles auswählen

py -m esptool --port COM3 --baud 460800 write_flash 0x500000 .\Desktop\data.bin
Die Idee ist das Python programm esptool so zu modifizieren, dass es vorher im User Dialog die Variablen
WLANSSID, WLANPWD, CLOUDID, CLOUDPWD, DEVICEID, vom Anwender abfragt und als Python-Variablen annimmt.

Prüfen, dass die Userangaben nicht länger, als z.B. 16 Bytes sind. (wir haben kein Unicode im ESP8266)

Dann die Datei .\Desktop\data.bin lesen (sich die Gesamtlänge merken),

Im Inhalt, die Zeichenketten WLANSSID, WLANPWD, CLOUDID, CLOUDPWD, DEVICEID, suchen. (Die sind so im original C++ Program gespeichert)
Diese Zeichenketten durch die Python-Variablen überschreiben, (nicht im Append-modus, sondern Byte für Byte ersetzen, dabei auf die Terminierung der Zeichenkette mit NULL achten) Das wird wohl das tricky Anteil sein.

Die Datei .\Desktop\data.bin zurückschreiben. (ich habe erstaunt festgestellt, dass es keinerlei Checksum in der Datei gibt)
Überprüfen, dass sich die Gesamtlänge nicht verändert hat.

Dann weiter mit dem Uploaden wie in esptool...

Und so wären wir schon am Ziel !
RIN67630
User
Beiträge: 141
Registriert: Sonntag 29. April 2018, 08:07

Sirius3 hat geschrieben: Mittwoch 26. Juni 2024, 07:08 ...man sollte dafür sorgen, dass der Rest des Strings wirklich mit 0en gefüllt wird und dass der String nicht zu lang wird:
Muss man nicht unbedingt, der String muss mit einem NULL enden, danach wird alles ignoriert.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1239
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Sirius3 hat geschrieben: Mittwoch 26. Juni 2024, 09:36 @DeaD_EyE: du decodierst in Deinem Code doch gar nichts. Was Du aber machst, ist Encodieren, und das bedeutet, dass die Länge deiner Bytes größer sein kann, als die Länge deines Strings.
Es hat schon seinen Sinn, dass ich in meinem Code die Länge von raw_value geprüft habe, und nicht die Länge von str_value.
Stimmt, können länger sein. Hab erst daran gedacht ASCII zu verwenden und dann ist mir eingefallen, dass Unicode in SSIDs vorkommen kann. Muss nicht, kann aber.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
RIN67630
User
Beiträge: 141
Registriert: Sonntag 29. April 2018, 08:07

DeaD_EyE hat geschrieben: Donnerstag 27. Juni 2024, 07:44
Sirius3 hat geschrieben: Mittwoch 26. Juni 2024, 09:36 @DeaD_EyE: du decodierst in Deinem Code doch gar nichts. Was Du aber machst, ist Encodieren, und das bedeutet, dass die Länge deiner Bytes größer sein kann, als die Länge deines Strings.
Es hat schon seinen Sinn, dass ich in meinem Code die Länge von raw_value geprüft habe, und nicht die Länge von str_value.
Stimmt, können länger sein. Hab erst daran gedacht ASCII zu verwenden und dann ist mir eingefallen, dass Unicode in SSIDs vorkommen kann. Muss nicht, kann aber.
Hallo,
entschuldige die späte Antwort.
Nein, in der ESP8266 und ESP32-Welt ist Unicode noch ein Fremdwort. Also reines ASCII.

Ich tue mir noch schwer deinen Code zu verstehen.

Wie kann ich denn eine Stringvariable so formatieren, dass sie immer z.B. 16 bytes lang ist, mit "trailing nulls" ergänzt?
Geht das überhaupt?

Wenn ja, könnte ich die Credentials vom User abfragen, auf exakt 16 Bytes normieren.

Dann die Datei einlesen, die Platzhaltern durch die normierte Credentials ersetzen und die Datei zurückschreiben.
(in der Hoffnung , dass txt.replace() mit den "trailing nulls" umgehen kann)

Oder sehe ich alles falsch?
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

@RIN67630: wie man das macht hab ich ja schon gepostet. `replace` funktioniert auch mit Bytes.
Antworten