Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • World
  • Users
  • Groups
Skins
  • Light
  • Dark

Collapse
SHARUR
Community Wiki DLMS/COSEM Tools
Search Sign in

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?

Scheduled Pinned Locked Moved General Discussion
dlmscosemprofilegenericselectiveaccessrangedescriptor
9 Posts 5 Posters 58 Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
  • M Offline
    M Offline
    marcus_vance
    wrote on last edited by
    #1

    [!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 with Get-Response-Normal, querying the 15-minute Billing Load Profile 1 (1.0.99.1.0.255, Class 7, Attribute 2 buffer) 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 00
    

    Meter Error Response:

    00 01 00 01 00 01 00 05 C4 01 81 01 02
    

    Dissecting the response:

    • C4 01: Get-Response-Normal
    • 81: invoke-id-and-priority (matches request)
    • 01: Get-Data-Result choice = data-access-result (error tag)
    • 02: data-access-result enum = 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-fault rejection when querying valid date ranges?

    1 Reply Last reply
    0
    • S Offline
      S Offline
      sarah_jenkins
      wrote on last edited by
      #2

      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 +43 to +54 and +57 to +68 in your Get-Request-Normal APDU:

      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_value
      

      The 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 = 0x8000 indicates 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!

      1 Reply Last reply
      0
      • E Offline
        E Offline
        elena_rostova
        wrote on last edited by
        #3

        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 elements
        

        According to DLMS Blue Book Section 4.3.4 (Profile Generic, Interface Class 7) and IEC 62056-62 Table 12, selected_columns defines the subset of capture_objects to return. While an empty array (0x01 0x00) theoretically denotes 'all columns' in some lenient implementations, strict Blue Book compliant firmware requires either:

        1. Explicit array entries containing capture_object_definition structures (class_id, logical_name, attribute_index, data_index), or
        2. Leaving selected_columns to match the exact schema registered in Attribute 3 (capture_objects).

        When selected_columns is empty AND deviation is 0x8000, the table indexer throws an internal null-dereference trap which maps directly to error code 2.

        1 Reply Last reply
        0
        • D Offline
          D Offline
          david_miller
          wrote on last edited by
          #4

          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:
          The RangeDescriptor for 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:

          1. DLMS UA CTT (Conformance Test Tool 3.x): Tests test-case PG_SelectiveAccess_Range_01 specifically checks that when the client supplies an explicit UTC offset (0x01 0xE0 for +480 min or 0x00 0x00 for UTC), the server must calculate the range boundaries using the formula:
            $$\text{UTC_Time} = \text{Local_Time} - \text{Deviation}$$
          2. Legacy Stacks: Many field meters fail if clock_status (Byte 11) is set to 0xFF instead of 0x00 because the firmware expects bitmasks (e.g. Bit 0 invalid_value, Bit 2 daylight_saving_active) to be clean during range boundary hashing.
          1 Reply Last reply
          0
          • M Offline
            M Offline
            marcus_vance
            wrote on last edited by
            #5

            This is an exceptional clarification. We updated the HES request payload to provide explicit timezone deviation (0x01 0xE0 for 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 00
            

            The 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 sends Get-Request-Next (0xC0 0x02) to fetch Block #2, the meter drops the TCP connection. Here is the Get-Request-Next frame we sent:

            00 01 00 01 00 01 00 06 C0 02 82 00 00 00 02
            

            Why would Block #1 succeed but Block #2 trigger a connection teardown?

            1 Reply Last reply
            0
            • D Offline
              D Offline
              david_miller
              wrote on last edited by
              #6

              Marcus, examine byte offset +10 in your Get-Request-Next frame!

              C0 02 82 00 00 00 02
              

              Let'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-Next tag
              • 82: invoke-id-and-priority (invoke_id = 2, priority = normal)
              • 00 00 00 02: block-number = 2

              The Two Critical Mistakes:

              1. 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 subsequent Get-Request-Next PDUs within the SAME block transfer transaction MUST maintain the exact same invoke_id (0x81), not increment to 0x82!
              2. 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-Datablock is 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 at 1 for the initial response and 2 for the second request.

              Because your invoke_id changed from 1 to 2, the meter COSEM dispatcher treats 0xC0 0x02 0x82... as an orphaned sub-transaction with no matching active context, terminates the APDU handler, and closes the socket!

              1 Reply Last reply
              0
              • A Offline
                A Offline
                alex_turner
                wrote on last edited by
                #7

                From the security and ciphering perspective, there is an even more hazardous trap when running block transfers over authenticated/encrypted associations (glo-get-request 0xC8 / 0xDB with Security Suite 1):

                Per DLMS Green Book 10th Ed Section 9.2.6 (Security Architecture):
                When transferring large Profile Generic buffers across multiple datablocks:

                1. Invocation Counter ($IC$) Monotonicity: Every single Get-Request-Next sent by the HES is a distinct ciphered APDU and MUST increment the Client Invocation Counter ($IC$) by exactly 1.
                2. IV Reconstruction: The initialization vector ($IV$) for each request block is:
                  $$IV = \text{Client System Title (8B)} \parallel IC_{i} \text{ (4B)}$$
                3. If HES reuses the same $IC$ from the initial Get-Request-Normal for the subsequent Get-Request-Next, the meter's AES-GCM-128 engine detects an Invocation Counter replay attack, rejects the frame with Service-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_id identical.

                1 Reply Last reply
                0
                • E Offline
                  E Offline
                  elena_rostova
                  wrote on last edited by
                  #8

                  Here is the complete, bit-accurate, verified exchange for the entire multi-block selective access sequence:

                  1. Client Get-Request-Normal with 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 00
                  

                  2. 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 F0
                  
                  • C4 02: Get-Response-With-Datablock
                  • 81: invoke_id = 1
                  • 00: last_block = false
                  • 00 00 00 01: block_number = 1
                  • 00 09 04 00: raw_data length = 1024 bytes

                  3. Corrected Client Get-Request-Next (Requesting Block 2, keeping invoke_id = 1😞

                  00 01 00 01 00 01 00 06 C0 02 81 00 00 00 02
                  
                  • C0 02: Get-Request-Next
                  • 81: 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 F2
                  
                  • 01: last_block = true
                  • 00 00 00 02: block_number = 2

                  This sequence passes 100% of DLMS UA CTT 3.1 conformance suites without exception.

                  1 Reply Last reply
                  0
                  • S Offline
                    S Offline
                    sarah_jenkins
                    wrote on last edited by
                    #9

                    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 rows
                    

                    Summary of Rules for Class 7 Selective Access:

                    1. Never use 0x8000 Wildcard Deviation: Always provide deterministic timezone offsets (0x01E0 for UTC+8, 0x0000 for UTC) matching the meter's active RTC configuration.
                    2. Clock Status Cleanliness: Set clock_status = 0x00 unless explicitly querying clock error states.
                    3. Preserve invoke_id Across DataBlocks: Ensure Get-Request-Next maintains the original transaction's invoke_id while monotonically incrementing Invocation Counters ($IC$) under ciphering.
                    4. 1-Based Block Indexing: Acknowledge Block $N$ and request Block $N+1$ per DLMS Green Book 10th Ed Section 9.2.5.2.
                    1 Reply Last reply
                    0

                    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
                    Reply
                    • Reply as topic
                    Log in to reply
                    • Oldest to Newest
                    • Newest to Oldest
                    • Most Votes


                    • Login

                    • Don't have an account? Register

                    • Login or register to search.
                    Powered by NodeBB Contributors
                    • First post
                      Last post
                    0
                    • Categories
                    • Recent
                    • Tags
                    • Popular
                    • World
                    • Users
                    • Groups