Skip to content

Commit 5764d94

Browse files
authored
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
1 parent 162f70d commit 5764d94

File tree

3 files changed

+83
-173
lines changed

3 files changed

+83
-173
lines changed

android-src/KV4PHT/app/src/main/java/com/vagell/kv4pht/radio/RadioAudioService.java

Lines changed: 3 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@ kv4p HT (see http://kv4p.com)
8181

8282
import org.apache.commons.lang3.ArrayUtils;
8383

84-
import java.io.ByteArrayOutputStream;
8584
import java.io.UnsupportedEncodingException;
8685
import java.nio.ByteBuffer;
8786
import java.nio.charset.StandardCharsets;
@@ -151,11 +150,10 @@ public class RadioAudioService extends Service {
151150
private LiveData<List<ChannelMemory>> channelMemoriesLiveData = null;
152151

153152
// Delimiter must match ESP32 code
154-
private static final byte[] COMMAND_DELIMITER = new byte[] {(byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00};
153+
static final byte[] COMMAND_DELIMITER = new byte[] {(byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF, (byte)0xDE, (byte)0xAD, (byte)0xBE, (byte)0xEF};
155154
private static final byte COMMAND_SMETER_REPORT = 0x53; // Ascii "S"
156155

157-
// This buffer holds leftover data that wasn’t fully parsed yet (from ESP32 audio stream)
158-
private final ByteArrayOutputStream leftoverBuffer = new ByteArrayOutputStream();
156+
private final RxStreamParser rxStreamParser = new RxStreamParser(this::handleParsedCommand);
159157

160158
// AFSK modem
161159
private Afsk1200Modulator afskModulator = null;
@@ -1333,7 +1331,7 @@ private void handleESP32Data(byte[] data) {
13331331

13341332
if (mode == MODE_RX || mode == MODE_SCAN) {
13351333
// Handle and remove any commands (e.g. S-meter updates) embedded in the audio.
1336-
data = extractAudioAndHandleCommands(data);
1334+
data = rxStreamParser.extractAudioAndHandleCommands(data);
13371335

13381336
if (prebufferComplete && audioTrack != null) {
13391337
synchronized (audioTrack) {
@@ -1403,145 +1401,6 @@ private void handleESP32Data(byte[] data) {
14031401
}
14041402
}
14051403

1406-
private synchronized byte[] extractAudioAndHandleCommands(byte[] newData) {
1407-
// 1. Append the new data to leftover.
1408-
leftoverBuffer.write(newData, 0, newData.length);
1409-
byte[] buffer = leftoverBuffer.toByteArray();
1410-
1411-
ByteArrayOutputStream audioOut = new ByteArrayOutputStream();
1412-
int parsePos = 0;
1413-
1414-
while (true) {
1415-
int startDelim = indexOf(buffer, COMMAND_DELIMITER, parsePos);
1416-
if (startDelim == -1) {
1417-
// -- NO FULL DELIMITER FOUND IN [buffer] STARTING AT parsePos --
1418-
1419-
// We might have a *partial* delimiter at the tail of [buffer].
1420-
// Figure out how many trailing bytes might match the start of the next command.
1421-
int partialLen = findPartialDelimiterTail(buffer, parsePos, buffer.length);
1422-
1423-
// "pureAudioEnd" is where pure audio stops and partial leftover begins.
1424-
int pureAudioEnd = buffer.length - partialLen;
1425-
1426-
// Write the "definitely audio" portion to our output.
1427-
if (pureAudioEnd > parsePos) {
1428-
audioOut.write(buffer, parsePos, pureAudioEnd - parsePos);
1429-
}
1430-
1431-
// Store ONLY the partial leftover so we can complete the delimiter/command next time.
1432-
leftoverBuffer.reset();
1433-
if (partialLen > 0) {
1434-
leftoverBuffer.write(buffer, pureAudioEnd, partialLen);
1435-
}
1436-
1437-
// Return everything we've decoded as audio so far.
1438-
return audioOut.toByteArray();
1439-
}
1440-
1441-
// -- FOUND A DELIMITER --
1442-
// Everything from parsePos..(startDelim) is audio
1443-
if (startDelim > parsePos) {
1444-
audioOut.write(buffer, parsePos, startDelim - parsePos);
1445-
}
1446-
1447-
// Check if we have enough bytes for "delimiter + cmd + paramLen"
1448-
int neededBeforeParams = COMMAND_DELIMITER.length + 2;
1449-
// (1 for cmd byte, 1 for paramLen byte)
1450-
if (startDelim + neededBeforeParams > buffer.length) {
1451-
// Not enough data => partial command leftover
1452-
storeTailForNextTime(buffer, startDelim);
1453-
return audioOut.toByteArray();
1454-
}
1455-
1456-
int cmdPos = startDelim + COMMAND_DELIMITER.length;
1457-
byte cmd = buffer[cmdPos];
1458-
int paramLen = (buffer[cmdPos + 1] & 0xFF);
1459-
int paramStart = cmdPos + 2;
1460-
int paramEnd = paramStart + paramLen; // one past the last param byte
1461-
1462-
if (paramEnd > buffer.length) {
1463-
// Again, partial command leftover
1464-
storeTailForNextTime(buffer, startDelim);
1465-
return audioOut.toByteArray();
1466-
}
1467-
1468-
// We have a full command => handle it
1469-
byte[] param = Arrays.copyOfRange(buffer, paramStart, paramEnd);
1470-
handleParsedCommand(cmd, param);
1471-
1472-
// Advance parsePos beyond this entire command block
1473-
parsePos = paramEnd;
1474-
}
1475-
}
1476-
1477-
/**
1478-
* Stores the tail of 'buffer' from 'startIndex' to end into leftoverBuffer,
1479-
* for the next invocation of extractAudioAndHandleCommands().
1480-
*/
1481-
private void storeTailForNextTime(byte[] buffer, int startIndex) {
1482-
leftoverBuffer.reset();
1483-
leftoverBuffer.write(buffer, startIndex, buffer.length - startIndex);
1484-
}
1485-
1486-
/**
1487-
* Finds the first occurrence of 'pattern' in 'data' at or after 'start'.
1488-
* Returns -1 if not found.
1489-
*/
1490-
private int indexOf(byte[] data, byte[] pattern, int start) {
1491-
if (pattern.length == 0 || start >= data.length) {
1492-
return -1;
1493-
}
1494-
for (int i = start; i <= data.length - pattern.length; i++) {
1495-
boolean found = true;
1496-
for (int j = 0; j < pattern.length; j++) {
1497-
if (data[i + j] != pattern[j]) {
1498-
found = false;
1499-
break;
1500-
}
1501-
}
1502-
if (found) {
1503-
return i;
1504-
}
1505-
}
1506-
return -1;
1507-
}
1508-
1509-
/**
1510-
* Checks how many trailing bytes in [data, from parsePos..end) might match the
1511-
* *start* of our delimiter (or partial command).
1512-
*
1513-
* For example, if COMMAND_DELIMITER = { (byte)0xFF, (byte)0x00, (byte)0xFF, (byte)0x00 },
1514-
* we see if the tail ends with 1, 2, or 3 bytes that match the first 1, 2, or 3 bytes
1515-
* of COMMAND_DELIMITER.
1516-
*
1517-
* Return value: the number of trailing bytes that match
1518-
* (range 0..COMMAND_DELIMITER.length - 1).
1519-
*/
1520-
private int findPartialDelimiterTail(byte[] data, int start, int end) {
1521-
final int dataLen = end - start;
1522-
// We'll check from the largest possible partial (delimiter.length - 1) down to 1
1523-
// because if a bigger partial matches, that's our answer.
1524-
for (int checkSize = COMMAND_DELIMITER.length - 1; checkSize >= 1; checkSize--) {
1525-
if (checkSize > dataLen) {
1526-
continue; // can't match if leftover is too small
1527-
}
1528-
boolean match = true;
1529-
// Compare data[end-checkSize .. end-1] to delimiter[0..checkSize-1]
1530-
for (int j = 0; j < checkSize; j++) {
1531-
if (data[end - checkSize + j] != COMMAND_DELIMITER[j]) {
1532-
match = false;
1533-
break;
1534-
}
1535-
}
1536-
if (match) {
1537-
// We found the largest partial match
1538-
return checkSize;
1539-
}
1540-
}
1541-
// If no partial match, return 0
1542-
return 0;
1543-
}
1544-
15451404
private void handleParsedCommand(byte cmd, byte[] param) {
15461405
if (cmd == COMMAND_SMETER_REPORT) {
15471406
if (param.length >= 1) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package com.vagell.kv4pht.radio;
2+
3+
import static com.vagell.kv4pht.radio.RadioAudioService.COMMAND_DELIMITER;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.util.function.BiConsumer;
7+
8+
public class RxStreamParser {
9+
10+
private int matchedDelimiterTokens = 0;
11+
private byte command;
12+
private byte commandParamLen;
13+
private final ByteArrayOutputStream commandParams = new ByteArrayOutputStream();
14+
private final ByteArrayOutputStream lookaheadBuffer = new ByteArrayOutputStream();
15+
16+
private final BiConsumer<Byte, byte[]> onCommand;
17+
18+
public RxStreamParser(BiConsumer<Byte, byte[]> onCommand) {
19+
this.onCommand = onCommand;
20+
}
21+
22+
public byte[] extractAudioAndHandleCommands(byte[] newData) {
23+
ByteArrayOutputStream audioOut = new ByteArrayOutputStream();
24+
for (byte b : newData) {
25+
lookaheadBuffer.write(b);
26+
if (matchedDelimiterTokens < COMMAND_DELIMITER.length) {
27+
if (b == COMMAND_DELIMITER[matchedDelimiterTokens]) {
28+
matchedDelimiterTokens++;
29+
} else {
30+
flushLookaheadBuffer(audioOut);
31+
matchedDelimiterTokens = 0;
32+
}
33+
} else if (matchedDelimiterTokens == COMMAND_DELIMITER.length) {
34+
command = b;
35+
matchedDelimiterTokens++;
36+
} else if (matchedDelimiterTokens == COMMAND_DELIMITER.length + 1) {
37+
commandParamLen = b;
38+
commandParams.reset();
39+
matchedDelimiterTokens++;
40+
} else {
41+
commandParams.write(b);
42+
matchedDelimiterTokens++;
43+
lookaheadBuffer.reset();
44+
if (commandParams.size() == commandParamLen) {
45+
onCommand.accept(command, commandParams.toByteArray());
46+
resetParser(audioOut);
47+
}
48+
}
49+
}
50+
return audioOut.toByteArray();
51+
}
52+
53+
private void flushLookaheadBuffer(ByteArrayOutputStream audioOut) {
54+
byte[] buffer = lookaheadBuffer.toByteArray();
55+
audioOut.write(buffer, 0, buffer.length);
56+
lookaheadBuffer.reset();
57+
}
58+
59+
private void resetParser(ByteArrayOutputStream audioOut) {
60+
flushLookaheadBuffer(audioOut);
61+
matchedDelimiterTokens = 0;
62+
commandParams.reset();
63+
commandParamLen = 0;
64+
}
65+
}

microcontroller-src/kv4p_ht_esp32_wroom_32/kv4p_ht_esp32_wroom_32.ino

Lines changed: 15 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ long lastSMeterReport = -1;
4545

4646
// Delimeter must also match Android app
4747
#define DELIMITER_LENGTH 8
48-
const uint8_t COMMAND_DELIMITER[DELIMITER_LENGTH] = {0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00};
48+
const uint8_t COMMAND_DELIMITER[DELIMITER_LENGTH] = {0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF};
4949
int matchedDelimiterTokens = 0;
5050
int matchedDelimiterTokensRx = 0;
5151

@@ -643,34 +643,20 @@ void loop() {
643643
* - paramLen is up to 255
644644
* - param data is 'paramLen' bytes
645645
*/
646-
void sendCmdToAndroid(byte cmdByte, const byte* params, size_t paramsLen)
647-
{
648-
// Safety check: limit paramsLen to 255 for 1-byte length
649-
if (paramsLen > 255) {
650-
paramsLen = 255; // or handle differently (split, or error, etc.)
651-
}
652-
653-
const size_t totalSize = DELIMITER_LENGTH + 1 + 1 + paramsLen;
654-
byte outBytes[totalSize];
655-
656-
// 1. Leading delimiter
657-
memcpy(outBytes, COMMAND_DELIMITER, DELIMITER_LENGTH);
658-
659-
// 2. Command byte
660-
outBytes[DELIMITER_LENGTH] = cmdByte;
661-
662-
// 3. Parameter length
663-
outBytes[DELIMITER_LENGTH + 1] = (byte)(paramsLen & 0xFF);
664-
665-
// 4. Parameter bytes
666-
memcpy(
667-
outBytes + DELIMITER_LENGTH + 2, // position after delim+cmd+paramLen
668-
params,
669-
paramsLen
670-
);
671-
672-
Serial.write(outBytes, totalSize);
673-
Serial.flush();
646+
void sendCmdToAndroid(byte cmdByte, const byte* params, size_t paramsLen) {
647+
// Safety check: limit paramsLen to 255 for 1-byte length
648+
if (paramsLen > 255) {
649+
paramsLen = 255; // or handle differently (split, or error, etc.)
650+
}
651+
// 1. Leading delimiter
652+
Serial.write(COMMAND_DELIMITER, DELIMITER_LENGTH);
653+
// 2. Command byte
654+
Serial.write(&cmdByte, 1);
655+
// 3. Parameter length
656+
uint8_t len = paramsLen;
657+
Serial.write(&len, 1);
658+
// 4. Parameter bytes
659+
Serial.write(params, paramsLen);
674660
}
675661

676662
void tuneTo(float freqTx, float freqRx, int txTone, int rxTone, int squelch, String bandwidth) {

0 commit comments

Comments
 (0)