tbnobody / OpenDTU

Software for ESP32 to talk to Hoymiles/TSUN/Solenso Inverters
GNU General Public License v2.0
1.81k stars 507 forks source link

Sniffer mode to capture firmware updates #1650

Open broth-itk opened 9 months ago

broth-itk commented 9 months ago

Is your feature request related to a problem? Please describe.

OpenDTU team want captures of firmware upgrades of the inverters.

Describe the solution you'd like

Switch OpenDTU in "sniffer" mode to listen, receive and store all communication from Hoymiles DTU to the inverters.

The captured data should be send over the network to syslog server for further analysis.

Wishlist:

Once we have (unencrypted?) firmware dumps of the inverters, we can start disassembling ;-)

Comments are welcome. Thanks!

Describe alternatives you've considered

No response

Additional context

No response

stefan123t commented 9 months ago

Promiscuous mode would be possible as you can set both DTU and inverter Serial ID on the current menus. Those could be added both/all to the maximum five NRF pipes.

Difficulty would be to guess the channels used by both Hoymiles DTU and inverter. But switching between the five NRF channels might be sufficient as the messages/packets are usually repeated 15 times/transmitted 16 times. So switching channel after 2-4ms may be sufficient, especially if configurable.

Payload parsing may not be needed in the first place. Though that and a pass-through repeater mode may be added at a later stage ;)

broth-itk commented 9 months ago

I don't like the idea of channel hopping for captures. As OpenDTU has the same inverters configured as the Hoymiles DTU, we can scan all channels until we see active communication with matching serials. That channel is locked in and dumped to syslog.

stefan123t commented 9 months ago

The typical WR <-> DTU messages are repeated by the NRF radio 15 times, i.e. 16 packets as you can see in the following pictures.

One packet with 15 repeats takes about ~35 ms to be sent, i.e. a single packet will take about 2,5-3 ms with a ~1 ms gap between repeated packets. (The larger gaps between consecutive packets / messages sent from the WR -> DTU are usually about 9-10 ms each.)

image

So switching the five channels after 2-5 ms listening on each should allow you to listen on each channel 2-3 times during the complete transmission period for a single packet with all its retransmits. And also the inverter is changing the channel from time to time, which has been known as channel hopping.

Please note that it may be easier (and has been done initially with the Hoymiles DTU Lite too) to simply hook two serial2usb converters onto the RX/TX pins of an original Hoymiles DTU: BAUD 125000, 8N1 should be the correct settings for the UART communication between the main cpu and the RF chips.

See https://github.com/tbnobody/OpenDTU/issues/1540#issuecomment-1885707736

broth-itk commented 9 months ago

@stefan123t OK, since I have an original DTU, it might be easier to make both signals available from the outside to capture data.

As long I don't have to open the lid of the transceiver, all is fine :)

broth-itk commented 9 months ago

@stefan123t

Here you have it, my new HMDUSI - HoyMiles Dual USB Sniffer Interface

IMG_5327 IMG_5328 IMG_5329

Now I need to do some software to capture data with exact time stamps.

Do you happen to know something which can be used?

phol commented 9 months ago

I think capturing the data should be very straightforward with a simple python script. If you run that twice in two separate terminal windows, I think you're good.

Make sure pyserial is installed: pip install pyserial

import serial
import datetime

# Replace baud_rate with the actual Hoymiles baud rate
def read_serial(port, baud_rate=9600, log_location='log.txt'):
    try:
        ser = serial.Serial(port, baud_rate, timeout=1)
        print(f"Connected to {port}")

        try:
            with open(log_location, "a") as file:
                while True:
                    if ser.in_waiting > 0:
                        data = ser.readline().decode('utf-8').rstrip()
                        timestamp = datetime.datetime.now()
                        log_entry = f"{timestamp}: {data}\n"
                        print(log_entry)
                        file.write(log_entry)

        except KeyboardInterrupt:
            print("Interrupted by user, exiting.")

        except Exception as e:
            print(f"Error: {e}")

    except serial.SerialException as e:
        print(f"Serial error: {e}")

    finally:
        try:
            ser.close()
            print("Serial port closed.")
        except:
            print("Error closing serial port.")

