fix: relay pins G1/G2, OTA CCCD+retry, single version source
- Relay pins: G39→G1, G38→G2 - BUNDLED_FIRMWARE_VERSION → SylDaVersions.SERVER (version.txt) - OTA: explicit CCCD descriptor write for Android 13+ - OTA: start packet retry (previously no retry, blocked by pending CCCD) - OTA: firmware path device-mode-aware (server/client subdirs) - Removed stale root firmware.bin, flashMethodGroup, dialog_overwrite_confirm - PrefsHelper: serverUUID/clientUUID persistence
This commit is contained in:
parent
45f8f38e81
commit
a84727abdc
22 changed files with 270 additions and 160 deletions
|
|
@ -8,9 +8,6 @@ def hasSigningConfig = signingPropsFile.exists()
|
|||
|
||||
android {
|
||||
namespace 'nl.eddydevink.sylda'
|
||||
// compileSdk/targetSdk stays on 34 (Android 14) by deliberate choice. The target audience
|
||||
// consists of physically disabled users who are very reluctant to update their phones. Staying
|
||||
// on API 34 maximises device compatibility. Re-evaluate when the user base has migrated.
|
||||
compileSdk 34
|
||||
|
||||
if (hasSigningConfig) {
|
||||
|
|
@ -59,6 +56,31 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
task generateSylDaVersions {
|
||||
def versionFile = rootProject.file('../version.txt')
|
||||
def outputFile = file('src/main/java/nl/eddydevink/sylda/SylDaVersions.kt')
|
||||
|
||||
inputs.file(versionFile)
|
||||
outputs.file(outputFile)
|
||||
|
||||
doLast {
|
||||
def versions = [:]
|
||||
versionFile.eachLine { line ->
|
||||
def parts = line.trim().split('=')
|
||||
if (parts.length == 2) versions[parts[0].trim()] = parts[1].trim()
|
||||
}
|
||||
outputFile.text = """// Auto-generated from version.txt — DO NOT EDIT
|
||||
package nl.eddydevink.sylda
|
||||
|
||||
object SylDaVersions {
|
||||
const val SERVER = "${versions.server}"
|
||||
const val CLIENT = "${versions.client}"
|
||||
}
|
||||
"""
|
||||
}
|
||||
}
|
||||
preBuild.dependsOn generateSylDaVersions
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
|
|
|
|||
BIN
android/app/src/main/assets/firmware/client/firmware.bin
Normal file
BIN
android/app/src/main/assets/firmware/client/firmware.bin
Normal file
Binary file not shown.
Binary file not shown.
BIN
android/app/src/main/assets/firmware/server/boot_app0.bin
Normal file
BIN
android/app/src/main/assets/firmware/server/boot_app0.bin
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/firmware/server/bootloader.bin
Normal file
BIN
android/app/src/main/assets/firmware/server/bootloader.bin
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/firmware/server/firmware.bin
Normal file
BIN
android/app/src/main/assets/firmware/server/firmware.bin
Normal file
Binary file not shown.
BIN
android/app/src/main/assets/firmware/server/partitions.bin
Normal file
BIN
android/app/src/main/assets/firmware/server/partitions.bin
Normal file
Binary file not shown.
|
|
@ -6,6 +6,7 @@ import android.annotation.SuppressLint
|
|||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothGatt
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattDescriptor
|
||||
import android.bluetooth.BluetoothProfile
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
|
|
@ -37,6 +38,7 @@ class BleRelayManager(
|
|||
private val UUID_CHAR_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26d9")
|
||||
private val VERSION_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26db")
|
||||
private val OTA_UUID: UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361f26da")
|
||||
private val OTA_CCCD_UUID: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
|
||||
|
||||
private const val SCAN_TIMEOUT_MS = 30000L
|
||||
private const val RECONNECT_DELAY_MS = 2000L
|
||||
|
|
@ -206,7 +208,18 @@ class BleRelayManager(
|
|||
fun enableOtaNotifications(): Boolean {
|
||||
val char = otaCharacteristic ?: return false
|
||||
val gatt = bluetoothGatt ?: return false
|
||||
return gatt.setCharacteristicNotification(char, true)
|
||||
if (!gatt.setCharacteristicNotification(char, true)) {
|
||||
Log.e(TAG, "setCharacteristicNotification returned false")
|
||||
return false
|
||||
}
|
||||
val desc = char.getDescriptor(OTA_CCCD_UUID)
|
||||
if (desc != null) {
|
||||
desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
|
||||
if (!gatt.writeDescriptor(desc)) {
|
||||
Log.w(TAG, "writeDescriptor for CCCD returned false")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun hasOtaCharacteristic(): Boolean {
|
||||
|
|
@ -259,6 +272,7 @@ class BleRelayManager(
|
|||
bluetoothGatt = null
|
||||
relayCharacteristic = null
|
||||
seq = 1
|
||||
triedAddresses.clear()
|
||||
handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS)
|
||||
}
|
||||
}
|
||||
|
|
@ -369,7 +383,8 @@ class BleRelayManager(
|
|||
val prefs = PrefsHelper.getPrefs(context)
|
||||
val hexKey = prefs.getString(PrefsHelper.PREF_KEY, null)
|
||||
hmacKey = hexKey?.let { hexStringToBytes(it) }
|
||||
val hexUuid = prefs.getString(PrefsHelper.PREF_UUID, null)
|
||||
val hexUuid = PrefsHelper.getServerUuid(context)
|
||||
?: prefs.getString(PrefsHelper.PREF_UUID, null)
|
||||
deviceUuid = hexUuid?.let { hexStringToBytes(it) }
|
||||
seq = prefs.getInt(PrefsHelper.PREF_SEQ, 1).coerceAtMost(PrefsHelper.SEQ_MAX)
|
||||
Log.d(TAG, "HMAC key loaded: ${hmacKey?.size ?: "none"} bytes, UUID: ${deviceUuid?.size ?: "none"} bytes, seq=$seq")
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
|||
private const val REQUEST_SETTINGS = 1
|
||||
private const val POST_SETTINGS_RESCAN_MS = 500L
|
||||
private const val ON_RESUME_SCAN_DELAY_MS = 300L
|
||||
private const val BUNDLED_FIRMWARE_VERSION = "6"
|
||||
}
|
||||
|
||||
private lateinit var manager: BleRelayManager
|
||||
|
|
@ -93,7 +92,7 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
|||
setContentView(R.layout.activity_main)
|
||||
|
||||
manager = BleRelayManager(this, this)
|
||||
manager.setAppVersion(BUNDLED_FIRMWARE_VERSION)
|
||||
manager.setAppVersion(SylDaVersions.SERVER)
|
||||
|
||||
statusText = findViewById(R.id.statusText)
|
||||
relay1Button = findViewById(R.id.relay1Button)
|
||||
|
|
@ -231,11 +230,9 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
|||
override fun onFirmwareVersion(espVersion: String, appVersion: String) {
|
||||
runOnUiThread {
|
||||
if (isFinishing || isDestroyed || !lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) return@runOnUiThread
|
||||
if (espVersion.isEmpty() || espVersion != appVersion) {
|
||||
if (espVersion.isNotEmpty() && espVersion != appVersion) {
|
||||
Toast.makeText(this, getString(R.string.ota_update_available), Toast.LENGTH_SHORT).show()
|
||||
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,13 +39,14 @@ class OtaManager(
|
|||
return minOf(maxOf(mtu - 3 - 4, 20), OTA_ESP_MAX_CHUNK)
|
||||
}
|
||||
|
||||
fun loadFirmware(): Boolean {
|
||||
fun loadFirmware(assetPath: String = "firmware/firmware.bin"): Boolean {
|
||||
return try {
|
||||
firmwareData = context.assets.open("firmware/firmware.bin").readBytes()
|
||||
firmwareData = context.assets.open(assetPath).readBytes()
|
||||
chunkDataSize = calculateChunkSize(relayManager.getNegotiatedMtu())
|
||||
Log.d(TAG, "Loaded firmware from $assetPath: ${firmwareData!!.size} bytes, chunkDataSize=$chunkDataSize")
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to load firmware", e)
|
||||
Log.e(TAG, "Failed to load firmware from $assetPath", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
@ -80,7 +81,21 @@ class OtaManager(
|
|||
val startPacket = ByteBuffer.allocate(5).order(ByteOrder.BIG_ENDIAN)
|
||||
startPacket.put(OTA_CMD_START)
|
||||
startPacket.putInt(fw.size)
|
||||
relayManager.sendOtaData(startPacket.array())
|
||||
var startSent = false
|
||||
for (attempt in 1..MAX_RETRIES) {
|
||||
if (relayManager.sendOtaData(startPacket.array())) {
|
||||
startSent = true
|
||||
Log.d(TAG, "OTA start packet sent on attempt $attempt")
|
||||
break
|
||||
}
|
||||
Log.w(TAG, "OTA start packet failed attempt $attempt, retrying...")
|
||||
delay(500)
|
||||
}
|
||||
if (!startSent) {
|
||||
relayManager.setOtaNotifyCallback(null)
|
||||
withContext(Dispatchers.Main) { callback.onError("OTA mislukt: start packet niet verzonden") }
|
||||
return@withContext false
|
||||
}
|
||||
|
||||
try {
|
||||
withTimeout(OTA_START_TIMEOUT_MS) {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,10 @@ object PrefsHelper {
|
|||
fun setDeviceMode(context: Context, mode: String) = getPrefs(context).edit().putString(KEY_DEVICE_MODE, mode).apply()
|
||||
fun getTransportMode(context: Context): String = getPrefs(context).getString("transportMode", "ble") ?: "ble"
|
||||
fun setTransportMode(context: Context, mode: String) = getPrefs(context).edit().putString("transportMode", mode).apply()
|
||||
fun getServerUuid(context: Context): String? = getPrefs(context).getString(KEY_SERVER_UUID, null)
|
||||
fun setServerUuid(context: Context, uuid: String) = getPrefs(context).edit().putString(KEY_SERVER_UUID, uuid).apply()
|
||||
fun getClientUuid(context: Context): String? = getPrefs(context).getString(KEY_CLIENT_UUID, null)
|
||||
fun setClientUuid(context: Context, uuid: String) = getPrefs(context).edit().putString(KEY_CLIENT_UUID, uuid).apply()
|
||||
|
||||
fun applyTheme(context: Context) {
|
||||
val prefs = getPrefs(context)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import android.util.Log
|
|||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Button
|
||||
import android.widget.CheckBox
|
||||
import android.widget.EditText
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.RadioGroup
|
||||
|
|
@ -32,6 +31,7 @@ import kotlinx.coroutines.CompletableDeferred
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.security.SecureRandom
|
||||
|
|
@ -64,7 +64,6 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private const val REBOOT_WAIT_MS = 5000L
|
||||
private const val REBOOT_POLL_MS = 500L
|
||||
private const val REBOOT_MAX_WAIT_MS = 15000L
|
||||
private const val EXPECTED_FIRMWARE_VERSION = "6"
|
||||
}
|
||||
|
||||
private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH }
|
||||
|
|
@ -131,6 +130,10 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getExpectedFirmwareVersion(): String {
|
||||
return if (PrefsHelper.getDeviceMode(this) == "client") SylDaVersions.CLIENT else SylDaVersions.SERVER
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
lastTheme = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
|
||||
PrefsHelper.applyTheme(this)
|
||||
|
|
@ -161,7 +164,8 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
findViewById<Button>(R.id.updateModuleButton).setOnClickListener {
|
||||
if (findViewById<RadioGroup>(R.id.flashMethodGroup).checkedRadioButtonId == R.id.methodUsb) {
|
||||
if (PrefsHelper.getDeviceMode(this) == "client" ||
|
||||
findViewById<RadioGroup>(R.id.radioTransport).checkedRadioButtonId == R.id.radioUsb) {
|
||||
startFlashing()
|
||||
} else {
|
||||
startOtaUpdate()
|
||||
|
|
@ -242,7 +246,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
val espVersion = manager.getEspVersion()
|
||||
if (espVersion != null && espVersion == EXPECTED_FIRMWARE_VERSION) {
|
||||
if (espVersion != null && espVersion == getExpectedFirmwareVersion()) {
|
||||
Toast.makeText(this, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
|
@ -251,7 +255,9 @@ class SettingsActivity : AppCompatActivity() {
|
|||
|
||||
otaManager = OtaManager(this, manager)
|
||||
|
||||
if (otaManager?.loadFirmware() != true) {
|
||||
val mode = PrefsHelper.getDeviceMode(this)
|
||||
val fwPath = "firmware/${if (mode == "client") "client" else "server"}/firmware.bin"
|
||||
if (otaManager?.loadFirmware(fwPath) != true) {
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
Toast.makeText(this, getString(R.string.ota_load_failed), Toast.LENGTH_SHORT).show()
|
||||
|
|
@ -261,6 +267,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
manager.enableOtaNotifications()
|
||||
|
||||
otaJob = lifecycleScope.launch {
|
||||
delay(300)
|
||||
val result = otaManager!!.performOta(object : OtaManager.OtaCallback {
|
||||
override fun onProgress(percent: Int, message: String) {
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
|
|
@ -381,6 +388,11 @@ class SettingsActivity : AppCompatActivity() {
|
|||
.putString(PrefsHelper.PREF_UUID, result.deviceUuid)
|
||||
.putString(PrefsHelper.PREF_KEY, result.hexKey)
|
||||
.apply()
|
||||
if (PrefsHelper.getDeviceMode(this@SettingsActivity) == "server") {
|
||||
PrefsHelper.setServerUuid(this@SettingsActivity, result.deviceUuid)
|
||||
} else {
|
||||
PrefsHelper.setClientUuid(this@SettingsActivity, result.deviceUuid)
|
||||
}
|
||||
cleanupAction()
|
||||
setResult(RESULT_SCAN)
|
||||
finish()
|
||||
|
|
@ -429,7 +441,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
|
||||
lifecycleScope.launch {
|
||||
val version = readEspVersion(usbManager, device)
|
||||
if (version == EXPECTED_FIRMWARE_VERSION && !expectingReboot) {
|
||||
if (version == getExpectedFirmwareVersion() && !expectingReboot) {
|
||||
Log.d(TAG, "Firmware version $version matches, up to date")
|
||||
progressDialog?.dismiss()
|
||||
progressDialog = null
|
||||
|
|
@ -643,8 +655,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
hexUuid.length == UUID_HEX_LEN && hexUuid.all { it in "0123456789ABCDEFabcdef" }) {
|
||||
val espKeyUpper = hexKey.uppercase()
|
||||
val espUuidUpper = hexUuid.uppercase()
|
||||
if (appUuid != null && appKey != null &&
|
||||
(appUuid != espUuidUpper || appKey != espKeyUpper)) {
|
||||
if (appUuid != espUuidUpper || appKey != espKeyUpper) {
|
||||
return KeyResult.Conflict(espUuidUpper, espKeyUpper)
|
||||
}
|
||||
return KeyResult.Success(espUuidUpper, espKeyUpper)
|
||||
|
|
@ -654,10 +665,14 @@ class SettingsActivity : AppCompatActivity() {
|
|||
} else if (noKey) {
|
||||
val newKey: String
|
||||
val newUuid: String
|
||||
if (appUuid != null && appKey != null) {
|
||||
Log.d(TAG, "ESP has no keys, using existing app keys")
|
||||
if (appKey != null) {
|
||||
Log.d(TAG, "ESP has no keys, using existing HMAC key")
|
||||
newKey = appKey
|
||||
newUuid = appUuid
|
||||
newUuid = if (PrefsHelper.getDeviceMode(this) == "server") {
|
||||
generateRandomHex(UUID_BYTE_COUNT)
|
||||
} else {
|
||||
generateRandomHex(UUID_BYTE_COUNT)
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Both have no keys, generating new pair")
|
||||
newKey = generateRandomHexKey()
|
||||
|
|
@ -691,6 +706,21 @@ class SettingsActivity : AppCompatActivity() {
|
|||
Log.d(TAG, "SET_ID response: ${okUuidResponse.take(200)}")
|
||||
|
||||
if (okUuidResponse.contains("OK")) {
|
||||
if (PrefsHelper.getDeviceMode(this) == "client") {
|
||||
val srvUuid = PrefsHelper.getServerUuid(this)
|
||||
if (srvUuid != null) {
|
||||
val setServerCmd = "SET_SERVER:$srvUuid\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
connection.bulkTransfer(epOut, setServerCmd, setServerCmd.size, USB_BULK_TIMEOUT_MS)
|
||||
Thread.sleep(RESPONSE_WAIT_MS)
|
||||
val okServerResp = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
||||
Log.d(TAG, "SET_SERVER response: ${okServerResp.take(200)}")
|
||||
if (!okServerResp.contains("OK")) {
|
||||
return KeyResult.Error(R.string.err_server_not_ack)
|
||||
}
|
||||
} else {
|
||||
return KeyResult.Error(R.string.err_no_server_uuid)
|
||||
}
|
||||
}
|
||||
return KeyResult.Success(newUuid, newKey)
|
||||
} else {
|
||||
return KeyResult.Error(R.string.err_uuid_not_ack)
|
||||
|
|
@ -787,52 +817,23 @@ class SettingsActivity : AppCompatActivity() {
|
|||
private fun showConflictDialog(usbManager: UsbManager, device: UsbDevice,
|
||||
espUuid: String, espKey: String, appKey: String) {
|
||||
val prefs = PrefsHelper.getPrefs(this)
|
||||
val appUuid = prefs.getString(PrefsHelper.PREF_UUID, "") ?: ""
|
||||
val appUuid = prefs.getString(PrefsHelper.PREF_UUID, null)
|
||||
val newAppKey = appKey.ifEmpty { generateRandomHexKey() }
|
||||
val newAppUuid = appUuid ?: generateRandomHex(UUID_BYTE_COUNT)
|
||||
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.title_key_conflict))
|
||||
.setMessage(getString(R.string.msg_key_conflict))
|
||||
.setPositiveButton(getString(R.string.button_keep_esp_keys)) { _, _ ->
|
||||
prefs.edit()
|
||||
.putString(PrefsHelper.PREF_UUID, espUuid)
|
||||
.putString(PrefsHelper.PREF_KEY, espKey)
|
||||
.apply()
|
||||
cleanupAction()
|
||||
setResult(RESULT_SCAN)
|
||||
finish()
|
||||
.setMessage(getString(R.string.msg_pair_device))
|
||||
.setPositiveButton(getString(R.string.button_pair)) { _, _ ->
|
||||
doOverwriteEspKeys(usbManager, device, newAppKey, newAppUuid)
|
||||
}
|
||||
.setNegativeButton(getString(R.string.button_overwrite_esp)) { _, _ ->
|
||||
showOverwriteConfirmDialog(usbManager, device, espUuid, espKey, appKey, appUuid)
|
||||
}
|
||||
.setNeutralButton(getString(R.string.button_cancel)) { _, _ ->
|
||||
.setNegativeButton(getString(R.string.button_cancel)) { _, _ ->
|
||||
cleanupAction()
|
||||
}
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun showOverwriteConfirmDialog(usbManager: UsbManager, device: UsbDevice,
|
||||
espUuid: String, espKey: String,
|
||||
appKey: String, appUuid: String) {
|
||||
val view = layoutInflater.inflate(R.layout.dialog_overwrite_confirm, null)
|
||||
val checkBox = view.findViewById<CheckBox>(R.id.confirmCheckBox)
|
||||
val dialog = AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.title_overwrite_confirm))
|
||||
.setView(view)
|
||||
.setPositiveButton(getString(R.string.button_confirm)) { _, _ ->
|
||||
if (checkBox.isChecked) {
|
||||
doOverwriteEspKeys(usbManager, device, appKey, appUuid)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(R.string.button_cancel)) { _, _ -> }
|
||||
.setCancelable(true)
|
||||
.show()
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
checkBox.setOnCheckedChangeListener { _, isChecked ->
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = isChecked
|
||||
}
|
||||
}
|
||||
|
||||
private fun doOverwriteEspKeys(usbManager: UsbManager, device: UsbDevice,
|
||||
appKey: String, appUuid: String) {
|
||||
showProgressDialog(getString(R.string.title_overwriting_keys), getString(R.string.msg_writing_keys), STYLE_SPINNER)
|
||||
|
|
@ -886,6 +887,12 @@ class SettingsActivity : AppCompatActivity() {
|
|||
Thread.sleep(KEY_SETTLE_MS)
|
||||
flushReadBuffer(connection, epIn)
|
||||
|
||||
val newUuid = if (PrefsHelper.getDeviceMode(this) == "client") {
|
||||
generateRandomHex(UUID_BYTE_COUNT)
|
||||
} else {
|
||||
appUuid
|
||||
}
|
||||
|
||||
val setKeyCmd = "SET_KEY:$appKey\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
val w = connection.bulkTransfer(epOut, setKeyCmd, setKeyCmd.size, USB_BULK_TIMEOUT_MS)
|
||||
if (w < 0) return KeyResult.Error(R.string.err_set_key_write)
|
||||
|
|
@ -895,7 +902,7 @@ class SettingsActivity : AppCompatActivity() {
|
|||
val okKeyResponse = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
||||
if (!okKeyResponse.contains("OK")) return KeyResult.Error(R.string.err_key_not_ack)
|
||||
|
||||
val setIdCmd = "SET_ID:$appUuid\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
val setIdCmd = "SET_ID:$newUuid\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
val w2 = connection.bulkTransfer(epOut, setIdCmd, setIdCmd.size, USB_BULK_TIMEOUT_MS)
|
||||
if (w2 < 0) return KeyResult.Error(R.string.err_set_id_write)
|
||||
|
||||
|
|
@ -903,12 +910,31 @@ class SettingsActivity : AppCompatActivity() {
|
|||
val okUuidResponse = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
||||
if (!okUuidResponse.contains("OK")) return KeyResult.Error(R.string.err_uuid_not_ack)
|
||||
|
||||
if (PrefsHelper.getDeviceMode(this) == "client") {
|
||||
val srvUuid = PrefsHelper.getServerUuid(this)
|
||||
if (srvUuid != null) {
|
||||
val setServerCmd = "SET_SERVER:$srvUuid\n".toByteArray(StandardCharsets.US_ASCII)
|
||||
connection.bulkTransfer(epOut, setServerCmd, setServerCmd.size, USB_BULK_TIMEOUT_MS)
|
||||
Thread.sleep(RESPONSE_WAIT_MS)
|
||||
val okServerResp = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
||||
Log.d(TAG, "SET_SERVER response: ${okServerResp.take(200)}")
|
||||
if (!okServerResp.contains("OK")) return KeyResult.Error(R.string.err_server_not_ack)
|
||||
} else {
|
||||
return KeyResult.Error(R.string.err_no_server_uuid)
|
||||
}
|
||||
}
|
||||
|
||||
val prefs = PrefsHelper.getPrefs(this)
|
||||
prefs.edit()
|
||||
.putString(PrefsHelper.PREF_UUID, appUuid)
|
||||
.putString(PrefsHelper.PREF_UUID, newUuid)
|
||||
.putString(PrefsHelper.PREF_KEY, appKey)
|
||||
.apply()
|
||||
return KeyResult.Success(appUuid, appKey)
|
||||
if (PrefsHelper.getDeviceMode(this) == "server") {
|
||||
PrefsHelper.setServerUuid(this, newUuid)
|
||||
} else {
|
||||
PrefsHelper.setClientUuid(this, newUuid)
|
||||
}
|
||||
return KeyResult.Success(newUuid, appKey)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Write keys error", e)
|
||||
return KeyResult.Error(R.string.err_generic, e.message)
|
||||
|
|
|
|||
|
|
@ -58,31 +58,6 @@
|
|||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="8dp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/flashMethodGroup"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:checkedButton="@id/methodBluetooth">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/methodBluetooth"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/method_bluetooth" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/methodUsb"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:text="@string/method_usb" />
|
||||
</RadioGroup>
|
||||
|
||||
<Button
|
||||
android:id="@+id/updateModuleButton"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/msg_overwrite_warning"
|
||||
android:textSize="14sp"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/confirmCheckBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/checkbox_confirm_overwrite" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -27,8 +27,6 @@
|
|||
|
||||
<!-- SettingsActivity -->
|
||||
<string name="title_settings">Instellingen</string>
|
||||
<string name="method_bluetooth">Bluetooth</string>
|
||||
<string name="method_usb">USB</string>
|
||||
<string name="button_update_module">Module bijwerken</string>
|
||||
<string name="usb_access_denied">USB-toegang geweigerd</string>
|
||||
<string name="key_read_success">Beveiligingssleutel gelezen</string>
|
||||
|
|
@ -36,16 +34,10 @@
|
|||
<string name="flash_failed">Flashen mislukt: %s</string>
|
||||
|
||||
<!-- Key conflict dialog -->
|
||||
<string name="title_key_conflict">Sleutels komen niet overeen</string>
|
||||
<string name="msg_key_conflict">De ESP (SylDa_Server) heeft andere sleutels dan deze app. Wat wil je doen?</string>
|
||||
<string name="button_keep_esp_keys">ESP sleutels overnemen</string>
|
||||
<string name="button_overwrite_esp">App sleutels overzetten</string>
|
||||
<string name="button_confirm">Bevestigen</string>
|
||||
<string name="title_key_conflict">Onbekende module</string>
|
||||
<string name="msg_pair_device">Deze module koppelen aan deze telefoon?</string>
|
||||
<string name="button_pair">Koppelen</string>
|
||||
|
||||
<!-- Overwrite confirmation dialog -->
|
||||
<string name="title_overwrite_confirm">ESP sleutels overschrijven?</string>
|
||||
<string name="msg_overwrite_warning">Dit vervangt de sleutels van de ESP. De ESP kan daarna niet meer met de oorspronkelijke app communiceren.</string>
|
||||
<string name="checkbox_confirm_overwrite">Ik weet het zeker</string>
|
||||
<string name="title_overwriting_keys">Sleutels overschrijven</string>
|
||||
<string name="msg_writing_keys">Sleutels schrijven naar ESP…</string>
|
||||
|
||||
|
|
@ -75,6 +67,8 @@
|
|||
<string name="err_set_id_write">SET_ID schrijven mislukt</string>
|
||||
<string name="err_key_not_ack">ESP bevestigde sleutel niet</string>
|
||||
<string name="err_uuid_not_ack">ESP bevestigde UUID niet</string>
|
||||
<string name="err_server_not_ack">ESP bevestigde server UUID niet</string>
|
||||
<string name="err_no_server_uuid">Server UUID niet beschikbaar — provisioneer eerst de server</string>
|
||||
|
||||
<!-- EspFlasher -->
|
||||
<string name="flash_connecting">Verbinden met ESP bootloader…</string>
|
||||
|
|
|
|||
87
scripts/ble_client.py
Normal file
87
scripts/ble_client.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
#!/usr/bin/env python3
|
||||
"""BLE client for SylDa_Server — send color commands from laptop."""
|
||||
|
||||
import asyncio
|
||||
import sys
|
||||
from bleak import BleakScanner, BleakClient
|
||||
|
||||
SERVICE_UUID = "0000abcd-0000-1000-8000-00805f9b34fb"
|
||||
CHAR_UUID = "0000abce-0000-1000-8000-00805f9b34fb"
|
||||
SERVER_NAME = "SylDa_Server"
|
||||
|
||||
COLORS = {
|
||||
"r": ("red", b"\x00"),
|
||||
"g": ("green", b"\x01"),
|
||||
"y": ("yellow", b"\x02"),
|
||||
"o": ("orange", b"\x03"),
|
||||
"b": ("blue", b"\x04"),
|
||||
}
|
||||
|
||||
|
||||
async def find_server():
|
||||
print(f"Scanning for {SERVER_NAME}...")
|
||||
device = await BleakScanner.find_device_by_name(SERVER_NAME, timeout=10)
|
||||
if not device:
|
||||
print("Not found.")
|
||||
return None
|
||||
print(f"Found: {device.name} ({device.address})")
|
||||
return device
|
||||
|
||||
|
||||
async def send_color(client, data):
|
||||
await client.write_gatt_char(CHAR_UUID, data, response=True)
|
||||
print(f"Sent: {COLORS[[k for k, v in COLORS.items() if v[1] == data][0]][0]}")
|
||||
|
||||
|
||||
def show_menu():
|
||||
print("\n┌─────────────────────┐")
|
||||
print("│ r = red │")
|
||||
print("│ g = green │")
|
||||
print("│ y = yellow │")
|
||||
print("│ o = orange │")
|
||||
print("│ b = blue │")
|
||||
print("│ q = quit │")
|
||||
print("└─────────────────────┘")
|
||||
|
||||
|
||||
async def connect_handler(client):
|
||||
print(f"Connected: {client.is_connected}")
|
||||
|
||||
|
||||
async def disconnect_handler(client):
|
||||
print("Disconnected from server.")
|
||||
|
||||
|
||||
async def main():
|
||||
device = await find_server()
|
||||
if not device:
|
||||
return
|
||||
|
||||
async with BleakClient(
|
||||
device,
|
||||
disconnected_callback=disconnect_handler,
|
||||
timeout=10,
|
||||
) as client:
|
||||
await connect_handler(client)
|
||||
|
||||
while True:
|
||||
try:
|
||||
show_menu()
|
||||
cmd = await asyncio.get_event_loop().run_in_executor(
|
||||
None, lambda: input("> ").strip().lower()
|
||||
)
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
break
|
||||
|
||||
if cmd == "q":
|
||||
break
|
||||
elif cmd in COLORS:
|
||||
await send_color(client, COLORS[cmd][1])
|
||||
else:
|
||||
print(f"Unknown: {cmd}")
|
||||
|
||||
print("Bye.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -223,12 +223,13 @@ void setup() {
|
|||
|
||||
if (allProvisioned) {
|
||||
NimBLEDevice::init("SylDa_Client");
|
||||
NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA);
|
||||
|
||||
NimBLEScan *pScan = NimBLEDevice::getScan();
|
||||
pScan->setScanCallbacks(new MyScanCallbacks());
|
||||
pScan->setInterval(97);
|
||||
pScan->setWindow(37);
|
||||
pScan->setActiveScan(false);
|
||||
pScan->setMaxResults(0);
|
||||
bleReady = true;
|
||||
Serial.println("BLE: initialized, will scan for SylDa_Server");
|
||||
}
|
||||
|
|
@ -244,19 +245,21 @@ void loop() {
|
|||
if (c == '\n') {
|
||||
serialBuf.trim();
|
||||
if (serialBuf == "KEY?") {
|
||||
if (keyProvisioned && uuidProvisioned) {
|
||||
Serial.print("DEVICE_ID:");
|
||||
for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]);
|
||||
Serial.println();
|
||||
Serial.print("HMAC_KEY:");
|
||||
if (keyProvisioned) {
|
||||
for (int i = 0; i < HMAC_KEY_LEN; i++) Serial.printf("%02X", hmacKey[i]);
|
||||
}
|
||||
Serial.println();
|
||||
Serial.print("SERVER_UUID:");
|
||||
if (serverProvisioned) {
|
||||
for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", serverUuid[i]);
|
||||
}
|
||||
Serial.println();
|
||||
} else {
|
||||
Serial.println("NO_KEY");
|
||||
}
|
||||
} else if (serialBuf == "VERSION?") {
|
||||
Serial.print("BUILD_NR:");
|
||||
Serial.println(FIRMWARE_VERSION_CLIENT);
|
||||
|
|
@ -274,11 +277,12 @@ void loop() {
|
|||
Serial.println("HMAC key stored in NVS");
|
||||
if (allProvisioned && !bleReady) {
|
||||
NimBLEDevice::init("SylDa_Client");
|
||||
NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA);
|
||||
NimBLEScan *pScan = NimBLEDevice::getScan();
|
||||
pScan->setScanCallbacks(new MyScanCallbacks());
|
||||
pScan->setInterval(97);
|
||||
pScan->setWindow(37);
|
||||
pScan->setActiveScan(false);
|
||||
pScan->setMaxResults(0);
|
||||
bleReady = true;
|
||||
Serial.println("BLE: initialized after provisioning");
|
||||
}
|
||||
|
|
@ -314,11 +318,12 @@ void loop() {
|
|||
Serial.println("Server UUID stored in NVS");
|
||||
if (allProvisioned && !bleReady) {
|
||||
NimBLEDevice::init("SylDa_Client");
|
||||
NimBLEDevice::setScanFilterMode(CONFIG_BTDM_SCAN_DUPL_TYPE_DATA);
|
||||
NimBLEScan *pScan = NimBLEDevice::getScan();
|
||||
pScan->setScanCallbacks(new MyScanCallbacks());
|
||||
pScan->setInterval(97);
|
||||
pScan->setWindow(37);
|
||||
pScan->setActiveScan(false);
|
||||
pScan->setMaxResults(0);
|
||||
bleReady = true;
|
||||
Serial.println("BLE: initialized after provisioning");
|
||||
}
|
||||
|
|
|
|||
39
src/main.cpp
39
src/main.cpp
|
|
@ -9,9 +9,10 @@
|
|||
#include <esp_ota_ops.h>
|
||||
#include <esp_partition.h>
|
||||
#include <nvs_flash.h>
|
||||
#include "version.h"
|
||||
|
||||
#define RELAY1_PIN 39
|
||||
#define RELAY2_PIN 38
|
||||
#define RELAY1_PIN 1
|
||||
#define RELAY2_PIN 2
|
||||
#define TOUCH1_PIN 8
|
||||
#define TOUCH2_PIN 6
|
||||
#define BUTTON_PIN 41
|
||||
|
|
@ -41,12 +42,11 @@ CRGB leds[NUM_LEDS];
|
|||
#define NVS_NAMESPACE "hmac"
|
||||
#define NVS_KEY_NAME "key"
|
||||
#define NVS_UUID_NAME "uuid"
|
||||
#define FIRMWARE_VERSION "6"
|
||||
#define FIRMWARE_VERSION FIRMWARE_VERSION_SERVER
|
||||
|
||||
NimBLECharacteristic *pCharacteristic;
|
||||
bool relay1State = false;
|
||||
bool relay2State = false;
|
||||
bool buttonToggle = false;
|
||||
|
||||
bool connActive[MAX_CLIENTS] = {false, false};
|
||||
bool bleRelay1[MAX_CLIENTS] = {false, false};
|
||||
|
|
@ -94,15 +94,14 @@ class MyServerCallbacks : public NimBLEServerCallbacks {
|
|||
bleRelay2[slot] = false;
|
||||
lastSeq[slot] = 0;
|
||||
connectedCount++;
|
||||
Serial.printf("BLE: device connected (conn=%d, slot=%d, clients=%d/%d)\n",
|
||||
handle, slot, connectedCount, MAX_CLIENTS);
|
||||
Serial.printf("BLE: device connected (conn=%d, slot=%d, clients=%d/%d, mac=%s)\n",
|
||||
handle, slot, connectedCount, MAX_CLIENTS,
|
||||
connInfo.getAddress().toString().c_str());
|
||||
} else {
|
||||
Serial.printf("BLE: device connected but no free slot (conn=%d, clients=%d/%d)\n",
|
||||
handle, connectedCount, MAX_CLIENTS);
|
||||
}
|
||||
if (connectedCount < MAX_CLIENTS) {
|
||||
NimBLEDevice::startAdvertising();
|
||||
}
|
||||
pServer->startAdvertising();
|
||||
}
|
||||
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override {
|
||||
uint16_t handle = connInfo.getConnHandle();
|
||||
|
|
@ -114,14 +113,12 @@ class MyServerCallbacks : public NimBLEServerCallbacks {
|
|||
lastSeq[i] = 0;
|
||||
connHandles[i] = 0xFFFF;
|
||||
connectedCount--;
|
||||
Serial.printf("BLE: device disconnected (conn=%d, slot=%d, clients=%d/%d)\n",
|
||||
handle, i, connectedCount, MAX_CLIENTS);
|
||||
Serial.printf("BLE: device disconnected (conn=%d, slot=%d, clients=%d/%d, mac=%s)\n",
|
||||
handle, i, connectedCount, MAX_CLIENTS,
|
||||
connInfo.getAddress().toString().c_str());
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (connectedCount < MAX_CLIENTS) {
|
||||
NimBLEDevice::startAdvertising();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -322,8 +319,9 @@ class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
|||
|
||||
std::string value = pChar->getValue();
|
||||
if (value.length() != HMAC_PKT_SIZE) {
|
||||
Serial.printf("BLE: bad packet length %d (expected %d)\n",
|
||||
(int)value.length(), HMAC_PKT_SIZE);
|
||||
Serial.printf("BLE: bad packet length %d (expected %d), slot=%d mac=%s\n",
|
||||
(int)value.length(), HMAC_PKT_SIZE, idx,
|
||||
connInfo.getAddress().toString().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -540,15 +538,8 @@ void loop() {
|
|||
bool t2Active = (millis() - t2Change > DEBOUNCE_MS) && t2Stable;
|
||||
bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable;
|
||||
|
||||
static bool prevBtnActive = false;
|
||||
if (btnActive && !prevBtnActive) {
|
||||
buttonToggle = !buttonToggle;
|
||||
Serial.printf("Button: toggle -> %s\n", buttonToggle ? "ON" : "OFF");
|
||||
}
|
||||
prevBtnActive = btnActive;
|
||||
|
||||
if (!otaInProgress) {
|
||||
relay1State = t1Active || (buttonToggle && digitalRead(BUTTON_PIN) != LOW) ||
|
||||
relay1State = t1Active || btnActive ||
|
||||
(connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]);
|
||||
relay2State = t2Active ||
|
||||
(connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]);
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
server=6
|
||||
server=8
|
||||
client=1
|
||||
|
|
|
|||
Loading…
Reference in a new issue