Skip to content

Instantly share code, notes, and snippets.

@symbioquine
Last active July 8, 2024 20:15
Show Gist options
  • Save symbioquine/95ba2abaf046c8e034b41e4cf3c334a9 to your computer and use it in GitHub Desktop.
Save symbioquine/95ba2abaf046c8e034b41e4cf3c334a9 to your computer and use it in GitHub Desktop.
Understanding the undocumented Epever 0x43 (67) modbus command

Warning: This is all based on what I have inferred through interacting with the Epever Tracer 3210AN {0: b'EPsolar Tech co., Ltd', 1: b'TriRon3210', 2: b'V01.55+V01.22'}. Please treat this repository as informational only and test carefully before assuming any of this will work with your device.

The undocumented Epever 0x43 (67) modbus command is similar to the regular read register command (0x4) except that it allows reading discontinguous registers.

The request format is the same - two big-endian short integers indicating the starting register address and the number of registers to retrieve;

+---------+--------------+------------------------+----------------+-----+
|         |              |                        |                |     |
| unit_id | command=0x43 | start_register_address | register_count | crc |
|         |              |                        |                |     |
+---------+--------------+------------------------+----------------+-----+

The response format allows reading discontinguous registers efficiently by including an extra bit mask which describes which of the requested registers exist to be returned.

+---------+--------------+------------+-------------------+-----------------------+-----+
|         |              |            |                   |                       |     |
| unit_id | command=0x43 | byte_count | register_bit_mask | sparse_register_bytes | crc |
|         |              |            |                   |                       |     |
+---------+--------------+------------+-------------------+-----------------------+-----+

Most of these fields will be known to those already familiar with the Modbus read register command so I will just discuss the fields which differ;

  • byte_count: This is the number of bytes which would be sent if all the registers exist. The value should be twice the number of requested registers and thus It can be used to infer the number of registers that were requested if the response is being processed statelessly.
  • register_bit_mask: This is N bytes of bit mask which are used to indicate which parts of the requested register range exist and are being returned. The mask includes one bit for each requested register plus some padding to make for an even number of bytes. The bits are in reverse order to ensure that the padding is easy to deal with. Thus the last bit of the mask represents the first register, the second to last bit of the mask represents the second register, and so on. A True/1 value in the bit mask indicates that the register at the corresponding position exists and is part of the data that follows.
  • sparse_register_bytes: Includes two bytes for each true register from the bit mask. Just like the regular read register command, each byte pair contains one big-endian short unsigned integer.
