From 5764d94d1e5a9385ffa89560ef7efefe1ce9d428 Mon Sep 17 00:00:00 2001 From: Dmitry Kaukov Date: Sun, 26 Jan 2025 07:11:16 +1100 Subject: [PATCH] More robust command extractor state machine (#187) * ADC bias injection - removes clipping - makes louder * ADC auto DC removal 1) deals with ESP32 ADC reference voltage variation 2) 16 bit i2s transfers * DRA module retries. * Android: Switch AudioTrack to the AudioFormat.ENCODING_PCM_16BIT As some devices have broken PCM8 support * More robust command extractor state machine Old one was loosing data * Cleanup * Fixed merge confilict * signed pcm8 on wire --- .../kv4pht/radio/RadioAudioService.java | 147 +----------------- .../vagell/kv4pht/radio/RxStreamParser.java | 65 ++++++++ .../kv4p_ht_esp32_wroom_32.ino | 44 ++---- 3 files changed, 83 insertions(+), 173 deletions(-) create mode 100644 android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RxStreamParser.java diff --git a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java index 5ad13ede..71f304ae 100644 --- a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java +++ b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java @@ -81,7 +81,6 @@ kv4p HT (see http://kv4p.com) import org.apache.commons.lang3.ArrayUtils; -import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; @@ -151,11 +150,10 @@ public class RadioAudioService extends Service { private LiveData> channelMemoriesLiveData = null; // Delimiter must match ESP32 code - private static final byte[] COMMAND_DELIMITER = new byte[] {(byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00}; + static final byte[] COMMAND_DELIMITER = new byte[] {(byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF}; private static final byte COMMAND_SMETER_REPORT = 0x53; // Ascii "S" - // This buffer holds leftover data that wasn’t fully parsed yet (from ESP32 audio stream) - private final ByteArrayOutputStream leftoverBuffer = new ByteArrayOutputStream(); + private final RxStreamParser rxStreamParser = new RxStreamParser(this::handleParsedCommand); // AFSK modem private Afsk1200Modulator afskModulator = null; @@ -1333,7 +1331,7 @@ private void handleESP32Data(byte[] data) { if (mode == MODE_RX || mode == MODE_SCAN) { // Handle and remove any commands (e.g. S-meter updates) embedded in the audio. - data = extractAudioAndHandleCommands(data); + data = rxStreamParser.extractAudioAndHandleCommands(data); if (prebufferComplete && audioTrack != null) { synchronized (audioTrack) { @@ -1403,145 +1401,6 @@ private void handleESP32Data(byte[] data) { } } - private synchronized byte[] extractAudioAndHandleCommands(byte[] newData) { - // 1. Append the new data to leftover. - leftoverBuffer.write(newData, 0, newData.length); - byte[] buffer = leftoverBuffer.toByteArray(); - - ByteArrayOutputStream audioOut = new ByteArrayOutputStream(); - int parsePos = 0; - - while (true) { - int startDelim = indexOf(buffer, COMMAND_DELIMITER, parsePos); - if (startDelim == -1) { - // -- NO FULL DELIMITER FOUND IN [buffer] STARTING AT parsePos -- - - // We might have a *partial* delimiter at the tail of [buffer]. - // Figure out how many trailing bytes might match the start of the next command. - int partialLen = findPartialDelimiterTail(buffer, parsePos, buffer.length); - - // "pureAudioEnd" is where pure audio stops and partial leftover begins. - int pureAudioEnd = buffer.length - partialLen; - - // Write the "definitely audio" portion to our output. - if (pureAudioEnd > parsePos) { - audioOut.write(buffer, parsePos, pureAudioEnd - parsePos); - } - - // Store ONLY the partial leftover so we can complete the delimiter/command next time. - leftoverBuffer.reset(); - if (partialLen > 0) { - leftoverBuffer.write(buffer, pureAudioEnd, partialLen); - } - - // Return everything we've decoded as audio so far. - return audioOut.toByteArray(); - } - - // -- FOUND A DELIMITER -- - // Everything from parsePos..(startDelim) is audio - if (startDelim > parsePos) { - audioOut.write(buffer, parsePos, startDelim - parsePos); - } - - // Check if we have enough bytes for "delimiter + cmd + paramLen" - int neededBeforeParams = COMMAND_DELIMITER.length + 2; - // (1 for cmd byte, 1 for paramLen byte) - if (startDelim + neededBeforeParams > buffer.length) { - // Not enough data => partial command leftover - storeTailForNextTime(buffer, startDelim); - return audioOut.toByteArray(); - } - - int cmdPos = startDelim + COMMAND_DELIMITER.length; - byte cmd = buffer[cmdPos]; - int paramLen = (buffer[cmdPos + 1] & 0xFF); - int paramStart = cmdPos + 2; - int paramEnd = paramStart + paramLen; // one past the last param byte - - if (paramEnd > buffer.length) { - // Again, partial command leftover - storeTailForNextTime(buffer, startDelim); - return audioOut.toByteArray(); - } - - // We have a full command => handle it - byte[] param = Arrays.copyOfRange(buffer, paramStart, paramEnd); - handleParsedCommand(cmd, param); - - // Advance parsePos beyond this entire command block - parsePos = paramEnd; - } - } - - /** - * Stores the tail of 'buffer' from 'startIndex' to end into leftoverBuffer, - * for the next invocation of extractAudioAndHandleCommands(). - */ - private void storeTailForNextTime(byte[] buffer, int startIndex) { - leftoverBuffer.reset(); - leftoverBuffer.write(buffer, startIndex, buffer.length - startIndex); - } - - /** - * Finds the first occurrence of 'pattern' in 'data' at or after 'start'. - * Returns -1 if not found. - */ - private int indexOf(byte[] data, byte[] pattern, int start) { - if (pattern.length == 0 || start >= data.length) { - return -1; - } - for (int i = start; i <= data.length - pattern.length; i++) { - boolean found = true; - for (int j = 0; j < pattern.length; j++) { - if (data[i + j] != pattern[j]) { - found = false; - break; - } - } - if (found) { - return i; - } - } - return -1; - } - - /** - * Checks how many trailing bytes in [data, from parsePos..end) might match the - * *start* of our delimiter (or partial command). - * - * For example, if COMMAND_DELIMITER = { (byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00 }, - * we see if the tail ends with 1, 2, or 3 bytes that match the first 1, 2, or 3 bytes - * of COMMAND_DELIMITER. - * - * Return value: the number of trailing bytes that match - * (range 0..COMMAND_DELIMITER.length - 1). - */ - private int findPartialDelimiterTail(byte[] data, int start, int end) { - final int dataLen = end - start; - // We'll check from the largest possible partial (delimiter.length - 1) down to 1 - // because if a bigger partial matches, that's our answer. - for (int checkSize = COMMAND_DELIMITER.length - 1; checkSize >= 1; checkSize--) { - if (checkSize > dataLen) { - continue; // can't match if leftover is too small - } - boolean match = true; - // Compare data[end-checkSize .. end-1] to delimiter[0..checkSize-1] - for (int j = 0; j < checkSize; j++) { - if (data[end - checkSize + j] != COMMAND_DELIMITER[j]) { - match = false; - break; - } - } - if (match) { - // We found the largest partial match - return checkSize; - } - } - // If no partial match, return 0 - return 0; - } - private void handleParsedCommand(byte cmd, byte[] param) { if (cmd == COMMAND_SMETER_REPORT) { if (param.length >= 1) { diff --git a/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RxStreamParser.java b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RxStreamParser.java new file mode 100644 index 00000000..b5370ae9 --- /dev/null +++ b/android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RxStreamParser.java @@ -0,0 +1,65 @@ +package com.vagell.kv4pht.radio; + +import static com.vagell.kv4pht.radio.RadioAudioService.COMMAND_DELIMITER; + +import java.io.ByteArrayOutputStream; +import java.util.function.BiConsumer; + +public class RxStreamParser { + + private int matchedDelimiterTokens = 0; + private byte command; + private byte commandParamLen; + private final ByteArrayOutputStream commandParams = new ByteArrayOutputStream(); + private final ByteArrayOutputStream lookaheadBuffer = new ByteArrayOutputStream(); + + private final BiConsumer onCommand; + + public RxStreamParser(BiConsumer onCommand) { + this.onCommand = onCommand; + } + + public byte[] extractAudioAndHandleCommands(byte[] newData) { + ByteArrayOutputStream audioOut = new ByteArrayOutputStream(); + for (byte b : newData) { + lookaheadBuffer.write(b); + if (matchedDelimiterTokens < COMMAND_DELIMITER.length) { + if (b == COMMAND_DELIMITER[matchedDelimiterTokens]) { + matchedDelimiterTokens++; + } else { + flushLookaheadBuffer(audioOut); + matchedDelimiterTokens = 0; + } + } else if (matchedDelimiterTokens == COMMAND_DELIMITER.length) { + command = b; + matchedDelimiterTokens++; + } else if (matchedDelimiterTokens == COMMAND_DELIMITER.length + 1) { + commandParamLen = b; + commandParams.reset(); + matchedDelimiterTokens++; + } else { + commandParams.write(b); + matchedDelimiterTokens++; + lookaheadBuffer.reset(); + if (commandParams.size() == commandParamLen) { + onCommand.accept(command, commandParams.toByteArray()); + resetParser(audioOut); + } + } + } + return audioOut.toByteArray(); + } + + private void flushLookaheadBuffer(ByteArrayOutputStream audioOut) { + byte[] buffer = lookaheadBuffer.toByteArray(); + audioOut.write(buffer, 0, buffer.length); + lookaheadBuffer.reset(); + } + + private void resetParser(ByteArrayOutputStream audioOut) { + flushLookaheadBuffer(audioOut); + matchedDelimiterTokens = 0; + commandParams.reset(); + commandParamLen = 0; + } +} \ No newline at end of file diff --git a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino index 1e2b8276..16e7587b 100644 --- a/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino +++ b/microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino @@ -45,7 +45,7 @@ long lastSMeterReport = -1; // Delimeter must also match Android app #define DELIMITER_LENGTH 8 -const uint8_t COMMAND_DELIMITER[DELIMITER_LENGTH] = {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00}; +const uint8_t COMMAND_DELIMITER[DELIMITER_LENGTH] = {0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF}; int matchedDelimiterTokens = 0; int matchedDelimiterTokensRx = 0; @@ -643,34 +643,20 @@ void loop() { * - paramLen is up to 255 * - param data is 'paramLen' bytes */ -void sendCmdToAndroid(byte cmdByte, const byte* params, size_t paramsLen) -{ - // Safety check: limit paramsLen to 255 for 1-byte length - if (paramsLen > 255) { - paramsLen = 255; // or handle differently (split, or error, etc.) - } - - const size_t totalSize = DELIMITER_LENGTH + 1 + 1 + paramsLen; - byte outBytes[totalSize]; - - // 1. Leading delimiter - memcpy(outBytes, COMMAND_DELIMITER, DELIMITER_LENGTH); - - // 2. Command byte - outBytes[DELIMITER_LENGTH] = cmdByte; - - // 3. Parameter length - outBytes[DELIMITER_LENGTH + 1] = (byte)(paramsLen & 0xFF); - - // 4. Parameter bytes - memcpy( - outBytes + DELIMITER_LENGTH + 2, // position after delim+cmd+paramLen - params, - paramsLen - ); - - Serial.write(outBytes, totalSize); - Serial.flush(); +void sendCmdToAndroid(byte cmdByte, const byte* params, size_t paramsLen) { + // Safety check: limit paramsLen to 255 for 1-byte length + if (paramsLen > 255) { + paramsLen = 255; // or handle differently (split, or error, etc.) + } + // 1. Leading delimiter + Serial.write(COMMAND_DELIMITER, DELIMITER_LENGTH); + // 2. Command byte + Serial.write(&cmdByte, 1); + // 3. Parameter length + uint8_t len = paramsLen; + Serial.write(&len, 1); + // 4. Parameter bytes + Serial.write(params, paramsLen); } void tuneTo(float freqTx, float freqRx, int txTone, int rxTone, int squelch, String bandwidth) {