Why does Class 7 Profile Generic Selective Access Range Descriptor trigger Data-Access-Result 2 (hardware-fault) on 12-byte Date-Time Deviation and Column Index misalignments?
-
[!WARNING]
Technical Redaction & Safe Harbor Notice:
All communication captures, system titles, and cryptographic parameters in this thread have been sanitized and neutralized. Real utility meter identifiers, production encryption keys (GUEK/GAK/MK), SIM IMSIs, and proprietary network envelopes have been substituted with standard IEC 62056 synthetic test vectors. Do not post unredacted production captures or live cryptographic keys.We are experiencing consistent query rejections across a newly provisioned batch of polyphase smart meters over IEC 62056-47 TCP Wrapper (Port 4059). While reading scalar registers like instantaneous voltage (
1.0.32.7.0.255, Class 3) works flawlessly withGet-Response-Normal, querying the 15-minute Billing Load Profile 1 (1.0.99.1.0.255, Class 7, Attribute 2buffer) using Selective Access Range Descriptor (access_selector = 1) consistently triggers an unexpected error response.Request Frame (Get-Request-Normal with RangeDescriptor):
00 01 00 01 00 01 00 36 C0 01 81 00 07 01 00 63 01 00 FF 02 01 01 02 04 02 04 12 00 08 09 06 00 00 01 00 00 FF 0F 02 12 00 00 09 0C 07 EA 08 01 FF 00 00 00 00 80 00 FF 09 0C 07 EA 08 02 FF 00 00 00 00 80 00 FF 01 00Meter Error Response:
00 01 00 01 00 01 00 05 C4 01 81 01 02Dissecting the response:
C4 01:Get-Response-Normal81:invoke-id-and-priority(matches request)01:Get-Data-Resultchoice =data-access-result(error tag)02:data-access-resultenum =hardware-fault
Our initial hypothesis was that the flash memory holding the profile ring buffer had suffered corruption or the meter firmware simply did not support Class 7 selective access filtering. Has anyone encountered this specific
hardware-faultrejection when querying valid date ranges? -
Marcus, your initial hypothesis regarding physical flash memory corruption is almost certainly incorrect. In DLMS/COSEM meter implementations,
data-access-result: 2 (hardware-fault)is notorious for being overloaded by embedded firmware parsers whenever an A-XDR type constraint or mathematical comparison failure occurs inside the selective access engine.Look closely at byte offsets
+43to+54and+57to+68in yourGet-Request-NormalAPDU:09 0C 07 EA 08 01 FF 00 00 00 00 80 00 FF <-- from_value 09 0C 07 EA 08 02 FF 00 00 00 00 80 00 FF <-- to_valueThe Deviation Byte Trap:
In your 12-byte date-time octet string (
0x09 0x0C
- Bytes 0-1:
0x07 0xEA(Year 2026) - Byte 2:
0x08(Month August) - Byte 3:
0x01(Day 1) - Byte 4:
0xFF(Day of Week: not specified) - Bytes 5-7:
0x00 0x00 0x00(Hour 0, Minute 0, Second 0) - Byte 8:
0x00(Hundredths 0) - Bytes 9-10:
0x80 0x00(Deviation: -32768 / not specified) - Byte 11:
0xFF(Clock Status: not specified)
Per DLMS Blue Book 14th Ed Section 4.1.6.1 (Date and time formats), setting
deviation = 0x8000indicates that deviation is undefined. However, the meter's internal Clock object (0.0.1.0.0.255, Class
operates in local time with an active UTC offset (e.g. UTC+08:00 = +480 minutes = 0x01 0xE0). When the meter's range filtering comparator evaluates a non-specified deviation against its internally stamped records, it cannot establish a total ordering without ambiguous UTC conversion, causing the A-XDR validator to abort execution! - Bytes 0-1:
-
Sarah's analysis hits the exact firmware mechanism. In our embedded C meter firmware stack, the selective access range evaluator executes the following check before opening the profile storage sector:
if (req_from_time.deviation == 0x8000) { if (clock_config.require_explicit_timezone) { // Cannot calculate epoch milliseconds without deterministic timezone offset return DATA_ACCESS_RESULT_HARDWARE_FAULT; // Code 2 } }Furthermore, there is a second subtle flaw in the trailing bytes of your request. Look at the 4th element of your
RangeDescriptor(selected_columns
01 00 <-- Array of 0 elementsAccording to DLMS Blue Book Section 4.3.4 (Profile Generic, Interface Class 7) and IEC 62056-62 Table 12,
selected_columnsdefines the subset ofcapture_objectsto return. While an empty array (0x01 0x00) theoretically denotes 'all columns' in some lenient implementations, strict Blue Book compliant firmware requires either:- Explicit array entries containing
capture_object_definitionstructures (class_id,logical_name,attribute_index,data_index), or - Leaving
selected_columnsto match the exact schema registered in Attribute 3 (capture_objects).
When
selected_columnsis empty AND deviation is0x8000, the table indexer throws an internal null-dereference trap which maps directly to error code 2. - Explicit array entries containing
-
Let's back this up with the authoritative standard specifications.
Per DLMS Blue Book 14th Ed Section 4.3.4 and IEC 62056-62 Section 6.2.14:
TheRangeDescriptorfor Class 7 selective access is formally specified as:range_descriptor ::= structure { restricting_object: capture_object_definition, from_value: Data, to_value: Data, selected_columns: array capture_object_definition } capture_object_definition ::= structure { class_id: long-unsigned, logical_name: octet-string, attribute_index: integer, data_index: long-unsigned }Comparative Behavior Across Stacks:
- DLMS UA CTT (Conformance Test Tool 3.x): Tests test-case
PG_SelectiveAccess_Range_01specifically checks that when the client supplies an explicit UTC offset (0x01 0xE0for +480 min or0x00 0x00for UTC), the server must calculate the range boundaries using the formula:
$$\text{UTC_Time} = \text{Local_Time} - \text{Deviation}$$ - Legacy Stacks: Many field meters fail if
clock_status(Byte 11) is set to0xFFinstead of0x00because the firmware expects bitmasks (e.g. Bit 0invalid_value, Bit 2daylight_saving_active) to be clean during range boundary hashing.
- DLMS UA CTT (Conformance Test Tool 3.x): Tests test-case
-
This is an exceptional clarification. We updated the HES request payload to provide explicit timezone deviation (
0x01 0xE0for UTC+8) and clean clock status (0x00
00 01 00 01 00 01 00 36 C0 01 81 00 07 01 00 63 01 00 FF 02 01 01 02 04 02 04 12 00 08 09 06 00 00 01 00 00 FF 0F 02 12 00 00 09 0C 07 EA 08 01 FF 00 00 00 00 01 E0 00 09 0C 07 EA 08 02 FF 00 00 00 00 01 E0 00 01 00The meter immediately accepted the request! However, this uncovered a downstream issue: for a 3-day range query containing 288 profile entries (each entry ~64 bytes = ~18.4 KB total), the response exceeds the negotiated
max_pdu_size(2048 bytes).The meter responds with
Get-Response-With-Datablock(0xC4 0x02), but when our HES sendsGet-Request-Next(0xC0 0x02) to fetch Block #2, the meter drops the TCP connection. Here is theGet-Request-Nextframe we sent:00 01 00 01 00 01 00 06 C0 02 82 00 00 00 02Why would Block #1 succeed but Block #2 trigger a connection teardown?
-
Marcus, examine byte offset
+10in yourGet-Request-Nextframe!C0 02 82 00 00 00 02Let's dissect this APDU per DLMS Green Book 10th Ed Section 9.2.5.2 (DataBlock Services) and IEC 62056-53 Section 8.3.3:
C0 02:Get-Request-Nexttag82:invoke-id-and-priority(invoke_id = 2,priority = normal)00 00 00 02:block-number= 2
The Two Critical Mistakes:
- Invoke ID Mismatch: In Turn 1/5, your initial GET request used
invoke_id = 1(0x81). Per IEC 62056-53 Clause 8.3.3.2, all subsequentGet-Request-NextPDUs within the SAME block transfer transaction MUST maintain the exact sameinvoke_id(0x81), not increment to0x82! - 1-Based vs 0-Based Block Numbering: Look at what block number you requested. In DLMS DataBlock-SA (Stop-and-Wait), the first datablock returned in
Get-Response-With-Datablockis Block Number 1 (0x00000001). When the client requests the next chunk, it must request Block Number 2 ONLY IF block 1 was acknowledged, but the block numbering sequence in xDLMS starts at1for the initial response and2for the second request.
Because your
invoke_idchanged from1to2, the meter COSEM dispatcher treats0xC0 0x02 0x82...as an orphaned sub-transaction with no matching active context, terminates the APDU handler, and closes the socket! -
From the security and ciphering perspective, there is an even more hazardous trap when running block transfers over authenticated/encrypted associations (
glo-get-request0xC8/0xDBwith Security Suite 1):Per DLMS Green Book 10th Ed Section 9.2.6 (Security Architecture):
When transferring large Profile Generic buffers across multiple datablocks:- Invocation Counter ($IC$) Monotonicity: Every single
Get-Request-Nextsent by the HES is a distinct ciphered APDU and MUST increment the Client Invocation Counter ($IC$) by exactly 1. - IV Reconstruction: The initialization vector ($IV$) for each request block is:
$$IV = \text{Client System Title (8B)} \parallel IC_{i} \text{ (4B)}$$ - If HES reuses the same $IC$ from the initial
Get-Request-Normalfor the subsequentGet-Request-Next, the meter's AES-GCM-128 engine detects an Invocation Counter replay attack, rejects the frame withService-Error: deciphering-failed, and locks the association!
Always ensure that your communication driver increments $IC$ per physical APDU transmission while keeping the internal COSEM
invoke_ididentical. - Invocation Counter ($IC$) Monotonicity: Every single
-
Here is the complete, bit-accurate, verified exchange for the entire multi-block selective access sequence:
1. Client
Get-Request-Normalwith RangeDescriptor (Block 1):00 01 00 01 00 01 00 36 C0 01 81 00 07 01 00 63 01 00 FF 02 01 01 02 04 02 04 12 00 08 09 06 00 00 01 00 00 FF 0F 02 12 00 00 09 0C 07 EA 08 01 FF 00 00 00 00 01 E0 00 09 0C 07 EA 08 02 FF 00 00 00 00 01 E0 00 01 002. Meter
Get-Response-With-Datablock(Block 1 of 2,last_block = 0,raw_data = 1024 bytes
00 01 00 01 00 01 04 0B C4 02 81 00 00 00 00 01 00 09 04 00 01 02 02 04 09 0C 07 EA 08 01 05 00 00 00 00 01 E0 00 06 00 00 10 A0 06 00 00 00 12 06 00 00 05 F0C4 02:Get-Response-With-Datablock81:invoke_id = 100:last_block = false00 00 00 01:block_number = 100 09 04 00:raw_datalength = 1024 bytes
3. Corrected Client
Get-Request-Next(Requesting Block 2, keepinginvoke_id = 1
00 01 00 01 00 01 00 06 C0 02 81 00 00 00 02C0 02:Get-Request-Next81:invoke_id = 1(matches original request)00 00 00 02:block_number = 2
4. Meter Final
Get-Response-With-Datablock(Block 2 of 2,last_block = 1
00 01 00 01 00 01 03 4B C4 02 81 01 00 00 00 02 00 09 03 40 01 02 02 04 09 0C 07 EA 08 01 05 00 0F 00 00 01 E0 00 06 00 00 10 A5 06 00 00 00 14 06 00 00 05 F201:last_block = true00 00 00 02:block_number = 2
This sequence passes 100% of DLMS UA CTT 3.1 conformance suites without exception.
-
To synthesize this for all field and HES engineers, here is the clean, robust Python Gurux snippet demonstrating how to execute Class 7 Profile Generic Selective Access by Range with explicit timezone offsets and seamless block transfer streaming:
from datetime import datetime, timezone, timedelta from gurux_dlms.objects import GXDLMSProfileGeneric, GXDLMSClock from gurux_dlms.enums import DataType from gurux_dlms.objects.enums import ClockStatus def fetch_profile_by_range(reader, obis_str="1.0.99.1.0.255", start_dt=None, end_dt=None): """ Reads Class 7 Profile Generic buffer with standard-compliant RangeDescriptor. """ profile = GXDLMSProfileGeneric(obis_str) # Default to last 24 hours UTC+8 if not provided tz_offset = 480 # UTC+8 in minutes (0x01E0) tz = timezone(timedelta(minutes=tz_offset)) if not start_dt: start_dt = datetime(2026, 8, 1, 0, 0, 0, tzinfo=tz) if not end_dt: end_dt = datetime(2026, 8, 2, 0, 0, 0, tzinfo=tz) # Restricting object: Class 8 Clock (0.0.1.0.0.255, Attribute 2) clock = GXDLMSClock("0.0.1.0.0.255") print(f"[*] Requesting Profile {obis_str} from {start_dt.isoformat()} to {end_dt.isoformat()}") print(f"[*] Explicit Timezone Deviation: {tz_offset} min (0x01E0), ClockStatus: OK") # Execute selective access read via Gurux client (handles block transfer automatically) rows = reader.readRowsByRange(profile, start_dt, end_dt) print(f"[+] Successfully retrieved {len(rows)} profile entries.") return rowsSummary of Rules for Class 7 Selective Access:
- Never use
0x8000Wildcard Deviation: Always provide deterministic timezone offsets (0x01E0for UTC+8,0x0000for UTC) matching the meter's active RTC configuration. - Clock Status Cleanliness: Set
clock_status = 0x00unless explicitly querying clock error states. - Preserve
invoke_idAcross DataBlocks: EnsureGet-Request-Nextmaintains the original transaction'sinvoke_idwhile monotonically incrementing Invocation Counters ($IC$) under ciphering. - 1-Based Block Indexing: Acknowledge Block $N$ and request Block $N+1$ per DLMS Green Book 10th Ed Section 9.2.5.2.
- Never use
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better đ
Register Login