$ python pymodbusclienttest.py
Connected successfully to /dev/ttyXRUSB0
2020-08-03 22:19:31,093 MainThread DEBUG transaction :117 Current transaction state - IDLE
2020-08-03 22:19:31,093 MainThread DEBUG transaction :122 Running transaction 1
2020-08-03 22:19:31,093 MainThread DEBUG transaction :230 SEND: 0x1 0x2b 0xe 0x1 0x0 0x70 0x77
2020-08-03 22:19:31,093 MainThread DEBUG sync :75 New Transaction state 'SENDING'
2020-08-03 22:19:31,093 MainThread DEBUG transaction :239 Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2020-08-03 22:19:31,132 MainThread DEBUG transaction :319 Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
2020-08-03 22:19:31,132 MainThread DEBUG transaction :248 RECV: 0x1 0x2b 0xe 0x1 0x1 0x0 0x0 0x3 0x0 0x15 0x45 0x50 0x73 0x6f 0x6c 0x61 0x72 0x20 0x54 0x65 0x63 0x68 0x20 0x63 0x6f 0x2e 0x2c 0x20 0x4c 0x74 0x64 0x1 0xa 0x54 0x72 0x69 0x52 0x6f 0x6e 0x33 0x32 0x31 0x30 0x2 0xd 0x56 0x30 0x31 0x2e 0x35 0x35 0x2b 0x56 0x30 0x31 0x2e 0x32 0x32 0x2a 0xe0
2020-08-03 22:19:31,132 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=43 - Current Frame in buffer - 0x1 0x2b 0xe 0x1 0x1 0x0 0x0 0x3 0x0 0x15 0x45 0x50 0x73 0x6f 0x6c 0x61 0x72 0x20 0x54 0x65 0x63 0x68 0x20 0x63 0x6f 0x2e 0x2c 0x20 0x4c 0x74 0x64 0x1 0xa 0x54 0x72 0x69 0x52 0x6f 0x6e 0x33 0x32 0x31 0x30 0x2 0xd 0x56 0x30 0x31 0x2e 0x35 0x35 0x2b 0x56 0x30 0x31 0x2e 0x32 0x32 0x2a 0xe0
2020-08-03 22:19:31,132 MainThread DEBUG rtu_framer :194 Getting Frame - 0x2b 0xe 0x1 0x1 0x0 0x0 0x3 0x0 0x15 0x45 0x50 0x73 0x6f 0x6c 0x61 0x72 0x20 0x54 0x65 0x63 0x68 0x20 0x63 0x6f 0x2e 0x2c 0x20 0x4c 0x74 0x64 0x1 0xa 0x54 0x72 0x69 0x52 0x6f 0x6e 0x33 0x32 0x31 0x30 0x2 0xd 0x56 0x30 0x31 0x2e 0x35 0x35 0x2b 0x56 0x30 0x31 0x2e 0x32 0x32
2020-08-03 22:19:31,132 MainThread DEBUG factory :266 Factory Response[ReadDeviceInformationResponse: 43]
2020-08-03 22:19:31,132 MainThread DEBUG rtu_framer :116 Frame advanced, resetting header!!
2020-08-03 22:19:31,132 MainThread DEBUG transaction :398 Adding transaction 1
2020-08-03 22:19:31,132 MainThread DEBUG rtu_framer :249 Frame - [0x1 0x2b 0xe 0x1 0x1 0x0 0x0 0x3 0x0 0x15 0x45 0x50 0x73 0x6f 0x6c 0x61 0x72 0x20 0x54 0x65 0x63 0x68 0x20 0x63 0x6f 0x2e 0x2c 0x20 0x4c 0x74 0x64 0x1 0xa 0x54 0x72 0x69 0x52 0x6f 0x6e 0x33 0x32 0x31 0x30 0x2 0xd 0x56 0x30 0x31 0x2e 0x35 0x35 0x2b 0x56 0x30 0x31 0x2e 0x32 0x32 0x2a 0xe0] not ready
2020-08-03 22:19:31,132 MainThread DEBUG rtu_framer :254 Done processIncomingPacket loop. Buffer contents: []
2020-08-03 22:19:31,132 MainThread DEBUG transaction :409 Getting transaction 1
2020-08-03 22:19:31,132 MainThread DEBUG transaction :204 Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
ReadDeviceInformationResponse(1)
1
{0: b'EPsolar Tech co., Ltd', 1: b'TriRon3210', 2: b'V01.55+V01.22'}
Manufacturer: b'EPsolar Tech co., Ltd'
Model: b'TriRon3210'
Version: b'V01.55+V01.22'
2020-08-03 22:19:31,133 MainThread DEBUG transaction :117 Current transaction state - TRANSACTION_COMPLETE
2020-08-03 22:19:31,133 MainThread DEBUG transaction :122 Running transaction 2
2020-08-03 22:19:31,133 MainThread DEBUG transaction :230 SEND: 0x1 0x4 0x31 0x1a 0x0 0x1 0x1e 0xf1
2020-08-03 22:19:31,133 MainThread DEBUG rtu_framer :282 Changing state to IDLE - Last Frame End - 1596518371.132479, Current Time stamp - 1596518371.133094
2020-08-03 22:19:31,133 MainThread DEBUG rtu_framer :290 Waiting for 3.5 char before next send - 1.75 ms
2020-08-03 22:19:31,134 MainThread DEBUG sync :75 New Transaction state 'SENDING'
2020-08-03 22:19:31,135 MainThread DEBUG transaction :239 Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2020-08-03 22:19:31,155 MainThread DEBUG transaction :319 Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
2020-08-03 22:19:31,155 MainThread DEBUG transaction :248 RECV: 0x1 0x4 0x2 0x0 0x61 0x78 0xd8
2020-08-03 22:19:31,155 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=4 - Current Frame in buffer - 0x1 0x4 0x2 0x0 0x61 0x78 0xd8
2020-08-03 22:19:31,155 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=4 - Current Frame in buffer - 0x1 0x4 0x2 0x0 0x61 0x78 0xd8
2020-08-03 22:19:31,156 MainThread DEBUG rtu_framer :194 Getting Frame - 0x4 0x2 0x0 0x61
2020-08-03 22:19:31,156 MainThread DEBUG factory :266 Factory Response[ReadInputRegistersResponse: 4]
2020-08-03 22:19:31,156 MainThread DEBUG rtu_framer :116 Frame advanced, resetting header!!
2020-08-03 22:19:31,156 MainThread DEBUG transaction :398 Adding transaction 1
2020-08-03 22:19:31,156 MainThread DEBUG rtu_framer :249 Frame - [0x1 0x4 0x2 0x0 0x61 0x78 0xd8] not ready
2020-08-03 22:19:31,156 MainThread DEBUG rtu_framer :254 Done processIncomingPacket loop. Buffer contents: []
2020-08-03 22:19:31,156 MainThread DEBUG transaction :409 Getting transaction 1
2020-08-03 22:19:31,157 MainThread DEBUG transaction :204 Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Battery SOC = 97%
2020-08-03 22:19:31,157 MainThread DEBUG transaction :117 Current transaction state - TRANSACTION_COMPLETE
2020-08-03 22:19:31,157 MainThread DEBUG transaction :122 Running transaction 3
2020-08-03 22:19:31,157 MainThread DEBUG transaction :230 SEND: 0x1 0x4 0x31 0x5 0x0 0x1 0x2f 0x37
2020-08-03 22:19:31,157 MainThread DEBUG rtu_framer :282 Changing state to IDLE - Last Frame End - 1596518371.155067, Current Time stamp - 1596518371.1578
2020-08-03 22:19:31,157 MainThread DEBUG rtu_framer :290 Waiting for 3.5 char before next send - 1.75 ms
2020-08-03 22:19:31,160 MainThread DEBUG sync :75 New Transaction state 'SENDING'
2020-08-03 22:19:31,160 MainThread DEBUG transaction :239 Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2020-08-03 22:19:31,184 MainThread DEBUG transaction :319 Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
2020-08-03 22:19:31,185 MainThread DEBUG transaction :248 RECV: 0x1 0x4 0x2 0x0 0x0 0xb9 0x30
2020-08-03 22:19:31,185 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=4 - Current Frame in buffer - 0x1 0x4 0x2 0x0 0x0 0xb9 0x30
2020-08-03 22:19:31,185 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=4 - Current Frame in buffer - 0x1 0x4 0x2 0x0 0x0 0xb9 0x30
2020-08-03 22:19:31,185 MainThread DEBUG rtu_framer :194 Getting Frame - 0x4 0x2 0x0 0x0
2020-08-03 22:19:31,185 MainThread DEBUG factory :266 Factory Response[ReadInputRegistersResponse: 4]
2020-08-03 22:19:31,185 MainThread DEBUG rtu_framer :116 Frame advanced, resetting header!!
2020-08-03 22:19:31,186 MainThread DEBUG transaction :398 Adding transaction 1
2020-08-03 22:19:31,186 MainThread DEBUG rtu_framer :249 Frame - [0x1 0x4 0x2 0x0 0x0 0xb9 0x30] not ready
2020-08-03 22:19:31,186 MainThread DEBUG rtu_framer :254 Done processIncomingPacket loop. Buffer contents: []
2020-08-03 22:19:31,186 MainThread DEBUG transaction :409 Getting transaction 1
2020-08-03 22:19:31,186 MainThread DEBUG transaction :204 Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
Charging equipment output current = 0.0A
2020-08-03 22:19:31,187 MainThread DEBUG transaction :117 Current transaction state - TRANSACTION_COMPLETE
2020-08-03 22:19:31,187 MainThread DEBUG transaction :122 Running transaction 4
2020-08-03 22:19:31,187 MainThread DEBUG transaction :230 SEND: 0x1 0x4 0x30 0x0 0x0 0x9 0x3f 0xc
2020-08-03 22:19:31,187 MainThread DEBUG rtu_framer :282 Changing state to IDLE - Last Frame End - 1596518371.184767, Current Time stamp - 1596518371.187603
2020-08-03 22:19:31,187 MainThread DEBUG rtu_framer :290 Waiting for 3.5 char before next send - 1.75 ms
2020-08-03 22:19:31,189 MainThread DEBUG sync :75 New Transaction state 'SENDING'
2020-08-03 22:19:31,190 MainThread DEBUG transaction :239 Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2020-08-03 22:19:31,215 MainThread DEBUG transaction :319 Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
2020-08-03 22:19:31,216 MainThread DEBUG transaction :248 RECV: 0x1 0x4 0x12 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0x10 0x49
2020-08-03 22:19:31,216 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=4 - Current Frame in buffer - 0x1 0x4 0x12 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0x10 0x49
2020-08-03 22:19:31,216 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=4 - Current Frame in buffer - 0x1 0x4 0x12 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0x10 0x49
2020-08-03 22:19:31,216 MainThread DEBUG rtu_framer :194 Getting Frame - 0x4 0x12 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2
2020-08-03 22:19:31,217 MainThread DEBUG factory :266 Factory Response[ReadInputRegistersResponse: 4]
2020-08-03 22:19:31,217 MainThread DEBUG rtu_framer :116 Frame advanced, resetting header!!
2020-08-03 22:19:31,217 MainThread DEBUG transaction :398 Adding transaction 1
2020-08-03 22:19:31,217 MainThread DEBUG rtu_framer :249 Frame - [0x1 0x4 0x12 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0x10 0x49] not ready
2020-08-03 22:19:31,217 MainThread DEBUG rtu_framer :254 Done processIncomingPacket loop. Buffer contents: []
2020-08-03 22:19:31,217 MainThread DEBUG transaction :409 Getting transaction 1
2020-08-03 22:19:31,217 MainThread DEBUG transaction :204 Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
ReadRegisterResponse (9)
[10000, 3000, 12464, 1, 2400, 3000, 12464, 1, 2]
2020-08-03 22:19:31,218 MainThread DEBUG transaction :117 Current transaction state - TRANSACTION_COMPLETE
2020-08-03 22:19:31,218 MainThread DEBUG transaction :122 Running transaction 5
2020-08-03 22:19:31,218 MainThread DEBUG transaction :230 SEND: 0x1 0x43 0x30 0x0 0x0 0x6e 0xca 0xe9
2020-08-03 22:19:31,218 MainThread DEBUG rtu_framer :282 Changing state to IDLE - Last Frame End - 1596518371.2159, Current Time stamp - 1596518371.218656
2020-08-03 22:19:31,218 MainThread DEBUG rtu_framer :290 Waiting for 3.5 char before next send - 1.75 ms
2020-08-03 22:19:31,220 MainThread DEBUG sync :75 New Transaction state 'SENDING'
2020-08-03 22:19:31,221 MainThread DEBUG transaction :239 Changing transaction state from 'SENDING' to 'WAITING FOR REPLY'
2020-08-03 22:19:31,266 MainThread DEBUG transaction :319 Changing transaction state from 'WAITING FOR REPLY' to 'PROCESSING REPLY'
2020-08-03 22:19:31,267 MainThread DEBUG transaction :248 RECV: 0x1 0x43 0xdc 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x41 0xff 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0xb 0xb8 0x0 0x0 0xfa 0xe3
2020-08-03 22:19:31,267 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=67 - Current Frame in buffer - 0x1 0x43 0xdc 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x41 0xff 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0xb 0xb8 0x0 0x0 0xfa 0xe3
2020-08-03 22:19:31,268 MainThread DEBUG rtu_framer :164 Got uid=1 func_code=67 - Current Frame in buffer - 0x1 0x43 0xdc 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x41 0xff 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0xb 0xb8 0x0 0x0 0xfa 0xe3
2020-08-03 22:19:31,268 MainThread DEBUG rtu_framer :194 Getting Frame - 0x43 0xdc 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x41 0xff 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0xb 0xb8 0x0 0x0
2020-08-03 22:19:31,268 MainThread DEBUG factory :266 Factory Response[ReadSparseRegistersResponse: 67]
2020-08-03 22:19:31,269 MainThread DEBUG rtu_framer :116 Frame advanced, resetting header!!
2020-08-03 22:19:31,269 MainThread DEBUG transaction :398 Adding transaction 1
2020-08-03 22:19:31,270 MainThread DEBUG rtu_framer :249 Frame - [0x1 0x43 0xdc 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x41 0xff 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0xb 0xb8 0x0 0x0 0xfa 0xe3] not ready
2020-08-03 22:19:31,270 MainThread DEBUG rtu_framer :254 Done processIncomingPacket loop. Buffer contents: []
2020-08-03 22:19:31,270 MainThread DEBUG transaction :409 Getting transaction 1
2020-08-03 22:19:31,270 MainThread DEBUG transaction :204 Changing transaction state from 'PROCESSING REPLY' to 'TRANSACTION_COMPLETE'
ReadSparseRegistersResponse (110)
[10000, 3000, 12464, 1, 2400, 3000, 12464, 1, 2, None, None, None, None, None, 3000, None, None, 0, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
0xdc 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x41 0xff 0x27 0x10 0xb 0xb8 0x30 0xb0 0x0 0x1 0x9 0x60 0xb 0xb8 0x30 0xb0 0x0 0x1 0x0 0x2 0xb 0xb8 0x0 0x0
import sys, struct, math
from bitstring import BitArray
from binascii import unhexlify
from pymodbus.mei_message import *
from pymodbus.register_read_message import ReadInputRegistersRequest
from pymodbus.pdu import ModbusRequest, ModbusResponse, ModbusExceptions
from pymodbus.compat import int2byte, byte2int
from pymodbus.utilities import hexlify_packets
from epsolar_tracer.client import EPsolarTracerClient
from epsolar_tracer.enums.RegisterTypeEnum import RegisterTypeEnum
import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s'
' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
log = logging.getLogger()
log.setLevel(logging.DEBUG)
class ReadSparseRegistersRequest(ModbusRequest):
function_code = 0x43
_rtu_frame_size = 8
def __init__(self, address, count, **kwargs):
''' Initializes a new instance
:param address: The address to start the read from
:param count: The number of registers to read
'''
ModbusRequest.__init__(self, **kwargs)
self.address = address
self.count = count
def encode(self):
''' Encodes the request packet
:return: The encoded packet
'''
return struct.pack('>HH', self.address, self.count)
def decode(self, data):
''' Decode a register request packet
:param data: The request to decode
'''
self.address, self.count = struct.unpack('>HH', data)
def __str__(self):
''' Returns a string representation of the instance
:returns: A string representation of the instance
'''
return "ReadSparseRegistersRequest (%d,%d)" % (self.address, self.count)
def execute(self, context):
''' Run a read holding request against a datastore
:param context: The datastore to request from
:returns: An initialized response, exception message otherwise
'''
if not (1 <= self.count <= 0x7d):
return self.doException(merror.IllegalValue)
if not context.validate(self.function_code, self.address, self.count):
return self.doException(merror.IllegalAddress)
values = context.getValues(self.function_code, self.address, self.count)
return ReadSparseRegistersResponse(values)
class ReadSparseRegistersResponse(ModbusResponse):
function_code = 0x43
_rtu_byte_count_pos = 2
@classmethod
def calculateRtuFrameSize(cls, buffer):
''' Calculates the size of the message
:param buffer: A buffer containing the data that have been received.
:returns: The number of bytes in the response.
'''
byte_count = byte2int(buffer[2])
bit_mask_bytes = math.ceil(byte_count / 16)
bit_mask = BitArray(bytes=buffer[3:3+bit_mask_bytes])
actual_bit_count = bit_mask.count(True) * 2
return 5 + bit_mask_bytes + actual_bit_count
def __init__(self, values=None, **kwargs):
''' Initializes a new instance
:param values: The values to write to
'''
ModbusResponse.__init__(self, **kwargs)
self.registers = values or []
def encode(self):
''' Encodes the response packet
:returns: The encoded packet
'''
byte_count = len(self.registers) * 2
register_count = int(byte_count / 2)
bit_mask_bytes = math.ceil(register_count / 8)
mask_bits = bit_mask_bytes * 8
bit_mask = BitArray(length=mask_bits)
data_bytes = b''
for register_idx, register in enumerate(self.registers):
if register is None:
continue
data_bytes += struct.pack('>H', register)
bit_mask[mask_bits - register_idx - 1] = True
return int2byte(byte_count) + bit_mask.bytes + data_bytes
def decode(self, data):
''' Decode a register response packet
:param data: The request to decode
'''
byte_count = byte2int(data[0])
register_count = int(byte_count / 2)
bit_mask_bytes = math.ceil(register_count / 8)
mask_bits = bit_mask_bytes * 8
bit_mask = BitArray(bytes=data[1:1+bit_mask_bytes])
self.registers = []
i = 1 + bit_mask_bytes
for register_idx in range(register_count):
mask_value = bit_mask[mask_bits - register_idx - 1]
if not mask_value:
self.registers.append(None)
continue
self.registers.append(struct.unpack('>H', data[i:i + 2])[0])
i += 2
def getRegister(self, index):
''' Get the requested register
:param index: The indexed register to retrieve
:returns: The request register
'''
return self.registers[index]
def __str__(self):
''' Returns a string representation of the instance
:returns: A string representation of the instance
'''
return "ReadSparseRegistersResponse (%d)" % len(self.registers)
port = '/dev/ttyXRUSB0'
client = EPsolarTracerClient(port=port)
if client.connect():
print('Connected successfully to {}'.format(port))
else:
print('Connection failed to {}'.format(port))
sys.exit(1)
response = client.read_device_info()
print(response)
print(response.read_code)
print(response.information)
print("Manufacturer: {}".format(repr(response.information[0])))
print("Model: {}".format(repr(response.information[1])))
print("Version: {}".format(repr(response.information[2])))
response = client.read_input(RegisterTypeEnum.BATTERY_SOC)
print(str(response))
response = client.read_input(RegisterTypeEnum.CHARGING_EQUIPMENT_OUTPUT_CURRENT)
print(str(response))
client.client.register(ReadSparseRegistersResponse)
request = ReadInputRegistersRequest(address=0x3000, count=9, unit=1)
result = client.client.execute(request)
print(result)
print(result.registers)
request = ReadSparseRegistersRequest(address=0x3000, count=110, unit=1)
result = client.client.execute(request)
print(result)
print(result.registers)
print(hexlify_packets(result.encode()))
@podarok
Copy link

podarok commented May 29, 2021

Hi.
Do you know by any chance how to change device Id via modbus?

@symbioquine
Copy link
Author

No. It's not in the EPever documentation http://www.solar-elektro.cz/data/dokumenty/1733_modbus_protocol.pdf and I don't see anything obvious from skimming the general MODBUS docs; https://modbus.org/docs/Modbus_Application_Protocol_V1_1b3.pdf

Please let me know though if you figure it out :)

