Hi Gurux Team,
I am facing an issue with FOTA Image Block Transfer in Gurux.DLMS Python.
The same meter and firmware FOTA work correctly using DLMS Director, but when I perform the same operation using the Python implementation, Image Transfer Initiate succeeds and Image Block Transfer fails with:
byte must be in range(0, 256)
The meter reports:
Image Block Size : 512 bytes
Image Transfer Enabled : True
Image Transfer Initiate (Method 1) is successful.
The failure occurs when sending Method 2 (Image Block Transfer), starting from Block 0.
The decrypted DLMS request shows:
Structure
UInt32 block number = 0
OctetString = 512 bytes
and the encoded OCTET STRING contains:
09 82 02 00
The complete communication log and current Python source code are attached.
Since the same FOTA works successfully with DLMS Director, could you please explain:
1. Why does the Python implementation generate the
"byte must be in range(0, 256)" error?
2. Is this related to the 512-byte OCTET STRING encoding,
GXByteBuffer, AXDR length encoding, or PDU/frame size?
3. What is the correct fix in the Gurux Python code so that
FOTA works the same way as DLMS Director?
Please point me to the exact code/function that needs to be changed.
Thank you
Kumaresh Dhara
Logs attached
======================================================================
FOTA CONFIGURATION
======================================================================
FOTA Mode : SEQUENTIAL
Firmware Path : D:\Script P_Shell\DLMS FOTA Tool\Testing_2024.bin
Firmware file found successfully.
Starting FOTA Update...
======================================================================
FOTA UPDATE STARTED
======================================================================
FOTA MODE : SEQUENTIAL
Firmware File : D:\Script P_Shell\DLMS FOTA Tool\Testing_2024.bin
Firmware Size : 261120 bytes
Image Transfer Object Found : 0.0.44.0.0.255
Reading Image Block Size from meter...
Decrypted PDU: C0 01 C1 00 12 00 00 2C 00 00 FF 02 00
<GetRequest>
<GetRequestNormal>
<InvokeIdAndPriority Value="C1" />
<AttributeDescriptor>
<ClassId Value="0012" />
<InstanceId Value="00002C0000FF" />
<AttributeId Value="02" />
</AttributeDescriptor>
</GetRequestNormal>
</GetRequest>
TX: 00:14:47 7E A0 2C 03 61 10 BC BE E6 E6 00 C8 1E 30 00 00 AC 95 59 95 7C 48 74 F9 A4 D2 3D F9 DB B7 C2 44 80 CF C5 67 18 A2 18 49 8C 45 BE 99 4C 7E
Encryption is applied.
Authentication is applied.
Decrypted PDU: C4 01 C1 00 06 00 00 02 00
<GetResponse>
<GetResponseNormal>
<InvokeIdAndPriority Value="C1" />
<Result>
<Data>
<UInt32 Value="00000200" />
</Data>
</Result>
</GetResponseNormal>
</GetResponse>
RX: 00:14:47 7E A0 28 61 03 30 42 0B E6 E7 00 CC 1A 30 00 00 AC 94 5A BE 51 E9 76 D6 62 7D 96 C0 7C D6 57 21 F5 C8 6E 47 F0 2D 99 89 27 7E
Meter Image Block Size : 512
Using Image Block Size : 512 bytes
Decrypted PDU: C0 01 C1 00 12 00 00 2C 00 00 FF 05 00
<GetRequest>
<GetRequestNormal>
<InvokeIdAndPriority Value="C1" />
<AttributeDescriptor>
<ClassId Value="0012" />
<InstanceId Value="00002C0000FF" />
<AttributeId Value="05" />
</AttributeDescriptor>
</GetRequestNormal>
</GetRequest>
TX: 00:14:47 7E A0 2C 03 61 32 AC BC E6 E6 00 C8 1E 30 00 00 AC 96 52 3C EC B5 64 6F 12 51 D0 20 EF 77 5A 06 3D F1 54 99 E5 B4 F0 A3 AF 33 6C F6 94 7E
Encryption is applied.
Authentication is applied.
Decrypted PDU: C4 01 C1 00 03 01
<GetResponse>
<GetResponseNormal>
<InvokeIdAndPriority Value="C1" />
<Result>
<Data>
<Boolean Value="True" />
</Data>
</Result>
</GetResponseNormal>
</GetResponse>
RX: 00:14:48 7E A0 25 61 03 52 D9 C0 E6 E7 00 CC 17 30 00 00 AC 95 B4 60 04 36 8A 4C 3C EB 7E 03 BF 6D DC B8 0A 77 31 2C A5 E5 7E
Image Transfer Enabled : True
Image Identifier : Testing_2024.bin
Reading firmware file...
Firmware loaded successfully : 261120 bytes
Total Blocks : 510
----------------------------------------------------------------------
SEQUENTIAL MODE SELECTED
Starting from Block 0
----------------------------------------------------------------------
----------------------------------------------------------------------
STEP 1 : IMAGE TRANSFER INITIATE
----------------------------------------------------------------------
Decrypted PDU: C3 01 C1 00 12 00 00 2C 00 00 FF 01 01 02 02 09 50 54 65 73 74 69 6E 67 5F 66 75 6C 6C 69 6D 61 67 65 5F 54 61 74 61 5F 31 50 5F 34 47 5F 43 53 50 44 43 4C 5F 4B 55 31 20 30 31 2E 32 34 2D 53 4B 30 33 30 30 2E 31 36 5F 54 41 43 48 30 2E 30 30 2E 34 32 5F 43 52 59 5F 32 30 32 34 2E 62 69 6E 06 00 03 FC 00
<ActionRequest>
<ActionRequestNormal>
<InvokeIdAndPriority Value="C1" />
<MethodDescriptor>
<ClassId Value="0012" />
<InstanceId Value="00002C0000FF" />
<MethodId Value="01" />
</MethodDescriptor>
<MethodInvocationParameters>
<Structure Qty="02" >
<OctetString Value="54657374696E675F66756C6C696D6167655F546174615F31505F34475F43535044434C5F4B55312030312E32342D534B303330302E31365F54414348302E30302E34325F4352595F323032342E62696E" />
<UInt32 Value="0003FC00" />
</Structure>
</MethodInvocationParameters>
</ActionRequestNormal>
</ActionRequest>
TX: 00:14:48 7E A0 85 03 61 54 C2 E1 E6 E6 00 CB 77 30 00 00 AC 97 76 22 CD B3 6C AC 73 FA F2 BC 39 86 D3 8B 57 99 51 CB 61 D5 77 79 97 A2 85 6F A1 BC 9C 48 11 49 CE 9B AE E5 7C E0 1B 5D 49 BE 3C DC B6 9B 75 A8 0B AB 2B F5 73 9E FF 42 17 98 87 FB 19 44 20 46 90 E4 63 0F 62 DE A8 B8 84 F5 EC ED 54 C6 6D 95 68 66 E3 53 F3 7F B2 DF 79 FC FE 5A B3 F3 E6 D9 FB 4C C9 75 8F 2B 0D DE 92 24 12 06 0D EF 91 D1 94 03 A1 6B 7E
Encryption is applied.
Authentication is applied.
Decrypted PDU: C7 01 C1 00 00
<ActionResponse>
<ActionResponseNormal>
<InvokeIdAndPriority Value="C1" />
<Result Value="Success" />
</ActionResponseNormal>
</ActionResponse>
RX: 00:14:48 7E A0 24 61 03 74 56 98 E6 E7 00 CF 16 30 00 00 AC 96 FB 09 C0 02 1E 55 0C B9 F8 52 D7 F1 2F 01 6C 44 86 7C D2 7E
Image Transfer Initiate : SUCCESS
----------------------------------------------------------------------
STEP 2 : IMAGE BLOCK TRANSFER
----------------------------------------------------------------------
Start Block : 0
End Block : 509
Block Size : 512
Sending Block 0/509 | 512 bytes
Decrypted PDU: C3 01 C1 00 12 00 00 2C 00 00 FF 02 01 02 02 06 00 00 00 00 09 82 02 00 86 15 FF FF 79 19 A6 16 00 00 6C 16 00 00 8D 16 00 00 00 00 CA 18 89 18 00 00 00 00 00 00 59 18 4B 16 14 18 00 00 2B 17 00 00 00 00 FD 18 F7 16 00 00 00 00 5C 19 00 00 E0 15 BF 16 00 00 11 17 00 00 00 00 E8 15 00 00 00 00 92 16 00 00 A1 16 9C 16 97 16 00 00 00 00 7B 19 00 00 00 00 BD 16 7D 19 00 00 00 00 00 00 00 00 00 00 A8 16 AE 16 B3 16 B8 16 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 7E 3F E0 84 00 00 00 00 00 00 00 00 00 00 FF FF FF FF FF FF FF FF FF FF 32 00 85 D2 3F BF 1C 95 33 C0 74 5D 82 BF 0E BE 33 C0 00 00 80 41 D2 08 00 40 00 00 80 3F C6 3C 00 40 00 F0 73 45 04 C6 4B 45 48 BC 4D 47 3B 57 A2 47 70 C3 23 4D 70 C3 23 4D 70 C3 23 4D BE 38 81 4D BE 38 81 4D BE 38 81 4D DA 00 00 00 E2 00 00 00 EA 00 00 00 F2 00 00 00 01 00 40 42 0F 00 20 A1 07 00 01 00 40 42 0F 00 20 A1 07 00 0A D7 23 3C 66 66 E6 3F 00 00 20 41 00 00 20 42 00 00 8C 42 00 00 48 41 80 0C 00 00 00 00 F0 41 01 01 01 00 04 04 01 03 00 00 00 00 06 00 96 00 00 00 2A 43 00 80 84 43 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
<ActionRequest>
<ActionRequestNormal>
<InvokeIdAndPriority Value="C1" />
<MethodDescriptor>
<ClassId Value="0012" />
<InstanceId Value="00002C0000FF" />
<MethodId Value="02" />
</MethodDescriptor>
<MethodInvocationParameters>
<Structure Qty="02" >
<UInt32 Value="00000000" />
<OctetString Value="8615FFFF7919A61600006C1600008D1600000000CA18891800000000000059184B16141800002B1700000000FD18F716000000005C190000E015BF160000111700000000E8150000000092160000A1169C169716000000007B1900000000BD167D1900000000000000000000A816AE16B316B816000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007E3FE08400000000000000000000FFFFFFFFFFFFFFFFFFFF320085D23FBF1C9533C0745D82BF0EBE33C000008041D20800400000803FC63C004000F0734504C64B4548BC4D473B57A24770C3234D70C3234D70C3234DBE38814DBE38814DBE38814DDA000000E2000000EA000000F2000000010040420F0020A10700010040420F0020A107000AD7233C6666E63F000020410000204200008C4200004841800C00000000F0410101010004040103000000000600960000002A430080844303000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" />
</Structure>
</MethodInvocationParameters>
</ActionRequestNormal>
</ActionRequest>
byte must be in range(0, 256)
DisconnectRequest
TX: 00:14:48 7E A0 11 03 61 76 F1 7F E6 E6 00 62 03 80 01 00 BD 9B 7E
RX: 00:14:48 7E A0 36 61 03 96 9D A6 E6 E7 00 63 28 80 01 00 BE 23 04 21 28 1F 30 00 00 AC 96 34 08 07 5D 01 C2 CD C8 12 1B D5 6E 0F E7 51 E4 A3 E7 8B B7 DF 4F 87 6A A5 9F 49 BB 7E
TX: 00:14:48 7E A0 07 03 61 53 65 81 7E
RX: 00:14:49 7E A0 20 61 03 73 05 9E 81 80 14 05 02 03 40 06 02 03 40 07 04 00 00 00 01 08 04 00 00 00 01 B7 29 7E
Ended. Press any key to continue.
Hi, The firmware is updated…
Hi,
The firmware is updated regularly, so there should be no reason for this issue.
Can you get the traceback from this so I can see where this error occurs?
Can you also get the hex trace from the meter reply?
Add a plain hex trace.
Regards,
Mikko
Hi Mikko, Thank you for the…
Hi Mikko,
Thank you for the response.
I checked this further and the issue is specifically related to the **Image Block Size returned by the meter**.
The same Python FOTA code is working correctly when the meter reports a smaller block size, for example **128 bytes**. In that case, the Python code reads the block size from the meter, creates the corresponding block, and the Image Block Transfer works successfully.
The issue occurs when the meter reports:
```text
Image Block Size : 512 bytes
Image Transfer Enabled : True
```
The Python code automatically uses the 512-byte block size and creates a 512-byte OCTET STRING for Method 2.
Image Transfer Initiate (Method 1) succeeds. The failure occurs when sending the first Image Block Transfer (Block 0).
The error is:
```text
byte must be in range(0, 256)
```
This error is generated locally by the Python code. There is no meter error response associated with this exception.
The decrypted request for Block 0 is:
```text
Structure
UInt32 = 00000000
OctetString = 512 bytes
```
The beginning of the encoded OCTET STRING is:
```text
09 82 02 00
```
where `02 00` represents the length of 512 bytes.
Therefore, it appears that the issue may be occurring while encoding the OCTET STRING length or adding the encoded data to the byte buffer when the length is greater than 255 bytes.
I have attached the traceback as requested, along with the plain hexadecimal communication trace.
The behavior can be summarized as:
```text
Block Size = 128 bytes
-> Image Block Transfer works
Block Size = 512 bytes
-> Image Transfer Initiate works
-> Image Block Transfer fails locally
-> "byte must be in range(0, 256)"
```
Could you please check the traceback and advise which Gurux Python function is responsible for this encoding?
If possible, please point me to the exact function/file that needs to be corrected for OCTET STRING lengths greater than 255 bytes.
Regards,
Kumaresh Dhara
Hi Kumaresh Dhara, This is…
Hi Kumaresh Dhara,
This is tested with an image block size of 512 bytes, and it worked without problems.
Can you run
python --version
Let me know the output and the Python version that you are using.
Regards,
Mikko
Hi Mikko, Yes, we checked…
Hi Mikko,
Yes, we checked the Python version as requested. Our current environment is Windows 64-bit with Python 3.14.3 and Gurux DLMS Python version 1.0.202. We investigated the FOTA issue end-to-end and were able to isolate and fix the problem locally. The FOTA process is working correctly up to the Image Block Transfer stage. The meter successfully reports an image block size of 512 bytes, the firmware size is 261120 bytes, the application calculates 510 blocks, and the Image Transfer Initiate request completes successfully. The failure occurs when the first 512-byte image block is converted into an HDLC frame by the Gurux library. Initially, the only error we received was "byte must be in range(0, 256)". We first verified the Python version and then verified the exact Gurux package and GXByteBuffer.py file being loaded by Python to make sure that the issue was not caused by an older/local installation. After confirming that the installed Gurux package was actually being used, we added temporary debugging inside GXByteBuffer.setUInt8() and found that the library was attempting to write the value 569 into a UInt8 field. Since a UInt8 can only contain values from 0 to 255, Python raised the ValueError. We then added traceback.print_stack() to identify exactly where this value was being generated. The stack trace showed that the value was coming from GXDLMS.getHdlcFrame(), specifically from the HDLC length calculation where the code executes bb.setUInt8(7 + len(secondaryAddress) + len(primaryAddress) + len1). For the 512-byte FOTA block, this calculation resulted in 569. We then reviewed the surrounding HDLC length encoding logic and found that the higher bits of the HDLC length are already being handled separately using (total_length >> 8) & 0x7, while the following byte is being written directly using setUInt8() without restricting it to the lower 8 bits. Therefore, the value 569 should not be passed directly to setUInt8(); its lower 8 bits should be written instead. We changed the code so that the calculated length is first stored in a variable and the value passed to setUInt8() is length & 0xFF. In our case, 569 is 0x239, and 569 & 0xFF gives 0x39, or 57, which is a valid UInt8 value. After applying this local change, the "byte must be in range(0, 256)" exception was removed and HDLC frame construction proceeded successfully. We did not modify GXByteBuffer.setUInt8() itself because it is a generic byte-writing function and silently truncating invalid values there could hide other issues. We believe the correction should be made in GXDLMS.getHdlcFrame(), where the HDLC length field is actually being constructed. We are sharing the complete investigation below so that the exact failure point, debugging process, code change and proposed permanent fix are clear.
Environment and initial verification
Our current environment is:
OS: Windows 64-bit
Python: 3.14.3
Gurux DLMS Python: 1.0.202
FOTA Mode: Sequential
Firmware Size: 261120 bytes
Image Block Size: 512 bytes
Total Blocks: 510
As the first step, we checked the Python version using:
python --version
Output:
Python 3.14.3
We then wanted to make sure that the application was not accidentally loading another Gurux package from the local project directory or an older installation.
We therefore checked the actual Gurux package being loaded:
python -c "import gurux_dlms; print(gurux_dlms.**file**)"
Output:
C:\Users\kumaresh.dhara\AppData\Local\Programs\Python\Python314\Lib\site-packages\gurux_dlms_*init*_.py
We also verified the exact GXByteBuffer.py file being executed:
python -c "from gurux_dlms import GXByteBuffer; import inspect; print(inspect.getfile(GXByteBuffer))"
Output:
C:\Users\kumaresh.dhara\AppData\Local\Programs\Python\Python314\Lib\site-packages\gurux_dlms\GXByteBuffer.py
This confirmed that the application is using the installed Gurux DLMS Python package and that we were debugging the actual package being executed.
FOTA sequence before the failure
The FOTA process itself starts normally.
The meter successfully returns the Image Block Size as:
<GetResponse>
<GetResponseNormal>
```
<InvokeIdAndPriority Value="C1" />
<Result>
<Data>
<UInt32 Value="00000200" />
</Data>
</Result>
```
</GetResponseNormal>
</GetResponse>
The value:
00000200
is 512 bytes.
Therefore, the application correctly reports:
Meter Image Block Size : 512
Using Image Block Size : 512 bytes
The firmware file size is:
261120 bytes
The application calculates:
Total Blocks : 510
The Image Transfer Initiate operation is also successful:
Image Transfer Initiate : SUCCESS
Therefore, the following parts of the FOTA process are already working correctly:
Association
Image Transfer Object discovery
Image block size read
Authentication
Encryption
Image Transfer Initiate
The failure occurs only after entering:
STEP 2 : IMAGE BLOCK TRANSFER
The application starts with:
Start Block : 0
End Block : 509
Block Size : 512
Sending Block 0/509 | 512 bytes
At this point, the firmware data is already correctly read and the Image Block Transfer ActionRequest is generated.
The generated APDU contains:
<ActionRequest>
<ActionRequestNormal>
```
<InvokeIdAndPriority Value="C1" />
<MethodDescriptor>
<ClassId Value="0012" />
<InstanceId Value="00002C0000FF" />
<MethodId Value="02" />
</MethodDescriptor>
<MethodInvocationParameters>
<Structure Qty="02">
<UInt32 Value="00000000" />
<OctetString Value="...512 bytes..." />
</Structure>
</MethodInvocationParameters>
```
</ActionRequestNormal>
</ActionRequest>
This was another important confirmation because it showed that the 512-byte image data was successfully incorporated into the DLMS ActionRequest.
The failure was therefore not occurring while reading the firmware or generating the DLMS APDU.
The failure was occurring after the APDU was generated, while Gurux was constructing the HDLC frame.
How we found the invalid value
Initially, the application only showed:
byte must be in range(0, 256)
This error is generated when Python tries to put a value outside the valid byte range into a bytearray.
Instead of assuming where the problem was, we added temporary debugging to GXByteBuffer.setUInt8().
The debugging showed:
========== GXByteBuffer INVALID BYTE ==========
item = 569
index = 2
capacity = 10
size = 2
position = 0
===============================================
Invalid byte value 569 at index 2
This was the first concrete indication of the actual problem.
The Gurux library was not trying to write a normal byte value such as 0–255.
It was trying to write:
569
into a UInt8 field.
Since:
569 > 255
Python correctly rejects the operation.
However, this still did not tell us why 569 was being generated.
Tracing the source of 569
To find the caller responsible for generating this value, we temporarily added:
traceback.print_stack()
inside the debugging section of GXByteBuffer.setUInt8().
The stack trace showed:
reply = GXDLMS.getLnMessages(p)
GXDLMS.getLnMessages()
then calls:
GXDLMS.getHdlcFrame()
and inside getHdlcFrame(), the following statement is executed:
bb.setUInt8(
7 + len(secondaryAddress) + len(primaryAddress) + len1
)
The relevant stack trace was:
File "...gurux_dlms\GXDLMS.py", line 731, in getLnMessages
messages.append(GXDLMS.getHdlcFrame(p.settings, frame_, reply))
File "...gurux_dlms\GXDLMS.py", line 1011, in getHdlcFrame
bb.setUInt8(
7 + len(secondaryAddress) + len(primaryAddress) + len1
)
File "...gurux_dlms\GXByteBuffer.py", line 228, in setUInt8
self.setUInt8(item, self.size)
File "...gurux_dlms\GXByteBuffer.py", line 243, in setUInt8
traceback.print_stack()
This was the point where we could conclusively isolate the problem.
The error was not coming from:
The meter
The firmware file
The 512-byte image block
Authentication
Encryption
The FOTA configuration
The Image Transfer Initiate operation
The DLMS ActionRequest generation
The Python installation
GXByteBuffer itself
The invalid value was being generated by the HDLC frame length calculation inside:
GXDLMS.getHdlcFrame()
Exact calculation causing the problem
The relevant existing code is:
if len1 == 0:
bb.setUInt8(
5 + len(secondaryAddress) + len(primaryAddress) + len1
)
else:
bb.setUInt8(
7 + len(secondaryAddress) + len(primaryAddress) + len1
)
For the failing FOTA block:
len1 = 512
The calculation therefore becomes:
7 + len(secondaryAddress) + len(primaryAddress) + 512
For our current addressing configuration, this results in:
569
Therefore, the library effectively executes:
bb.setUInt8(569)
That is exactly the value reported by our GXByteBuffer debugging:
item = 569
This establishes a direct connection between the observed error and the length calculation in GXDLMS.getHdlcFrame().
Why the issue only appears with the 512-byte FOTA block
This also explains why normal DLMS communication works correctly.
For normal DLMS requests, the generated frame is relatively small.
Therefore, the calculated value passed to setUInt8() remains within:
0–255
and there is no exception.
With a 512-byte FOTA image block, the calculated frame length becomes larger than 255.
Therefore, the same code path eventually produces:
569
which is outside the valid UInt8 range.
This is why the problem appears specifically when the first 512-byte image block is being transferred.
Review of the HDLC length encoding
After identifying the exact calculation, we reviewed the preceding HDLC length-byte logic.
The code already contains:
bb.setUInt8(
0xA0
| (
(
len(secondaryAddress)
+ len(primaryAddress)
+ len1
) >> 8
) & 0x7
)
The important part here is:
(total_length >> 8) & 0x7
This extracts the higher bits of the total length.
The following byte is then supposed to contain the lower 8 bits of the length.
However, the current code directly passes the complete calculated value:
bb.setUInt8(length)
without explicitly restricting it to the lower 8 bits.
For our example:
569 decimal = 0x239
The binary/hex representation contains more than 8 bits.
The lower 8 bits are:
0x39
which is:
57 decimal
Therefore:
569 & 0xFF = 57
So the lower byte can safely be written as:
57
while the higher length bits are handled separately by the preceding HDLC length field logic.
Local change we tested
We changed the code from:
if len1 == 0:
bb.setUInt8(
5 + len(secondaryAddress) + len(primaryAddress) + len1
)
else:
bb.setUInt8(
7 + len(secondaryAddress) + len(primaryAddress) + len1
)
to:
if len1 == 0:
length = (
5
+ len(secondaryAddress)
+ len(primaryAddress)
+ len1
)
else:
length = (
7
+ len(secondaryAddress)
+ len(primaryAddress)
+ len1
)
bb.setUInt8(length & 0xFF)
The important change is:
bb.setUInt8(length & 0xFF)
instead of:
bb.setUInt8(length)
For the failing case:
length = 569
Then:
569 & 0xFF = 57
Therefore:
bb.setUInt8(57)
is executed instead of:
bb.setUInt8(569)
This removes the invalid UInt8 condition.
Result after the local modification
After applying this change locally, the previous error:
byte must be in range(0, 256)
was removed from this part of the FOTA process.
The HDLC frame construction was able to proceed instead of failing inside GXByteBuffer.setUInt8().
This was important because it confirmed that the change was not simply hiding the original exception. The failure was directly connected to the invalid value generated by the HDLC length calculation.
Why we suggest changing GXDLMS.getHdlcFrame() rather than GXByteBuffer
We also considered making the change inside:
GXByteBuffer.setUInt8()
For example:
self._data[index] = item & 0xFF
We do not recommend this as the primary fix.
GXByteBuffer.setUInt8() is a generic function whose purpose is to write a UInt8.
If an application accidentally passes:
569
to a generic UInt8 function, silently converting it to:
57
could hide an unrelated programming error.
In this case, however, the value is being generated intentionally by the HDLC frame length calculation.
Therefore, the correction should preferably happen at the point where the HDLC length field is constructed:
GXDLMS.getHdlcFrame()
This keeps the generic GXByteBuffer behavior strict and fixes the actual source of the invalid value.
Suggested implementation
Our suggested implementation is:
if len1 == 0:
length = (
5
+ len(secondaryAddress)
+ len(primaryAddress)
+ len1
)
else:
length = (
7
+ len(secondaryAddress)
+ len(primaryAddress)
+ len1
)
bb.setUInt8(length & 0xFF)
The existing higher-bit calculation can remain unchanged:
(
(
len(secondaryAddress)
+ len(primaryAddress)
+ len1
) >> 8
) & 0x7
Conceptually, the complete HDLC length is therefore split into:
Complete HDLC Length
```
|
+---- Higher length bits
| (length >> 8) & 0x7
|
+---- Lower 8 bits
length & 0xFF
```
Complete relevant modified code
For reference, the relevant section after our local modification is:
if not data:
len1 = 0
bb.setUInt8(0xA0)
elif len(data) - data.position <= frameSize:
len1 = len(data) - data.position
```
bb.setUInt8(
0xA0
| (
(
len(secondaryAddress)
+ len(primaryAddress)
+ len1
) >> 8
) & 0x7
)
```
else:
len1 = frameSize
```
bb.setUInt8(
0xA8
| (
(
len(secondaryAddress)
+ len(primaryAddress)
+ len1
) >> 8
) & 0x7
)
```
if len1 == 0:
length = (
5
+ len(secondaryAddress)
+ len(primaryAddress)
+ len1
)
else:
length = (
7
+ len(secondaryAddress)
+ len(primaryAddress)
+ len1
)
bb.setUInt8(length & 0xFF)
What we are requesting from Gurux
Could you please review this behavior in the official Gurux DLMS Python implementation, specifically the HDLC length encoding inside:
GXDLMS.getHdlcFrame()
The current implementation directly passes:
7 + len(secondaryAddress) + len(primaryAddress) + len1
to:
bb.setUInt8()
When the calculated value exceeds 255, this results in a Python ValueError.
In our case:
Image Block Size = 512 bytes
Calculated value = 569
Attempted operation:
bb.setUInt8(569)
Result:
ValueError: byte must be in range(0, 256)
We believe the lower 8 bits should be explicitly written using:
bb.setUInt8(length & 0xFF)
while the higher length bits continue to be encoded using the existing:
(length >> 8) & 0x7
logic.
We would appreciate it if you could confirm whether this is the intended HDLC length encoding.
If so, we request that this correction be incorporated into the official Gurux DLMS Python library so that larger FOTA image blocks, including 512-byte blocks, can be handled without requiring a local modification.
To summarize the complete debugging process:
FOTA starts normally.
The meter reports an image block size of 512 bytes.
The firmware is loaded successfully.
510 image blocks are calculated.
Image Transfer Initiate succeeds.
Block 0 is prepared with 512 bytes.
The DLMS ActionRequest is generated successfully.
HDLC frame construction starts.
GXDLMS.getHdlcFrame() calculates the frame length.
The calculated value becomes 569.
569 is passed directly to GXByteBuffer.setUInt8().
GXByteBuffer attempts to write 569 into a bytearray.
Python rejects the value because a UInt8 can only contain 0–255.
We added debugging and confirmed the exact invalid value was 569.
We added traceback.print_stack() and traced the caller to GXDLMS.getHdlcFrame().
We reviewed the HDLC length calculation and found that the higher length bits are already handled separately.
We changed the lower-byte write to length & 0xFF.
The invalid UInt8 exception was removed and HDLC frame construction proceeded.
So the issue was finally isolated to the HDLC length-byte construction rather than the FOTA data or meter communication itself.
We have not changed the meter configuration, firmware file, authentication settings, encryption settings or FOTA block size to work around the issue. The meter is correctly providing a 512-byte block size, and we are using that value as reported by the meter.
Please review the above behavior and let us know if our interpretation of the HDLC length encoding is correct. If it is, we would appreciate an official correction in the Gurux DLMS Python library.
Regards,
Kumaresh Dhara
Hi Kumaresh Dhara, This…
Hi Kumaresh Dhara,
This works without problems on the test Windows PC that is using the same version.
SetUInt8 is now updated to version 1.0.203. Let me know if you have problems.
Regards,
Mikko
Hi Mikko, Thank you for the…
Hi Mikko,
Thank you for the quick update and for looking into this.
We have updated the Gurux DLMS Python library to version 1.0.203 and tested the FOTA operation with the same 512-byte image block size.
The FOTA transfer is now working correctly without any issues. The previous SetUInt8 / byte must be in range(0, 256) error is no longer occurring.
We have also verified that the image block transfer is proceeding successfully.
Thanks again for the support and for providing the fix in version 1.0.203.
Regards,
Kumaresh Dhara