python-pylontech icon indicating copy to clipboard operation
python-pylontech copied to clipboard

Which USB to RS485 converter to use

Open stuartornum opened this issue 4 years ago • 22 comments

Hi @Frankkkkk

Would you mind letting us know what hardware you are using to communicate with the Pylontech batteries. For example, which USB to RS485 cable you are using, which OS/Hardware etc.

I'm struggling with a Raspberry Pi 3B+ and USB to RS485 adapter (https://www.amazon.com/gp/product/B08RDZVP49)

Cheers!

stuartornum avatar Sep 05 '21 10:09 stuartornum

Hi @stuartornum , I'm using a really cheap one like the one below: image (search terms: USB RS485).

The dip switches must all be down (the first dip switch sets the speed: 115200 vs 9600 Bd).

Cheers and don't hesitate if you need more info !

Frankkkkk avatar Sep 05 '21 18:09 Frankkkkk

PS: I don't know the pinout on the one you showed, but I'm pretty sure that it doesn't match the Pylontech RJ45 pinout ! You'd need a custom RJ45 patch cable (easy to crimp).

It's easy to do with the one I showed above because you can just take a standard RJ45/ethernet cable, cut it in half and take the two strands you're interested in

Frankkkkk avatar Sep 05 '21 18:09 Frankkkkk

Awesome, thanks @Frankkkkk - I've ordered exactly the same from Amazon. I also asked the question to the seller regarding the pinout for the converter I have... no response yet. I'll keep you posted... thanks again!

stuartornum avatar Sep 06 '21 10:09 stuartornum

Hi @Frankkkkk ,

I managed to get my hands on the USB RS485 adapter you referenced above. Also, made a new cable from some CAT6 on to a RJ45 (568b). As per the Pylontech manual it says pin 7 and 8 are recommended for RS485, so converting that to 568b we get (Pin 7: brown/white, Pin 8: brown).

I get the following when trying to run the library: `

import pylontech p = pylontech.Pylontech() print(p.get_values()) Traceback (most recent call last): File "", line 1, in File "/home/pi/python-pylontech/pylontech/pylontech.py", line 211, in get_values d = self.get_values_fmt.parse(f.info[1:]) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 288, in parse return self.parse_stream(io.BytesIO(data), **contextkw) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 300, in parse_stream return self._parsereport(stream, context, "(parsing)") File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2120, in _parse subobj = sc._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2653, in _parse return self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2413, in _parse e = self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2120, in _parse subobj = sc._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2653, in _parse return self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 2413, in _parse e = self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 703, in _parse obj = self.subcon._parsereport(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 312, in _parsereport obj = self._parse(stream, context, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 1041, in _parse data = stream_read(stream, self.length, path) File "/home/pi/.local/lib/python3.7/site-packages/construct/core.py", line 91, in stream_read raise StreamError("stream read less than specified amount, expected %d, found %d" % (length, len(data)), path=path) construct.core.StreamError: Error in path (parsing) -> Module -> GroupedCellsTemperatures stream read less than specified amount, expected 2, found 1`

I also switched the brown/brown-white wires around on the RS485-to-USB adapter to see if I got a different output, I did...:

`

p = pylontech.Pylontech() print(p.get_values()) Traceback (most recent call last): File "", line 1, in File "/home/pi/python-pylontech/pylontech/pylontech.py", line 208, in get_values f = self.read_frame() File "/home/pi/python-pylontech/pylontech/pylontech.py", line 167, in read_frame f = self._decode_hw_frame(raw_frame=raw_frame) File "/home/pi/python-pylontech/pylontech/pylontech.py", line 148, in _decode_hw_frame assert got_frame_checksum == int(frame_chksum, 16) ValueError: invalid literal for int() with base 16: b''`

Any thoughts?

Thanks again for your help! Really appreciate it.

(I'm using a Raspberry Pi 3b+)

stuartornum avatar Sep 08 '21 13:09 stuartornum

Hi,

How are your dip switches set ? They should be all four off (down). Which pylontech modules have you got (model) ?

It would be great to show the received raw frame by either adding a print(raw_frame) after here or by launching wireshark and capturing the serial port communications.

Cheers !

Frankkkkk avatar Sep 08 '21 14:09 Frankkkkk

I suppose that your first wiring must be correct as the frame passed the checksum validation.

Maybe your setup is a bit different than mine and we must change the frame protocol. If you can manage to dump the raw frame I can try to check the differences and patch the lib ;-)

Frankkkkk avatar Sep 08 '21 14:09 Frankkkkk

Hi @Frankkkkk , thanks for getting back to me.

  • All DIP switches are down: https://imgur.com/a/pIJbQBH
  • I have 1x US3000 Plus and 1x US2000 Plus
  • I've switched back the wires to be the first way around

Debug out from printing L169: b'~2002460010F011020F0CCD0CCE0CCC0CCE0CCB0CCC0CCD0CCC0CCD0CCB0CCC0CCD0CCD0CCE0CCC050BE10BCD0BCD0BD70BCDFFC3BFFDFFFF04FFFF0234007F300121100F0CCA0CCA0CCB0CCC0CCA0CCC0CCB0CCB0CCB0CCB0CCB0CCA0CCC0CCC0CCB050BEB0BCD0BCD0BCD0BC3FFD1BFE5FFFF04FFFF0292005FB400C350C4A7\r'

Cheers

stuartornum avatar Sep 08 '21 15:09 stuartornum

Well, I managed to decode part of the frame:

Container: 
    NumberOfModules = 2
    Module = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.277
                3.278
                3.276
                3.278
                3.275
                3.276
                3.277
                3.276
                3.277
                3.275
                3.276
                3.277
                3.277
                3.278
                3.276
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.41
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.31
                30.21
            Current = -6.1
            Voltage = 49.149
            Power = -299.8089
            RemainingCapacity = 65.535
            TotalCapacity = 65.535
            CycleNumber = 564
    foobar = 16
    Module2 = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.274
                3.274
                3.275
                3.276
                3.274
                3.276
                3.275
                3.275
                3.275
                3.275
                3.275
                3.274
                3.276
                3.276
                3.275
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.51
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.21
                30.11
            Current = -4.7
            Voltage = 49.125
            Power = -230.88750000000002
            RemainingCapacity = 65.535
            TotalCapacity = 65.535
            CycleNumber = 658
    greedy = ListContainer: 
        0
        95
        180
        0
        195
        80
    TotalPower = -299.8089
    StateOfCharge = 1.0

But I specified the protocol manually:

    get_values_fmt = construct.Struct(
        "NumberOfModules" / construct.Byte,
        "Module" / construct.Array(1, construct.Struct(
            "NumberOfCells" / construct.Int8ub,
            "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)),
            "NumberOfTemperatures" / construct.Int8ub,
            "AverageBMSTemperature" / ToCelsius(construct.Int16sb),
            "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)),
            "Current" / ToAmp(construct.Int16sb),
            "Voltage" / ToVolt(construct.Int16ub),
            "Power" / construct.Computed(construct.this.Current * construct.this.Voltage),
            "RemainingCapacity" / DivideBy1000(construct.Int16ub),
            "_undef1" / construct.Int8ub,
            "TotalCapacity" / DivideBy1000(construct.Int16ub),
            "CycleNumber" / construct.Int16ub,
        )),
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "foobar" / construct.Int8ub,
        "Module2" / construct.Array(1, construct.Struct(
            "NumberOfCells" / construct.Int8ub,
            "CellVoltages" / construct.Array(construct.this.NumberOfCells, ToVolt(construct.Int16sb)),
            "NumberOfTemperatures" / construct.Int8ub,
            "AverageBMSTemperature" / ToCelsius(construct.Int16sb),
            "GroupedCellsTemperatures" / construct.Array(construct.this.NumberOfTemperatures - 1, ToCelsius(construct.Int16sb)),
            "Current" / ToAmp(construct.Int16sb),
            "Voltage" / ToVolt(construct.Int16ub),
            "Power" / construct.Computed(construct.this.Current * construct.this.Voltage),
            "RemainingCapacity" / DivideBy1000(construct.Int16ub),
            "_undef1" / construct.Int8ub,
            "TotalCapacity" / DivideBy1000(construct.Int16ub),
            "CycleNumber" / construct.Int16ub,
        )),
        "greedy" / construct.GreedyRange(construct.Byte),
        "TotalPower" / construct.Computed(lambda this: sum([x.Power for x in this.Module])),
        "StateOfCharge" / construct.Computed(lambda this: sum([x.RemainingCapacity for x in this.Module]) / sum([x.TotalCapacity for x in this.Module])),

    )

There seems to be some extra bytes sent after the first module (written as foobar in the construct proto). So, my questions are:

  • Is the first module an US3000 ?
  • Can you try launching the program with only the US2000 connected (do a powercycle first) ? Do it work ?
  • Same question for US3000
  • Can you try by swapping the master/slave modules ?

Interesting though

Cheers !

Frankkkkk avatar Sep 08 '21 19:09 Frankkkkk

