
Declarative, type-safe DSL enabling bit-precise packet definitions and parsing; supports arbitrary bit-fields, automatic sign-extension, nested composition, validation (CRC), and packet construction.
PacketDefinition is a declarative, type-safe DSL for defining and working with binary data packets in Kotlin Multiplatform projects.
Working with binary protocols (such as BLE (Bluetooth Low Energy), custom radio protocols, or hardware sensor data) often involves tedious and error-prone manual byte manipulation, mask operations, and offset tracking. This library abstracts away these complexities, allowing you to describe your packet structure in a clean, readable way.
At the heart of the library is BitBuffer. While standard programming environments typically treat 8 bits (1 byte) as the smallest addressable unit, BitBuffer allows you to navigate and extract data at the bit level.
It maintains a bit-level position and handles all the necessary bitwise operations (shifting and masking) under the hood. When you request 7 bits, BitBuffer extracts exactly those 7 bits from the current stream, even if they span across two different bytes.
When working with bits, representing negative numbers can be tricky. PacketDefinition handles this by applying Sign Extension.
For example, if you have a 3-bit signed integer with the value 100 (binary):
-4.Int (32-bit), the library ensures it remains -4 by representing it as 1111...1100 (hex 0xFFFF FF0C or similar depending on size).
This ensures that your logic remains consistent regardless of the underlying bit-width of the protocol field.The library is split into two modules to separate low-level buffer operations from high-level DSL:
packet-definition-core: Provides BitBuffer, and DataSize. Use this for low-level manual data manipulation.packet-definition: Provides the packet { ... } and fillPacket { ... } DSLs for a high-level, declarative approach.Note that the library is experimental, and the API is subject to change.
Make sure that you have mavenCentral() in the list of repositories:
repositories {
mavenCentral()
}Add the library to dependencies:
dependencies {
implementation("io.github.pavelstalone:packet-definition:0.1.0")
}For Kotlin Multiplatform:
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("io.github.pavelstalone:packet-definition:0.1.0")
}
}
}
}You can define a packet once and use it to parse data from ByteArray or ByteBuffer.
val myPacket = packet {
val version = int(4.bits).value // Reads 4 bits as an Int
val flag = boolean().value // Reads 1 bit as a Boolean
val type = int(3.bits).value // Reads 3 bits as an Int
val payloadSize = int(1.bytes).value
val payload = string(payloadSize.bytes).value
"Packet v$version, Type: $type, Data: $payload"
}
// Filling the packet
val result: String = myPacket.fill(byteArrayOf(0xB2.toByte(), 0x04.toByte(), 0x48, 0x69))The library makes it easy to add validation logic directly into the parsing scope.
data class SensorData(val value: Float, val isActive: Boolean)
val sensorPacket = packet {
val isActive = boolean() // Returns Value<Boolean>
val reserved = reserve(7.bits)
val value = float(4.bytes) // Returns Value<Float>
val crc = byte().value
// Validate using the Value objects directly
require(CRC8Validation(crc).validate(isActive, value)) { "Invalid CRC!" }
SensorData(value.value, isActive.value)
}You can compose packets using from.
val connectionPacket = packet {
val idSize = int(1.bytes).value
val id = string(idSize.bytes).value
// Using 'from' to parse a sub-structure from the current remaining bytes
val details = from(bytes(remainingSize)) {
val port = int(2.bytes).value
val isSecure = boolean().value
port to isSecure
}
id to details
}Or calling a pre-defined parsing function (Composition).
val mainDefinition = packet {
val header = int(1.bytes).value
when(header) {
0x01 -> sensorType()
else -> "Unknown"
}
}
fun PacketScope.sensorType(): String {
val type = int(1.bytes).value
return when(type) {
0x2F -> "PulseOx"
else -> "Unknown"
}
}To create raw binary data, use fillPacket. It allows you to specify values and their bit-sizes.
val buffer: ByteBuffer = fillPacket {
val version = int(value = 1, size = 4.bits)
val flag = boolean(value = true)
val type = int(value = 2, size = 3.bits)
val message = "Hello"
int(value = message.length, size = 1.bytes)
string(value = message)
val crc = CRC8Validation.calculate(version, flag, type)
byte(crc)
}Copyright 2026 Pavel Shoplik
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
I hope this library makes your life a little easier when dealing with binary protocols! May the Force be with you!
PacketDefinition is a declarative, type-safe DSL for defining and working with binary data packets in Kotlin Multiplatform projects.
Working with binary protocols (such as BLE (Bluetooth Low Energy), custom radio protocols, or hardware sensor data) often involves tedious and error-prone manual byte manipulation, mask operations, and offset tracking. This library abstracts away these complexities, allowing you to describe your packet structure in a clean, readable way.
At the heart of the library is BitBuffer. While standard programming environments typically treat 8 bits (1 byte) as the smallest addressable unit, BitBuffer allows you to navigate and extract data at the bit level.
It maintains a bit-level position and handles all the necessary bitwise operations (shifting and masking) under the hood. When you request 7 bits, BitBuffer extracts exactly those 7 bits from the current stream, even if they span across two different bytes.
When working with bits, representing negative numbers can be tricky. PacketDefinition handles this by applying Sign Extension.
For example, if you have a 3-bit signed integer with the value 100 (binary):
-4.Int (32-bit), the library ensures it remains -4 by representing it as 1111...1100 (hex 0xFFFF FF0C or similar depending on size).
This ensures that your logic remains consistent regardless of the underlying bit-width of the protocol field.The library is split into two modules to separate low-level buffer operations from high-level DSL:
packet-definition-core: Provides BitBuffer, and DataSize. Use this for low-level manual data manipulation.packet-definition: Provides the packet { ... } and fillPacket { ... } DSLs for a high-level, declarative approach.Note that the library is experimental, and the API is subject to change.
Make sure that you have mavenCentral() in the list of repositories:
repositories {
mavenCentral()
}Add the library to dependencies:
dependencies {
implementation("io.github.pavelstalone:packet-definition:0.1.0")
}For Kotlin Multiplatform:
kotlin {
sourceSets {
commonMain {
dependencies {
implementation("io.github.pavelstalone:packet-definition:0.1.0")
}
}
}
}You can define a packet once and use it to parse data from ByteArray or ByteBuffer.
val myPacket = packet {
val version = int(4.bits).value // Reads 4 bits as an Int
val flag = boolean().value // Reads 1 bit as a Boolean
val type = int(3.bits).value // Reads 3 bits as an Int
val payloadSize = int(1.bytes).value
val payload = string(payloadSize.bytes).value
"Packet v$version, Type: $type, Data: $payload"
}
// Filling the packet
val result: String = myPacket.fill(byteArrayOf(0xB2.toByte(), 0x04.toByte(), 0x48, 0x69))The library makes it easy to add validation logic directly into the parsing scope.
data class SensorData(val value: Float, val isActive: Boolean)
val sensorPacket = packet {
val isActive = boolean() // Returns Value<Boolean>
val reserved = reserve(7.bits)
val value = float(4.bytes) // Returns Value<Float>
val crc = byte().value
// Validate using the Value objects directly
require(CRC8Validation(crc).validate(isActive, value)) { "Invalid CRC!" }
SensorData(value.value, isActive.value)
}You can compose packets using from.
val connectionPacket = packet {
val idSize = int(1.bytes).value
val id = string(idSize.bytes).value
// Using 'from' to parse a sub-structure from the current remaining bytes
val details = from(bytes(remainingSize)) {
val port = int(2.bytes).value
val isSecure = boolean().value
port to isSecure
}
id to details
}Or calling a pre-defined parsing function (Composition).
val mainDefinition = packet {
val header = int(1.bytes).value
when(header) {
0x01 -> sensorType()
else -> "Unknown"
}
}
fun PacketScope.sensorType(): String {
val type = int(1.bytes).value
return when(type) {
0x2F -> "PulseOx"
else -> "Unknown"
}
}To create raw binary data, use fillPacket. It allows you to specify values and their bit-sizes.
val buffer: ByteBuffer = fillPacket {
val version = int(value = 1, size = 4.bits)
val flag = boolean(value = true)
val type = int(value = 2, size = 3.bits)
val message = "Hello"
int(value = message.length, size = 1.bytes)
string(value = message)
val crc = CRC8Validation.calculate(version, flag, type)
byte(crc)
}Copyright 2026 Pavel Shoplik
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
I hope this library makes your life a little easier when dealing with binary protocols! May the Force be with you!