Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a84727abdc | ||
|
|
45f8f38e81 | ||
|
|
92c79ac93f | ||
|
|
ccfd7cb693 | ||
|
|
793c8f190c | ||
|
|
bce5fc4107 | ||
|
|
97c79b9f4c | ||
|
|
1e47fc3de7 | ||
|
|
a371e29058 |
28 changed files with 2295 additions and 166 deletions
18
.gitignore
vendored
18
.gitignore
vendored
|
|
@ -1,3 +1,7 @@
|
||||||
|
# Generated from version.txt
|
||||||
|
src/version.h
|
||||||
|
android/app/src/main/java/nl/eddydevink/sylda/SylDaVersions.kt
|
||||||
|
|
||||||
# PlatformIO
|
# PlatformIO
|
||||||
.pio/
|
.pio/
|
||||||
.pioenvs/
|
.pioenvs/
|
||||||
|
|
@ -43,4 +47,18 @@ codereview.md
|
||||||
DC3-24V*.md
|
DC3-24V*.md
|
||||||
DC3-24V*.pdf
|
DC3-24V*.pdf
|
||||||
esplibalternatief.md
|
esplibalternatief.md
|
||||||
|
docs/handleiding.md
|
||||||
release/
|
release/
|
||||||
|
releases/
|
||||||
|
|
||||||
|
# Test/debug firmware files
|
||||||
|
src/blink.cpp
|
||||||
|
src/c1.cpp
|
||||||
|
src/get_mac.cpp
|
||||||
|
src/min_cli.cpp
|
||||||
|
src/min_srv.cpp
|
||||||
|
src/s1.cpp
|
||||||
|
src/test_*.cpp
|
||||||
|
|
||||||
|
# Python cache
|
||||||
|
scripts/__pycache__/
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ def hasSigningConfig = signingPropsFile.exists()
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace 'nl.eddydevink.sylda'
|
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
|
compileSdk 34
|
||||||
|
|
||||||
if (hasSigningConfig) {
|
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 {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.12.0'
|
implementation 'androidx.core:core-ktx:1.12.0'
|
||||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
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.BluetoothAdapter
|
||||||
import android.bluetooth.BluetoothGatt
|
import android.bluetooth.BluetoothGatt
|
||||||
import android.bluetooth.BluetoothGattCharacteristic
|
import android.bluetooth.BluetoothGattCharacteristic
|
||||||
|
import android.bluetooth.BluetoothGattDescriptor
|
||||||
import android.bluetooth.BluetoothProfile
|
import android.bluetooth.BluetoothProfile
|
||||||
import android.bluetooth.le.ScanCallback
|
import android.bluetooth.le.ScanCallback
|
||||||
import android.bluetooth.le.ScanFilter
|
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 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 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_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 SCAN_TIMEOUT_MS = 30000L
|
||||||
private const val RECONNECT_DELAY_MS = 2000L
|
private const val RECONNECT_DELAY_MS = 2000L
|
||||||
|
|
@ -206,7 +208,18 @@ class BleRelayManager(
|
||||||
fun enableOtaNotifications(): Boolean {
|
fun enableOtaNotifications(): Boolean {
|
||||||
val char = otaCharacteristic ?: return false
|
val char = otaCharacteristic ?: return false
|
||||||
val gatt = bluetoothGatt ?: 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 {
|
fun hasOtaCharacteristic(): Boolean {
|
||||||
|
|
@ -259,6 +272,7 @@ class BleRelayManager(
|
||||||
bluetoothGatt = null
|
bluetoothGatt = null
|
||||||
relayCharacteristic = null
|
relayCharacteristic = null
|
||||||
seq = 1
|
seq = 1
|
||||||
|
triedAddresses.clear()
|
||||||
handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS)
|
handler.postDelayed(scanRunnable, RECONNECT_DELAY_MS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +383,8 @@ class BleRelayManager(
|
||||||
val prefs = PrefsHelper.getPrefs(context)
|
val prefs = PrefsHelper.getPrefs(context)
|
||||||
val hexKey = prefs.getString(PrefsHelper.PREF_KEY, null)
|
val hexKey = prefs.getString(PrefsHelper.PREF_KEY, null)
|
||||||
hmacKey = hexKey?.let { hexStringToBytes(it) }
|
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) }
|
deviceUuid = hexUuid?.let { hexStringToBytes(it) }
|
||||||
seq = prefs.getInt(PrefsHelper.PREF_SEQ, 1).coerceAtMost(PrefsHelper.SEQ_MAX)
|
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")
|
Log.d(TAG, "HMAC key loaded: ${hmacKey?.size ?: "none"} bytes, UUID: ${deviceUuid?.size ?: "none"} bytes, seq=$seq")
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,11 @@ class EspFlasher(private val context: Context) {
|
||||||
fun onProgress(percent: Int, message: String)
|
fun onProgress(percent: Int, message: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun assetPath(baseName: String): String {
|
||||||
|
val prefix = if (PrefsHelper.getDeviceMode(context) == "client") "client/" else "server/"
|
||||||
|
return "firmware/$prefix$baseName"
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
|
suspend fun flashDevice(usbManager: UsbManager, device: UsbDevice, callback: FlashCallback) {
|
||||||
try {
|
try {
|
||||||
withContext(kotlinx.coroutines.Dispatchers.IO) {
|
withContext(kotlinx.coroutines.Dispatchers.IO) {
|
||||||
|
|
@ -77,10 +82,10 @@ class EspFlasher(private val context: Context) {
|
||||||
coroutineContext.ensureActive()
|
coroutineContext.ensureActive()
|
||||||
|
|
||||||
val assets = context.assets
|
val assets = context.assets
|
||||||
val bootloader = assets.open("firmware/bootloader.bin").readBytes()
|
val bootloader = assets.open(assetPath("bootloader.bin")).readBytes()
|
||||||
val partitions = assets.open("firmware/partitions.bin").readBytes()
|
val partitions = assets.open(assetPath("partitions.bin")).readBytes()
|
||||||
val bootApp0 = assets.open("firmware/boot_app0.bin").readBytes()
|
val bootApp0 = assets.open(assetPath("boot_app0.bin")).readBytes()
|
||||||
val firmware = assets.open("firmware/firmware.bin").readBytes()
|
val firmware = assets.open(assetPath("firmware.bin")).readBytes()
|
||||||
|
|
||||||
val steps = listOf(
|
val steps = listOf(
|
||||||
FlashStep(R.string.flash_step_bootloader, 0x0, bootloader, 30, 10, false),
|
FlashStep(R.string.flash_step_bootloader, 0x0, bootloader, 30, 10, false),
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
private const val REQUEST_SETTINGS = 1
|
private const val REQUEST_SETTINGS = 1
|
||||||
private const val POST_SETTINGS_RESCAN_MS = 500L
|
private const val POST_SETTINGS_RESCAN_MS = 500L
|
||||||
private const val ON_RESUME_SCAN_DELAY_MS = 300L
|
private const val ON_RESUME_SCAN_DELAY_MS = 300L
|
||||||
private const val BUNDLED_FIRMWARE_VERSION = "6"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var manager: BleRelayManager
|
private lateinit var manager: BleRelayManager
|
||||||
|
|
@ -41,8 +40,9 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
statusText.text = getString(R.string.status_connected)
|
statusText.text = getString(R.string.status_connected)
|
||||||
setupRelayButton()
|
setupRelayButton()
|
||||||
setupRelay2Button()
|
setupRelay2Button()
|
||||||
relay1Button.isEnabled = true
|
val isServer = PrefsHelper.getDeviceMode(this@MainActivity) == "server"
|
||||||
relay2Button.isEnabled = true
|
relay1Button.isEnabled = isServer
|
||||||
|
relay2Button.isEnabled = isServer
|
||||||
}
|
}
|
||||||
BleStatus.Disconnected -> {
|
BleStatus.Disconnected -> {
|
||||||
statusText.text = getString(R.string.status_connecting)
|
statusText.text = getString(R.string.status_connecting)
|
||||||
|
|
@ -92,7 +92,7 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
manager = BleRelayManager(this, this)
|
manager = BleRelayManager(this, this)
|
||||||
manager.setAppVersion(BUNDLED_FIRMWARE_VERSION)
|
manager.setAppVersion(SylDaVersions.SERVER)
|
||||||
|
|
||||||
statusText = findViewById(R.id.statusText)
|
statusText = findViewById(R.id.statusText)
|
||||||
relay1Button = findViewById(R.id.relay1Button)
|
relay1Button = findViewById(R.id.relay1Button)
|
||||||
|
|
@ -107,6 +107,9 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
}
|
}
|
||||||
setupRelayButton()
|
setupRelayButton()
|
||||||
setupRelay2Button()
|
setupRelay2Button()
|
||||||
|
val isServer = PrefsHelper.getDeviceMode(this) == "server"
|
||||||
|
relay1Button.isEnabled = isServer
|
||||||
|
relay2Button.isEnabled = isServer
|
||||||
}
|
}
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
|
|
@ -227,11 +230,9 @@ class MainActivity : AppCompatActivity(), BleCallbacks {
|
||||||
override fun onFirmwareVersion(espVersion: String, appVersion: String) {
|
override fun onFirmwareVersion(espVersion: String, appVersion: String) {
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
if (isFinishing || isDestroyed || !lifecycle.currentState.isAtLeast(androidx.lifecycle.Lifecycle.State.RESUMED)) return@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()
|
Toast.makeText(this, getString(R.string.ota_update_available), Toast.LENGTH_SHORT).show()
|
||||||
startActivityForResult(Intent(this, SettingsActivity::class.java), REQUEST_SETTINGS)
|
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)
|
return minOf(maxOf(mtu - 3 - 4, 20), OTA_ESP_MAX_CHUNK)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadFirmware(): Boolean {
|
fun loadFirmware(assetPath: String = "firmware/firmware.bin"): Boolean {
|
||||||
return try {
|
return try {
|
||||||
firmwareData = context.assets.open("firmware/firmware.bin").readBytes()
|
firmwareData = context.assets.open(assetPath).readBytes()
|
||||||
chunkDataSize = calculateChunkSize(relayManager.getNegotiatedMtu())
|
chunkDataSize = calculateChunkSize(relayManager.getNegotiatedMtu())
|
||||||
|
Log.d(TAG, "Loaded firmware from $assetPath: ${firmwareData!!.size} bytes, chunkDataSize=$chunkDataSize")
|
||||||
true
|
true
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to load firmware", e)
|
Log.e(TAG, "Failed to load firmware from $assetPath", e)
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +81,21 @@ class OtaManager(
|
||||||
val startPacket = ByteBuffer.allocate(5).order(ByteOrder.BIG_ENDIAN)
|
val startPacket = ByteBuffer.allocate(5).order(ByteOrder.BIG_ENDIAN)
|
||||||
startPacket.put(OTA_CMD_START)
|
startPacket.put(OTA_CMD_START)
|
||||||
startPacket.putInt(fw.size)
|
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 {
|
try {
|
||||||
withTimeout(OTA_START_TIMEOUT_MS) {
|
withTimeout(OTA_START_TIMEOUT_MS) {
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,20 @@ object PrefsHelper {
|
||||||
const val PREF_THEME = "theme"
|
const val PREF_THEME = "theme"
|
||||||
const val PREF_LABEL1 = "label_relay1"
|
const val PREF_LABEL1 = "label_relay1"
|
||||||
const val PREF_LABEL2 = "label_relay2"
|
const val PREF_LABEL2 = "label_relay2"
|
||||||
|
const val KEY_DEVICE_MODE = "deviceMode"
|
||||||
|
const val KEY_SERVER_UUID = "serverUuid"
|
||||||
|
const val KEY_CLIENT_UUID = "clientUuid"
|
||||||
const val SEQ_MAX = 1_000_000
|
const val SEQ_MAX = 1_000_000
|
||||||
|
|
||||||
|
fun getDeviceMode(context: Context): String = getPrefs(context).getString(KEY_DEVICE_MODE, "server") ?: "server"
|
||||||
|
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) {
|
fun applyTheme(context: Context) {
|
||||||
val prefs = getPrefs(context)
|
val prefs = getPrefs(context)
|
||||||
val mode = when (prefs.getString(PREF_THEME, "system")) {
|
val mode = when (prefs.getString(PREF_THEME, "system")) {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ import android.util.Log
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputMethodManager
|
import android.view.inputmethod.InputMethodManager
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.CheckBox
|
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
import android.widget.RadioGroup
|
import android.widget.RadioGroup
|
||||||
|
|
@ -32,6 +31,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
import java.security.SecureRandom
|
import java.security.SecureRandom
|
||||||
|
|
@ -64,7 +64,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
private const val REBOOT_WAIT_MS = 5000L
|
private const val REBOOT_WAIT_MS = 5000L
|
||||||
private const val REBOOT_POLL_MS = 500L
|
private const val REBOOT_POLL_MS = 500L
|
||||||
private const val REBOOT_MAX_WAIT_MS = 15000L
|
private const val REBOOT_MAX_WAIT_MS = 15000L
|
||||||
private const val EXPECTED_FIRMWARE_VERSION = "6"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum class UsbAction { NONE, EXCHANGE_KEY, FLASH }
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
lastTheme = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
|
lastTheme = PrefsHelper.getPrefs(this).getString(PrefsHelper.PREF_THEME, "system") ?: "system"
|
||||||
PrefsHelper.applyTheme(this)
|
PrefsHelper.applyTheme(this)
|
||||||
|
|
@ -140,8 +143,29 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.title = getString(R.string.title_settings)
|
supportActionBar?.title = getString(R.string.title_settings)
|
||||||
|
|
||||||
|
val radioDeviceMode = findViewById<RadioGroup>(R.id.radioDeviceMode)
|
||||||
|
val radioTransport = findViewById<RadioGroup>(R.id.radioTransport)
|
||||||
|
|
||||||
|
radioDeviceMode.check(
|
||||||
|
if (PrefsHelper.getDeviceMode(this) == "client") R.id.radioClient else R.id.radioServer
|
||||||
|
)
|
||||||
|
radioTransport.check(
|
||||||
|
if (PrefsHelper.getTransportMode(this) == "usb") R.id.radioUsb else R.id.radioBle
|
||||||
|
)
|
||||||
|
|
||||||
|
radioDeviceMode.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
val mode = if (checkedId == R.id.radioClient) "client" else "server"
|
||||||
|
PrefsHelper.setDeviceMode(this, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
radioTransport.setOnCheckedChangeListener { _, checkedId ->
|
||||||
|
val mode = if (checkedId == R.id.radioUsb) "usb" else "ble"
|
||||||
|
PrefsHelper.setTransportMode(this, mode)
|
||||||
|
}
|
||||||
|
|
||||||
findViewById<Button>(R.id.updateModuleButton).setOnClickListener {
|
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()
|
startFlashing()
|
||||||
} else {
|
} else {
|
||||||
startOtaUpdate()
|
startOtaUpdate()
|
||||||
|
|
@ -222,7 +246,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val espVersion = manager.getEspVersion()
|
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()
|
Toast.makeText(this, getString(R.string.ota_up_to_date), Toast.LENGTH_SHORT).show()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -231,7 +255,9 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
otaManager = OtaManager(this, manager)
|
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?.dismiss()
|
||||||
progressDialog = null
|
progressDialog = null
|
||||||
Toast.makeText(this, getString(R.string.ota_load_failed), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.ota_load_failed), Toast.LENGTH_SHORT).show()
|
||||||
|
|
@ -241,6 +267,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
manager.enableOtaNotifications()
|
manager.enableOtaNotifications()
|
||||||
|
|
||||||
otaJob = lifecycleScope.launch {
|
otaJob = lifecycleScope.launch {
|
||||||
|
delay(300)
|
||||||
val result = otaManager!!.performOta(object : OtaManager.OtaCallback {
|
val result = otaManager!!.performOta(object : OtaManager.OtaCallback {
|
||||||
override fun onProgress(percent: Int, message: String) {
|
override fun onProgress(percent: Int, message: String) {
|
||||||
lifecycleScope.launch(Dispatchers.Main) {
|
lifecycleScope.launch(Dispatchers.Main) {
|
||||||
|
|
@ -361,6 +388,11 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
.putString(PrefsHelper.PREF_UUID, result.deviceUuid)
|
.putString(PrefsHelper.PREF_UUID, result.deviceUuid)
|
||||||
.putString(PrefsHelper.PREF_KEY, result.hexKey)
|
.putString(PrefsHelper.PREF_KEY, result.hexKey)
|
||||||
.apply()
|
.apply()
|
||||||
|
if (PrefsHelper.getDeviceMode(this@SettingsActivity) == "server") {
|
||||||
|
PrefsHelper.setServerUuid(this@SettingsActivity, result.deviceUuid)
|
||||||
|
} else {
|
||||||
|
PrefsHelper.setClientUuid(this@SettingsActivity, result.deviceUuid)
|
||||||
|
}
|
||||||
cleanupAction()
|
cleanupAction()
|
||||||
setResult(RESULT_SCAN)
|
setResult(RESULT_SCAN)
|
||||||
finish()
|
finish()
|
||||||
|
|
@ -409,7 +441,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val version = readEspVersion(usbManager, device)
|
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")
|
Log.d(TAG, "Firmware version $version matches, up to date")
|
||||||
progressDialog?.dismiss()
|
progressDialog?.dismiss()
|
||||||
progressDialog = null
|
progressDialog = null
|
||||||
|
|
@ -623,8 +655,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
hexUuid.length == UUID_HEX_LEN && hexUuid.all { it in "0123456789ABCDEFabcdef" }) {
|
hexUuid.length == UUID_HEX_LEN && hexUuid.all { it in "0123456789ABCDEFabcdef" }) {
|
||||||
val espKeyUpper = hexKey.uppercase()
|
val espKeyUpper = hexKey.uppercase()
|
||||||
val espUuidUpper = hexUuid.uppercase()
|
val espUuidUpper = hexUuid.uppercase()
|
||||||
if (appUuid != null && appKey != null &&
|
if (appUuid != espUuidUpper || appKey != espKeyUpper) {
|
||||||
(appUuid != espUuidUpper || appKey != espKeyUpper)) {
|
|
||||||
return KeyResult.Conflict(espUuidUpper, espKeyUpper)
|
return KeyResult.Conflict(espUuidUpper, espKeyUpper)
|
||||||
}
|
}
|
||||||
return KeyResult.Success(espUuidUpper, espKeyUpper)
|
return KeyResult.Success(espUuidUpper, espKeyUpper)
|
||||||
|
|
@ -634,10 +665,14 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
} else if (noKey) {
|
} else if (noKey) {
|
||||||
val newKey: String
|
val newKey: String
|
||||||
val newUuid: String
|
val newUuid: String
|
||||||
if (appUuid != null && appKey != null) {
|
if (appKey != null) {
|
||||||
Log.d(TAG, "ESP has no keys, using existing app keys")
|
Log.d(TAG, "ESP has no keys, using existing HMAC key")
|
||||||
newKey = appKey
|
newKey = appKey
|
||||||
newUuid = appUuid
|
newUuid = if (PrefsHelper.getDeviceMode(this) == "server") {
|
||||||
|
generateRandomHex(UUID_BYTE_COUNT)
|
||||||
|
} else {
|
||||||
|
generateRandomHex(UUID_BYTE_COUNT)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "Both have no keys, generating new pair")
|
Log.d(TAG, "Both have no keys, generating new pair")
|
||||||
newKey = generateRandomHexKey()
|
newKey = generateRandomHexKey()
|
||||||
|
|
@ -671,6 +706,21 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
Log.d(TAG, "SET_ID response: ${okUuidResponse.take(200)}")
|
Log.d(TAG, "SET_ID response: ${okUuidResponse.take(200)}")
|
||||||
|
|
||||||
if (okUuidResponse.contains("OK")) {
|
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)
|
return KeyResult.Success(newUuid, newKey)
|
||||||
} else {
|
} else {
|
||||||
return KeyResult.Error(R.string.err_uuid_not_ack)
|
return KeyResult.Error(R.string.err_uuid_not_ack)
|
||||||
|
|
@ -767,52 +817,23 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
private fun showConflictDialog(usbManager: UsbManager, device: UsbDevice,
|
private fun showConflictDialog(usbManager: UsbManager, device: UsbDevice,
|
||||||
espUuid: String, espKey: String, appKey: String) {
|
espUuid: String, espKey: String, appKey: String) {
|
||||||
val prefs = PrefsHelper.getPrefs(this)
|
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)
|
AlertDialog.Builder(this)
|
||||||
.setTitle(getString(R.string.title_key_conflict))
|
.setTitle(getString(R.string.title_key_conflict))
|
||||||
.setMessage(getString(R.string.msg_key_conflict))
|
.setMessage(getString(R.string.msg_pair_device))
|
||||||
.setPositiveButton(getString(R.string.button_keep_esp_keys)) { _, _ ->
|
.setPositiveButton(getString(R.string.button_pair)) { _, _ ->
|
||||||
prefs.edit()
|
doOverwriteEspKeys(usbManager, device, newAppKey, newAppUuid)
|
||||||
.putString(PrefsHelper.PREF_UUID, espUuid)
|
|
||||||
.putString(PrefsHelper.PREF_KEY, espKey)
|
|
||||||
.apply()
|
|
||||||
cleanupAction()
|
|
||||||
setResult(RESULT_SCAN)
|
|
||||||
finish()
|
|
||||||
}
|
}
|
||||||
.setNegativeButton(getString(R.string.button_overwrite_esp)) { _, _ ->
|
.setNegativeButton(getString(R.string.button_cancel)) { _, _ ->
|
||||||
showOverwriteConfirmDialog(usbManager, device, espUuid, espKey, appKey, appUuid)
|
|
||||||
}
|
|
||||||
.setNeutralButton(getString(R.string.button_cancel)) { _, _ ->
|
|
||||||
cleanupAction()
|
cleanupAction()
|
||||||
}
|
}
|
||||||
.setCancelable(false)
|
.setCancelable(false)
|
||||||
.show()
|
.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,
|
private fun doOverwriteEspKeys(usbManager: UsbManager, device: UsbDevice,
|
||||||
appKey: String, appUuid: String) {
|
appKey: String, appUuid: String) {
|
||||||
showProgressDialog(getString(R.string.title_overwriting_keys), getString(R.string.msg_writing_keys), STYLE_SPINNER)
|
showProgressDialog(getString(R.string.title_overwriting_keys), getString(R.string.msg_writing_keys), STYLE_SPINNER)
|
||||||
|
|
@ -866,6 +887,12 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
Thread.sleep(KEY_SETTLE_MS)
|
Thread.sleep(KEY_SETTLE_MS)
|
||||||
flushReadBuffer(connection, epIn)
|
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 setKeyCmd = "SET_KEY:$appKey\n".toByteArray(StandardCharsets.US_ASCII)
|
||||||
val w = connection.bulkTransfer(epOut, setKeyCmd, setKeyCmd.size, USB_BULK_TIMEOUT_MS)
|
val w = connection.bulkTransfer(epOut, setKeyCmd, setKeyCmd.size, USB_BULK_TIMEOUT_MS)
|
||||||
if (w < 0) return KeyResult.Error(R.string.err_set_key_write)
|
if (w < 0) return KeyResult.Error(R.string.err_set_key_write)
|
||||||
|
|
@ -875,7 +902,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
val okKeyResponse = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
val okKeyResponse = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
||||||
if (!okKeyResponse.contains("OK")) return KeyResult.Error(R.string.err_key_not_ack)
|
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)
|
val w2 = connection.bulkTransfer(epOut, setIdCmd, setIdCmd.size, USB_BULK_TIMEOUT_MS)
|
||||||
if (w2 < 0) return KeyResult.Error(R.string.err_set_id_write)
|
if (w2 < 0) return KeyResult.Error(R.string.err_set_id_write)
|
||||||
|
|
||||||
|
|
@ -883,12 +910,31 @@ class SettingsActivity : AppCompatActivity() {
|
||||||
val okUuidResponse = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
val okUuidResponse = readResponse(connection, epIn, SET_KEY_TIMEOUT_MS)
|
||||||
if (!okUuidResponse.contains("OK")) return KeyResult.Error(R.string.err_uuid_not_ack)
|
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)
|
val prefs = PrefsHelper.getPrefs(this)
|
||||||
prefs.edit()
|
prefs.edit()
|
||||||
.putString(PrefsHelper.PREF_UUID, appUuid)
|
.putString(PrefsHelper.PREF_UUID, newUuid)
|
||||||
.putString(PrefsHelper.PREF_KEY, appKey)
|
.putString(PrefsHelper.PREF_KEY, appKey)
|
||||||
.apply()
|
.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) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Write keys error", e)
|
Log.e(TAG, "Write keys error", e)
|
||||||
return KeyResult.Error(R.string.err_generic, e.message)
|
return KeyResult.Error(R.string.err_generic, e.message)
|
||||||
|
|
|
||||||
|
|
@ -6,30 +6,58 @@
|
||||||
android:padding="24dp">
|
android:padding="24dp">
|
||||||
|
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
android:id="@+id/flashMethodGroup"
|
android:id="@+id/radioDeviceMode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_marginBottom="8dp"
|
android:layout_marginTop="12dp"
|
||||||
android:checkedButton="@id/methodBluetooth">
|
android:checkedButton="@+id/radioServer">
|
||||||
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
android:id="@+id/methodBluetooth"
|
android:id="@+id/radioServer"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center"
|
android:text="Server" />
|
||||||
android:text="@string/method_bluetooth" />
|
|
||||||
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
android:id="@+id/methodUsb"
|
android:id="@+id/radioClient"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:gravity="center"
|
android:text="Client" />
|
||||||
android:text="@string/method_usb" />
|
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
|
|
||||||
|
<RadioGroup
|
||||||
|
android:id="@+id/radioTransport"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:checkedButton="@+id/radioBle">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioBle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="BLE" />
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/radioUsb"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="USB" />
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="?android:attr/listDivider"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
android:layout_marginBottom="8dp" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/updateModuleButton"
|
android:id="@+id/updateModuleButton"
|
||||||
android:layout_width="match_parent"
|
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 -->
|
<!-- SettingsActivity -->
|
||||||
<string name="title_settings">Instellingen</string>
|
<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="button_update_module">Module bijwerken</string>
|
||||||
<string name="usb_access_denied">USB-toegang geweigerd</string>
|
<string name="usb_access_denied">USB-toegang geweigerd</string>
|
||||||
<string name="key_read_success">Beveiligingssleutel gelezen</string>
|
<string name="key_read_success">Beveiligingssleutel gelezen</string>
|
||||||
|
|
@ -36,16 +34,10 @@
|
||||||
<string name="flash_failed">Flashen mislukt: %s</string>
|
<string name="flash_failed">Flashen mislukt: %s</string>
|
||||||
|
|
||||||
<!-- Key conflict dialog -->
|
<!-- Key conflict dialog -->
|
||||||
<string name="title_key_conflict">Sleutels komen niet overeen</string>
|
<string name="title_key_conflict">Onbekende module</string>
|
||||||
<string name="msg_key_conflict">De ESP (SylDa_Server) heeft andere sleutels dan deze app. Wat wil je doen?</string>
|
<string name="msg_pair_device">Deze module koppelen aan deze telefoon?</string>
|
||||||
<string name="button_keep_esp_keys">ESP sleutels overnemen</string>
|
<string name="button_pair">Koppelen</string>
|
||||||
<string name="button_overwrite_esp">App sleutels overzetten</string>
|
|
||||||
<string name="button_confirm">Bevestigen</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="title_overwriting_keys">Sleutels overschrijven</string>
|
||||||
<string name="msg_writing_keys">Sleutels schrijven naar ESP…</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_set_id_write">SET_ID schrijven mislukt</string>
|
||||||
<string name="err_key_not_ack">ESP bevestigde sleutel niet</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_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 -->
|
<!-- EspFlasher -->
|
||||||
<string name="flash_connecting">Verbinden met ESP bootloader…</string>
|
<string name="flash_connecting">Verbinden met ESP bootloader…</string>
|
||||||
|
|
|
||||||
1075
docs/superpowers/plans/2025-07-31-sylda-nimble-hmac-plan.md
Normal file
1075
docs/superpowers/plans/2025-07-31-sylda-nimble-hmac-plan.md
Normal file
File diff suppressed because it is too large
Load diff
269
docs/superpowers/specs/2025-07-31-sylda-nimble-hmac-design.md
Normal file
269
docs/superpowers/specs/2025-07-31-sylda-nimble-hmac-design.md
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
# SylDa NimBLE + HMAC — Design Spec
|
||||||
|
|
||||||
|
Datum: 2025-07-31
|
||||||
|
Branch: nieuw, van `main`
|
||||||
|
|
||||||
|
## Doel
|
||||||
|
|
||||||
|
Stabiele basis: 1× Atom S3 Lite als SylDa_Server + 1× Atom S3 Lite als Sylda_Client + Android app, met NimBLE, UUID identificatie, HMAC-SHA256 authenticatie. Later refactoren naar AES-GCM.
|
||||||
|
|
||||||
|
## Architectuur
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ Android App │
|
||||||
|
│ USB CDC: flash + provision (server & client) │
|
||||||
|
│ BLE: relay knoppen → HMAC → server │
|
||||||
|
│ OTA firmware update → server & client │
|
||||||
|
│ Radio buttons: [Server | Client] × [BLE|USB] │
|
||||||
|
└────────────────┬──────────────────────────────┘
|
||||||
|
│ BLE (client 2) │ USB CDC
|
||||||
|
▼ ▼
|
||||||
|
┌─────────────────────────┐ ┌─────────────────────────┐
|
||||||
|
│ SylDa_Server │ │ Sylda_Client │
|
||||||
|
│ AtomS3 Lite │ │ AtomS3 Lite │
|
||||||
|
│ │◄─┤ │
|
||||||
|
│ Relais G1, G2 (ext) │ │ Touch1 G8, Touch2 G6 │
|
||||||
|
│ Touch1 G8, Touch2 G6 │ │ Button G41 │
|
||||||
|
│ Button G41 │ │ Relay-state → BLE HMAC │
|
||||||
|
│ OR-logica │ │ OTA (BLE+USB) │
|
||||||
|
│ OTA (BLE+USB) │ │ │
|
||||||
|
│ LED G35 (NeoPixel) │ │ LED G35 (NeoPixel) │
|
||||||
|
└─────────────────────────┘ └─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Provisioning flow
|
||||||
|
|
||||||
|
1. Android app flasht firmware via USB CDC (EspFlasher)
|
||||||
|
2. Android leest server UUID via BLE READ
|
||||||
|
3. Android stuurt `SET_KEY`, `SET_ID`, `SET_SERVER` naar client via USB CDC
|
||||||
|
4. Server genereert eigen UUID bij eerste boot (NVS persistent)
|
||||||
|
5. Gedeelde HMAC key (32 bytes) = enige authenticatie voor beide clients
|
||||||
|
6. Server UUID in client NVS = verificatie bij connect (geen spoofing)
|
||||||
|
7. UUIDs + HMAC key overleven NVS bij firmware updates (partities 0x9000-0xDFFF)
|
||||||
|
|
||||||
|
## Hardware
|
||||||
|
|
||||||
|
Beide devices: AtomS3 Lite (ESP32-S3), qio_qspi flash.
|
||||||
|
|
||||||
|
### Server
|
||||||
|
|
||||||
|
| Pin | Functie | Type |
|
||||||
|
|-----|---------|------|
|
||||||
|
| G39 | RELAY1 | OUTPUT (externe relais module) |
|
||||||
|
| G38 | RELAY2 | OUTPUT (externe relais module) |
|
||||||
|
| G8 | TOUCH1 | INPUT_PULLUP |
|
||||||
|
| G6 | TOUCH2 | INPUT_PULLUP |
|
||||||
|
| G41 | BUTTON | INPUT (active low) |
|
||||||
|
| G35 | LED | FastLED NeoPixel |
|
||||||
|
|
||||||
|
### Client
|
||||||
|
|
||||||
|
| Pin | Functie | Type |
|
||||||
|
|-----|---------|------|
|
||||||
|
| G8 | TOUCH1 | INPUT_PULLUP |
|
||||||
|
| G6 | TOUCH2 | INPUT_PULLUP |
|
||||||
|
| G41 | BUTTON | INPUT (active low) |
|
||||||
|
| G35 | LED | FastLED NeoPixel |
|
||||||
|
|
||||||
|
## Server firmware (`src/main.cpp`)
|
||||||
|
|
||||||
|
Basis: `main` branch. NimBLE, UUID, HMAC, relays, touch, button, OTA, NVS.
|
||||||
|
|
||||||
|
### Connection tracking
|
||||||
|
|
||||||
|
Max 2 verbindingen: client1 (AtomS3 Lite) + client2 (Android app).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool bleRelay1[2] = {false, false};
|
||||||
|
bool bleRelay2[2] = {false, false};
|
||||||
|
bool connActive[2] = {false, false};
|
||||||
|
int connectedCount = 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Server callbacks
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
onConnect(pServer, connInfo):
|
||||||
|
connActive[cid] = true;
|
||||||
|
connectedCount++;
|
||||||
|
if (connectedCount < 2) NimBLEDevice::startAdvertising();
|
||||||
|
|
||||||
|
onDisconnect(pServer, connInfo, reason):
|
||||||
|
connActive[cid] = false;
|
||||||
|
bleRelay1[cid] = false;
|
||||||
|
bleRelay2[cid] = false;
|
||||||
|
connectedCount--;
|
||||||
|
NimBLEDevice::startAdvertising();
|
||||||
|
```
|
||||||
|
|
||||||
|
CID mapping: via `NimBLEConnInfo::getConnHandle()` in callbacks. ConnHandle → index 0/1 in arrays. Eerste connect = index 0, tweede = index 1. Disconnect wist de entry en reset die index.
|
||||||
|
|
||||||
|
### Relay logica (OR over alle sources)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
relay1 = touch1 || button || (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]);
|
||||||
|
relay2 = touch2 || (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]);
|
||||||
|
```
|
||||||
|
|
||||||
|
OTA schakelt alle relays uit. LED = FastLED NeoPixel op G35 (geen digitalWrite meer).
|
||||||
|
|
||||||
|
### BLE characteristics
|
||||||
|
|
||||||
|
| UUID | Type | Functie |
|
||||||
|
|------|------|---------|
|
||||||
|
| `4fafc201-...` | Service | |
|
||||||
|
| `...f26d8` | WRITE_NR | Relay commands (HMAC) |
|
||||||
|
| `...f26d9` | READ | Server UUID (identificatie) |
|
||||||
|
| `...f26db` | READ | Firmware version |
|
||||||
|
| `...f26da` | WRITE_NR + NOTIFY | OTA |
|
||||||
|
|
||||||
|
### Protocol
|
||||||
|
|
||||||
|
HMAC-SHA256, 37-byte packets: `[cmd:1][seq:4][hmac:32]`
|
||||||
|
|
||||||
|
| Cmd | Actie |
|
||||||
|
|-----|-------|
|
||||||
|
| 0x00 | Relay1 OFF |
|
||||||
|
| 0x01 | Relay1 ON |
|
||||||
|
| 0x02 | Relay2 OFF |
|
||||||
|
| 0x03 | Relay2 ON |
|
||||||
|
|
||||||
|
Seq = monotoon per connection, replay protection per client.
|
||||||
|
|
||||||
|
### USB CDC
|
||||||
|
|
||||||
|
```
|
||||||
|
KEY? → DEVICE_ID:<32 hex> + HMAC_KEY:<64 hex>
|
||||||
|
SET_KEY:<64> → "OK" (slaat op in NVS)
|
||||||
|
SET_ID:<32> → "OK" (slaat op in NVS)
|
||||||
|
VERSION? → BUILD_NR:<n>
|
||||||
|
```
|
||||||
|
|
||||||
|
### FIRMWARE_VERSION
|
||||||
|
|
||||||
|
Server: "6"
|
||||||
|
Client: "1"
|
||||||
|
|
||||||
|
Single source of truth: `version.txt` in project root.
|
||||||
|
|
||||||
|
```
|
||||||
|
server=6
|
||||||
|
client=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Build scripts lezen `version.txt`:
|
||||||
|
- PlatformIO: `-DFIRMWARE_VERSION=<n>` via `extra_scripts = pre:tools/pio_version_pre.py`
|
||||||
|
- Android: `build.gradle` leest `version.txt` voor `EXPECTED_FIRMWARE_VERSION`
|
||||||
|
|
||||||
|
## Client firmware (`src/client.cpp`)
|
||||||
|
|
||||||
|
Nieuw bestand op deze branch. HMAC, geen AES-GCM.
|
||||||
|
|
||||||
|
### BLE flow
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Scan (actief, interval=100, window=99) → filter op naam "SylDa_Server"
|
||||||
|
2. Connect → READ UUID characteristic
|
||||||
|
3. Verify: server UUID == geprovisioneerde serverUuid in NVS
|
||||||
|
✓ match → HMAC relay commands
|
||||||
|
✗ mismatch → disconnect, scan opnieuw
|
||||||
|
```
|
||||||
|
|
||||||
|
### Relay logica (lokaal, stuurt eindresultaat)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
relay1 = touch1 || button → stuurt 0x01/0x00
|
||||||
|
relay2 = touch2 → stuurt 0x02/0x03
|
||||||
|
```
|
||||||
|
|
||||||
|
Debounce: 50ms, identiek aan server.
|
||||||
|
|
||||||
|
### HMAC packet build
|
||||||
|
|
||||||
|
Zelfde 37-byte packet: `[cmd:1][seq:4][hmac:32]`, zelfde HMAC key.
|
||||||
|
|
||||||
|
### NVS
|
||||||
|
|
||||||
|
```
|
||||||
|
namespace "hmac": key "key" (32 bytes HMAC), "uuid" (16 bytes eigen UUID), "server" (16 bytes server UUID)
|
||||||
|
```
|
||||||
|
|
||||||
|
### USB CDC
|
||||||
|
|
||||||
|
```
|
||||||
|
KEY? → DEVICE_ID:<32 hex> + HMAC_KEY:<64 hex> + SERVER_UUID:<32 hex>
|
||||||
|
SET_KEY:<64> → "OK"
|
||||||
|
SET_ID:<32> → "OK"
|
||||||
|
SET_SERVER:<32> → "OK"
|
||||||
|
VERSION? → BUILD_NR:<n>
|
||||||
|
```
|
||||||
|
|
||||||
|
### OTA
|
||||||
|
|
||||||
|
Zelfde mechanisme als server, via BLE OTA characteristic. Chunks: 240 bytes.
|
||||||
|
|
||||||
|
## Android app
|
||||||
|
|
||||||
|
Basis: `main` branch. Wijzigingen t.o.v. `main`:
|
||||||
|
|
||||||
|
### SettingsActivity
|
||||||
|
|
||||||
|
Radio buttons:
|
||||||
|
```
|
||||||
|
[Server ▸ Client] × [BLE ▸ USB]
|
||||||
|
Server + USB = flash + provision (SET_KEY, SET_ID)
|
||||||
|
Server + BLE = relay knoppen + OTA + UUID READ
|
||||||
|
Client + USB = flash + provision (SET_KEY, SET_ID, SET_SERVER)
|
||||||
|
Client + BLE = OTA (via client UUID)
|
||||||
|
```
|
||||||
|
|
||||||
|
### BleRelayManager
|
||||||
|
|
||||||
|
Ongewijzigd — HMAC 37-byte packets, commands 0x00-0x03.
|
||||||
|
|
||||||
|
### EspFlasher
|
||||||
|
|
||||||
|
Server én client firmware binaries. Assets gesplitst:
|
||||||
|
```
|
||||||
|
assets/firmware/server/{bootloader,partitions,boot_app0,firmware}.bin
|
||||||
|
assets/firmware/client/{bootloader,partitions,boot_app0,firmware}.bin
|
||||||
|
```
|
||||||
|
|
||||||
|
### PrefsHelper
|
||||||
|
|
||||||
|
Extra keys: `deviceMode` (server/client), `serverUuid`, `clientUuid`.
|
||||||
|
|
||||||
|
## OTA protocol
|
||||||
|
|
||||||
|
Blijft ongewijzigd van `main`. Chunk size: 240 bytes (past in MTU 248 single write).
|
||||||
|
|
||||||
|
| Packet | Grootte | Inhoud |
|
||||||
|
|--------|---------|--------|
|
||||||
|
| START | 5 bytes | `[0x10][size:4]` |
|
||||||
|
| Chunk | 4 + 240 bytes | `[seq:4][data:240]` |
|
||||||
|
| Checksum | 32 bytes | SHA256 hash |
|
||||||
|
| Response | 5 bytes | `[status:1][seq:4]` via notify |
|
||||||
|
|
||||||
|
## PlatformIO
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[env:s3lite-server] # AtomS3 Lite, qio_qspi, main.cpp
|
||||||
|
[env:s3lite-client] # AtomS3 Lite, qio_qspi, DEVICE_ID=1, client.cpp
|
||||||
|
upload_flags = --no-stub # beide envs (QSPI flash)
|
||||||
|
```
|
||||||
|
|
||||||
|
Beide envs identieke `qio_qspi` (beide AtomS3 Lite).
|
||||||
|
|
||||||
|
## Teststrategie
|
||||||
|
|
||||||
|
1. Server solo: touch/knop schakelen relais lokaal
|
||||||
|
2. Server + client: BLE relay commands via touch/knop op client
|
||||||
|
3. Server + client + Android: alle drie bronnen tegelijk, OR-logica
|
||||||
|
4. OTA: firmware update server én client via BLE
|
||||||
|
5. Reconnect: client + Android herstellen na disconnect
|
||||||
|
6. NVS persistentie: HMAC key + UUIDs overleven reboot
|
||||||
|
|
||||||
|
## Toekomst: AES-GCM refactor
|
||||||
|
|
||||||
|
Na stabiele NimBLE+HMAC basis: vervang HMAC door AES-256-GCM (18-byte packets, encryptie + authenticatie). Code op `Sylda_Client` branch dient als referentie.
|
||||||
|
|
@ -1,18 +1,38 @@
|
||||||
[env:m5stack-atoms3r]
|
[env:s3lite-server]
|
||||||
platform = espressif32@6.7.0
|
platform = espressif32@6.7.0
|
||||||
board = esp32-s3-devkitc-1
|
board = esp32-s3-devkitc-1
|
||||||
framework = arduino
|
framework = arduino
|
||||||
board_build.arduino.memory_type = qio_opi
|
board_build.arduino.memory_type = qio_qspi
|
||||||
build_flags =
|
build_flags =
|
||||||
-DESP32S3
|
-DESP32S3
|
||||||
-DBOARD_HAS_PSRAM
|
|
||||||
-mfix-esp32-psram-cache-issue
|
|
||||||
-DCORE_DEBUG_LEVEL=5
|
-DCORE_DEBUG_LEVEL=5
|
||||||
-DARDUINO_USB_CDC_ON_BOOT=1
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
-DARDUINO_USB_MODE=1
|
-DARDUINO_USB_MODE=1
|
||||||
-DCONFIG_BT_NIMBLE_ENABLED
|
|
||||||
lib_deps =
|
lib_deps =
|
||||||
|
fastled/FastLED@^3
|
||||||
h2zero/NimBLE-Arduino@^2
|
h2zero/NimBLE-Arduino@^2
|
||||||
upload_port = /dev/ttyACM1
|
upload_port = /dev/ttyACM1
|
||||||
upload_speed = 115200
|
upload_speed = 115200
|
||||||
upload_flags = --no-stub
|
upload_flags = --no-stub
|
||||||
|
build_src_filter = +<main.cpp> -<client.cpp>
|
||||||
|
extra_scripts = pre:tools/pio_version_pre.py
|
||||||
|
|
||||||
|
[env:s3lite-client]
|
||||||
|
platform = espressif32@6.7.0
|
||||||
|
board = esp32-s3-devkitc-1
|
||||||
|
framework = arduino
|
||||||
|
board_build.arduino.memory_type = qio_qspi
|
||||||
|
build_flags =
|
||||||
|
-DESP32S3
|
||||||
|
-DCORE_DEBUG_LEVEL=5
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-DARDUINO_USB_MODE=1
|
||||||
|
-DDEVICE_ID=1
|
||||||
|
lib_deps =
|
||||||
|
fastled/FastLED@^3
|
||||||
|
h2zero/NimBLE-Arduino@^2
|
||||||
|
upload_port = /dev/ttyACM2
|
||||||
|
upload_speed = 115200
|
||||||
|
upload_flags = --no-stub
|
||||||
|
build_src_filter = +<client.cpp> -<main.cpp>
|
||||||
|
extra_scripts = pre:tools/pio_version_pre.py
|
||||||
|
|
|
||||||
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())
|
||||||
439
src/client.cpp
Normal file
439
src/client.cpp
Normal file
|
|
@ -0,0 +1,439 @@
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <NimBLEDevice.h>
|
||||||
|
#include <FastLED.h>
|
||||||
|
|
||||||
|
#include <mbedtls/md.h>
|
||||||
|
#include <mbedtls/sha256.h>
|
||||||
|
#include <Preferences.h>
|
||||||
|
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
|
#define TOUCH1_PIN 8
|
||||||
|
#define TOUCH2_PIN 6
|
||||||
|
#define BUTTON_PIN 41
|
||||||
|
#define LED_PIN 35
|
||||||
|
#define NUM_LEDS 1
|
||||||
|
|
||||||
|
CRGB leds[NUM_LEDS];
|
||||||
|
|
||||||
|
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
||||||
|
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
|
||||||
|
#define UUID_CHAR_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d9"
|
||||||
|
#define HMAC_PKT_SIZE 37
|
||||||
|
#define HMAC_KEY_LEN 32
|
||||||
|
#define ESP_UUID_LEN 16
|
||||||
|
#define NVS_NAMESPACE "hmac"
|
||||||
|
#define NVS_KEY_NAME "key"
|
||||||
|
#define NVS_UUID_NAME "uuid"
|
||||||
|
#define NVS_SERVER_NAME "server"
|
||||||
|
#define DEBOUNCE_MS 50
|
||||||
|
#define SCAN_INTERVAL 6000
|
||||||
|
#define SCAN_TIMEOUT 5000
|
||||||
|
|
||||||
|
uint8_t hmacKey[HMAC_KEY_LEN];
|
||||||
|
uint8_t deviceUuid[ESP_UUID_LEN];
|
||||||
|
uint8_t serverUuid[ESP_UUID_LEN];
|
||||||
|
bool keyProvisioned = false;
|
||||||
|
bool uuidProvisioned = false;
|
||||||
|
bool serverProvisioned = false;
|
||||||
|
bool allProvisioned = false;
|
||||||
|
Preferences preferences;
|
||||||
|
|
||||||
|
static bool connected = false;
|
||||||
|
NimBLEClient *pClient = nullptr;
|
||||||
|
NimBLERemoteCharacteristic *pCmdChar = nullptr;
|
||||||
|
|
||||||
|
static bool relay1SendState = false;
|
||||||
|
static bool relay2SendState = false;
|
||||||
|
static uint32_t seq = 0;
|
||||||
|
|
||||||
|
static bool bleReady = false;
|
||||||
|
static bool serverFound = false;
|
||||||
|
static NimBLEAddress serverAddress;
|
||||||
|
|
||||||
|
class MyScanCallbacks : public NimBLEScanCallbacks {
|
||||||
|
void onResult(const NimBLEAdvertisedDevice *advertisedDevice) override {
|
||||||
|
if (advertisedDevice->haveName() && advertisedDevice->getName() == "SylDa_Server") {
|
||||||
|
serverAddress = advertisedDevice->getAddress();
|
||||||
|
serverFound = true;
|
||||||
|
NimBLEDevice::getScan()->stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class MyClientCallbacks : public NimBLEClientCallbacks {
|
||||||
|
void onConnect(NimBLEClient *client) override {
|
||||||
|
connected = true;
|
||||||
|
Serial.println("BLE: connected to server");
|
||||||
|
}
|
||||||
|
void onDisconnect(NimBLEClient *client, int reason) override {
|
||||||
|
connected = false;
|
||||||
|
relay1SendState = false;
|
||||||
|
relay2SendState = false;
|
||||||
|
pCmdChar = nullptr;
|
||||||
|
Serial.printf("BLE: disconnected (reason=%d)\n", reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static MyClientCallbacks clientCallbacks;
|
||||||
|
|
||||||
|
static bool isHexKey(const String &hex) {
|
||||||
|
for (int i = 0; i < hex.length(); i++) {
|
||||||
|
char c = hex.charAt(i);
|
||||||
|
if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F'))) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parseHexKey(const String &hex, uint8_t *out, int len) {
|
||||||
|
for (int i = 0; i < len; i++) {
|
||||||
|
out[i] = (uint8_t)strtoul(hex.substring(i * 2, i * 2 + 2).c_str(), nullptr, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void buildHmacPacket(uint8_t cmd, uint8_t *packet) {
|
||||||
|
uint32_t s = ++seq;
|
||||||
|
packet[0] = cmd;
|
||||||
|
packet[1] = (s >> 24) & 0xFF;
|
||||||
|
packet[2] = (s >> 16) & 0xFF;
|
||||||
|
packet[3] = (s >> 8) & 0xFF;
|
||||||
|
packet[4] = s & 0xFF;
|
||||||
|
uint8_t hmacData[5];
|
||||||
|
hmacData[0] = cmd;
|
||||||
|
memcpy(hmacData + 1, packet + 1, 4);
|
||||||
|
const mbedtls_md_info_t *mdInfo = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256);
|
||||||
|
mbedtls_md_hmac(mdInfo, hmacKey, HMAC_KEY_LEN, hmacData, 5, packet + 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disconnectClient() {
|
||||||
|
if (pClient != nullptr) {
|
||||||
|
pClient->disconnect();
|
||||||
|
delay(100);
|
||||||
|
NimBLEDevice::deleteClient(pClient);
|
||||||
|
pClient = nullptr;
|
||||||
|
}
|
||||||
|
pCmdChar = nullptr;
|
||||||
|
connected = false;
|
||||||
|
relay1SendState = false;
|
||||||
|
relay2SendState = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool connectToServer() {
|
||||||
|
pClient = NimBLEDevice::createClient();
|
||||||
|
pClient->setClientCallbacks(&clientCallbacks);
|
||||||
|
|
||||||
|
if (!pClient->connect(serverAddress)) {
|
||||||
|
Serial.println("BLE: connect failed");
|
||||||
|
NimBLEDevice::deleteClient(pClient);
|
||||||
|
pClient = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NimBLERemoteService *pSvc = pClient->getService(SERVICE_UUID);
|
||||||
|
if (pSvc == nullptr) {
|
||||||
|
Serial.println("BLE: service not found");
|
||||||
|
disconnectClient();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
NimBLERemoteCharacteristic *pUuidChar = pSvc->getCharacteristic(UUID_CHAR_UUID);
|
||||||
|
if (pUuidChar == nullptr) {
|
||||||
|
Serial.println("BLE: UUID characteristic not found");
|
||||||
|
disconnectClient();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string uuidVal = pUuidChar->readValue();
|
||||||
|
if (uuidVal.length() != ESP_UUID_LEN ||
|
||||||
|
memcmp(uuidVal.data(), serverUuid, ESP_UUID_LEN) != 0) {
|
||||||
|
Serial.println("BLE: server UUID mismatch — disconnecting");
|
||||||
|
disconnectClient();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pCmdChar = pSvc->getCharacteristic(CHARACTERISTIC_UUID);
|
||||||
|
if (pCmdChar == nullptr) {
|
||||||
|
Serial.println("BLE: command characteristic not found");
|
||||||
|
disconnectClient();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seq = 0;
|
||||||
|
relay1SendState = false;
|
||||||
|
relay2SendState = false;
|
||||||
|
Serial.println("BLE: server UUID verified, ready");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
delay(2000);
|
||||||
|
Serial.println("=== SylDa Client ===");
|
||||||
|
|
||||||
|
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
|
||||||
|
leds[0] = CRGB::Red;
|
||||||
|
FastLED.show();
|
||||||
|
|
||||||
|
preferences.begin(NVS_NAMESPACE, true);
|
||||||
|
size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
|
||||||
|
size_t uuidLen = preferences.getBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN);
|
||||||
|
size_t serverLen = preferences.getBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN);
|
||||||
|
preferences.end();
|
||||||
|
|
||||||
|
if (len == HMAC_KEY_LEN) {
|
||||||
|
keyProvisioned = true;
|
||||||
|
Serial.println("HMAC key loaded from NVS");
|
||||||
|
} else {
|
||||||
|
keyProvisioned = false;
|
||||||
|
Serial.println("No HMAC key — use USB SET_KEY to provision");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uuidLen == ESP_UUID_LEN) {
|
||||||
|
uuidProvisioned = true;
|
||||||
|
Serial.print("UUID loaded from NVS: ");
|
||||||
|
for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]);
|
||||||
|
Serial.println();
|
||||||
|
} else {
|
||||||
|
uuidProvisioned = false;
|
||||||
|
for (int i = 0; i < ESP_UUID_LEN; i++) deviceUuid[i] = (uint8_t)(esp_random() & 0xFF);
|
||||||
|
preferences.begin(NVS_NAMESPACE, false);
|
||||||
|
preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN);
|
||||||
|
preferences.end();
|
||||||
|
uuidProvisioned = true;
|
||||||
|
Serial.print("Generated new UUID: ");
|
||||||
|
for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", deviceUuid[i]);
|
||||||
|
Serial.println();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (serverLen == ESP_UUID_LEN) {
|
||||||
|
serverProvisioned = true;
|
||||||
|
Serial.print("Server UUID loaded from NVS: ");
|
||||||
|
for (int i = 0; i < ESP_UUID_LEN; i++) Serial.printf("%02X", serverUuid[i]);
|
||||||
|
Serial.println();
|
||||||
|
} else {
|
||||||
|
serverProvisioned = false;
|
||||||
|
Serial.println("No server UUID — use USB SET_SERVER to provision");
|
||||||
|
}
|
||||||
|
|
||||||
|
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||||
|
|
||||||
|
pinMode(TOUCH1_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(TOUCH2_PIN, INPUT_PULLUP);
|
||||||
|
pinMode(BUTTON_PIN, INPUT);
|
||||||
|
|
||||||
|
if (allProvisioned) {
|
||||||
|
NimBLEDevice::init("SylDa_Client");
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Setup done.");
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
if (Serial.available()) {
|
||||||
|
static String serialBuf;
|
||||||
|
char c = Serial.read();
|
||||||
|
serialBuf += c;
|
||||||
|
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:");
|
||||||
|
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);
|
||||||
|
} else if (serialBuf.startsWith("SET_KEY:")) {
|
||||||
|
String hex = serialBuf.substring(8);
|
||||||
|
hex.toUpperCase();
|
||||||
|
if (hex.length() == 64 && isHexKey(hex)) {
|
||||||
|
parseHexKey(hex, hmacKey, HMAC_KEY_LEN);
|
||||||
|
preferences.begin(NVS_NAMESPACE, false);
|
||||||
|
preferences.putBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
|
||||||
|
preferences.end();
|
||||||
|
keyProvisioned = true;
|
||||||
|
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||||
|
Serial.println("OK");
|
||||||
|
Serial.println("HMAC key stored in NVS");
|
||||||
|
if (allProvisioned && !bleReady) {
|
||||||
|
NimBLEDevice::init("SylDa_Client");
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("ERROR: invalid key format");
|
||||||
|
}
|
||||||
|
} else if (serialBuf.startsWith("SET_ID:")) {
|
||||||
|
String hex = serialBuf.substring(7);
|
||||||
|
hex.toUpperCase();
|
||||||
|
if (hex.length() == 32 && isHexKey(hex)) {
|
||||||
|
parseHexKey(hex, deviceUuid, ESP_UUID_LEN);
|
||||||
|
preferences.begin(NVS_NAMESPACE, false);
|
||||||
|
preferences.putBytes(NVS_UUID_NAME, deviceUuid, ESP_UUID_LEN);
|
||||||
|
preferences.end();
|
||||||
|
uuidProvisioned = true;
|
||||||
|
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||||
|
Serial.println("OK");
|
||||||
|
Serial.println("UUID stored in NVS");
|
||||||
|
} else {
|
||||||
|
Serial.println("ERROR: invalid UUID format");
|
||||||
|
}
|
||||||
|
} else if (serialBuf.startsWith("SET_SERVER:")) {
|
||||||
|
String hex = serialBuf.substring(11);
|
||||||
|
hex.toUpperCase();
|
||||||
|
if (hex.length() == 32 && isHexKey(hex)) {
|
||||||
|
parseHexKey(hex, serverUuid, ESP_UUID_LEN);
|
||||||
|
preferences.begin(NVS_NAMESPACE, false);
|
||||||
|
preferences.putBytes(NVS_SERVER_NAME, serverUuid, ESP_UUID_LEN);
|
||||||
|
preferences.end();
|
||||||
|
serverProvisioned = true;
|
||||||
|
allProvisioned = keyProvisioned && uuidProvisioned && serverProvisioned;
|
||||||
|
Serial.println("OK");
|
||||||
|
Serial.println("Server UUID stored in NVS");
|
||||||
|
if (allProvisioned && !bleReady) {
|
||||||
|
NimBLEDevice::init("SylDa_Client");
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("ERROR: invalid server UUID format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
serialBuf = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allProvisioned) {
|
||||||
|
delay(20);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool t1Stable = false;
|
||||||
|
static bool t2Stable = false;
|
||||||
|
static bool btnStable = false;
|
||||||
|
static uint32_t t1Change = 0;
|
||||||
|
static uint32_t t2Change = 0;
|
||||||
|
static uint32_t btnChange = 0;
|
||||||
|
bool t1Raw = (digitalRead(TOUCH1_PIN) == LOW);
|
||||||
|
bool t2Raw = (digitalRead(TOUCH2_PIN) == LOW);
|
||||||
|
bool btnRaw = (digitalRead(BUTTON_PIN) == LOW);
|
||||||
|
if (t1Raw != t1Stable) { t1Change = millis(); t1Stable = t1Raw; }
|
||||||
|
if (t2Raw != t2Stable) { t2Change = millis(); t2Stable = t2Raw; }
|
||||||
|
if (btnRaw != btnStable) { btnChange = millis(); btnStable = btnRaw; }
|
||||||
|
bool t1Active = (millis() - t1Change > DEBOUNCE_MS) && t1Stable;
|
||||||
|
bool t2Active = (millis() - t2Change > DEBOUNCE_MS) && t2Stable;
|
||||||
|
bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable;
|
||||||
|
|
||||||
|
static bool prevBtnActive = false;
|
||||||
|
if (btnActive && !prevBtnActive) {
|
||||||
|
Serial.printf("Button: pressed\n");
|
||||||
|
}
|
||||||
|
prevBtnActive = btnActive;
|
||||||
|
|
||||||
|
bool relay1State = t1Active || btnActive;
|
||||||
|
bool relay2State = t2Active;
|
||||||
|
|
||||||
|
if (relay1State || relay2State) {
|
||||||
|
leds[0] = CRGB::Green;
|
||||||
|
} else if (connected) {
|
||||||
|
leds[0] = CRGB::Blue;
|
||||||
|
} else {
|
||||||
|
leds[0] = CRGB::Red;
|
||||||
|
}
|
||||||
|
FastLED.show();
|
||||||
|
|
||||||
|
if (connected && pCmdChar != nullptr) {
|
||||||
|
static uint32_t lastCmd = 0;
|
||||||
|
if (millis() - lastCmd > 200) {
|
||||||
|
if (relay1State != relay1SendState) {
|
||||||
|
uint8_t pkt[HMAC_PKT_SIZE];
|
||||||
|
buildHmacPacket(relay1State ? 0x01 : 0x00, pkt);
|
||||||
|
pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false);
|
||||||
|
relay1SendState = relay1State;
|
||||||
|
Serial.printf("TX: relay1 %s (seq=%lu)\n", relay1State ? "ON" : "OFF", seq);
|
||||||
|
lastCmd = millis();
|
||||||
|
}
|
||||||
|
if (relay2State != relay2SendState) {
|
||||||
|
uint8_t pkt[HMAC_PKT_SIZE];
|
||||||
|
buildHmacPacket(relay2State ? 0x02 : 0x03, pkt);
|
||||||
|
pCmdChar->writeValue(pkt, HMAC_PKT_SIZE, false);
|
||||||
|
relay2SendState = relay2State;
|
||||||
|
Serial.printf("TX: relay2 %s (seq=%lu)\n", relay2State ? "ON" : "OFF", seq);
|
||||||
|
lastCmd = millis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t lastScanStart = 0;
|
||||||
|
static bool scanRunnable = false;
|
||||||
|
|
||||||
|
if (!connected) {
|
||||||
|
NimBLEScan *pScan = NimBLEDevice::getScan();
|
||||||
|
if ((millis() - lastScanStart > SCAN_INTERVAL) && !pScan->isScanning()) {
|
||||||
|
serverFound = false;
|
||||||
|
pScan->start(SCAN_TIMEOUT / 1000, false);
|
||||||
|
lastScanStart = millis();
|
||||||
|
scanRunnable = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pScan->isScanning() && scanRunnable) {
|
||||||
|
scanRunnable = false;
|
||||||
|
if (serverFound) {
|
||||||
|
Serial.printf("BLE: found SylDa_Server (%s), connecting...\n",
|
||||||
|
serverAddress.toString().c_str());
|
||||||
|
connectToServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connected && pClient != nullptr && !pClient->isConnected()) {
|
||||||
|
Serial.println("BLE: lost connection");
|
||||||
|
disconnectClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t heartbeat = 0;
|
||||||
|
if (millis() - heartbeat > 5000) {
|
||||||
|
Serial.printf("[heartbeat] t1=%s t2=%s btn=%s r1=%s r2=%s connected=%s\n",
|
||||||
|
t1Active ? "YES" : "no",
|
||||||
|
t2Active ? "YES" : "no",
|
||||||
|
btnActive ? "YES" : "no",
|
||||||
|
relay1State ? "ON" : "OFF",
|
||||||
|
relay2State ? "ON" : "OFF",
|
||||||
|
connected ? "yes" : "no");
|
||||||
|
heartbeat = millis();
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(20);
|
||||||
|
}
|
||||||
171
src/main.cpp
171
src/main.cpp
|
|
@ -1,6 +1,7 @@
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <NimBLEDevice.h>
|
#include <NimBLEDevice.h>
|
||||||
#include <NimBLEAdvertising.h>
|
#include <NimBLEAdvertising.h>
|
||||||
|
#include <FastLED.h>
|
||||||
|
|
||||||
#include <mbedtls/md.h>
|
#include <mbedtls/md.h>
|
||||||
#include <mbedtls/sha256.h>
|
#include <mbedtls/sha256.h>
|
||||||
|
|
@ -8,13 +9,19 @@
|
||||||
#include <esp_ota_ops.h>
|
#include <esp_ota_ops.h>
|
||||||
#include <esp_partition.h>
|
#include <esp_partition.h>
|
||||||
#include <nvs_flash.h>
|
#include <nvs_flash.h>
|
||||||
|
#include "version.h"
|
||||||
|
|
||||||
#define RELAY1_PIN 1
|
#define RELAY1_PIN 1
|
||||||
#define RELAY2_PIN 2
|
#define RELAY2_PIN 2
|
||||||
#define TOUCH1_PIN 8
|
#define TOUCH1_PIN 8
|
||||||
#define TOUCH2_PIN 6
|
#define TOUCH2_PIN 6
|
||||||
#define BUTTON_PIN 41
|
#define BUTTON_PIN 41
|
||||||
#define LED_PIN 46
|
#define LED_PIN 35
|
||||||
|
#define NUM_LEDS 1
|
||||||
|
|
||||||
|
CRGB leds[NUM_LEDS];
|
||||||
|
|
||||||
|
#define MAX_CLIENTS 2
|
||||||
|
|
||||||
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
#define SERVICE_UUID "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
|
||||||
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
|
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361f26d8"
|
||||||
|
|
@ -29,21 +36,24 @@
|
||||||
#define OTA_STATUS_ACK 0x00
|
#define OTA_STATUS_ACK 0x00
|
||||||
#define OTA_STATUS_NAK 0x01
|
#define OTA_STATUS_NAK 0x01
|
||||||
#define OTA_STATUS_COMPLETE 0x10
|
#define OTA_STATUS_COMPLETE 0x10
|
||||||
#define OTA_STATUS_ERROR 0x11 // 1 cmd + 4 seq + 32 hmac
|
#define OTA_STATUS_ERROR 0x11
|
||||||
#define HMAC_KEY_LEN 32
|
#define HMAC_KEY_LEN 32
|
||||||
#define ESP_UUID_LEN 16
|
#define ESP_UUID_LEN 16
|
||||||
#define NVS_NAMESPACE "hmac"
|
#define NVS_NAMESPACE "hmac"
|
||||||
#define NVS_KEY_NAME "key"
|
#define NVS_KEY_NAME "key"
|
||||||
#define NVS_UUID_NAME "uuid"
|
#define NVS_UUID_NAME "uuid"
|
||||||
#define FIRMWARE_VERSION "6"
|
#define FIRMWARE_VERSION FIRMWARE_VERSION_SERVER
|
||||||
|
|
||||||
NimBLECharacteristic *pCharacteristic;
|
NimBLECharacteristic *pCharacteristic;
|
||||||
bool deviceConnected = false;
|
|
||||||
bool bleRelay1 = false;
|
|
||||||
bool bleRelay2 = false;
|
|
||||||
bool relay1State = false;
|
bool relay1State = false;
|
||||||
bool relay2State = false;
|
bool relay2State = false;
|
||||||
uint32_t lastSeq = 0;
|
|
||||||
|
bool connActive[MAX_CLIENTS] = {false, false};
|
||||||
|
bool bleRelay1[MAX_CLIENTS] = {false, false};
|
||||||
|
bool bleRelay2[MAX_CLIENTS] = {false, false};
|
||||||
|
uint32_t lastSeq[MAX_CLIENTS] = {0, 0};
|
||||||
|
uint16_t connHandles[MAX_CLIENTS] = {0xFFFF, 0xFFFF};
|
||||||
|
int connectedCount = 0;
|
||||||
|
|
||||||
bool otaInProgress = false;
|
bool otaInProgress = false;
|
||||||
esp_ota_handle_t otaHandle = 0;
|
esp_ota_handle_t otaHandle = 0;
|
||||||
|
|
@ -60,31 +70,72 @@ bool keyProvisioned = false;
|
||||||
bool uuidProvisioned = false;
|
bool uuidProvisioned = false;
|
||||||
Preferences preferences;
|
Preferences preferences;
|
||||||
|
|
||||||
|
static int findClientIdx(uint16_t connHandle) {
|
||||||
|
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||||
|
if (connActive[i] && connHandles[i] == connHandle) return i;
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
class MyServerCallbacks : public NimBLEServerCallbacks {
|
class MyServerCallbacks : public NimBLEServerCallbacks {
|
||||||
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override {
|
void onConnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo) override {
|
||||||
deviceConnected = true;
|
uint16_t handle = connInfo.getConnHandle();
|
||||||
lastSeq = 0;
|
int slot = -1;
|
||||||
Serial.println("BLE: device connected");
|
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||||
|
if (!connActive[i]) {
|
||||||
|
slot = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot >= 0) {
|
||||||
|
connHandles[slot] = handle;
|
||||||
|
connActive[slot] = true;
|
||||||
|
bleRelay1[slot] = false;
|
||||||
|
bleRelay2[slot] = false;
|
||||||
|
lastSeq[slot] = 0;
|
||||||
|
connectedCount++;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
pServer->startAdvertising();
|
||||||
}
|
}
|
||||||
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override {
|
void onDisconnect(NimBLEServer *pServer, NimBLEConnInfo &connInfo, int reason) override {
|
||||||
deviceConnected = false;
|
uint16_t handle = connInfo.getConnHandle();
|
||||||
bleRelay1 = false;
|
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||||
bleRelay2 = false;
|
if (connActive[i] && connHandles[i] == handle) {
|
||||||
lastSeq = 0;
|
connActive[i] = false;
|
||||||
Serial.println("BLE: device disconnected");
|
bleRelay1[i] = false;
|
||||||
NimBLEDevice::startAdvertising();
|
bleRelay2[i] = false;
|
||||||
|
lastSeq[i] = 0;
|
||||||
|
connHandles[i] = 0xFFFF;
|
||||||
|
connectedCount--;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static bool verifyHmac(const uint8_t *packet) {
|
static bool verifyHmac(const uint8_t *packet, int idx) {
|
||||||
|
if (idx < 0 || idx >= MAX_CLIENTS || !connActive[idx]) {
|
||||||
|
Serial.printf("BLE: HMAC verify called for invalid idx=%d\n", idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
uint8_t cmd = packet[0];
|
uint8_t cmd = packet[0];
|
||||||
uint32_t seq = ((uint32_t)packet[1] << 24) |
|
uint32_t seq = ((uint32_t)packet[1] << 24) |
|
||||||
((uint32_t)packet[2] << 16) |
|
((uint32_t)packet[2] << 16) |
|
||||||
((uint32_t)packet[3] << 8) |
|
((uint32_t)packet[3] << 8) |
|
||||||
(uint32_t)packet[4];
|
(uint32_t)packet[4];
|
||||||
|
|
||||||
if (seq <= lastSeq) {
|
if (seq <= lastSeq[idx]) {
|
||||||
Serial.printf("BLE: replay rejected (seq=%lu, last=%lu)\n", seq, lastSeq);
|
Serial.printf("BLE: replay rejected (seq=%lu, last=%lu, slot=%d)\n", seq, lastSeq[idx], idx);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -104,7 +155,7 @@ static bool verifyHmac(const uint8_t *packet) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastSeq = seq;
|
lastSeq[idx] = seq;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,7 +174,7 @@ static void parseHexKey(const String &hex, uint8_t *out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ledOtaBusy() {
|
static void ledOtaBusy() {
|
||||||
digitalWrite(LED_PIN, (millis() / 100) % 2 ? HIGH : LOW);
|
leds[0] = (millis() / 100) % 2 ? CRGB::Green : CRGB::Black;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void otaReset() {
|
static void otaReset() {
|
||||||
|
|
@ -193,8 +244,10 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
digitalWrite(RELAY2_PIN, LOW);
|
digitalWrite(RELAY2_PIN, LOW);
|
||||||
relay1State = false;
|
relay1State = false;
|
||||||
relay2State = false;
|
relay2State = false;
|
||||||
bleRelay1 = false;
|
for (int i = 0; i < MAX_CLIENTS; i++) {
|
||||||
bleRelay2 = false;
|
bleRelay1[i] = false;
|
||||||
|
bleRelay2[i] = false;
|
||||||
|
}
|
||||||
Serial.printf("OTA: started, expecting %lu bytes\n", otaExpectedSize);
|
Serial.printf("OTA: started, expecting %lu bytes\n", otaExpectedSize);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -225,7 +278,8 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
|
sendOtaNotify(pChar, OTA_STATUS_ERROR, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
digitalWrite(LED_PIN, HIGH);
|
leds[0] = CRGB::Green;
|
||||||
|
FastLED.show();
|
||||||
sendOtaNotify(pChar, OTA_STATUS_COMPLETE, 0);
|
sendOtaNotify(pChar, OTA_STATUS_COMPLETE, 0);
|
||||||
Serial.println("OTA: SHA256 OK, rebooting...");
|
Serial.println("OTA: SHA256 OK, rebooting...");
|
||||||
delay(500);
|
delay(500);
|
||||||
|
|
@ -256,34 +310,42 @@ class OtaCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override {
|
void onWrite(NimBLECharacteristic *pChar, NimBLEConnInfo &connInfo) override {
|
||||||
if (otaInProgress) return;
|
if (otaInProgress) return;
|
||||||
|
|
||||||
|
int idx = findClientIdx(connInfo.getConnHandle());
|
||||||
|
if (idx < 0) {
|
||||||
|
Serial.printf("BLE: write from unknown conn handle %d\n", connInfo.getConnHandle());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string value = pChar->getValue();
|
std::string value = pChar->getValue();
|
||||||
if (value.length() != HMAC_PKT_SIZE) {
|
if (value.length() != HMAC_PKT_SIZE) {
|
||||||
Serial.printf("BLE: bad packet length %d (expected %d)\n",
|
Serial.printf("BLE: bad packet length %d (expected %d), slot=%d mac=%s\n",
|
||||||
(int)value.length(), HMAC_PKT_SIZE);
|
(int)value.length(), HMAC_PKT_SIZE, idx,
|
||||||
|
connInfo.getAddress().toString().c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t *pkt = (const uint8_t *)value.data();
|
const uint8_t *pkt = (const uint8_t *)value.data();
|
||||||
|
|
||||||
if (!verifyHmac(pkt)) {
|
if (!verifyHmac(pkt, idx)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t cmd = pkt[0];
|
uint8_t cmd = pkt[0];
|
||||||
uint32_t seq = lastSeq;
|
uint32_t seq = lastSeq[idx];
|
||||||
|
|
||||||
if (cmd == 0x01) {
|
if (cmd == 0x01) {
|
||||||
bleRelay1 = true;
|
bleRelay1[idx] = true;
|
||||||
Serial.printf("BLE: relay1 ON (seq=%lu)\n", seq);
|
Serial.printf("BLE: relay1 ON (seq=%lu, slot=%d)\n", seq, idx);
|
||||||
} else if (cmd == 0x00) {
|
} else if (cmd == 0x00) {
|
||||||
bleRelay1 = false;
|
bleRelay1[idx] = false;
|
||||||
Serial.printf("BLE: relay1 OFF (seq=%lu)\n", seq);
|
Serial.printf("BLE: relay1 OFF (seq=%lu, slot=%d)\n", seq, idx);
|
||||||
} else if (cmd == 0x02) {
|
} else if (cmd == 0x02) {
|
||||||
bleRelay2 = true;
|
bleRelay2[idx] = true;
|
||||||
Serial.printf("BLE: relay2 ON (seq=%lu)\n", seq);
|
Serial.printf("BLE: relay2 ON (seq=%lu, slot=%d)\n", seq, idx);
|
||||||
} else if (cmd == 0x03) {
|
} else if (cmd == 0x03) {
|
||||||
bleRelay2 = false;
|
bleRelay2[idx] = false;
|
||||||
Serial.printf("BLE: relay2 OFF (seq=%lu)\n", seq);
|
Serial.printf("BLE: relay2 OFF (seq=%lu, slot=%d)\n", seq, idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -291,7 +353,9 @@ class MyCharacteristicCallbacks : public NimBLECharacteristicCallbacks {
|
||||||
void setup() {
|
void setup() {
|
||||||
Serial.begin(115200);
|
Serial.begin(115200);
|
||||||
delay(2000);
|
delay(2000);
|
||||||
Serial.println("=== Atom VoiceS3R BLE Relay ===");
|
Serial.println("=== AtomS3 Lite BLE Relay ===");
|
||||||
|
|
||||||
|
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
|
||||||
|
|
||||||
preferences.begin(NVS_NAMESPACE, true);
|
preferences.begin(NVS_NAMESPACE, true);
|
||||||
size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
|
size_t len = preferences.getBytes(NVS_KEY_NAME, hmacKey, HMAC_KEY_LEN);
|
||||||
|
|
@ -328,12 +392,13 @@ void setup() {
|
||||||
pinMode(BUTTON_PIN, INPUT);
|
pinMode(BUTTON_PIN, INPUT);
|
||||||
pinMode(RELAY1_PIN, OUTPUT);
|
pinMode(RELAY1_PIN, OUTPUT);
|
||||||
pinMode(RELAY2_PIN, OUTPUT);
|
pinMode(RELAY2_PIN, OUTPUT);
|
||||||
pinMode(LED_PIN, OUTPUT);
|
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
digitalWrite(LED_PIN, HIGH);
|
leds[0] = CRGB::Green;
|
||||||
|
FastLED.show();
|
||||||
delay(200);
|
delay(200);
|
||||||
digitalWrite(LED_PIN, LOW);
|
leds[0] = CRGB::Black;
|
||||||
|
FastLED.show();
|
||||||
delay(200);
|
delay(200);
|
||||||
}
|
}
|
||||||
Serial.println("LED blink done, starting BLE...");
|
Serial.println("LED blink done, starting BLE...");
|
||||||
|
|
@ -343,6 +408,7 @@ void setup() {
|
||||||
|
|
||||||
NimBLEServer *pServer = NimBLEDevice::createServer();
|
NimBLEServer *pServer = NimBLEDevice::createServer();
|
||||||
pServer->setCallbacks(new MyServerCallbacks());
|
pServer->setCallbacks(new MyServerCallbacks());
|
||||||
|
pServer->advertiseOnDisconnect(true);
|
||||||
|
|
||||||
NimBLEService *pService = pServer->createService(SERVICE_UUID);
|
NimBLEService *pService = pServer->createService(SERVICE_UUID);
|
||||||
pCharacteristic = pService->createCharacteristic(
|
pCharacteristic = pService->createCharacteristic(
|
||||||
|
|
@ -473,8 +539,12 @@ void loop() {
|
||||||
bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable;
|
bool btnActive = (millis() - btnChange > DEBOUNCE_MS) && btnStable;
|
||||||
|
|
||||||
if (!otaInProgress) {
|
if (!otaInProgress) {
|
||||||
relay1State = t1Active || btnActive || (deviceConnected && bleRelay1);
|
relay1State = t1Active || btnActive ||
|
||||||
relay2State = t2Active || (deviceConnected && bleRelay2);
|
(connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]);
|
||||||
|
relay2State = t2Active ||
|
||||||
|
(connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]);
|
||||||
|
|
||||||
|
leds[0] = (connectedCount > 0) ? CRGB::Green : CRGB::Black;
|
||||||
} else {
|
} else {
|
||||||
relay1State = false;
|
relay1State = false;
|
||||||
relay2State = false;
|
relay2State = false;
|
||||||
|
|
@ -484,33 +554,38 @@ void loop() {
|
||||||
digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW);
|
digitalWrite(RELAY1_PIN, relay1State ? HIGH : LOW);
|
||||||
digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW);
|
digitalWrite(RELAY2_PIN, relay2State ? HIGH : LOW);
|
||||||
|
|
||||||
digitalWrite(LED_PIN, (relay1State || relay2State) ? HIGH : LOW);
|
FastLED.show();
|
||||||
|
|
||||||
static bool lastRelay1 = false;
|
static bool lastRelay1 = false;
|
||||||
static bool lastRelay2 = false;
|
static bool lastRelay2 = false;
|
||||||
if (relay1State != lastRelay1 || relay2State != lastRelay2) {
|
if (relay1State != lastRelay1 || relay2State != lastRelay2) {
|
||||||
|
bool anyBle1 = (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]);
|
||||||
|
bool anyBle2 = (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]);
|
||||||
Serial.printf("Relays: R1=%s R2=%s (t1=%s t2=%s btn=%s ble1=%s ble2=%s)\n",
|
Serial.printf("Relays: R1=%s R2=%s (t1=%s t2=%s btn=%s ble1=%s ble2=%s)\n",
|
||||||
relay1State ? "ON" : "OFF",
|
relay1State ? "ON" : "OFF",
|
||||||
relay2State ? "ON" : "OFF",
|
relay2State ? "ON" : "OFF",
|
||||||
t1Active ? "YES" : "no",
|
t1Active ? "YES" : "no",
|
||||||
t2Active ? "YES" : "no",
|
t2Active ? "YES" : "no",
|
||||||
btnActive ? "YES" : "no",
|
btnActive ? "YES" : "no",
|
||||||
bleRelay1 ? "ON" : "OFF",
|
anyBle1 ? "ON" : "OFF",
|
||||||
bleRelay2 ? "ON" : "OFF");
|
anyBle2 ? "ON" : "OFF");
|
||||||
lastRelay1 = relay1State;
|
lastRelay1 = relay1State;
|
||||||
lastRelay2 = relay2State;
|
lastRelay2 = relay2State;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint32_t heartbeat = 0;
|
static uint32_t heartbeat = 0;
|
||||||
if (!otaInProgress && millis() - heartbeat > 5000) {
|
if (!otaInProgress && millis() - heartbeat > 5000) {
|
||||||
Serial.printf("[heartbeat] R1=%s R2=%s t1=%s t2=%s btn=%s ble1=%s ble2=%s\n",
|
bool anyBle1 = (connActive[0] && bleRelay1[0]) || (connActive[1] && bleRelay1[1]);
|
||||||
|
bool anyBle2 = (connActive[0] && bleRelay2[0]) || (connActive[1] && bleRelay2[1]);
|
||||||
|
Serial.printf("[heartbeat] R1=%s R2=%s t1=%s t2=%s btn=%s ble1=%s ble2=%s clients=%d\n",
|
||||||
relay1State ? "ON" : "OFF",
|
relay1State ? "ON" : "OFF",
|
||||||
relay2State ? "ON" : "OFF",
|
relay2State ? "ON" : "OFF",
|
||||||
t1Active ? "YES" : "no",
|
t1Active ? "YES" : "no",
|
||||||
t2Active ? "YES" : "no",
|
t2Active ? "YES" : "no",
|
||||||
btnActive ? "YES" : "no",
|
btnActive ? "YES" : "no",
|
||||||
bleRelay1 ? "ON" : "OFF",
|
anyBle1 ? "ON" : "OFF",
|
||||||
bleRelay2 ? "ON" : "OFF");
|
anyBle2 ? "ON" : "OFF",
|
||||||
|
connectedCount);
|
||||||
heartbeat = millis();
|
heartbeat = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
27
tools/pio_version_pre.py
Executable file
27
tools/pio_version_pre.py
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Pre-build: generate src/version.h from version.txt so the firmware
|
||||||
|
uses the single source of truth for version numbers."""
|
||||||
|
Import("env")
|
||||||
|
import os
|
||||||
|
|
||||||
|
project_dir = env.subst("$PROJECT_DIR")
|
||||||
|
ver_file = os.path.join(project_dir, "version.txt")
|
||||||
|
out_file = os.path.join(project_dir, "src", "version.h")
|
||||||
|
|
||||||
|
versions = {}
|
||||||
|
with open(ver_file) as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if "=" in line:
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
versions[k.strip()] = v.strip()
|
||||||
|
|
||||||
|
header = f"""// Auto-generated from version.txt — DO NOT EDIT
|
||||||
|
#define FIRMWARE_VERSION_SERVER "{versions['server']}"
|
||||||
|
#define FIRMWARE_VERSION_CLIENT "{versions['client']}"
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(out_file, "w") as f:
|
||||||
|
f.write(header)
|
||||||
|
|
||||||
|
print(f" versions: server={versions['server']} client={versions['client']} -> src/version.h")
|
||||||
2
version.txt
Normal file
2
version.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
server=8
|
||||||
|
client=1
|
||||||
Loading…
Reference in a new issue