EUCProtocol.kt
package io.github.tritbool.euc.ble.protocols
import io.github.tritbool.euc.ble.models.BMSData
import io.github.tritbool.euc.ble.models.EUCData
import io.github.tritbool.euc.ble.models.EUCDevice
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow
import java.io.Closeable
import java.util.UUID
/**
* Base interface for EUC manufacturer protocols
*/
interface EUCProtocol : Closeable {
/**
* Manufacturer name
*/
val manufacturer: String
/**
* List of supported models
*/
val supportedModels: List<String>
val dataFlow: Flow<EUCData>
/**
* Flow that emits every raw BLE characteristic notification received by this protocol,
* as a defensive copy of the original byte array. Collectors can use this to write raw
* logs or perform any custom processing on the unmodified BLE data.
*
* The default implementation emits nothing; concrete protocols override this to provide
* a live stream of incoming bytes.
*/
val rawFrameFlow: Flow<ByteArray> get() = emptyFlow()
/**
* Flow of commands or raw bytes that the protocol needs to write to the device automatically.
*/
val writeFlow: Flow<ByteArray> get() = emptyFlow()
/**
* Check if this protocol can handle the given device
*/
fun canHandle(device: EUCDevice): Boolean
/**
* Decode raw BLE data into EUCData
*/
fun decode(data: ByteArray): EUCData?
/**
* Optional fast header check: returns true if [chunk] appears to contain frames belonging
* to this protocol, based on magic-byte patterns alone. The default returns false (opt-out).
* Protocols override this to enable frame-header-based routing without full decoding.
*
* Note: header-based routing is a hint only. [canHandle] (device-name / manufacturer-ID)
* remains the authoritative gate for protocol selection at connection time.
*/
fun looksLikeMyFrames(chunk: ByteArray): Boolean = false
/**
* Get the UUID for the data characteristic
*/
fun getDataCharacteristicUUID(): UUID
/**
* Get the UUID for the service
*/
fun getServiceUUID(): UUID
/**
* Create a command for the EUC
*/
fun createCommand(commandType: CommandType, value: Any): ByteArray
/**
* Explicit command support matrix for this protocol.
* Commands outside this set are considered unsupported by design.
*/
val supportedCommandTypes: Set<CommandType>
get() = emptySet()
/**
* API-level command support check (used by framework and clients).
*/
fun getCommandSupport(commandType: CommandType): CommandSupport {
return if (supportedCommandTypes.contains(commandType)) {
CommandSupport.SUPPORTED
} else {
CommandSupport.UNSUPPORTED
}
}
/**
* Optional polling/query plan consumed by BLEManager orchestration.
*/
fun getPollingPlan(): ProtocolPollingPlan = ProtocolPollingPlan.disabled()
/**
* Optional query/response matcher used by BLEManager observability and retry loop.
*/
fun matchesQueryResponse(query: ProtocolQuerySpec, data: ByteArray): Boolean = false
/**
* Get the UUID for the write characteristic (if different from data characteristic)
*/
fun getWriteCharacteristicUUID(): UUID = getDataCharacteristicUUID()
/**
* Check if the device is ready for operation
*/
fun isDeviceReady(data: EUCData): Boolean
/**
* Get BMS (Battery Management System) data for this protocol.
* Returns null if this protocol does not support BMS data extraction.
* Protocols that support BMS data should override this method to return
* a list of BMSData objects representing the current state of all battery packs.
*/
fun getBMSData(): List<BMSData>? = null
}
/**
* Standard command types for EUCs
*/
enum class CommandType {
LIGHT_ON,
LIGHT_OFF,
SET_LIGHT_MODE,
LIGHT_BRIGHTNESS,
SPEAKER_VOLUME,
BEEP,
POWER_OFF,
LOCK,
UNLOCK,
SET_PEDALS_MODE,
SET_LED_MODE,
SET_SPEED_LIMIT,
SET_ALARM_SPEED,
CALIBRATE,
REQUEST_SERIAL,
REQUEST_FIRMWARE,
REQUEST_BATTERY_INFO,
RESET_TRIP,
CUSTOM
}
enum class CommandSupport {
SUPPORTED,
UNSUPPORTED
}
data class ProtocolQuerySpec(
val id: String,
val commandType: CommandType,
val value: Any = Unit,
val initialDelayMs: Long = 0L,
val intervalMs: Long = 0L,
val responseTimeoutMs: Long = 1500L,
val maxRetries: Int = 2,
val retryBackoffMs: Long = 500L
)
data class ProtocolPollingPlan(
val enabled: Boolean,
val startupQueries: List<ProtocolQuerySpec> = emptyList(),
val periodicQueries: List<ProtocolQuerySpec> = emptyList()
) {
companion object {
fun disabled() = ProtocolPollingPlan(enabled = false)
}
}