Skip to content

Commit

Permalink
More robust command extractor state machine (#187)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dkaukov authored Jan 25, 2025
1 parent 162f70d commit 5764d94
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 173 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -151,11 +150,10 @@ public class RadioAudioService extends Service {
private LiveData<List<ChannelMemory>> 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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Byte, byte[]> onCommand;

public RxStreamParser(BiConsumer<Byte, byte[]> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) {
Expand Down

0 comments on commit 5764d94

Please sign in to comment.