This document provides a detailed explanation of how messages are constructed and deconstructed in the BiSecure Gateway protocol.
- Overview
- Message Structure
- Transport Container
- BiPackage (Inner Package)
- Payload Formats
- Checksum Calculation
- Encoding and Decoding
- Examples
The BiSecure Gateway protocol uses a layered message structure where:
- The Transport Container is the outer layer containing sender/receiver addresses and a checksum
- The BiPackage is the inner layer containing the command, session token, and payload
- The Payload contains command-specific data
All messages are converted to hexadecimal string representation before being sent over TCP/IP.
┌─────────────────── Transport Container ───────────────────┐
│ │
│ Sender Receiver BiPackage TC Checksum │
│ (6 bytes) (6 bytes) (variable length) (1 byte) │
│ │
│ ┌────────── BiPackage ──────────┐ │
│ │ │ │
│ │ Length Tag Token Command │ │
│ │ (2 bytes)(1) (4 bytes) (1) │ │
│ │ │ │
│ │ Payload BP Checksum │ │
│ │ (variable) (1 byte) │ │
│ └────────────────────────────────┘ │
└────────────────────────────────────────────────────────────┘
The Transport Container wraps the entire message and provides addressing information.
| Field | Size (bytes) | Description |
|---|---|---|
| Sender Address | 6 | MAC address of sender (app uses 000000000000) |
| Receiver Address | 6 | MAC address of receiver (gateway's MAC) |
| BiPackage | Variable | The inner package (see below) |
| Checksum | 1 | XOR checksum of all bytes in the container |
fun constructTransportContainer(
sender: String, // e.g., "000000000000"
receiver: String, // e.g., "5410EC036150" (gateway MAC)
biPackage: BiPackage
): TransportContainer {
val checksum = calculateTransportChecksum(sender, receiver, biPackage)
return TransportContainer(sender, receiver, biPackage, checksum)
}- Extract Sender (bytes 0-5): Convert to hex string
- Extract Receiver (bytes 6-11): Convert to hex string
- Extract BiPackage (bytes 12 to length-1): Parse as BiPackage (see below)
- Extract Checksum (last byte): Verify against calculated checksum
Request (GET_NAME):
0000000000005410EC03615000090000000000262F4A
│────────││────────││─────────────────││──│
Sender(6) Receiver(6) BiPackage(9) CS(1)
Where:
- Sender:
000000000000(app) - Receiver:
5410EC036150(gateway) - BiPackage:
00090000000000262F(see BiPackage section) - Checksum:
4A
The BiPackage contains the actual command and data.
| Field | Size (bytes) | Description |
|---|---|---|
| Length | 2 | Total length of BiPackage (including length field itself) |
| Tag | 1 | Message tag/identifier (usually 0 for requests) |
| Token | 4 | Session token (from login response, 00000000 before login) |
| Command | 1 | Command code (bit 7 set for responses) |
| Payload | Variable | Command-specific data |
| Checksum | 1 | Calculated checksum of the BiPackage |
| Command | Code (hex) | Description | Auth Required |
|---|---|---|---|
| PING | 0x00 | Ping gateway | No |
| ERROR | 0x01 | Error response | No |
| GET_MAC | 0x02 | Get MAC address | No |
| SET_VALUE | 0x03 | Set a value | Yes |
| JMCP | 0x06 | JSON over MCP | Yes |
| LOGIN | 0x10 | Login with credentials | No |
| LOGOUT | 0x11 | Logout | Yes |
| GET_NAME | 0x26 | Get gateway name | No |
| SET_STATE | 0x33 | Set device state (open/close door) | Yes |
| HM_GET_TRANSITION | 0x70 | Get device transition/state | Yes |
Note: Response messages have bit 7 set in the command byte. For example:
- GET_NAME request:
0x26 - GET_NAME response:
0xA6(0x26 | 0x80)
fun constructBiPackage(
command: Command,
token: String = "00000000",
payload: Payload,
tag: Int = 0
): BiPackage {
val length = calculateLength(payload) // Frame size + payload size
val commandByte = command.code.toByte()
val checksum = calculatePackageChecksum(length, tag, token, commandByte, payload)
return BiPackage(
command = command,
tag = tag,
token = token,
payload = payload,
checksum = checksum
)
}
fun toByteArray(biPackage: BiPackage): ByteArray {
return length.toByteArray(2) +
tag.toByte() +
token.toHexByteArray() +
commandCode.toByte() +
payload.toByteArray() +
checksum.toByte()
}- Extract Length (bytes 0-1): Convert to integer (big-endian)
- Extract Tag (byte 2): Convert to integer
- Extract Token (bytes 3-6): Convert to hex string (4 bytes)
- Extract Command (byte 7):
- Check bit 7 to determine if it's a response
- Mask out bit 7 to get command code
- Extract Payload (bytes 8 to length-2): Variable length data
- Extract Checksum (byte at position length-1): Verify checksum
Length = Frame Size + Payload Size
Frame Size = 2 (length) + 1 (tag) + 4 (token) + 1 (command) + 1 (checksum) = 9 bytes
Total Length = 9 + payload.size
GET_NAME Request:
00090000000000262F
││││││││││││││││││
│││││││││││││││││└─ Checksum: 0x2F
││││││││││││││││└── Command: 0x26 (GET_NAME)
│││││││││││└─────── Token: 0x00000000
│││└────────────── Tag: 0x00
└──────────────── Length: 0x0009 (9 bytes)
Payloads vary by command. Here are the common formats:
Structure:
┌──────────────┬──────────────┬──────────────┐
│ Username Len │ Username │ Password │
│ (1 byte) │ (variable) │ (variable) │
└──────────────┴──────────────┴──────────────┘
Example: Username="thomas", Password="aaabbbccc"
0674686F6D6173616161626262636363
││└────────┘└──────────────────┘
│└─ Username: "thomas" (ASCII hex)
└── Length: 0x06 (6 characters)
Password: "aaabbbccc" (ASCII hex, no length prefix)
No payload (empty byte array)
Structure: JSON string encoded as ASCII bytes
Example: {"cmd":"GET_VALUES"}
7B22636D64223A224745545F56414C554553227D
└────────────────────────────────────────┘
JSON string as ASCII hex bytes
Structure:
┌──────────┐
│ Port ID │
│ (1 byte) │
└──────────┘
Example: Port ID = 0
00
└─ Port ID: 0x00
Structure:
┌──────────┬────────┐
│ Port ID │ State │
│ (1 byte) │(1 byte)│
└──────────┴────────┘
Example: Port ID = 0, State = 0xFF (toggle)
00FF
││└─ State: 0xFF (toggle)
└──── Port ID: 0x00
The BiPackage checksum is calculated by summing all bytes in the package (except the checksum itself) and taking the result modulo 256.
Algorithm:
fun calculatePackageChecksum(biPackage: BiPackage): Int {
var value = biPackage.length
value += biPackage.tag
value += (token.toHexInt() and 0xFF) // Token byte 0
value += (token.toHexInt() shr 8 and 0xFF) // Token byte 1
value += (token.toHexInt() shr 16 and 0xFF) // Token byte 2
value += (token.toHexInt() shr 24 and 0xFF) // Token byte 3
value += commandCode
biPackage.payload.toByteArray().forEach { value += it }
return value and 0xFF // Modulo 256
}Example (GET_NAME):
Length: 0x0009 → 9
Tag: 0x00 → 0
Token: 0x00000000 → 0 + 0 + 0 + 0
Command: 0x26 → 38
Payload: (empty) → 0
Sum: 9 + 0 + 0 + 38 = 47 = 0x2F
The Transport Container checksum is calculated by summing the ASCII values of all characters in the hex string representation (except the checksum itself) and taking the result modulo 256.
Algorithm:
fun calculateTransportChecksum(sender: String, receiver: String, pack: BiPackage): Byte {
val hexString = sender + receiver + pack.toHexString()
var value = 0
hexString.forEach { char ->
value += char.toByte() // Add ASCII value of each hex character
}
return (value and 0xFF).toByte() // Modulo 256
}Example (GET_NAME):
Hex string: "0000000000005410EC03615000090000000000262F"
Sum of ASCII values of each character:
'0'(48) + '0'(48) + ... + '2'(50) + 'F'(70) = calculated sum
Result & 0xFF = 0x4A (checksum)
All byte arrays are converted to hex strings for transmission over TCP/IP.
Byte Array to Hex String:
fun ByteArray.toHexString(): String {
return joinToString("") {
it.toUByte().toString(16).padStart(2, '0').toUpperCase()
}
}Example:
Bytes: [0x00, 0x09, 0xAB, 0xCD]
Hex String: "0009ABCD"
Hex String to Byte Array:
fun String.toHexByteArray(): ByteArray {
return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}Example:
Hex String: "0009ABCD"
Bytes: [0x00, 0x09, 0xAB, 0xCD]
The gateway uses a special encoding for certain data types where each hex digit is converted to its ASCII representation.
Example:
- Internal representation:
0x00(one byte) - Gateway encoding:
0x30 0x30(two bytes: ASCII '0' and '0')
Encoding to Gateway Format:
fun ByteArray.encodeToGW(): ByteArray {
return toHexString().toByteArray() // Convert to hex string, then to ASCII bytes
}Decoding from Gateway Format:
fun ByteArray.decodeFromGW(): ByteArray {
return joinToString("") {
it.toByte(16).toChar()
}.toHexByteArray()
}Goal: Login with username "thomas" and password "aaabbbccc"
Step 1: Construct Payload
username = "thomas" (6 characters)
password = "aaabbbccc"
payload = [0x06] + "thomas".toByteArray() + "aaabbbccc".toByteArray()
= [0x06, 0x74, 0x68, 0x6F, 0x6D, 0x61, 0x73,
0x61, 0x61, 0x61, 0x62, 0x62, 0x62, 0x63, 0x63, 0x63]
hex = "0674686F6D61736161616262626363636"Step 2: Construct BiPackage
command = LOGIN (0x10)
token = "00000000" (no token yet)
tag = 0
length = 9 + 16 = 25 = 0x0019
checksum calculation:
length + tag + token_bytes + command + payload_bytes
= 25 + 0 + (0+0+0+0) + 16 + (6+116+104+111+109+97+115+97+97+97+98+98+98+99+99+99)
= 25 + 0 + 0 + 16 + 1540
= 1581
= 1581 mod 256 = 45 = 0x2D
BiPackage hex: "00190000000000100674686F6D61736161616262626363632D"Step 3: Construct Transport Container
sender = "000000000000" (app)
receiver = "5410EC036150" (gateway MAC)
checksum = sum of ASCII values of hex string characters
hexString = "0000000000005410EC03615000190000000000100674686F6D61736161616262626363632D"
Sum ASCII values of each character, then & 0xFF
= 0xF0 (from test example)
Final hex: "0000000000005410EC03615000190000000000100674686F6D61736161616262626363632DF0"Goal: Get the gateway name
Step 1: Construct Payload
payload = [] (empty)Step 2: Construct BiPackage
command = GET_NAME (0x26 = 38)
token = "00000000"
tag = 0
length = 9 + 0 = 9 = 0x0009
checksum = 9 + 0 + 0 + 38 = 47 = 0x2F
BiPackage hex: "00090000000000262F"Step 3: Construct Transport Container
sender = "000000000000"
receiver = "5410EC036150"
checksum = sum of ASCII values of hex string characters
hexString = "0000000000005410EC03615000090000000000262F"
Sum ASCII values of each character, then & 0xFF
= 0x4A (from test example)
Final hex: "0000000000005410EC03615000090000000000262F4A"Goal: Parse a GET_NAME response from gateway
Raw message:
5410EC03615000000000000600180100000000A64269536563757220476174657761795E97
Step 1: Parse Transport Container
sender = "5410EC036150" (gateway)
receiver = "000000000006" (app)
checksum = 0x97Step 2: Parse BiPackage
Length: 0x0018 = 24 bytes
Tag: 0x01
Token: 0x00000000
Command: 0xA6 → bit 7 is set (response), command = 0x26 (GET_NAME)
Payload length: 24 - 9 = 15 bytes
Payload: "4269536563757220476174657761795E"
Checksum: 0x5E (last byte of payload section)
Step 3: Decode Payload
payload hex: "426953656375722047617465776179"
ASCII: "BiSecur Gateway"Goal: Send JMCP command to get values
Step 1: Construct Payload
json = "{\"cmd\":\"GET_VALUES\"}"
payload = json.toByteArray() // ASCII encoding
hex = "7B22636D64223A224745545F56414C554553227D"Step 2: Construct BiPackage
command = JMCP (0x06)
token = "EC25B186" (example session token from login)
tag = 0
length = 9 + 20 = 29 = 0x001D
checksum = 29 + 0 + (token bytes) + 6 + (payload sum)
= 29 + 0 + 0xEC + 0x25 + 0xB1 + 0x86 + 6 + payload_sum
= ... = 0x20 (example)
BiPackage hex: "001D00EC25B1860067B22636D64223A224745545F56414C554553227D20"Response messages follow the same structure as requests with the following differences:
- Sender/Receiver are swapped: Gateway is sender, app is receiver
- Command byte has bit 7 set: For example, GET_NAME (0x26) becomes 0xA6
- Tag is incremented: Request tag 0 → Response tag 1
- Token remains the same: Session token from login
- Checksum Calculation Order: Always calculate BiPackage checksum before Transport Container checksum
- Response Command Code: Don't forget to set bit 7 for responses
- Byte Order: All multi-byte integers use big-endian (network byte order)
- Hex String Case: Convention is uppercase, but comparison should be case-insensitive
- Token Padding: Always pad token to 8 hex characters (4 bytes)
- Gateway Encoding: Only certain payloads use the special gateway encoding (mainly JMCP responses)
A typical session follows this flow:
- Discovery (UDP): Find gateway on network
- Connect (TCP): Establish TCP connection to gateway:4000
- GET_NAME: First request to verify connection
- LOGIN: Authenticate and receive session token
- Commands: Send authenticated commands (GET_GROUPS, HM_GET_TRANSITION, SET_STATE, etc.)
- LOGOUT: End session gracefully
Each request-response pair increments the tag counter and maintains the session token throughout.