feat: add raw binary stream logging, foreground service support, and improved frame header validation

- Implement RawStreamLogger for binary datastream recording with .bin file output
- Add foreground service (BluetoothForegroundService) to maintain Bluetooth connection during background operation
- Enhance frame header detection with 27-byte lookahead validation to filter false AA 55 sequences in payload data
- Add raw data recording toggle in settings with persistent preference storage
- Refactor Main
This commit is contained in:
2026-06-12 10:18:31 +01:00
parent 89e2ed4cd0
commit 7653876fed
12 changed files with 423 additions and 42 deletions
@@ -5,6 +5,7 @@ import com.example.esp32aldldashboard.bluetooth.BluetoothService
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
import com.example.esp32aldldashboard.logging.CsvLogger
import com.example.esp32aldldashboard.logging.RawStreamLogger
import com.example.esp32aldldashboard.data.database.TelemetryDatabase
class AldlApplication : Application() {
@@ -13,14 +14,17 @@ class AldlApplication : Application() {
lateinit var settingsRepository: SettingsRepository
lateinit var csvLogger: CsvLogger
lateinit var rawStreamLogger: RawStreamLogger
override fun onCreate() {
super.onCreate()
val database = TelemetryDatabase.getDatabase(this)
settingsRepository = SettingsRepository(this)
csvLogger = CsvLogger(this)
bluetoothService = BluetoothService(this)
rawStreamLogger = RawStreamLogger(this)
bluetoothService = BluetoothService(this, rawStreamLogger, settingsRepository)
telemetryRepository = TelemetryRepository(
this,
bluetoothService,
database.telemetryDao(),
csvLogger,
@@ -9,6 +9,9 @@ import android.content.Context
import android.util.Log
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.parser.ALDLParser
import com.example.esp32aldldashboard.logging.RawStreamLogger
import com.example.esp32aldldashboard.repository.SettingsRepository
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -23,7 +26,11 @@ enum class ConnectionState {
ERROR
}
class BluetoothService(private val context: Context) {
class BluetoothService(
private val context: Context,
private val rawStreamLogger: RawStreamLogger,
private val settingsRepository: SettingsRepository
) {
private val TAG = "ALDLBluetoothService"
private val SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB")
@@ -49,6 +56,61 @@ class BluetoothService(private val context: Context) {
private val _errorMessage = MutableStateFlow("")
val errorMessage: StateFlow<String> = _errorMessage
private data class HeaderResult(val found: Boolean, val index: Int = -1)
/**
* Finds a valid AA 55 header in the buffer using 27-byte lookahead validation.
* If another AA 55 is found within 27 bytes of a detected header, the first is treated
* as false (payload data) and search continues.
*/
private fun findValidHeader(buffer: ArrayDeque<Byte>): HeaderResult {
if (buffer.size < 27) return HeaderResult(false)
val bytes = buffer.toList()
var searchStart = 0
while (searchStart < bytes.size - 1) {
// Look for AA 55 starting at searchStart
var headerIdx = -1
for (i in searchStart until bytes.size - 1) {
val prev = bytes[i].toInt() and 0xFF
val curr = bytes[i + 1].toInt() and 0xFF
if (prev == 0xAA && curr == 0x55) {
headerIdx = i
break
}
}
if (headerIdx == -1) {
return HeaderResult(false) // No header found
}
// Check if there's another AA 55 within 27 bytes (indicating false header)
val lookAheadEnd = minOf(headerIdx + 27, bytes.size - 1)
var foundFalseHeader = false
for (i in headerIdx + 2 until lookAheadEnd) {
if (i + 1 >= bytes.size) break
val prev = bytes[i].toInt() and 0xFF
val curr = bytes[i + 1].toInt() and 0xFF
if (prev == 0xAA && curr == 0x55) {
// Found another header within 27 bytes - first one is likely false
foundFalseHeader = true
searchStart = headerIdx + 1 // Continue searching from after first AA
break
}
}
if (!foundFalseHeader) {
// Valid header found
return HeaderResult(true, headerIdx)
}
// Otherwise continue loop to find next candidate
}
return HeaderResult(false)
}
private var connectionJob: Job? = null
private var socket: BluetoothSocket? = null
private var isConnected = false
@@ -77,6 +139,12 @@ class BluetoothService(private val context: Context) {
_errorMessage.value = ""
connectionJob = CoroutineScope(Dispatchers.Default).launch {
// Start raw recording if enabled
val shouldRecordRaw = settingsRepository.recordRawDataFlow.first()
if (shouldRecordRaw) {
rawStreamLogger.startRecording()
}
var simStep = 0
val basePayload = byteArrayOf(
0x20.toByte(), 0x00.toByte(), 0x2A.toByte(), 0x5F.toByte(), 0x59.toByte(),
@@ -170,10 +238,22 @@ class BluetoothService(private val context: Context) {
_latestFrame.value = parsed.frame
val hexString = payload.joinToString(" ") { String.format("%02X", it) }
addRawHexLog("AA 55 $hexString (SIMULATED)")
// Log raw frame if recording is enabled
if (rawStreamLogger.isRecording()) {
val rawFrame = ByteArray(27)
rawFrame[0] = 0xAA.toByte()
rawFrame[1] = 0x55.toByte()
payload.copyInto(rawFrame, 2, 0, 25)
rawStreamLogger.logFrame(rawFrame)
}
}
delay(1000)
}
// Stop raw recording when simulation ends
rawStreamLogger.stopRecording()
}
}
@@ -227,8 +307,17 @@ class BluetoothService(private val context: Context) {
_connectionState.value = ConnectionState.CONNECTED
}
// Start raw recording if enabled
val shouldRecordRaw = settingsRepository.recordRawDataFlow.first()
if (shouldRecordRaw) {
rawStreamLogger.startRecording()
}
readDataStream(socket!!.inputStream)
// Stop raw recording when connection ends
rawStreamLogger.stopRecording()
} catch (e: Exception) {
Log.e(TAG, "Connection failed: ${e.message}", e)
withContext(Dispatchers.Main) {
@@ -261,25 +350,11 @@ class BluetoothService(private val context: Context) {
// Check for frame matches in buffer
while (syncBuffer.size >= 27) {
var foundHeader = false
val iterator = syncBuffer.iterator()
var idx = 0
var headerIdx = -1
val headerResult = findValidHeader(syncBuffer)
// Find header AA 55
var prev = iterator.next().toInt() and 0xFF
while (iterator.hasNext()) {
val curr = iterator.next().toInt() and 0xFF
if (prev == 0xAA && curr == 0x55) {
headerIdx = idx
foundHeader = true
break
}
prev = curr
idx++
}
if (foundHeader) {
if (headerResult.found) {
val headerIdx = headerResult.index
// Discard garbage preceding header
if (headerIdx > 0) {
_parseErrors.value += 1
@@ -288,13 +363,19 @@ class BluetoothService(private val context: Context) {
}
}
// Validate we have enough data for full frame
if (syncBuffer.size >= 27) {
syncBuffer.removeFirst() // AA
syncBuffer.removeFirst() // 55
val payload = ByteArray(25)
val rawFrame = ByteArray(27) // Include header for raw logging
rawFrame[0] = 0xAA.toByte()
rawFrame[1] = 0x55.toByte()
for (p in 0 until 25) {
payload[p] = syncBuffer.removeFirst()
rawFrame[p + 2] = payload[p]
}
val parsed = ALDLParser.parseFrame(payload)
@@ -324,12 +405,17 @@ class BluetoothService(private val context: Context) {
// Handled by size check
}
}
// Log raw frame if recording is enabled
if (rawStreamLogger.isRecording()) {
rawStreamLogger.logFrame(rawFrame)
}
} else {
// Header found, but waiting for full 27-byte frame
break
}
} else {
// Header sequence not found, purge all but last byte if it is part of a potential header
// No valid header found, purge all but potential header start
val lastByte = syncBuffer.last()
syncBuffer.clear()
if ((lastByte.toInt() and 0xFF) == 0xAA) {
@@ -0,0 +1,98 @@
package com.example.esp32aldldashboard.logging
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Logs raw binary ALDL data streams for debugging purposes.
* Records full 27-byte frames (AA 55 header + 25-byte payload) to Downloads/ALDLLogs/
*/
class RawStreamLogger(private val context: Context) {
private var currentOutputStream: OutputStream? = null
private var currentUri: Uri? = null
private var isRecording = false
/**
* Starts a new raw recording session.
* @return true if recording started successfully, false otherwise
*/
fun startRecording(): Boolean {
if (isRecording) return true
val timeStamp: String = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US).format(Date())
val fileName = "raw_$timeStamp.bin"
return try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
put(MediaStore.MediaColumns.MIME_TYPE, "application/octet-stream")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/ALDLLogs")
}
val uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)
if (uri != null) {
currentUri = uri
currentOutputStream = resolver.openOutputStream(uri)
isRecording = true
true
} else {
false
}
} else {
// Legacy support not implemented for this debug feature
false
}
} catch (e: Exception) {
e.printStackTrace()
false
}
}
/**
* Logs a single 27-byte frame (including AA 55 header).
* @param frame The complete 27-byte frame to log
*/
fun logFrame(frame: ByteArray) {
if (!isRecording || frame.size != 27) return
try {
currentOutputStream?.write(frame)
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* Stops the current recording session and closes the file.
*/
fun stopRecording() {
if (!isRecording) return
try {
currentOutputStream?.flush()
currentOutputStream?.close()
} catch (e: Exception) {
e.printStackTrace()
} finally {
currentOutputStream = null
currentUri = null
isRecording = false
}
}
/**
* Returns whether a recording session is currently active.
*/
fun isRecording(): Boolean = isRecording
}
@@ -16,6 +16,7 @@ class SettingsRepository(private val context: Context) {
val COOLANT_ALERT_THRESHOLD = floatPreferencesKey("coolant_alert_threshold")
val BATTERY_LOW_THRESHOLD = floatPreferencesKey("battery_low_threshold")
val AUTO_LOGGING = booleanPreferencesKey("auto_logging")
val RECORD_RAW_DATA = booleanPreferencesKey("record_raw_data")
}
val isCelsiusFlow: Flow<Boolean> = context.dataStore.data
@@ -38,6 +39,11 @@ class SettingsRepository(private val context: Context) {
preferences[AUTO_LOGGING] ?: false
}
val recordRawDataFlow: Flow<Boolean> = context.dataStore.data
.map { preferences ->
preferences[RECORD_RAW_DATA] ?: false
}
suspend fun setIsCelsius(isCelsius: Boolean) {
context.dataStore.edit { preferences ->
preferences[IS_CELSIUS] = isCelsius
@@ -61,4 +67,10 @@ class SettingsRepository(private val context: Context) {
preferences[AUTO_LOGGING] = autoLog
}
}
suspend fun setRecordRawData(recordRaw: Boolean) {
context.dataStore.edit { preferences ->
preferences[RECORD_RAW_DATA] = recordRaw
}
}
}
@@ -1,6 +1,9 @@
package com.example.esp32aldldashboard.repository
import android.content.Context
import android.content.Intent
import com.example.esp32aldldashboard.bluetooth.BluetoothService
import com.example.esp32aldldashboard.bluetooth.BluetoothForegroundService
import com.example.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame
import com.example.esp32aldldashboard.data.database.SessionEntity
@@ -16,6 +19,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.StateFlow
class TelemetryRepository(
private val context: Context,
private val bluetoothService: BluetoothService,
private val telemetryDao: TelemetryDao,
private val csvLogger: CsvLogger,
@@ -99,14 +103,31 @@ class TelemetryRepository(
}
fun connect() {
startForegroundService()
bluetoothService.connect()
}
fun disconnect() {
stopForegroundService()
bluetoothService.disconnect()
}
fun startSimulation() {
startForegroundService()
bluetoothService.startSimulation()
}
private fun startForegroundService() {
val intent = Intent(context, BluetoothForegroundService::class.java).apply {
action = BluetoothForegroundService.ACTION_START
}
context.startForegroundService(intent)
}
private fun stopForegroundService() {
val intent = Intent(context, BluetoothForegroundService::class.java).apply {
action = BluetoothForegroundService.ACTION_STOP
}
context.startService(intent)
}
}
@@ -26,7 +26,14 @@ fun MainScreen(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val viewModel: MainScreenViewModel = viewModel { MainScreenViewModel(context) }
val app = context.applicationContext as AldlApplication
val viewModel: MainScreenViewModel = viewModel(
factory = MainScreenViewModelFactory(
telemetryRepository = app.telemetryRepository,
settingsRepository = app.settingsRepository
)
)
val connState by viewModel.connectionState.collectAsStateWithLifecycle()
val frame by viewModel.latestFrame.collectAsStateWithLifecycle()
@@ -70,7 +77,6 @@ fun MainScreen(
}
var selectedTab by remember { mutableStateOf(0) }
val app = context.applicationContext as AldlApplication
Scaffold(
bottomBar = {
@@ -1,46 +1,51 @@
package com.example.esp32aldldashboard.ui.main
import android.content.Context
import androidx.lifecycle.ViewModel
import com.example.esp32aldldashboard.bluetooth.BluetoothService
import androidx.lifecycle.viewModelScope
import com.example.esp32aldldashboard.bluetooth.ConnectionState
import com.example.esp32aldldashboard.parser.ALDLFrame
import kotlinx.coroutines.flow.MutableStateFlow
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
class MainScreenViewModel(context: Context) : ViewModel() {
private val bluetoothService = BluetoothService(context.applicationContext)
class MainScreenViewModel(
private val telemetryRepository: TelemetryRepository,
private val settingsRepository: SettingsRepository
) : ViewModel() {
val connectionState: StateFlow<ConnectionState> = bluetoothService.connectionState
val latestFrame: StateFlow<ALDLFrame?> = bluetoothService.latestFrame
val rawHexLog: StateFlow<List<String>> = bluetoothService.rawHexLog
val errorMessage: StateFlow<String> = bluetoothService.errorMessage
val connectionState: StateFlow<ConnectionState> = telemetryRepository.connectionState
val latestFrame: StateFlow<ALDLFrame?> = telemetryRepository.latestFrame
val rawHexLog: StateFlow<List<String>> = telemetryRepository.rawHexLog
val errorMessage: StateFlow<String> = telemetryRepository.errorMessage
val framesReceived: StateFlow<Int> = bluetoothService.framesReceived
val parseErrors: StateFlow<Int> = bluetoothService.parseErrors
val currentFrameRate: StateFlow<Int> = bluetoothService.currentFrameRate
val framesReceived: StateFlow<Int> = telemetryRepository.framesReceived
val parseErrors: StateFlow<Int> = telemetryRepository.parseErrors
val currentFrameRate: StateFlow<Int> = telemetryRepository.currentFrameRate
private val _isCelsius = MutableStateFlow(false) // Default to Fahrenheit for standard 80s GM telemetry
val isCelsius: StateFlow<Boolean> = _isCelsius
val isCelsius: StateFlow<Boolean> = settingsRepository.isCelsiusFlow
fun toggleTemperatureUnit() {
_isCelsius.value = !_isCelsius.value
viewModelScope.launch {
val currentValue = settingsRepository.isCelsiusFlow.value
settingsRepository.setIsCelsius(!currentValue)
}
}
fun connect() {
bluetoothService.connect()
telemetryRepository.connect()
}
fun disconnect() {
bluetoothService.disconnect()
telemetryRepository.disconnect()
}
fun startSimulation() {
bluetoothService.startSimulation()
telemetryRepository.startSimulation()
}
override fun onCleared() {
super.onCleared()
bluetoothService.disconnect()
telemetryRepository.disconnect()
}
}
@@ -0,0 +1,20 @@
package com.example.esp32aldldashboard.ui.main
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.example.esp32aldldashboard.repository.SettingsRepository
import com.example.esp32aldldashboard.repository.TelemetryRepository
class MainScreenViewModelFactory(
private val telemetryRepository: TelemetryRepository,
private val settingsRepository: SettingsRepository
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainScreenViewModel::class.java)) {
return MainScreenViewModel(telemetryRepository, settingsRepository) as T
}
throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
}
}
@@ -17,6 +17,7 @@ fun SettingsScreen(
) {
val isCelsius by settingsRepository.isCelsiusFlow.collectAsStateWithLifecycle(initialValue = false)
val autoLogging by settingsRepository.autoLoggingFlow.collectAsStateWithLifecycle(initialValue = false)
val recordRawData by settingsRepository.recordRawDataFlow.collectAsStateWithLifecycle(initialValue = false)
val coolantThreshold by settingsRepository.coolantAlertThresholdFlow.collectAsStateWithLifecycle(initialValue = 100f)
val coroutineScope = rememberCoroutineScope()
@@ -67,6 +68,25 @@ fun SettingsScreen(
}
Divider(modifier = Modifier.padding(vertical = 8.dp))
// Raw Data Recording Toggle (Debug)
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(text = "Record Raw Datastream (Debug)", style = MaterialTheme.typography.titleMedium)
Text(text = "Save binary .bin files for debugging.", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Switch(
checked = recordRawData,
onCheckedChange = {
coroutineScope.launch { settingsRepository.setRecordRawData(it) }
}
)
}
Divider(modifier = Modifier.padding(vertical = 8.dp))
// Coolant Alert Threshold
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
Text(text = "Coolant Alert Threshold", style = MaterialTheme.typography.titleMedium)
@@ -70,4 +70,105 @@ class ALDLParserTest {
assertFalse(frame.isClosedLoop) // bit 7 of u[14] is 0
assertFalse(frame.isRich) // bit 6 of u[14] is 0
}
@Test
fun testRpmBoundaryValues() {
// Max valid RPM: 8000 (320 * 25 = 8000)
val validPayload = createBasePayload().apply {
this[7] = 320.toByte() // 320 * 25 = 8000 RPM
}
val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("RPM at exactly 8000 should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Invalid RPM: 8001 (321 * 25 = 8025)
val invalidPayload = createBasePayload().apply {
this[7] = 321.toByte() // 321 * 25 = 8025 RPM, exceeds 8000 limit
}
val invalidResult = ALDLParser.parseFrame(invalidPayload)
assertTrue("RPM above 8000 should be rejected", invalidResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
}
@Test
fun testBatteryVoltageBoundaryValues() {
// Min valid: 8V (80 * 0.1 = 8.0V)
val minPayload = createBasePayload().apply {
this[17] = 80.toByte()
}
val minResult = ALDLParser.parseFrame(minPayload)
assertTrue("Battery at 8V should be valid", minResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Max valid: 18V (180 * 0.1 = 18.0V)
val maxPayload = createBasePayload().apply {
this[17] = 180.toByte()
}
val maxResult = ALDLParser.parseFrame(maxPayload)
assertTrue("Battery at 18V should be valid", maxResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Too low: 4.9V (49 * 0.1 = 4.9V, below 5V minimum)
val lowPayload = createBasePayload().apply {
this[17] = 49.toByte()
}
val lowResult = ALDLParser.parseFrame(lowPayload)
assertTrue("Battery below 5V should be rejected", lowResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
// Too high: 20.1V (201 * 0.1 = 20.1V, above 20V maximum)
val highPayload = createBasePayload().apply {
this[17] = 201.toByte()
}
val highResult = ALDLParser.parseFrame(highPayload)
assertTrue("Battery above 20V should be rejected", highResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
}
@Test
fun testTpsVoltageBoundaryValues() {
// Max valid: 5.1V (260 * 0.019608 = 5.098V ~ 5.1V)
val validPayload = createBasePayload().apply {
this[8] = 260.toByte()
}
val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("TPS at 5.1V should be valid", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Too high: 5.2V (265 * 0.019608 = 5.196V ~ 5.2V, exceeds 5.1V)
val invalidPayload = createBasePayload().apply {
this[8] = 265.toByte()
}
val invalidResult = ALDLParser.parseFrame(invalidPayload)
assertTrue("TPS above 5.1V should be rejected", invalidResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
}
@Test
fun testCoolantTempBoundaryValues() {
// Valid temperature: 89 * 0.75 - 40 = 26.75C (within -45 to 220 range)
val validPayload = createBasePayload()
val validResult = ALDLParser.parseFrame(validPayload)
assertTrue("Valid coolant temp should be accepted", validResult is com.example.esp32aldldashboard.parser.ALDLParseResult.Success)
// Too low: raw value 0 -> -40C, but below -45C limit
// Actually raw 0 gives -40C which is within bounds
// Let's test a very high raw value that gives > 220C
// raw 350 * 0.75 - 40 = 262.5 - 40 = 222.5C (exceeds 220C)
val highTempPayload = createBasePayload().apply {
this[4] = 350.toByte()
}
val highResult = ALDLParser.parseFrame(highTempPayload)
assertTrue("Coolant temp above 220C should be rejected", highResult is com.example.esp32aldldashboard.parser.ALDLParseResult.InvalidData)
}
@Test
fun testIncompletePayload() {
// Payload with only 24 bytes (incomplete)
val incompletePayload = ByteArray(24) { 0x20.toByte() }
val result = ALDLParser.parseFrame(incompletePayload)
assertTrue("Incomplete payload should return Incomplete result", result is com.example.esp32aldldashboard.parser.ALDLParseResult.Incomplete)
}
private fun createBasePayload(): ByteArray {
return byteArrayOf(
0x20.toByte(), 0x00.toByte(), 0x2A.toByte(), 0x5F.toByte(), 0x59.toByte(),
0x00.toByte(), 0xF4.toByte(), 0x00.toByte(), 0x1E.toByte(), 0x80.toByte(),
0x65.toByte(), 0x08.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte(),
0x25.toByte(), 0x18.toByte(), 0x7D.toByte(), 0x80.toByte(), 0x00.toByte(),
0x00.toByte(), 0x00.toByte(), 0xC9.toByte(), 0x02.toByte(), 0x62.toByte()
)
}
}