# Replace with your port, e.g., 'COM3' for Windows, '/dev/ttyUSB0' for Linux/macOS
read_serial('/dev/tty.usbserial-0001', 9600, "log_1.txt")

Use Ctrl + C to interrupt and close the script. Make sure NOT to call the script serial.py, as that will cause conflicts with the serial library. Call it something else instead like serial_log.py.

stefan123t commented 9 months ago

According to other users info the BAUD rate is 125000 and 8N1 for parity, start/stop bits.

The Grid Profile update has been traced https://github.com/tbnobody/OpenDTU/issues/900#issuecomment-1899406249 by now, but you can still contribute the firmware update if you want. Eventuall they both use the 0x0A DOWN_DAT main command, though that ist just a quick guess.

To get a firmware dump you would need access to the JTAG/SWD port on the inverter. Some other users do have that at the moment and have contributed a firmware dump already.

broth-itk commented 9 months ago

I modified the code a bit to provide a more or less usable output. Attached some files with the outputs (oringial towards this final version)-

Before doing any more steps with the DTU and updates, can you please take a look to the dumps and check if this is usable at all?

Thanks!

log_1.txt log_2.txt

#!/usr/bin/python3

import serial
import datetime
import binascii

# Replace baud_rate with the actual Hoymiles baud rate

def read_serial(port, baud_rate=9600, log_location='log.txt'):
    try:
        ser = serial.Serial(port, baud_rate, timeout=1)
        print(f"Connected to {port}")

        try:
            with open(log_location, "a") as file:
                while True:
                    if ser.in_waiting > 0:
                        data = ser.read(ser.in_waiting)
                        h = binascii.hexlify(data)
                        timestamp = datetime.datetime.now()
                        log_entry = f"A: {timestamp}: {h}\n"
                        print(log_entry)
                        file.write(log_entry)
                        file.flush()

        except KeyboardInterrupt:
            print("Interrupted by user, exiting.")

        except Exception as e:
            print(f"Error: {e}")

    except serial.SerialException as e:
        print(f"Serial error: {e}")

    finally:
        try:
            ser.close()
            print("Serial port closed.")
        except:
            print("Error closing serial port.")

# Replace with your port, e.g., 'COM3' for Windows, '/dev/ttyUSB0' for Linux/macOS
read_serial('/dev/ttyUSB0', 125000, "log_1.txt")
phol commented 9 months ago

Nice that you got it working! I'm glad I could contribute this small bit. I'm unfortunately not the right person to answer your question, but hopefully this will get us closer to supporting firmware updates and setting grid profiles.

PS: If you add the file extension to the start of a markdown code block, you enable syntax highlighting. :)

