diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt new file mode 100644 index 0000000..9a6cd0b --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/CanBridgeProtocol.kt @@ -0,0 +1,338 @@ +package ru.setcorp.setflash.core + +import ru.setcorp.setprotocol.NativeSetProtocol as NativeProtoCan + +/** + * Wire-compatible port of SETGUI `core/can_transport.py` and the + * legacy frame module from `templates/c/set-protocol`. + * + * Frame layout: + * `AA 55 | LEN | SEQ | FLAGS | CAN_ID little-endian | DATA[0..8] | CRC16 LE`. + */ +object CanBridgeProtocol { + const val FLAG_IDE = 0x01 + const val FLAG_RTR = 0x02 + const val FLAG_DIRECTION = 0x04 + const val FLAG_ERROR = 0x08 + + const val STANDARD_ID_MAX = 0x7FFL + const val EXTENDED_ID_MAX = 0x1FFF_FFFFL + + private const val SOF0 = 0xAA + private const val SOF1 = 0x55 + private const val MIN_LENGTH = 6 + private const val MAX_LENGTH = 14 + + data class Frame( + val sequence: Int, + val flags: Int, + val canId: Long, + val data: ByteArray, + ) { + init { + require(sequence in 0..0xFF) { "SEQ должен быть в диапазоне 0…255" } + require(flags in 0..0xFF) { "FLAGS должен быть байтом" } + require(data.size <= 8) { "DLC не может превышать 8 байт" } + validateCanId(canId, isExtended) + } + + val isExtended: Boolean get() = flags and FLAG_IDE != 0 + val isRemote: Boolean get() = flags and FLAG_RTR != 0 + val toCan: Boolean get() = flags and FLAG_DIRECTION != 0 + val isError: Boolean get() = flags and FLAG_ERROR != 0 + val direction: String get() = if (isError) "ERR" else if (toCan) "TX" else "RX" + + fun encode(): ByteArray { + if (NativeProtoCan.available) { + return requireNotNull( + NativeProtoCan.nativeEncodeFrame(sequence, flags, canId, data), + ) { "SETProtocol отклонил CAN-кадр" } + } + val length = MIN_LENGTH + data.size + val protected = ByteArray(1 + length) + protected[0] = length.toByte() + protected[1] = sequence.toByte() + protected[2] = flags.toByte() + repeat(4) { index -> protected[3 + index] = (canId ushr (index * 8)).toByte() } + data.copyInto(protected, 7) + val crc = crc16Ccitt(protected) + return byteArrayOf(SOF0.toByte(), SOF1.toByte()) + protected + + byteArrayOf(crc.toByte(), (crc ushr 8).toByte()) + } + + override fun equals(other: Any?): Boolean = other is Frame && + sequence == other.sequence && flags == other.flags && canId == other.canId && + data.contentEquals(other.data) + + override fun hashCode(): Int = 31 * + (31 * (31 * sequence + flags) + canId.hashCode()) + data.contentHashCode() + } + + data class Stats( + val frames: Int = 0, + val crcErrors: Int = 0, + val resyncBytes: Int = 0, + val sequenceLost: Int = 0, + ) + + fun buildFrame( + canId: Long, + data: ByteArray, + sequence: Int = 0, + extended: Boolean = true, + remote: Boolean = false, + toCan: Boolean = true, + ): Frame { + validateCanId(canId, extended) + require(data.size <= 8) { "DLC не может превышать 8 байт" } + var flags = 0 + if (extended) flags = flags or FLAG_IDE + if (remote) flags = flags or FLAG_RTR + if (toCan) flags = flags or FLAG_DIRECTION + return Frame(sequence and 0xFF, flags, canId, data.copyOf()) + } + + fun validateCanId(canId: Long, extended: Boolean) { + val maximum = if (extended) EXTENDED_ID_MAX else STANDARD_ID_MAX + val kind = if (extended) "Расширенный (29 бит)" else "Стандартный (11 бит)" + require(canId in 0..maximum) { "$kind CAN ID должен быть в диапазоне 0x0..0x${maximum.toString(16).uppercase()}" } + } + + fun crc16Ccitt(data: ByteArray, initial: Int = 0xFFFF): Int { + if (NativeProtoCan.available && initial == 0xFFFF) { + return NativeProtoCan.nativeCrc16(data) + } + var crc = initial and 0xFFFF + data.forEach { value -> + crc = crc xor ((value.toInt() and 0xFF) shl 8) + repeat(8) { + crc = if (crc and 0x8000 != 0) ((crc shl 1) xor 0x1021) and 0xFFFF + else (crc shl 1) and 0xFFFF + } + } + return crc + } + + fun parseHex(text: String): ByteArray { + val compact = text.replace(Regex("(?i)0x"), "") + .replace(Regex("[\\s,;:-]+"), "") + require(compact.matches(Regex("[0-9A-Fa-f]*")) && compact.length % 2 == 0) { + "DATA должна состоять из пар HEX-цифр" + } + require(compact.length <= 16) { "CAN 2.0 содержит не более 8 байт" } + return ByteArray(compact.length / 2) { index -> + compact.substring(index * 2, index * 2 + 2).toInt(16).toByte() + } + } + + class Parser { + private var buffer = byteArrayOf() + private var lastSequence: Int? = null + private var nativeHandle: Long = if (NativeProtoCan.available) { + NativeProtoCan.nativeCreateParser() + } else { + 0L + } + var stats = Stats() + private set + + fun reset(clearStats: Boolean = true) { + buffer = byteArrayOf() + lastSequence = null + if (nativeHandle != 0L) { + NativeProtoCan.nativeDestroyParser(nativeHandle) + nativeHandle = NativeProtoCan.nativeCreateParser() + } + if (clearStats) stats = Stats() + } + + fun feed(chunk: ByteArray): List { + if (chunk.isEmpty()) return emptyList() + if (nativeHandle != 0L) { + val records = requireNotNull( + NativeProtoCan.nativeFeedParser(nativeHandle, chunk), + ) { "SETProtocol parser failed" } + require(records.size % 15 == 0) { "Некорректный ответ SETProtocol" } + val result = ArrayList(records.size / 15) + for (offset in records.indices step 15) { + val sequence = records[offset].toInt() and 0xFF + val flags = records[offset + 1].toInt() and 0xFF + var canId = 0L + repeat(4) { index -> + canId = canId or + ((records[offset + 2 + index].toLong() and 0xFF) shl (index * 8)) + } + val dlc = records[offset + 6].toInt() and 0xFF + result += Frame( + sequence, + flags, + canId, + records.copyOfRange(offset + 7, offset + 7 + dlc), + ) + } + NativeProtoCan.nativeParserStats(nativeHandle)?.let { values -> + if (values.size >= 5) { + stats = Stats( + frames = values[0], + crcErrors = values[1], + resyncBytes = values[2] + values[3], + sequenceLost = values[4], + ) + } + } + return result + } + buffer += chunk + val result = mutableListOf() + + while (true) { + val start = findSignature(buffer) + if (start < 0) { + val keep = if (buffer.lastOrNull() == SOF0.toByte()) 1 else 0 + val dropped = buffer.size - keep + if (dropped > 0) stats = stats.copy(resyncBytes = stats.resyncBytes + dropped) + buffer = if (keep == 1) byteArrayOf(SOF0.toByte()) else byteArrayOf() + break + } + if (start > 0) { + stats = stats.copy(resyncBytes = stats.resyncBytes + start) + buffer = buffer.copyOfRange(start, buffer.size) + } + if (buffer.size < 3) break + + val length = buffer[2].toInt() and 0xFF + if (length !in MIN_LENGTH..MAX_LENGTH) { + stats = stats.copy(resyncBytes = stats.resyncBytes + 1) + buffer = buffer.copyOfRange(1, buffer.size) + continue + } + val total = 2 + 1 + length + 2 + if (buffer.size < total) break + + val protected = buffer.copyOfRange(2, 3 + length) + val receivedCrc = (buffer[3 + length].toInt() and 0xFF) or + ((buffer[4 + length].toInt() and 0xFF) shl 8) + if (crc16Ccitt(protected) != receivedCrc) { + stats = stats.copy( + crcErrors = stats.crcErrors + 1, + resyncBytes = stats.resyncBytes + 1, + ) + buffer = buffer.copyOfRange(1, buffer.size) + continue + } + + val sequence = protected[1].toInt() and 0xFF + val flags = protected[2].toInt() and 0xFF + var canId = 0L + repeat(4) { index -> + canId = canId or ((protected[3 + index].toLong() and 0xFF) shl (index * 8)) + } + canId = canId and EXTENDED_ID_MAX + val maximum = if (flags and FLAG_IDE != 0) EXTENDED_ID_MAX else STANDARD_ID_MAX + if (canId > maximum) { + buffer = buffer.copyOfRange(total, buffer.size) + continue + } + val frame = Frame(sequence, flags, canId, protected.copyOfRange(7, protected.size)) + val previous = lastSequence + val lost = if (previous == null) 0 else (sequence - previous - 1) and 0xFF + lastSequence = sequence + stats = stats.copy(frames = stats.frames + 1, sequenceLost = stats.sequenceLost + lost) + result += frame + buffer = buffer.copyOfRange(total, buffer.size) + } + return result + } + + private fun findSignature(data: ByteArray): Int { + for (index in 0 until data.size - 1) { + if (data[index] == SOF0.toByte() && data[index + 1] == SOF1.toByte()) return index + } + return -1 + } + } +} + +/** 29-bit SETCAN identifier layout from SETGUI `core/protocan.py`. */ +data class ProtoCanId( + val raw: Long, + val priority: Int, + val pm: Int, + val deviceType: Int, + val device: Int, + val messageType: Int, + val body: Int, +) { + val messageTypeName: String get() = MESSAGE_TYPES[messageType] ?: "Reserved 0x${messageType.toString(16).uppercase()}" + val deviceName: String? get() = DEVICE_NAMES[device] + + fun summary(): String = "P$priority · PM$pm · Type $deviceType · Dev $device" + + (deviceName?.let { " ($it)" } ?: "") + " · $messageTypeName · Body 0x%04X".format(body) + + companion object { + private val MESSAGE_TYPES = mapOf( + 0x0 to "Broadcast", + 0x1 to "Discrete", + 0x2 to "Analog", + 0x3 to "General Address Space", + 0x4 to "Modbus Coil", + 0x5 to "Modbus Discrete", + 0x6 to "Modbus Holding", + 0x7 to "Modbus Input", + 0x8 to "Error", + 0x9 to "Boot Control", + 0xA to "Boot Data A", + 0xB to "Boot Data B", + 0xC to "Boot Status", + 0xD to "Boot Discovery", + 0xE to "Settings", + 0xF to "Pulse", + ) + private val DEVICE_NAMES = mapOf( + 0xD to "DS_CONTROL", + 0xE to "Android GUI", + 0xF to "SLCAN", + ) + + fun parse(raw: Long): ProtoCanId { + val value = raw and CanBridgeProtocol.EXTENDED_ID_MAX + if (NativeProtoCan.available) { + NativeProtoCan.nativeUnpackId(value)?.let { fields -> + if (fields.size == 6) { + return ProtoCanId( + raw = value, + priority = fields[0], + pm = fields[1], + deviceType = fields[2], + device = fields[3], + messageType = fields[4], + body = fields[5], + ) + } + } + } + return ProtoCanId( + raw = value, + priority = ((value ushr 28) and 0x1).toInt(), + pm = ((value ushr 27) and 0x1).toInt(), + deviceType = ((value ushr 24) and 0x7).toInt(), + device = ((value ushr 20) and 0xF).toInt(), + messageType = ((value ushr 16) and 0xF).toInt(), + body = (value and 0xFFFF).toInt(), + ) + } + + fun build(priority: Int, pm: Int, deviceType: Int, device: Int, messageType: Int, body: Int): Long { + require(priority in 0..1 && pm in 0..1 && deviceType in 0..7) + require(device in 0..15 && messageType in 0..15 && body in 0..0xFFFF) + if (NativeProtoCan.available) { + return NativeProtoCan.nativePackId( + priority, pm, deviceType, device, messageType, body, + ) + } + return (priority.toLong() shl 28) or (pm.toLong() shl 27) or + (deviceType.toLong() shl 24) or (device.toLong() shl 20) or + (messageType.toLong() shl 16) or body.toLong() + } + } +} diff --git a/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/GuiProtocol.kt b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/GuiProtocol.kt new file mode 100644 index 0000000..1cd41d8 --- /dev/null +++ b/c/set-protocol/ports/android/kotlin/ru/setcorp/setflash/core/GuiProtocol.kt @@ -0,0 +1,261 @@ +package ru.setcorp.setflash.core + +import java.util.zip.CRC32 +import ru.setcorp.setprotocol.NativeSetProtocol as NativeProtoCan + +object MessageType { + const val PING = 0x01 + const val FIRMWARE_BEGIN = 0x0B + const val FIRMWARE_DATA = 0x0C + const val FIRMWARE_END = 0x0D + const val FIRMWARE_ABORT = 0x0E + const val FIRMWARE_STATUS = 0x0F + const val SENSOR_SCAN = 0x20 + const val SENSOR_LIST = 0x21 + const val SENSOR_READ = 0x22 + const val SENSOR_DATA = 0x23 + const val SET_USER_BYTES = 0x24 + const val SET_RESOLUTION = 0x25 + const val SET_POLL_PERIOD = 0x26 + const val SEND_ID_CAN = 0x27 + const val EEPROM_SCAN = 0x2B + const val EEPROM_INFO = 0x2C + const val EEPROM_READ = 0x2D + const val EEPROM_LIST = 0x2E + const val NACK = 0x80 + const val ACK = 0x81 + const val ERROR = 0x82 + + fun name(value: Int): String = when (value) { + PING -> "PING" + FIRMWARE_BEGIN -> "FIRMWARE_BEGIN" + FIRMWARE_DATA -> "FIRMWARE_DATA" + FIRMWARE_END -> "FIRMWARE_END" + FIRMWARE_ABORT -> "FIRMWARE_ABORT" + FIRMWARE_STATUS -> "FIRMWARE_STATUS" + SENSOR_SCAN -> "SENSOR_SCAN" + SENSOR_LIST -> "SENSOR_LIST" + SENSOR_READ -> "SENSOR_READ" + SENSOR_DATA -> "SENSOR_DATA" + SET_USER_BYTES -> "SET_USER_BYTES" + SET_RESOLUTION -> "SET_RESOLUTION" + SET_POLL_PERIOD -> "SET_POLL_PERIOD" + SEND_ID_CAN -> "SEND_ID_CAN" + EEPROM_SCAN -> "EEPROM_SCAN" + EEPROM_INFO -> "EEPROM_INFO" + EEPROM_READ -> "EEPROM_READ" + EEPROM_LIST -> "EEPROM_LIST" + NACK -> "NACK" + ACK -> "ACK" + ERROR -> "ERROR" + else -> "0x${value.toString(16).padStart(2, '0').uppercase()}" + } +} + +data class ProtocolFrame( + val messageType: Int, + val sequence: Int, + val payload: ByteArray = byteArrayOf(), +) { + init { + require(messageType in 0..0xFF) { "Тип сообщения вне диапазона" } + require(sequence in 0..0xFFFF) { "Sequence вне диапазона" } + require(payload.size <= GuiProtocol.MAX_PAYLOAD_SIZE) { "Payload превышает 512 байт" } + } + + override fun equals(other: Any?): Boolean = other is ProtocolFrame && + messageType == other.messageType && sequence == other.sequence && + payload.contentEquals(other.payload) + + override fun hashCode(): Int = 31 * (31 * messageType + sequence) + payload.contentHashCode() +} + +object GuiProtocol { + private const val VERSION = 0x01 + const val MAX_PAYLOAD_SIZE = 512 + private const val HEADER_SIZE = 8 + private const val CRC_SIZE = 4 + + fun buildFrame(frame: ProtocolFrame): ByteArray { + if (NativeProtoCan.available) { + return requireNotNull( + NativeProtoCan.nativeGuiEncode( + frame.messageType, frame.sequence, frame.payload, + ), + ) { "SETProtocol отклонил GUI-кадр" } + } + val protected = ByteArray(6 + frame.payload.size) + protected[0] = VERSION.toByte() + protected[1] = frame.messageType.toByte() + protected[2] = (frame.sequence ushr 8).toByte() + protected[3] = frame.sequence.toByte() + protected[4] = (frame.payload.size ushr 8).toByte() + protected[5] = frame.payload.size.toByte() + frame.payload.copyInto(protected, 6) + + val result = ByteArray(2 + protected.size + CRC_SIZE) + result[0] = 0xA5.toByte() + result[1] = 0x5A + protected.copyInto(result, 2) + putU32Le(result, result.size - CRC_SIZE, crc32(protected)) + return result + } + + fun crc32(data: ByteArray): Long { + val crc = CRC32() + crc.update(data) + return crc.value + } + + fun u16Le(value: Int): ByteArray { + require(value in 0..0xFFFF) + return byteArrayOf(value.toByte(), (value ushr 8).toByte()) + } + + fun u32Le(value: Long): ByteArray { + require(value in 0..0xFFFF_FFFFL) + return ByteArray(4).also { putU32Le(it, 0, value) } + } + + fun readU16Le(data: ByteArray, offset: Int = 0): Int { + require(offset >= 0 && offset + 2 <= data.size) { "Payload не содержит u16" } + return (data[offset].toInt() and 0xFF) or + ((data[offset + 1].toInt() and 0xFF) shl 8) + } + + fun readU32Le(data: ByteArray, offset: Int = 0): Long { + require(offset >= 0 && offset + 4 <= data.size) { "Payload не содержит u32" } + return (data[offset].toLong() and 0xFF) or + ((data[offset + 1].toLong() and 0xFF) shl 8) or + ((data[offset + 2].toLong() and 0xFF) shl 16) or + ((data[offset + 3].toLong() and 0xFF) shl 24) + } + + fun concat(vararg parts: ByteArray): ByteArray { + val result = ByteArray(parts.sumOf(ByteArray::size)) + var offset = 0 + parts.forEach { + it.copyInto(result, offset) + offset += it.size + } + return result + } + + fun hex(data: ByteArray): String = data.joinToString(" ") { + (it.toInt() and 0xFF).toString(16).padStart(2, '0').uppercase() + } + + private fun putU32Le(target: ByteArray, offset: Int, value: Long) { + repeat(4) { index -> target[offset + index] = (value ushr (index * 8)).toByte() } + } + + class Parser { + private var buffer = byteArrayOf() + private var nativeHandle = if (NativeProtoCan.available) { + NativeProtoCan.nativeCreateGuiParser() + } else { + 0L + } + private var nativeCrcOffset = 0 + private var nativeVersionOffset = 0 + private var nativeLengthOffset = 0 + var crcErrors: Int = 0 + private set + var versionErrors: Int = 0 + private set + var lengthErrors: Int = 0 + private set + + fun reset() { + buffer = byteArrayOf() + if (nativeHandle != 0L) { + nativeCrcOffset = crcErrors + nativeVersionOffset = versionErrors + nativeLengthOffset = lengthErrors + NativeProtoCan.nativeDestroyGuiParser(nativeHandle) + nativeHandle = NativeProtoCan.nativeCreateGuiParser() + } + } + + fun feed(chunk: ByteArray): List { + if (chunk.isEmpty()) return emptyList() + if (nativeHandle != 0L) { + val records = requireNotNull( + NativeProtoCan.nativeFeedGuiParser(nativeHandle, chunk), + ) { "SETProtocol GUI parser failed" } + val frames = mutableListOf() + var offset = 0 + while (offset < records.size) { + require(offset + 5 <= records.size) { "Некорректный ответ SETProtocol" } + val messageType = records[offset].toInt() and 0xFF + val sequence = (records[offset + 1].toInt() and 0xFF) or + ((records[offset + 2].toInt() and 0xFF) shl 8) + val size = (records[offset + 3].toInt() and 0xFF) or + ((records[offset + 4].toInt() and 0xFF) shl 8) + offset += 5 + require(offset + size <= records.size) { "Некорректный payload SETProtocol" } + frames += ProtocolFrame( + messageType, sequence, records.copyOfRange(offset, offset + size), + ) + offset += size + } + NativeProtoCan.nativeGuiParserStats(nativeHandle)?.let { values -> + if (values.size >= 4) { + crcErrors = nativeCrcOffset + values[1] + versionErrors = nativeVersionOffset + values[2] + lengthErrors = nativeLengthOffset + values[3] + } + } + return frames + } + buffer += chunk + val frames = mutableListOf() + + while (true) { + val sof = findSof(buffer) + if (sof < 0) { + buffer = if (buffer.lastOrNull() == 0xA5.toByte()) byteArrayOf(0xA5.toByte()) else byteArrayOf() + break + } + if (sof > 0) buffer = buffer.copyOfRange(sof, buffer.size) + if (buffer.size < HEADER_SIZE) break + if ((buffer[2].toInt() and 0xFF) != VERSION) { + versionErrors++ + buffer = buffer.copyOfRange(1, buffer.size) + continue + } + val payloadSize = ((buffer[6].toInt() and 0xFF) shl 8) or + (buffer[7].toInt() and 0xFF) + if (payloadSize > MAX_PAYLOAD_SIZE) { + lengthErrors++ + buffer = buffer.copyOfRange(1, buffer.size) + continue + } + val total = HEADER_SIZE + payloadSize + CRC_SIZE + if (buffer.size < total) break + val protected = buffer.copyOfRange(2, total - CRC_SIZE) + val received = readU32Le(buffer, total - CRC_SIZE) + if (crc32(protected) != received) { + crcErrors++ + buffer = buffer.copyOfRange(1, buffer.size) + continue + } + frames += ProtocolFrame( + messageType = buffer[3].toInt() and 0xFF, + sequence = ((buffer[4].toInt() and 0xFF) shl 8) or + (buffer[5].toInt() and 0xFF), + payload = buffer.copyOfRange(HEADER_SIZE, HEADER_SIZE + payloadSize), + ) + buffer = buffer.copyOfRange(total, buffer.size) + } + return frames + } + + private fun findSof(data: ByteArray): Int { + for (index in 0 until data.size - 1) { + if (data[index] == 0xA5.toByte() && data[index + 1] == 0x5A.toByte()) return index + } + return -1 + } + } +}