Otherwise, I guess your best bet might be some sort of proxy/facade that can multiplex/translate the ids on the way to/from the device...

@symbioquine
Copy link
Author

Actually @podarok, I think I've discovered that changing the device id may be possible since the Epever "Solar Station v1.95" seems to have that ability. I haven't fully figured out how that command works, but this is what it's sending;

Setting the device id to 1;

0xf8 0x45 0x0 0x1 0x1 0x1 0x49 0xfc

Setting the device id to 2;

0xf8 0x45 0x0 0x1 0x1 0x2 0x9 0xfd
  • 0xf8 - seems like a "broadcast" device id
  • 0x45 - seems like another undocumented modbus command
  • If I had to guess, I might say the third byte 0x0 is probably some sort of sub-command code
  • If I had to guess, I might say the fourth byte 0x1 is probably the number of registers/locations to set
  • If I had to guess, I might say the fifth byte 0x1 is probably the number of bytes that follow
  • It seems like the 6th byte is the device id that's being set
  • Of course the last two bytes are the CRC

@cgm999
Copy link

cgm999 commented Sep 30, 2023

Hi @symbioquine
I see that you found a few hidden epever "things" (I found you also on DIY Solar Forum) . Do you know any of the below registers ? Thank you :)
One of the things I wanted to find is if Epever supports "tail current".. to stop boost charge if bat current drops below some value