And the code if you want to try manually:

    def get_values(self):
        #self.send_cmd(2, 0x42, b'FF')
        #f = self.read_frame()
        rf =  b'~2002460010F011020F0CCD0CCE0CCC0CCE0CCB0CCC0CCD0CCC0CCD0CCB0CCC0CCD0CCD0CCE0CCC050BE10BCD0BCD0BD70BCDFFC3BFFDFFFF04FFFF0234007F300121100F0CCA0CCA0CCB0CCC0CCA0CCC0CCB0CCB0CCB0CCB0CCB0CCA0CCC0CCC0CCB050BEB0BCD0BCD0BCD0BC3FFD1BFE5FFFF04FFFF0292005FB400C350C4A7\r'
        ff = self._decode_hw_frame(raw_frame=rf)
        f = self._decode_frame(ff)
        print(f)
        print(f.info[1:])

        # infoflag = f.info[0]
        d = self.get_values_fmt.parse(f.info[1:])
        return d

Frankkkkk avatar Sep 08 '21 19:09 Frankkkkk

Hi @Frankkkkk ,

Apologies for the delay in getting back to you:

  • Is the first module an US3000 ? - Yes
  • Can you try launching the program with only the US2000 connected (do a powercycle first) ? Do it work ? - It DOES work perfectly with just the US2000 connected!!
  • Same question for US3000 - It does work perfectly with the US3000 on it's own as well!!!
  • Can you try by swapping the master/slave modules ? - I'll read up on how to do this now and get back to you.

Thanks for your help on this, it's really is appreciated

stuartornum avatar Sep 09 '21 10:09 stuartornum

I believe I've switched the US2000 to become the master, however, the documentation says to always use the US3000 as the primary when you have a mixture of US2000's/US3000's.

Here is the output:

    NumberOfModules = 2
    Module = ListContainer: 
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.287
                3.289
                3.288
                3.287
                3.287
                3.289
                3.289
                3.288
                3.286
                3.287
                3.287
                3.288
                3.288
                3.287
                3.288
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.11
            GroupedCellsTemperatures = ListContainer: 
                30.11
                30.11
                30.21
                30.11
            Current = -1.8
            Voltage = 49.315
            Power = -88.767
            RemainingCapacity = 29.0
            TotalCapacity = 50.0
            CycleNumber = 659
        Container: 
            NumberOfCells = 15
            CellVoltages = ListContainer: 
                3.288
                3.289
                3.289
                3.288
                3.289
                3.288
                3.288
                3.29
                3.289
                3.289
                3.289
                3.289
                3.289
                3.29
                3.289
            NumberOfTemperatures = 5
            AverageBMSTemperature = 30.21
            GroupedCellsTemperatures = ListContainer: 
                30.21
                30.21
                30.31
                30.21
            Current = -1.7
            Voltage = 49.333
            Power = -83.86609999999999
            RemainingCapacity = 43.66
            TotalCapacity = 8.464
            CycleNumber = 565
    TotalPower = -172.63309999999998
    StateOfCharge = 1.242816091954023

I'm not sure how much I believe some of the numbers, "StateOfCharge" for example...?

stuartornum avatar Sep 09 '21 10:09 stuartornum

Hi, No problem for the delays ! :-) Interesting results :thinking:

I suppose we could patch the decoding to handle US3000 as primary modules.. It's strange though. Sadly I don't have one so I can't really test this edge case. Maybe we could add a warning in the readme as a workaround.

As for the StateOfCharge, it's meant to be a percent (0-1: sum(all remaining capacities)/sum(total capacities)). However in your case the US3000 states a RemainingCapacity of 43 but a TotalCapacity of... 8.464 :frowning: which thus skews the calculation

If that's okay with you, I'll just add a warning for this edge case in the readme, and maybe in the future we can fix the US3000-US2000 bug ?

Cheers

Frankkkkk avatar Sep 09 '21 13:09 Frankkkkk

I would like to assist on this, what would be the preferred next steps. I have both US2000 and US3000 with the 3000 as the primary right now

petero-dk avatar Aug 07 '22 12:08 petero-dk

