Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Which USB to RS485 converter to use #2

Open
stuartornum opened this issue Sep 5, 2021 · 21 comments
Open

Which USB to RS485 converter to use #2

stuartornum opened this issue Sep 5, 2021 · 21 comments

Comments

@stuartornum
Copy link

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!

@Frankkkkk
Copy link
Owner

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
Copy link
Owner

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

@stuartornum
Copy link
Author

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
Copy link
Author

stuartornum commented Sep 8, 2021

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+)

@Frankkkkk
Copy link
Owner

Frankkkkk commented Sep 8, 2021

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
Copy link
Owner

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 ;-)

@stuartornum
Copy link
Author

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

@Frankkkkk
Copy link
Owner

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
Copy link
Owner

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

@stuartornum
Copy link
Author

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
Copy link
Author

stuartornum commented Sep 9, 2021

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...?

@Frankkkkk
Copy link
Owner

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

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 😦 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

@petero-dk
Copy link

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

@michaelhutter
Copy link

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
Copy link

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?

@Frankkkkk
Copy link
Owner

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

@michaelhutter
Copy link

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
Copy link

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 :-(

@maxx-ukoo
Copy link

@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?

@EmCeBeh
Copy link

EmCeBeh commented Jul 6, 2023

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!

@Frankkkkk
Copy link
Owner

Frankkkkk commented Jul 7, 2023

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 !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants