πŸ›°οΈZafer SatΔ±lmış - Aviora

AppProtocolMetallixTLV β€” Binary TLV Protocol

Lightweight binary protocol for IoT communication. Every packet is delimited by $ … # and contains one or more flat TLV (Tag-Length-Value) fields. No nesting β€” each tag carries exactly one piece of data. Shares the same AppTcpConnManager transport and AppMeterOperations backend as ProtocolZD.

Binary TLV Big-Endian Push + Pull Compact & Fast

Wire format

A packet is a byte stream starting with $ (0x24) and ending with # (0x23). Between the delimiters, one or more TLV triplets are placed sequentially:

$0x24
TAG2 bytes
LEN2 bytes
VALUELEN bytes
TAG2 bytes
LEN2 bytes
VALUELEN bytes
…
#0x23
TLV Field Structure
block-beta
                                        columns 6
                                        TAG["TAG\n2 bytes\n(big-endian)"]:2
                                        LEN["LEN\n2 bytes\n(big-endian)"]:2
                                        VAL["VALUE\nLEN bytes\n(raw data)"]:2
                  

Data type encoding

TypeWire SizeEncoding
StringN bytesRaw ASCII, 1 byte per character, no null terminator
Bool1 byte0x01 = true, 0x00 = false
uint81 byteUnsigned
uint162 bytesBig-endian unsigned
uint324 bytesBig-endian unsigned
int162 bytesBig-endian two's complement

Protocol State Machine

Registration β†’ alive β†’ pull handling
stateDiagram-v2
  [*] --> Init: appProtocolMetallixTLVStart
  Init --> ConnectPush: create TCP task
  ConnectPush --> SendIdent: push connected
  ConnectPush --> ConnectPush: timeout β†’ retry 30s
  SendIdent --> WaitRegister: ident packet sent
  WaitRegister --> Registered: TAG_REGISTER = true
  WaitRegister --> SendIdent: timeout 30s β†’ retry
  Registered --> Registered: alive every 5 min (push)
  Registered --> Registered: handle pull requests
  Registered --> Registered: deliver meter data (push)
  Registered --> [*]: appProtocolMetallixTLVStop

Complete TAG Table

Tags are grouped by function area. High byte determines the group, low byte the field within the group.

TAGNameTypeDescription
0x00XX β€” Common / Header
0x0001FLAGstringProduct flag, e.g. "AVI"
0x0002SERIAL_NUMBERstringDevice serial number
0x0003FUNCTIONuint8Message type (MetallixTLV_Function_t enum)
0x01XX β€” Ident / Alive
0x0101REGISTEREDboolDevice β†’ server: current registration state
0x0102DEVICE_BRANDstringDevice brand, e.g. "AVI"
0x0103DEVICE_MODELstringDevice model, e.g. "AVIO2622"
0x0104DEVICE_DATEstring"YYYY-MM-DD HH:MM:SS"
0x0105PULL_IPstringDevice's own IP address
0x0106PULL_PORTuint16Pull-server port on device
0x0107REGISTERboolServer β†’ device: registration accepted
0x02XX β€” Packet Streaming
0x0201PACKET_NUMuint16Sequential packet number
0x0202PACKET_STREAMbooltrue = more packets follow
0x03XX β€” ACK / NACK
0x0301ACK_STATUSbooltrue = ACK, false = NACK
0x04XX β€” Log
0x0401LOG_DATAstringLog text chunk
0x05XX β€” Meter Fields
0x0501METER_OPERATIONstring"add" or "remove"
0x0502METER_PROTOCOLstringe.g. "IEC62056"
0x0503METER_TYPEstringe.g. "electricity"
0x0504METER_BRANDstringe.g. "MKL"
0x0505METER_SERIAL_NUMstringMeter serial number
0x0506METER_SERIAL_PORTstringe.g. "port-1"
0x0507METER_INIT_BAUDuint32Initial baud rate
0x0508METER_FIX_BAUDboolFixed baud rate?
0x0509METER_FRAMEstringe.g. "7E1"
0x050AMETER_CUSTOMER_NUMstringCustomer number
0x050BMETER_INDEXuint8Multi-meter separator (0, 1, 2 …)
0x06XX β€” Server Config
0x0601SERVER_IPstringNew server IP address
0x0602SERVER_PORTuint16New server port
0x07XX β€” Readout / Load Profile
0x0701METER_IDstringMeter identification string
0x0702READOUT_DATAstringReadout / load-profile data chunk
0x0703DIRECTIVE_NAMEstringDirective name used in request
0x0704START_DATEstringLoad-profile start date
0x0705END_DATEstringLoad-profile end date
0x08XX β€” Directive
0x0801DIRECTIVE_IDstringDirective identifier
0x0802DIRECTIVE_DATAstringRaw directive body (JSON blob)
0x09XX β€” FW Update
0x0901FW_ADDRESSstringFTP URL for firmware download
0x0AXX β€” Error
0x0A01ERROR_CODEint16ERR_CODE_T value

Function Types Values and Directions

ValueNameDirectionDescription
0x01FUNC_IDENTPushRegistration / identification
0x02FUNC_ALIVEPushKeep-alive heartbeat
0x03FUNC_ACKBothPositive acknowledgment
0x04FUNC_NACKBothNegative acknowledgment
0x05FUNC_LOGPullSystem log request / response
0x06FUNC_SETTINGPullServer / meter configuration
0x07FUNC_FW_UPDATEPullFirmware update trigger
0x08FUNC_READOUTPull+PushMeter readout request + data delivery
0x09FUNC_LOADPROFILEPull+PushLoad profile request + data delivery
0x0AFUNC_DIRECTIVE_LISTPullList stored directives
0x0BFUNC_DIRECTIVE_ADDPullAdd a new directive
0x0CFUNC_DIRECTIVE_DELPullDelete a directive

Architecture Overview

Module Interaction
flowchart TB
  subgraph Server["Central server"]
    SP["Push port\n(server listens)"]
    PC["Pull client\n(server connects)"]
  end
  subgraph Device["Gateway device"]
    METALLIX["AppProtocolMetallixTLV\n(state machine + TLV builder/parser)"]
    TCP["AppTcpConnManager\n(push + pull sockets)"]
    MTR["AppMeterOperations\n(meter CRUD, readout jobs)"]
    FS["File System\n(register, server config)"]
    METALLIX -->|"build packet, send"| TCP
    TCP -->|"raw bytes callback"| METALLIX
    METALLIX -->|"add/remove meter\nreadout/profile task"| MTR
    MTR -->|"callback: task done"| METALLIX
    METALLIX -->|"persist state"| FS
    TCP -->|"push: device connects"| SP
    PC -->|"pull: server connects to device"| TCP
  end

1. Ident (Registration) β€” Push

Device β†’ Server

$
  0001 0003 "AVI"                     TAG_FLAG
  0002 000F "0123456789ABCDE"         TAG_SERIAL_NUMBER
  0003 0001 01                        TAG_FUNCTION = FUNC_IDENT
  0101 0001 00                        TAG_REGISTERED = false
  0102 0003 "AVI"                     TAG_DEVICE_BRAND
  0103 0008 "AVIO2622"               TAG_DEVICE_MODEL
  0104 0013 "2021-06-02 17:19:58"    TAG_DEVICE_DATE
  0105 000C "192.168.1.10"           TAG_PULL_IP
  0106 0002 0A3E                      TAG_PULL_PORT = 2622
#

TAG_REGISTERED = 0x00 on first boot. If the device was previously registered (persisted to file), sends 0x01.

Server β†’ Device (push socket reply)

$
  0001 0003 "AVI"                     TAG_FLAG
  0002 000F "0123456789ABCDE"         TAG_SERIAL_NUMBER
  0003 0001 01                        TAG_FUNCTION = FUNC_IDENT
  0107 0001 01                        TAG_REGISTER = true
#

TAG_REGISTER = 0x01 means the server accepts the device. State is saved to metallixtlv_register.dat.

Ident Sequence
sequenceDiagram
  participant D as Device
  participant S as Server (push port)
  D->>S: $ FLAG + SERIAL + FUNC_IDENT + fields… #
  alt Server accepts
    S->>D: $ FLAG + SERIAL + FUNC_IDENT + REGISTER=true #
    D->>D: save registered state
  else No response / rejected
    D->>D: wait 30s, retry
  end

2. Alive β€” Push

Device β†’ Server (Every 30 Minutes)

$
  0001 0003 "AVI"                     TAG_FLAG
  0002 000F "0123456789ABCDE"         TAG_SERIAL_NUMBER
  0003 0001 02                        TAG_FUNCTION = FUNC_ALIVE
  0104 0013 "2026-03-29 14:30:00"    TAG_DEVICE_DATE
#

Server β†’ Device (Optional ACK)

$
  0001 0003 "AVI"    0002 000F "0123456789ABCDE"
  0003 0001 03       TAG_FUNCTION = FUNC_ACK
  0301 0001 01       TAG_ACK_STATUS = true
#

3. ACK / NACK

-- ACK --
$  0001 0003 "AVI"  0002 000F "…"  0003 0001 03  0301 0001 01  #

-- NACK --
$  0001 0003 "AVI"  0002 000F "…"  0003 0001 04  0301 0001 00  #

ACK = TAG_FUNCTION=0x03 + TAG_ACK_STATUS=true. NACK = TAG_FUNCTION=0x04 + TAG_ACK_STATUS=false.

4. Log β€” Pull

Server β†’ Device (Request)

$  0001 0003 "AVI"  0002 000F "…"  0003 0001 05  #

Device β†’ Server (Response, Packetized)

$
  0001 0003 "AVI"  0002 000F "0123456789ABCDE"
  0003 0001 05                        TAG_FUNCTION = FUNC_LOG
  0201 0002 0001                      TAG_PACKET_NUM = 1
  0202 0001 01                        TAG_PACKET_STREAM = true (more coming)
  0401 01F4 "…700 bytes of log…"     TAG_LOG_DATA
#

Last packet: TAG_PACKET_STREAM = 0x00. If no log exists, sends TAG_LOG_DATA = "NO-LOG".

5. Setting β€” Pull

Server β†’ Device (server config + meter add/remove)

$
  0001 0003 "AVI"  0002 000F "0123456789ABCDE"
  0003 0001 06                        TAG_FUNCTION = FUNC_SETTING

  -- Server address update --
  0601 000D "192.168.1.100"          TAG_SERVER_IP
  0602 0002 2212                      TAG_SERVER_PORT = 8722

  -- Meter #0: add --
  050B 0001 00                        TAG_METER_INDEX = 0
  0501 0003 "add"                    TAG_METER_OPERATION
  0502 0008 "IEC62056"               TAG_METER_PROTOCOL
  0503 000B "electricity"            TAG_METER_TYPE
  0504 0003 "MKL"                    TAG_METER_BRAND
  0505 0008 "12345678"               TAG_METER_SERIAL_NUM
  0506 0006 "port-1"                 TAG_METER_SERIAL_PORT
  0507 0004 0000012C                  TAG_METER_INIT_BAUD = 300
  0508 0001 00                        TAG_METER_FIX_BAUD = false
  0509 0003 "7E1"                    TAG_METER_FRAME

  -- Meter #1: remove --
  050B 0001 01                        TAG_METER_INDEX = 1
  0501 0006 "remove"                 TAG_METER_OPERATION
  0504 0003 "MKL"                    TAG_METER_BRAND
  0505 0008 "12345678"               TAG_METER_SERIAL_NUM
#

TAG_METER_INDEX acts as a separator: each occurrence starts a new meter entry. The parser commits the previous meter when it encounters a new index.

Device β†’ Server

$  …  0003 0001 03  0301 0001 01  #   (ACK)
Setting Parse Flow
flowchart LR
  A["Parse packet"] --> B{"TAG_SERVER_IP\npresent?"}
  B -- Yes --> C["Update server\nconfig + reconnect"]
  B -- No --> D["Skip"]
  C --> E{"Scan for\nTAG_METER_INDEX"}
  D --> E
  E -- "Found idx=0" --> F["Collect meter\nfields until\nnext INDEX or end"]
  F --> G{"OPERATION?"}
  G -- add --> H["appMeterOperationsAddMeter"]
  G -- remove --> I["appMeterOperationsDeleteMeter"]
  H --> E
  I --> E
  E -- "No more" --> J["Send ACK/NACK"]

6. Firmware Update β€” Pull

Server β†’ Device

$
  0001 0003 "AVI"  0002 000F "0123456789ABCDE"
  0003 0001 07                        TAG_FUNCTION = FUNC_FW_UPDATE
  0901 002E "ftp://user:pass@192.168.1.50:21/fw/v2.bin"   TAG_FW_ADDRESS
#

Device β†’ Server

$  …  0003 0001 03  0301 0001 01  #   (ACK, then starts FTP download)

7. Readout β€” Pull + Push

a) Server β†’ Device (pull: request)

$
  0001 0003 "AVI"  0002 000F "0123456789ABCDE"
  0003 0001 08                        TAG_FUNCTION = FUNC_READOUT
  0703 0012 "ReadoutDirective1"      TAG_DIRECTIVE_NAME
  0505 0008 "12345678"               TAG_METER_SERIAL_NUM
#

b) Device β†’ Server (Pull: ACK)

$  …  0003 0001 03  0301 0001 01  #   (ACK, job queued)

c) Device β†’ Server (push: data delivery, packetized)

$
  0001 0003 "AVI"  0002 000F "0123456789ABCDE"
  0003 0001 08                        TAG_FUNCTION = FUNC_READOUT
  0201 0002 0001                      TAG_PACKET_NUM = 1
  0202 0001 00                        TAG_PACKET_STREAM = false (last packet)
  0701 0016 "/LGZ5\\2ZMG405000b.P07" TAG_METER_ID
  0702 00C8 "0.0.0(23660088)..."     TAG_READOUT_DATA
#

d) Server β†’ Device (Push: ACK)

$  …  0003 0001 03  0301 0001 01  #
End-to-End Readout Sequence
sequenceDiagram
  participant S as Server
  participant Pull as Device pull :2622
  participant OT as MetallixTLV
  participant MO as MeterOps
  participant Push as Device push→server

  S->>Pull: $ FUNC_READOUT + DIRECTIVE_NAME + METER_SN #
  Pull->>OT: PutIncomingMessage
  OT->>S: $ FUNC_ACK #  (pull)
  OT->>MO: AddReadoutTask
  Note over MO: meter communication...
  MO-->>OT: callback(SUCCESS)
  OT->>Push: $ FUNC_READOUT + PACKET_NUM + METER_ID + DATA #
  S->>Push: $ FUNC_ACK #
  OT->>OT: delete output files

8. Load Profile β€” Pull + Push

Server β†’ Device (pull: request)

$
  0001 0003 "AVI"  0002 000F "0123456789ABCDE"
  0003 0001 09                        TAG_FUNCTION = FUNC_LOADPROFILE
  0703 0012 "ProfileDirective1"      TAG_DIRECTIVE_NAME
  0505 0008 "12345678"               TAG_METER_SERIAL_NUM
  0704 0013 "2021-06-22 00:00:00"    TAG_START_DATE
  0705 0013 "2021-06-22 12:05:00"    TAG_END_DATE
#

Same ACK β†’ job queue β†’ push delivery flow as readout. TAG_METER_ID uses "load-profile-no-id" in data packets.

9. Directive CRUD β€” Pull

DirectiveList Request

$
  …  0003 0001 0A                     TAG_FUNCTION = FUNC_DIRECTIVE_LIST
  0801 0010 "ReadoutDirective"       TAG_DIRECTIVE_ID  (filter, "*" = all)
#

Response: one packet per matching directive with TAG_DIRECTIVE_DATA + packetStream fields.

DirectiveAdd Request

$
  …  0003 0001 0B                     TAG_FUNCTION = FUNC_DIRECTIVE_ADD
  0802 00A0 "{\"id\":\"Readout\",…}" TAG_DIRECTIVE_DATA (raw JSON blob)
#

DirectiveDelete Request

$
  …  0003 0001 0C                     TAG_FUNCTION = FUNC_DIRECTIVE_DEL
  0801 0007 "Readout"                TAG_DIRECTIVE_ID  ("*" = delete all)
#

Reply: ACK or NACK.

ProtocolZD (JSON) vs ProtocolMetallixTLV (TLV) Comparison

Same Ident Message β€” Size Comparison
xychart-beta
  title "Ident packet size (bytes)"
  x-axis ["ProtocolZD\n(JSON)", "ProtocolMetallixTLV\n(TLV)"]
  y-axis "Bytes" 0 --> 300
  bar [247, 79]
AspectProtocolZD (JSON)ProtocolMetallixTLV (TLV)
FormatJSON textBinary TLV
Human readableYesNo (hex dump needed)
Parsing costString scanning, key lookupDirect offset reads, O(n) tag scan
Packet sizeLarger (keys + quotes + braces)Compact (4-byte header per field)
NestingArbitrary JSON nestingFlat β€” one level only
TransportAppTcpConnManagerAppTcpConnManager (shared)
BackendAppMeterOperationsAppMeterOperations (shared)

Builder & Parser C API

Building a Packet

MetallixTLV_Builder_t b;
uint8_t buf[256];

metallixTLV_BuilderInit(&b, buf, sizeof(buf));
metallixTLV_PacketBegin(&b);                        // writes '$'
metallixTLV_AddDeviceHeader(&b);                    // FLAG + SERIAL_NUMBER
metallixTLV_AddUint8(&b, TAG_FUNCTION, FUNC_ALIVE);
metallixTLV_AddString(&b, TAG_DEVICE_DATE, "2026-03-29 14:30:00");
uint16_t len = metallixTLV_PacketEnd(&b);           // writes '#', returns total

appTcpConnManagerSend(PUSH_TCP_SOCK_NAME, (char *)buf, len);

Parsing a Received Packet

MetallixTLV_Parser_t p;
if (metallixTLV_ParserInit(&p, rawData, rawLen))
{
    MetallixTLV_Function_t func;
    metallixTLV_GetFunction(&p, &func);     // reads TAG_FUNCTION

    char sn[20];
    metallixTLV_GetString(&p, TAG_SERIAL_NUMBER, sn, sizeof(sn));

    BOOL regVal;
    metallixTLV_GetBool(&p, TAG_REGISTER, ®Val);

    uint16_t port;
    metallixTLV_GetUint16(&p, TAG_PULL_PORT, &port);

    // or iterate all TLVs:
    uint16_t tag; const uint8_t *val; uint16_t vLen;
    metallixTLV_ParserReset(&p);
    while (metallixTLV_ParserNext(&p, &tag, &val, &vLen)) { … }
}

Full Protocol Lifecycle

From boot to steady-state operation
sequenceDiagram
  participant App as Application
  participant OT as MetallixTLV
  participant TCP as TcpConnManager
  participant S as Server

  App->>OT: appProtocolMetallixTLVInit(serial, serverIP, port, deviceIP, pullPort)
  App->>OT: appProtocolMetallixTLVStart()
  OT->>TCP: appTcpConnManagerStart()
  OT->>TCP: requestConnect()

  rect rgb(40, 60, 90)
    Note over OT,S: Registration phase
    OT->>TCP: requestPushConnect()
    TCP->>S: TCP connect (push)
    OT->>S: $ FUNC_IDENT packet #
    S->>OT: $ FUNC_IDENT + REGISTER=true #
    OT->>OT: registerStateSave(TRUE)
  end

  rect rgb(40, 70, 50)
    Note over OT,S: Steady state (registered)
    loop Every 5 minutes
      OT->>S: $ FUNC_ALIVE + DEVICE_DATE #
    end

    S->>OT: $ FUNC_LOG #
    OT->>S: $ FUNC_LOG + LOG_DATA packets #

    S->>OT: $ FUNC_SETTING + meters #
    OT->>S: $ FUNC_ACK #

    S->>OT: $ FUNC_READOUT + request #
    OT->>S: $ FUNC_ACK #
    Note over OT: meter job runs...
    OT->>S: $ FUNC_READOUT + data packets #
  end

  App->>OT: appProtocolMetallixTLVStop()
  OT->>TCP: appTcpConnManagerStop()