Hello, using a cable like mentioned above I get the following error message:

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1171, in wrapper
    result = method(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1158, in wrapper
    return method(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/thonny/backend.py", line 1232, in _execute_prepared_user_code
    exec(statements, global_vars)
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/Pylontech-test.py", line 6, in <module>
    print(p.get_values())
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/pylontech.py", line 273, in get_values
    f = self.read_frame()
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/pylontech.py", line 203, in read_frame
    f = self._decode_hw_frame(raw_frame=raw_frame)
  File "/home/pi/syncthing/Lixy/Python/Pylontech/pylontech/pylontech.py", line 184, in _decode_hw_frame
    assert got_frame_checksum == int(frame_chksum, 16)
ValueError: invalid literal for int() with base 16: b'\xbf\xff\xff\xff'

If I insert a print(raw_frame) in _decode_hw_frame() I get the result b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xbf\xff\xff\xff\x1f'

Do you have any idea what could be wrong? I have a single US3000C connected on port "B/RS485" and I only wired pins 7 and 8.

michaelhutter avatar Sep 26 '22 14:09 michaelhutter

Without any change now I get the following result as raw_data: b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xff\xff\xbf\xf7\xf7\x1f~20024600F07A11020F0CF80CF80CF80CF80CF90CF80CF80CF80CF80CF80CF90CF90CF90CF90CF9050B9D0B7A0B770B770B8D0000C28EFFFF04FFFF000000DBB0012110E1B6\r\x00'

What does the header mean or how can I remove it?

michaelhutter avatar Sep 27 '22 06:09 michaelhutter

Hi @michaelhutter Indeed it is strange as part of the second frame looks valid enough. Is your cable shielded or near high-enough EM radiations ? Did you try changing your USB-rs485 converter ? Cheers

Frankkkkk avatar Sep 27 '22 19:09 Frankkkkk

The first adapter which I bought did not work at all. The second adapter is working but gives these extra bytes in the beginning. I think I try to change your code a little bit, so that it removes all chars until the first occurence of '~'. May be using a regex or so. I am experienced in other languages, but not in Python. So would it be possible for you to give me a hint about which Python commands could do me the job? Is there a command like "FindFirstOccurence(haystack, needle)" and "right(string, pos)"?

michaelhutter avatar Sep 29 '22 14:09 michaelhutter

I am still struggling with my Pylontech US3000C. If I run print(p.get_values()) then raw_frame in _decode_hw_frame() has the value b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xbf\xff\xff\xff\x1f'

If I run print(p.get_values_single(2)) then raw_frame in _decode_hw_frame() has the value b'\xf0\xff\xff\xff\xff\xbf\xff\xff\xff\xff\xff\xff\xff\xbf\xf7\xf7\x1f~20024600F07A11020F0CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF60CF6050B8F0B710B6E0B6E0B7E0000C26AFFFF04FFFF000000D8CC012110E1CB\r\x00'

@Frankkkkk Can you explain what is the difference between get_values() and get_values_single(2)? What does the parameter (2) mean? In both cases the CRC does not match and I don't get any result. I tried a lot of things but unfortunately was not successful until now :-(

michaelhutter avatar Oct 10 '22 21:10 michaelhutter

@michaelhutter i not python guy but I want to create pylontech emulator and investigate this scripts and pylon protocol description. Get_values and get_values_single is the same command but values hardcoded address as 255. I didn't find this address descruption in pylontech documentation. 2 is number of battery if we have more than one pattery. Do you have real battery? Could you try to execute requests on real battery with Hterm an provide responses? These request ask for protocol version from master battery from grpup 0, 1, 2, 3, 4, 5.

52 => 7E 30 30 35 32 34 36 34 46 30 30 30 30 46 44 39 35 0D 42 => 7E 30 30 34 32 34 36 34 46 30 30 30 30 46 44 39 36 0D 32 => 7E 30 30 33 32 34 36 34 46 30 30 30 30 46 44 39 37 0D 22 => 7E 30 30 32 32 34 36 34 46 30 30 30 30 46 44 39 38 0D 12 => 7E 30 30 31 32 34 36 34 46 30 30 30 30 46 44 39 39 0D 02 => 7E 30 30 30 32 34 36 34 46 30 30 30 30 46 44 39 41 0D

Could you execute these requests with HTerm 9https://www.der-hammer.info/pages/terminal.html) on real battery? Just copy 7E .. OD an send to battery?

maxx-ukoo avatar Dec 27 '22 10:12 maxx-ukoo

Hello, unsure if I should make a new issue.

I tried using an ethernet - USB adapter and the unit (US3000c) just beeps like crazy.

So probably it's the wrong adapter?

Thanks!

EmCeBeh avatar Jul 06 '23 23:07 EmCeBeh

Hi @EmCeBeh , yes please create a new issue. Unless I'm mistaken, even though they both use the same form factor (RJ45), the protocols are completely different. You need an RS485 adapter, and not ethernet ! Cheers !

Frankkkkk avatar Jul 07 '23 08:07 Frankkkkk