/* * MEDiDFU — VesiSeek firmware updater * * VesiSeek BLE identifiers and the NUS command framing shared with the firmware. * * Author: Charles KWON OhJun * MEDiThings Inc. */ package com.medithings.vesiscan.firmware import java.util.UUID /** * VesiSeek BLE identifiers. * * During normal operation the device advertises the Nordic UART Service. After the `mdf?` command * it disconnects and re-advertises with the Zephyr SMP service instead, keeping the same * advertised name (its serial number). */ object VesiSeekUuids { val NUS_SERVICE: UUID = UUID.fromString("6E400001-B5A3-F393-E0A9-E50E24DCCA9E") /** host -> device (write) */ val NUS_WRITE: UUID = UUID.fromString("6E400002-B5A3-F393-E0A9-E50E24DCCA9E") /** device -> host (notify) */ val NUS_NOTIFY: UUID = UUID.fromString("6E400003-B5A3-F393-E0A9-E50E24DCCA9E") val CCCD: UUID = UUID.fromString("00002902-0000-1000-8000-00805F9B34FB") /** Zephyr SMP-over-BLE service, advertised only in DFU mode. */ val SMP_SERVICE: UUID = UUID.fromString("8D53DC1D-1DB7-4CD3-868B-8A527460AA84") } /** * CRC-16/CCITT (poly 0x1021, init 0xFFFF) — byte-for-byte identical to the firmware's * `cmd_crc16_compute` (Nordic SDK `crc16_compute`). */ object Crc16 { fun compute(data: ByteArray): Int { var crc = 0xFFFF for (byte in data) { crc = crc xor ((byte.toInt() and 0xFF) shl 8) repeat(8) { crc = if ((crc and 0x8000) != 0) (crc shl 1) xor 0x1021 else crc shl 1 crc = crc and 0xFFFF } } return crc } /** Appends the CRC little-endian, matching the firmware's packet layout. */ fun appendTo(data: ByteArray): ByteArray { val crc = compute(data) return data + byteArrayOf((crc and 0xFF).toByte(), ((crc shr 8) and 0xFF).toByte()) } } /** NUS packet framing: `[TAG 4B][DATA N][CRC16 2B little-endian]`. */ object NusPacket { fun build(tag: String, data: ByteArray = ByteArray(0)): ByteArray { require(tag.length == 4) { "tag must be 4 characters, got '$tag'" } return Crc16.appendTo(tag.toByteArray(Charsets.US_ASCII) + data) } /** * Enter-DFU command. The firmware ignores the payload but the command dispatcher needs at * least one data byte, so a single 0x00 is sent — the same packet the Go host tool uses. */ fun enterDfu(): ByteArray = build("mdf?", byteArrayOf(0x00)) /** Returns the 4-character response tag, or null if the frame is too short or the CRC fails. */ fun tagOf(packet: ByteArray): String? { if (packet.size < 6) return null val body = packet.copyOfRange(0, packet.size - 2) val received = (packet[packet.size - 2].toInt() and 0xFF) or ((packet[packet.size - 1].toInt() and 0xFF) shl 8) if (Crc16.compute(body) != received) return null return String(packet, 0, 4, Charsets.US_ASCII) } }