From 57d74747c2ee53e005fcadf794a21fe6ed4374c3 Mon Sep 17 00:00:00 2001 From: unlogisch04 <98281608+unlogisch04@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:07:37 +0100 Subject: [PATCH 1/3] Fix Serial output --- .../java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt b/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt index 1b911837be..e70be04786 100644 --- a/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt +++ b/server/desktop/src/main/java/dev/slimevr/desktop/serial/DesktopSerialHandler.kt @@ -190,7 +190,7 @@ class DesktopSerialHandler : } override fun write(buff: ByteArray) { - LogManager.info("[SerialHandler] WRITING $buff") + LogManager.info("[SerialHandler] WRITING ${buff.toString(Charsets.UTF_8)}") currentPort?.outputStream?.write(buff) } From bd6d9b693ab2936dbc6390f4bb26f8f95cdf6319 Mon Sep 17 00:00:00 2001 From: unlogisch04 <98281608+unlogisch04@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:12:52 +0100 Subject: [PATCH 2/3] Try to set MCU into flash mode over Serial --- .../java/dev/slimevr/firmware/FirmwareUpdateHandler.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt index ecc3e160c0..ced72fd745 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt @@ -156,6 +156,15 @@ class FirmwareUpdateHandler(private val server: VRServer) : flasher.addBin(part.firmware, part.offset.toInt()) } +// TODO: +// - Check if FW is able to use flashmode +// - Add check if the flashmode was successfully set to surpress the request +// for manual flashmode setting prompt in gui + + server.serialHandler.openSerial(deviceId.id, false) + server.serialHandler.write("SET FLASHMODE\r\n".toByteArray()) + server.serialHandler.closeSerial() + flasher.addProgressListener(object : FlashingProgressListener { override fun progress(progress: Float) { onStatusChange( From 5e89e64e27f5e591de351ccee685f8cbc58080ad Mon Sep 17 00:00:00 2001 From: unlogisch04 <98281608+unlogisch04@users.noreply.github.com> Date: Sun, 25 Jan 2026 23:29:06 +0100 Subject: [PATCH 3/3] Add ESP32 Upload possibility --- .../slimevr/firmware/FirmwareUpdateHandler.kt | 17 ++++++ .../dev/slimevr/firmware/OTAUpdateTask.kt | 53 +++++++++++++++---- 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt index ced72fd745..00a4d8c084 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/FirmwareUpdateHandler.kt @@ -11,6 +11,7 @@ import dev.slimevr.serial.SerialPort import dev.slimevr.tracking.trackers.Tracker import dev.slimevr.tracking.trackers.TrackerStatus import dev.slimevr.tracking.trackers.TrackerStatusListener +import dev.slimevr.tracking.trackers.udp.MCUType import dev.slimevr.tracking.trackers.udp.UDPDevice import io.eiren.util.logging.LogManager import kotlinx.coroutines.* @@ -101,11 +102,27 @@ class FirmwareUpdateHandler(private val server: VRServer) : ) return@suspendCancellableCoroutine } + +// TODO: +// - Use the Firmware Builder to get the expected MCU +// It would be wrong to assume that the Target MCU is the correct one, +// just because the device is listening on the correct port. +// The Upload protocol does not verify the compatibility of the firmware with the MCU. + + val port = when (udpDevice.mcuType) { + MCUType.ESP8266 -> 8266 + MCUType.ESP32, MCUType.ESP32_C3 -> 3232 + else -> error("MCU-Typ: ${udpDevice.mcuType} not supported for OTA updates") + } + + LogManager.info("[FirmwareUpdateHandler] Starting OTA update for device ${deviceId.id} at ${udpDevice.ipAddress.hostAddress}:$port and MCU ${udpDevice.mcuType}") + val task = OTAUpdateTask( part.firmware, deviceId, udpDevice.ipAddress, ::onStatusChange, + port, ) c.invokeOnCancellation { task.cancel() diff --git a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt index 7d934b8588..19ad97626f 100644 --- a/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt +++ b/server/core/src/main/java/dev/slimevr/firmware/OTAUpdateTask.kt @@ -14,6 +14,8 @@ import java.security.MessageDigest import java.security.NoSuchAlgorithmException import java.util.* import java.util.function.Consumer +import javax.crypto.SecretKeyFactory +import javax.crypto.spec.PBEKeySpec import kotlin.math.min class OTAUpdateTask( @@ -21,8 +23,9 @@ class OTAUpdateTask( private val deviceId: UpdateDeviceId, private val deviceIp: InetAddress, private val statusCallback: Consumer>, + private val port: Int = 8266, ) { - private val receiveBuffer: ByteArray = ByteArray(38) + private val receiveBuffer: ByteArray = ByteArray(69) var socketServer: ServerSocket? = null var uploadSocket: Socket? = null var authSocket: DatagramSocket? = null @@ -40,17 +43,35 @@ class OTAUpdateTask( return md5str.toString() } + @Throws(NoSuchAlgorithmException::class) + private fun bytesToSha256(bytes: ByteArray): String { + val sha256 = MessageDigest.getInstance("SHA-256") + sha256.update(bytes) + val digest = sha256.digest() + val sha256str = StringBuilder() + for (b in digest) { + sha256str.append(String.format("%02x", b)) + } + return sha256str.toString() + } + + fun pbkdf2Hmac(password: String, salt: ByteArray, iterations: Int, keyLength: Int): ByteArray { + val spec = PBEKeySpec(password.toCharArray(), salt, iterations, keyLength * 8) + val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") + return factory.generateSecret(spec).encoded + } + private fun authenticate(localPort: Int): Boolean { try { DatagramSocket().use { socket -> authSocket = socket statusCallback.accept(UpdateStatusEvent(deviceId, FirmwareUpdateStatus.AUTHENTICATING)) - LogManager.info("[OTAUpdate] Sending OTA invitation to: $deviceIp") + LogManager.info("[OTAUpdate] Sending OTA invitation to: $deviceIp:$port") val fileMd5 = bytesToMd5(firmware) val message = "$FLASH $localPort ${firmware.size} $fileMd5\n" - socket.send(DatagramPacket(message.toByteArray(), message.length, deviceIp, PORT)) + socket.send(DatagramPacket(message.toByteArray(), message.length, deviceIp, port)) socket.soTimeout = 10000 val authPacket = DatagramPacket(receiveBuffer, receiveBuffer.size) @@ -68,13 +89,26 @@ class OTAUpdateTask( if (args.size != 2 || args[0] != "AUTH") return false LogManager.info("[OTAUpdate] Authenticating...") + var payload = "" + var signature = "" val authToken = args[1] - val signature = bytesToMd5(UUID.randomUUID().toString().toByteArray()) - val hashedPassword = bytesToMd5(PASSWORD.toByteArray()) - val resultText = "$hashedPassword:$authToken:$signature" - val payload = bytesToMd5(resultText.toByteArray()) - + if (authToken.length == 32) { + signature = + bytesToMd5(UUID.randomUUID().toString().toByteArray()) + val hashedPassword = bytesToMd5(PASSWORD.toByteArray()) + val resultText = "$hashedPassword:$authToken:$signature" + payload = bytesToMd5(resultText.toByteArray()) + } else if (authToken.length == 64) { + signature = + bytesToSha256(UUID.randomUUID().toString().toByteArray()) + val salt = "$authToken:$signature" + val hashedPassword = bytesToSha256(PASSWORD.toByteArray()) + val derivedkey = pbkdf2Hmac(hashedPassword, salt.toByteArray(), 10000, 32) + val derivedkeyHex = derivedkey.joinToString("") { "%02x".format(it) } + val challenge = "$derivedkeyHex:$authToken:$signature" + payload = bytesToSha256(challenge.toByteArray()) + } val authMessage = "$AUTH $signature $payload\n" socket.soTimeout = 10000 @@ -83,7 +117,7 @@ class OTAUpdateTask( authMessage.toByteArray(), authMessage.length, deviceIp, - PORT, + port, ), ) @@ -198,7 +232,6 @@ class OTAUpdateTask( companion object { private const val FLASH = 0 - private const val PORT = 8266 private const val PASSWORD = "SlimeVR-OTA" private const val AUTH = 200 }