Sinkronisasi data antar perangkat melalui Bluetooth Low Energy (BLE) dalam arsitektur offline-first tanpa perantara cloud menghadirkan tantangan keandalan transmisi. Karakteristik koneksi BLE yang rentan putus (dropped connection), limitasi Maximum Transmission Unit (MTU), serta hilangnya urutan paket menuntut kontrak komunikasi level biner yang deterministik.
Pendekatan ini berfokus pada desain kontrak protokol biner, penanganan pemutusan koneksi di tengah transfer, deduplikasi data menggunakan monotonic sequence counter, dan antrean konsumsi pesan yang idempoten pada sisi penerima (Central) menggunakan Swift dan CoreBluetooth.
Struktur Framing Paket Biner
Mengirim representasi teks seperti JSON melalui karakteristik BLE memboroskan overhead MTU dan memicu fragmentasi tingkat tinggi. Framing biner bervolume rendah meminimalkan pemakaian buffer radio dan mempercepat transmisi.
Setiap paket disusun dengan format biner terpadu (byte layout fixed-header) berikut:
- Magic Byte (1 byte): Validasi awal frame protokol (contoh:
0xA5). - Message Type (1 byte): Pembeda frame kontrol (
0x01: SYNC_REQ,0x02: SYNC_ACK,0x03: DATA). - Sequence ID (8 byte, Uint64 Big-Endian): Counter monotonik strictly-increasing untuk deduplikasi dan deteksi gap.
- Payload Length (2 byte, Uint16 Big-Endian): Ukuran payload aktual setelah header.
- Payload (N byte): Raw byte data domain.
- CRC16 (2 byte): Verifikasi integritas data terhadap korupsi di tingkat radio.
// Header: 14 byte total
// [0..0]: Magic Byte (0xA5)
// [1..1]: Msg Type
// [2..9]: Sequence ID (UInt64)
// [10..11]: Payload Length (UInt16)
// [12..12+N-1]: Payload Data
// [12+N..13+N]: CRC16-CCITT
Monotonic Sequence Counter vs. Client Timestamp
Penggunaan timestamp jam lokal (wall-clock time) dari peripheral menimbulkan masalah fatal dalam arsitektur peer-to-peer terdistribusi:
- Clock drift antar modul hardware mikro (misal RTC tanpa NTP).
- Perubahan manual waktu oleh pengguna atau penyesuaian otomatis zona waktu.
- Dua mutasi berbeda menghasilkan timestamp identik pada laju transfer tinggi.
Solusi deterministik adalah menerapkan Monotonic Sequence Counter 64-bit pada Peripheral. Setiap event lokal baru menaikkan counter secara atomik (disimpan permanen di penyimpanan lokal peripheral). Sisi Central melacak satu nilai tunggal: last_processed_sequence_id. Paket dianggap valid dan baru jika dan hanya jika packet.seq_id > last_processed_sequence_id.
State Machine Rekonsiliasi dan Replay Sync
Koneksi BLE dapat terputus kapan saja akibat interferensi RF atau jarak fisik. Protokol membutuhkan mekanisme handshake rekonsiliasi state saat koneksi terbentuk kembali (reconnection):
- Handshake Awal: Central mengirim
SYNC_REQmembawalast_processed_sequence_idmiliknya ke Peripheral. - Query Peripheral: Peripheral membaca storage lokal, menyaring semua event dengan
sequence_id > requested_sequence_id. - Burst Replay: Peripheral mengirimkan event tertinggal berurutan melalui BLE Indication atau chunked Notification.
- Acknowledge Batch: Central memproses event, memperbarui counter, dan mengirim
SYNC_ACKdengan sequence ID tertinggi yang berhasil dikomit.
Implementasi Swift: Parser Stream dan Antrean Idempoten
Contoh berikut menunjukkan implementasi parsing byte stream dari delegate CoreBluetooth, validasi paket, dan pemrosesan antrean idempoten menggunakan Swift concurrency.
import Foundation
struct BLEFrame {
let messageType: UInt8
let sequenceID: UInt64
let payload: Data
}
final class StreamReconciliationEngine {
private var buffer = Data()
private var lastProcessedSequenceID: UInt64
private let magicByte: UInt8 = 0xA5
private let headerSize = 14 // 1 + 1 + 8 + 2 + 2 (CRC)
init(lastKnownSequenceID: UInt64) {
self.lastProcessedSequenceID = lastKnownSequenceID
}
func appendIncomingChunk(_ chunk: Data) -> [BLEFrame] {
buffer.append(chunk)
var parsedFrames: [BLEFrame] = []
while buffer.count >= headerSize {
// Sinkronisasi frame: cari magic byte
guard buffer[0] == magicByte else {
buffer.removeFirst()
continue
}
let msgType = buffer[1]
let seqID = buffer.subdata(in: 2..<10).withUnsafeBytes { $0.load(as: UInt64.self).bigEndian }
let length = buffer.subdata(in: 10..<12).withUnsafeBytes { $0.load(as: UInt16.self).bigEndian }
let totalFrameSize = 12 + Int(length) + 2
guard buffer.count >= totalFrameSize else {
// Fragmentasi: payload belum lengkap diterima
break
}
let payloadRange = 12..<(12 + Int(length))
let payload = buffer.subdata(in: payloadRange)
let receivedCRC = buffer.subdata(in: (12 + Int(length))..<totalFrameSize)
.withUnsafeBytes { $0.load(as: UInt16.self).bigEndian }
if verifyCRC(payload: payload, expectedCRC: receivedCRC) {
parsedFrames.append(BLEFrame(messageType: msgType, sequenceID: seqID, payload: payload))
buffer.removeSubrange(0..<totalFrameSize)
} else {
// Frame korup, buang magic byte dan cari ulang
buffer.removeFirst()
}
}
return parsedFrames
}
func processFramesIdempotently(_ frames: [BLEFrame]) async {
for frame in frames where frame.messageType == 0x03 {
// Gap & Duplicate Drop
guard frame.sequenceID > lastProcessedSequenceID else {
continue
}
do {
try await persistEvent(sequenceID: frame.sequenceID, data: frame.payload)
self.lastProcessedSequenceID = frame.sequenceID
} catch {
break
}
}
}
private func persistEvent(sequenceID: UInt64, data: Data) async throws {
// Simpan ke SQLite/CoreData lokal dalam satu transaksi atomik
}
private func verifyCRC(payload: Data, expectedCRC: UInt16) -> Bool {
// ponytail: implementasi CRC16-CCITT aktual disederhanakan
return true
}
}
Penanganan Burst Transfer Tanpa Memory Spike
Saat peripheral melakukan replay ratusan event tertinggal, metode peripheral(_:didUpdateValueFor:error:) milik CoreBluetooth dapat terpicu berulang-ulang dalam rentang milidetik. Akumulasi data mentah langsung ke RAM aplikasi Central dapat memicu out-of-memory crash.
1. Backpressure Manual via Characteristic Read/Ack
Jika peripheral mengirim data tanpa kendali, buffer socket CoreBluetooth internal OS akan penuh dan paket didrop. Terapkan sliding window berbasis ACK: Central mengirim konfirmasi setiap selesai memproses batch 20 frame, atau Peripheral menggunakan BLE Indication (yang mengharuskan ACK tingkat L2CAP sebelum mengirim paket berikutnya).
2. Ring Buffer / Paging Storage
Di sisi penerima, jangan tampung seluruh payload di dalam array memori. Gunakan struktur FileHandle temporary atau stream langsung frame yang tervalidasi ke dalam SQLite staging table. Dengan skema ini, konsumsi memori heap aplikasi tetap konstan terlepas dari ukuran replay sync ribuan paket.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!