0x9011 [0]
0x9064 [5]
0x906f [40]

0x9090 [0]
0x9091 [0]
0x9092 [108]
0x9093 [0]
0x9094 [1323]
0x9095 [65433]
0x9096 [65535]
0x9097 [2500]
0x9098 [2500]
0x90a0 [0]
0x90a1 [0]
0x90a2 [0]
0x90a3 [0]
0x90a4 [0]
0x90bf [1000]
0x9100 [12336]
0x9101 [12336]
0x9102 [12336]
0x9103 [12337]
0x9104 [12850]
0x9105 [13872]
0x9106 [0]
0x9107 [1024]
0x9180 [0]
0x9181 [0]
0x9183 [0]

@symbioquine
Copy link
Author

symbioquine commented Sep 30, 2023

@cgm999 I don't know off-hand, but based on the data, I would guess some of those are "bit sets" - particularly those ones with values that are near powers of two. For instance the registers you've posted 0x9095 & 0x9096 both have values close to 2^16 (possibly implying a bit set with just a few bits - including the most significant bit - set to 1).

Here's the corresponding values from a dump I have of a Tracer8420AN if that helps...

0x9011 = 65516
0x9064 = 2
0x906f = 40

0x9090 = 37
0x9091 = 0
0x9092 = 0
0x9093 = 0
0x9094 = 2873
0x9095 = 38
0x9096 = 0
0x9097 = 2213
0x9098 = 2169