```py
broth-itk commented 9 months ago

Thanks for your hint, slways something new to learn ;-)

I will keep the logging running for a while. Did "grid profile update" already and some other maintenance tasks. Maybe we catch something of interest here.

broth-itk commented 9 months ago

Here we go, these logs were collected during 5 to 6 hours until night came to send the inverters asleep.

log_1.txt.gz log_2.txt.gz

I can't tell which of these logs are RX or TX but following log has above combined and sorted by time:

log_combined_sorted.txt.gz

This should make analysis easier. Maybe someone can spot a new command or something we haven't found yet.

I'll detach the DTU and use OpenDTU instead now.

broth-itk commented 9 months ago

Any feedback about the logs? Are the captures usable?

I plan to perform firmware updates on all inverters on friday.

stefan123t commented 9 months ago

Hi @broth-itk, I did not have time to look into the logs in detail. The hex format is a bit difficult to read, so I will have to post-process them to understand what is going on. Before that I can not say what the content / commands are that are used in the logs.

A: 2024-01-28 11:46:54.330986: b'~\xd6\x80\x16F3\x80\x16F3\x01\x00\x15!\xe3\x7f~\xd6\x80\x16F3\x80\x16F3\x01\x00\x15!\xe3\x7f~\xd6\x80\x16F3\x80\x16F3\x01\x00\x15!\xe3\x7f~\xd6\x80\x16F3\x80\x16F3\x01\x00\x15!\xe3\x7f'
...
A: 2024-01-28 11:53:58.630405: b'7ed6801646338016463301001521e37f'
B: 2024-01-28 11:53:58.684149: b'7e5683199047831990470215212114557f'
B: 2024-01-28 11:53:58.828190: b'7e5683199047831990470215212114557f'
B: 2024-01-28 11:53:58.988290: b'7e5683199047831990470215212114557f'
A: 2024-01-28 11:53:59.078706: b'7ed6801646338016463301001521e37f'
B: 2024-01-28 11:53:59.084321: b'7e5680164633801646330115212114567f'
B: 2024-01-28 11:53:59.132321: b'7e5680164633801646330115212114567f'
A: 2024-01-28 11:53:59.142698: b'7ed6801646338016463301001521e37f'
B: 2024-01-28 11:53:59.196374: b'7e5683199047831990470215212114557f'
B: 2024-01-28 11:53:59.340453: b'7e5683199047831990470215212114557f'
B: 2024-01-28 11:53:59.484530: b'7e5683199047831990470215212114557f'
A: 2024-01-28 11:53:59.591014: b'7ed6801646338016463301001521e37f'
B: 2024-01-28 11:53:59.596584: b'7e5680164633801646330115212114567f'
B: 2024-01-28 11:53:59.644618: b'7e158319904783199047800b0065b6437c000000000000000015a7c07f'
B: 2024-01-28 11:54:00.092834: b'7e15831990478319904780120065b6437c0000000000000000ccbf187f'
B: 2024-01-28 11:54:00.845265: b'7e15813130708131307080120065b6437d5d00000000000000005cb2847f'
A: 2024-01-28 11:54:01.592603: b'7ed6801646338016463301001521e37f'
B: 2024-01-28 11:54:01.597569: b'7e5680164633801646330115212114567f'
B: 2024-01-28 11:54:01.645613: b'7e5683199047831990470215212114557f'
B: 2024-01-28 11:54:01.789726: b'7e5683199047'
B: 2024-01-28 11:54:01.805766: b'831990470215212114557f'

I just looked into the combined output again and found that after several minutes of the \xd6 chatter the signal improves and I can actually read part of the data being sent. The last two lines are obviously separated, though they belong to the same frame starting with 7e and ending with 7f. The SPI communication uses 7e SOF (Start of Frame) and 7f EOF (End of Frame) and some escape mechanism to replace these two bytes in case they occur otherwise in the data.

Given the above communication B: are the TX messages from the DTU to the inverter (e.g. Main command 0x56) whereas A: are the RX responses from the inverter to the DTU (e.g. reply to command 0x56 | 0x80 = 0xd6).

broth-itk commented 9 months ago

Thanks for your feedback! Well, the proposed python script gave weird outputs and I don't think the DTU is using UTF-8 to communicate to its radio module. The script can still be improved by

But as time is precious, if the collected dumps are complete, postprocessing can always be done.

broth-itk commented 9 months ago

Here you go, these logs include the firmware update of two -4T inverters to 1.0.27 (Firmware Build Date 2023-06-05 10:24:00):

log_1.txt.fwupd.gz log_2.txt.fwupd.gz

stefan123t commented 9 months ago

@broth-itk I have downloaded them but I will not be able to analyse them in the coming weeks. But many thanks for making the traces, I am already excited to learn some new "commands" which we can add as a new feature to OpenDTU/AhoyDTU at a later stage.

broth-itk commented 9 months ago

@stefan123t Thanks! The logs contain all data since startup of the DTU. Maybe there is something interesting - indeed. Good luck! Cheers!

stefan123t commented 2 weeks ago

Huch ich sollte mal die Logs vom Firmware Update von @broth-itk auswerten und wie versprochen dokumentieren.

@broth-itk ich habe das log_1.txt ein wenig aufgeräumt, die Kommandos auf dem UART / der Seriellen Schnittstelle zwischen MCU und NRF / CMT Modul beginnen immer mit 7e und enden auf 7f, wenn das nicht der Fall ist, dann wurde beim Logging etwas umgebrochen, was nicht sein muß / sein sollte. Ich habe den folgenden Regex dazu verwendet (?<!7f)'\n[AB]: [0-9-]{10} [0-9:.]{15}: b'(?!7e), vielleicht willst Du das noch in Dein Python Script einarbeiten ?

broth-itk commented 2 weeks ago

@stefan123t ist das tatsächlich so, dass die Frames stets mit 7e beginnen und mit 7f enden? Ist sichergestellt, dass ein 7e innerhalb eines Pakets nicht auftritt ;-)

Das Script kann ich gerne anpassen und demnächst einen neuen Trace (wegen Power Distribution Logic) ziehen. Das wird sicherlich einige interessieren und wir sollten sicherstellen, dass die Tracedaten vollständig und verwendbar sind.

stefan123t commented 2 weeks ago

Hallo Bernhard @broth-itk ja die 7e/7f werden durch 7d5e/7d5f escaped, 7d selbst durch 7d5d. Ich habe es hier detailliert beschrieben: https://github.com/tbnobody/OpenDTU/issues/1421#issuecomment-2425232860

broth-itk commented 2 weeks ago

Danke @stefan123t, habe das Script angepasst und werde das morgen tagsüber ausprobieren. Bevor ich das Firmware/Grid Profile Update laufen lasse, würde ich Dir einen kurzen Trace zur Verfügung stellen. Nicht, dass am Ende die Traces nicht passen :)

stefan123t commented 2 weeks ago

@broth-itk Danke die Traces passen prinzipiell schon so wie sie jetzt sind. Die Umbrüche und Ersetzungen der Escapes kann man ja leicht auch nachträglich machen. Ich habe lediglich für die einfachere Lesbarkeit ein paar Leerzeichen eingefügt.

broth-itk commented 2 weeks ago

Ich habe das Script so erweitert, dass nun alles in einer Datei (Timestamp, Hex-Dump), nach Frames getrennt und mit Prefix RX/TX versehen ist. So ist es für den menschlichen Betrachter besser sichtbar und der zeitliche Zusammenhang erkennbar. Das davor war eher Q&D

broth-itk commented 2 weeks ago

So sieht das nun aus, denke damit kann man etwas besser arbeiten:

image

Die Frames werden einzeln aus dem Datenstrom herausgesucht und ins Log geschrieben. Anstelle von RX/TX habe ich schlicht << und >> ins Log geschrieben da ich schlicht nicht weiß welcher USB/Serial Adapter auf welcher Leitung hängt. Sollte aber so passen.

Ich lasse die DTU morgen mal den ganzen Tag online und beobachte.

stefan123t commented 2 weeks ago

@broth-itk ja sieht gut aus die Richtung passt.

Z.B. das MainCmd 0x15 REQ_ARW_DAT_ALL mit dem SubCmd RealTimeRunData_Debug [sic] 0x0B ist die Anfrage nach den aktuellen Werten. Die geht von der DTU zum WR >>.

Die Antwort wäre dann 0x95 (immer MainCmd | 0x80) vom WR zur DTU <<.


Die DTU stellt aber nebenher auch ganz viele 0x56 Anfragen und bekommt 0xD6 Antworten zurück.

Weiß jemand zufällig wie das 0x56 genau aufgebaut ist ? Es ist wohl das ChannelChangeCommand bei den HMS/HMT mit CMT2300A RF Modul.

Siehe auch hier die Dokumentation im Source Code: https://github.com/tbnobody/OpenDTU/blob/dc5eb96f5035ba9e058e38d83df2d6c691b0764b/lib/Hoymiles/src/commands/ChannelChangeCommand.cpp#L9-L18

Die Diskussion zum ChannelChangeCommand sollten wir m.E. hier weiterführen: https://github.com/tbnobody/OpenDTU/issues/2137#issuecomment-2427788809

broth-itk commented 2 weeks ago

@stefan123t

Anbei ein großer Trace von heute morgen. Darin sind folgende Updates enthalten:

update_communication_log.txt.gz

AlexJacu commented 1 week ago

@stefan123t Nach dem update meines 1600 bekomme ich keine Verbindung mehr mit der Opendtu. Wele FW der opendtu hast du?

Update nachdem ich beide Geräte einmal vom Strom getrennt habe und sie wieder angeschlossen habe hat es nach einer knappen halben Stunde wieder funktioniert sie haben sich synchronisiert und jetzt läuft es wieder

stefan123t commented 1 week ago

@AlexJacu bitte ein eigenes Issue aufmachen oder Deinen Kommentar löschen. Hier geht es nur um die Implementierung / Dokumentation des Sniffer Modus. Danke.