0x90a0 = 0
0x90a1 = 0
0x90a2 = 0
0x90a3 = 0
0x90a4 = 0

0x90b0 = 8581
0x90b1 = 20986
0x90b2 = 21
0x90b3 = 1402
0x90b4 = 4352
0x90b5 = 5320
0x90b6 = 5009
0x90b7 = 5338
0x90b8 = 4019
0x90b9 = 5337
0x90ba = 18445
0x90bb = 0

0x90bd = 8000
0x90be = 35
0x90bf = 12000

0x9100 = 12336
0x9101 = 12336
0x9102 = 12336
0x9103 = 12593
0x9104 = 12593
0x9105 = 12593
0x9106 = 0
0x9107 = 3072

0x9180 = 0
0x9181 = 0
0x9182 = 0
0x9183 = 0

I kind of suspect that the tracer units themselves probably won't support that tail current detection mode for switching to from boost/eq to float, but if I were going to dig into it deeper, I would start by capturing more command traffic between the solar station (desktop) software and the tracer units. The other thing I might do is try and capture more traffic between the PAL-ADP-50AN unit and the tracer unit since I believe the PAL-ADP-50AN uses some real-time-register setting functionality that isn't used otherwise. I have a couple dumps of that kind of traffic that I could share if you're interested, but they'll take some serious sleuthing to use.

Good luck and let me know what you find. I'd be curious to know more about the units I have and might be open to some light-weight collaboration on the reverse-engineering/documenting front.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment