diff --git a/src/main/java/ru/r2cloud/jradio/openlst/OpenLst.java b/src/main/java/ru/r2cloud/jradio/openlst/OpenLst.java new file mode 100644 index 00000000..06594098 --- /dev/null +++ b/src/main/java/ru/r2cloud/jradio/openlst/OpenLst.java @@ -0,0 +1,246 @@ +package ru.r2cloud.jradio.openlst; + +import ru.r2cloud.jradio.blocks.AdditiveScrambler; +import ru.r2cloud.jradio.crc.Crc16Cc11xx; +import ru.r2cloud.jradio.fec.ccsds.UncorrectableException; + +// Implementation is based on: +// Design Note DN504 +// Design Note DN507 +// The only difference is checksum. OpenLst include checksum into length, while cc11xx doesn't +public class OpenLst { + + private static final int[] FEC_ENCODE_TABLE = new int[] { 0, 3, 1, 2, 3, 0, 2, 1, 3, 0, 2, 1, 0, 3, 1, 2 }; + // @formatter:off + private static final int[][] aTrellisSourceStateLut = new int[][] { + {0, 4}, // State {0,4} -> State 0 + {0, 4}, // State {0,4} -> State 1 + {1, 5}, // State {1,5} -> State 2 + {1, 5}, // State {1,5} -> State 3 + {2, 6}, // State {2,6} -> State 4 + {2, 6}, // State {2,6} -> State 5 + {3, 7}, // State {3,7} -> State 6 + {3, 7}, // State {3,7} -> State 7 + }; + private static final int[][] aTrellisTransitionOutput = new int[][] { + {0, 3}, // State {0,4} -> State 0 produces {"00", "11"} + {3, 0}, // State {0,4} -> State 1 produces {"11", "00"} + {1, 2}, // State {1,5} -> State 2 produces {"01", "10"} + {2, 1}, // State {1,5} -> State 3 produces {"10", "01"} + {3, 0}, // State {2,6} -> State 4 produces {"11", "00"} + {0, 3}, // State {2,6} -> State 5 produces {"00", "11"} + {2, 1}, // State {3,7} -> State 6 produces {"10", "01"} + {1, 2}, // State {3,7} -> State 7 produces {"01", "10"} + }; + // @formatter:on + private static final int[] aTrellisTransitionInput = new int[] { 0, 1, 0, 1, 0, 1, 0, 1 }; + + private final int[][] nCost = new int[2][8]; // Accumulated path cost + private final long[][] aPath = new long[2][8]; // Encoder input data (32b window) + private final boolean scrambling; + + private final byte[] temp = new byte[4]; + private final byte[] chunkTemp = new byte[4]; + private final byte[] dataTemp; + private final AdditiveScrambler scrambler = new AdditiveScrambler(0x21, 0x1ff, 8, 8); + + // Indices of (last, current) buffer for each iteration + private int iLastBuf; + private int iCurrBuf; + // Number of bits in each path buffer + private int nPathBits; + + public OpenLst(int dataLength) { + this(true, dataLength); + } + + public OpenLst(boolean scrambling, int dataLength) { + reset(); + this.scrambling = scrambling; + // might be overkill, but we're dealing with very small messages + this.dataTemp = new byte[dataLength]; + } + + public byte[] decode(byte[] data) throws UncorrectableException { + int currentTempIndex = 0; + for (int i = 0; i < data.length; i += 4) { + int actualBytes = fecDecode(data, i, 4, chunkTemp, data.length - i); + for (int j = 0; j < actualBytes; j++) { + dataTemp[currentTempIndex++] = chunkTemp[j]; + } + } + reset(); + if (scrambling) { + scrambler.shuffle(dataTemp); + } + int length = (dataTemp[0] & 0xFF) + 1; + if (length > currentTempIndex) { + throw new UncorrectableException("unexpected length: " + length); + } + int actual = ((dataTemp[length - 1] & 0xFF) << 8) | (dataTemp[length - 2] & 0xFF); + int expected = Crc16Cc11xx.calculate(dataTemp, 0, length - 2); + if (expected != actual) { + throw new UncorrectableException("crc mismatch"); + } + byte[] result = new byte[length - 3]; + System.arraycopy(dataTemp, 1, result, 0, result.length); + return result; + } + + public byte[] encode(byte[] data) { + int inputNum = data.length + 1; + byte[] input = new byte[inputNum + 2 + 2]; + input[0] = (byte) (data.length + 2); + System.arraycopy(data, 0, input, 1, data.length); + + int crc = Crc16Cc11xx.calculate(input, 0, inputNum); + input[inputNum++] = (byte) (crc & 0x00FF); + input[inputNum++] = (byte) (crc >> 8); + + input[inputNum] = 0x0B; + input[inputNum + 1] = 0x0B; + + if (scrambling) { + scrambler.shuffle(input); + } + + int fecNum = 2 * ((inputNum / 2) + 1); + int fecReg = 0; + byte[] fec = new byte[fecNum * 2]; + for (int i = 0; i < fecNum; i++) { + fecReg = (fecReg & 0x700) | (input[i] & 0xFF); + int fecOutput = 0; + for (int j = 0; j < 8; j++) { + fecOutput = (fecOutput << 2) | FEC_ENCODE_TABLE[fecReg >> 7]; + fecReg = (fecReg << 1) & 0x7FF; + } + fec[i * 2] = (byte) (fecOutput >> 8); + fec[i * 2 + 1] = (byte) (fecOutput & 0xFF); + } + + for (int i = 0; i < fecNum * 2; i += 4) { + int intOutput = 0; + for (int j = 0; j < 4 * 4; j++) { + intOutput = (intOutput << 2) | ((fec[i + (~j & 0x03)] >> (2 * ((j & 0x0C) >> 2))) & 0x03); + } + fec[i] = (byte) ((intOutput >> 24) & 0xFF); + fec[i + 1] = (byte) ((intOutput >> 16) & 0xFF); + fec[i + 2] = (byte) ((intOutput >> 8) & 0xFF); + fec[i + 3] = (byte) ((intOutput >> 0) & 0xFF); + } + + return fec; + } + + private int fecDecode(byte[] data, int offset, int length, byte[] output, int nRemBytes) { + if (length != 4 || offset + length > data.length) { + throw new IllegalArgumentException(); + } + // De-interleave received data + for (int iOut = 0; iOut < 4; iOut++) { + byte dataByte = 0; + for (int iIn = 3; iIn >= 0; iIn--) { + dataByte = (byte) ((dataByte << 2) | (((data[iIn + offset] & 0xFF) >> (2 * iOut)) & 0x03)); + } + temp[iOut] = dataByte; + } + + // Variables used to hold # Viterbi iterations to run, # bytes output, + // minimum cost for any destination state, bit index of input symbol + int nMinCost = 0xFF; + int iBit = 8 - 2; + // Process up to 4 bytes of de-interleaved input data, processing one encoder + // symbol (2b) at a time + int currentIn = 0; + int currentOut = 0; + for (int nIterations = 16; nIterations > 0; nIterations--) { + int symbol = ((temp[currentIn]) >> iBit) & 0x03; + // Find minimum cost so that we can normalize costs (only last iteration used) + nMinCost = 0xFF; + // Get 2b input symbol (MSB first) and do one iteration of Viterbi decoding + iBit -= 2; + if (iBit < 0) { + iBit = 6; + currentIn++; // Update pointer to the next byte of received data + } + // For each destination state in the trellis, calculate hamming costs for both + // possible paths into state and + // select the one with lowest cost. + for (int iDestState = 0; iDestState < 8; iDestState++) { + int nCost0; + int nCost1; + int iSrcState0; + int iSrcState1; + int nInputBit; + nInputBit = aTrellisTransitionInput[iDestState]; + // Calculate cost of transition from each of the two source states (cost is + // Hamming difference between + // received 2b symbol and expected symbol for transition) + iSrcState0 = aTrellisSourceStateLut[iDestState][0]; + nCost0 = nCost[iLastBuf][iSrcState0]; + nCost0 += hammWeight(symbol ^ aTrellisTransitionOutput[iDestState][0]); + iSrcState1 = aTrellisSourceStateLut[iDestState][1]; + nCost1 = nCost[iLastBuf][iSrcState1]; + nCost1 += hammWeight(symbol ^ aTrellisTransitionOutput[iDestState][1]); + // Select transition that gives lowest cost in destination state, copy that + // source state's path and add + // new decoded bit + if (nCost0 <= nCost1) { + nCost[iCurrBuf][iDestState] = nCost0; + nMinCost = Math.min(nMinCost, nCost0); + aPath[iCurrBuf][iDestState] = ((aPath[iLastBuf][iSrcState0] << 1) | nInputBit); + } else { + nCost[iCurrBuf][iDestState] = nCost1; + nMinCost = Math.min(nMinCost, nCost1); + aPath[iCurrBuf][iDestState] = ((aPath[iLastBuf][iSrcState1] << 1) | nInputBit); + } + } + nPathBits++; + // If trellis history is sufficiently long, output a byte of decoded data + if (nPathBits == 32) { + output[currentOut] = (byte) ((aPath[iCurrBuf][0] >> 24) & 0xFF); + currentOut++; + nPathBits -= 8; + nRemBytes--; + } + // After having processed 3-symbol trellis terminator, flush out remaining data + if ((nRemBytes <= 3) && (nPathBits == ((8 * nRemBytes) + 3))) { + while (nPathBits >= 8) { + output[currentOut] = (byte) ((aPath[iCurrBuf][0] >> (nPathBits - 8)) & 0xFF); + currentOut++; + nPathBits -= 8; + } + return currentOut; + } + // Swap current and last buffers for next iteration + iLastBuf = (iLastBuf + 1) % 2; + iCurrBuf = (iCurrBuf + 1) % 2; + } + // Normalize costs so that minimum cost becomes 0 + for (int iState = 0; iState < 8; iState++) { + nCost[iLastBuf][iState] -= nMinCost; + } + return currentOut; + } + + private void reset() { + // Initialize variables at start of packet (and return without doing any more) + for (int i = 0; i < 8; i++) { + nCost[0][i] = 100; + nCost[1][i] = 0; + aPath[0][i] = 0; + aPath[1][i] = 0; + } + iLastBuf = 0; + iCurrBuf = 1; + nPathBits = 0; + } + + private static int hammWeight(int a) { + a = ((a & 0xAA) >> 1) + (a & 0x55); + a = ((a & 0xCC) >> 2) + (a & 0x33); + a = ((a & 0xF0) >> 4) + (a & 0x0F); + return a; + } + +} diff --git a/src/main/java/ru/r2cloud/jradio/openlst/OpenLstBeacon.java b/src/main/java/ru/r2cloud/jradio/openlst/OpenLstBeacon.java new file mode 100644 index 00000000..92d26b5b --- /dev/null +++ b/src/main/java/ru/r2cloud/jradio/openlst/OpenLstBeacon.java @@ -0,0 +1,65 @@ +package ru.r2cloud.jradio.openlst; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; + +import ru.r2cloud.jradio.Beacon; +import ru.r2cloud.jradio.fec.ccsds.UncorrectableException; + +public class OpenLstBeacon extends Beacon { + + private int flags; + private int seqnum; + private int hwid; + + private byte[] payload; + + @Override + public void readBeacon(byte[] data) throws IOException, UncorrectableException { + DataInputStream dis = new DataInputStream(new ByteArrayInputStream(data)); + flags = dis.readUnsignedByte(); + seqnum = dis.readUnsignedShort(); + readBeacon(dis); + hwid = dis.readUnsignedShort(); + } + + @SuppressWarnings("unused") + public void readBeacon(DataInputStream dis) throws IOException, UncorrectableException { + payload = new byte[dis.available() - 2]; + dis.readFully(payload); + } + + public int getFlags() { + return flags; + } + + public void setFlags(int flags) { + this.flags = flags; + } + + public int getSeqnum() { + return seqnum; + } + + public void setSeqnum(int seqnum) { + this.seqnum = seqnum; + } + + public int getHwid() { + return hwid; + } + + public void setHwid(int hwid) { + this.hwid = hwid; + } + + public byte[] getPayload() { + return payload; + } + + public void setPayload(byte[] payload) { + this.payload = payload; + } + +} diff --git a/src/main/java/ru/r2cloud/jradio/openlst/OpenLstBeaconSource.java b/src/main/java/ru/r2cloud/jradio/openlst/OpenLstBeaconSource.java new file mode 100644 index 00000000..35378dc4 --- /dev/null +++ b/src/main/java/ru/r2cloud/jradio/openlst/OpenLstBeaconSource.java @@ -0,0 +1,48 @@ +package ru.r2cloud.jradio.openlst; + +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ru.r2cloud.jradio.BeaconSource; +import ru.r2cloud.jradio.ByteInput; +import ru.r2cloud.jradio.blocks.CorrelateSyncword; +import ru.r2cloud.jradio.blocks.CorrelatedMarker; +import ru.r2cloud.jradio.blocks.SoftToHard; +import ru.r2cloud.jradio.blocks.UnpackedToPacked; +import ru.r2cloud.jradio.fec.ccsds.UncorrectableException; + +public class OpenLstBeaconSource extends BeaconSource { + + private static final Logger LOG = LoggerFactory.getLogger(OpenLstBeaconSource.class); + private final static int MAX_MESSAGE_SIZE = 520; + private final OpenLst fec = new OpenLst(MAX_MESSAGE_SIZE); + + private final Class clazz; + + public OpenLstBeaconSource(ByteInput input, Class clazz) { + super(new CorrelateSyncword(new SoftToHard(input), 4, "11010011100100011101001110010001", MAX_MESSAGE_SIZE * 8)); + this.clazz = clazz; + } + + @Override + protected T parseBeacon(byte[] raw) throws UncorrectableException, IOException { + byte[] data = fec.decode(UnpackedToPacked.pack(raw)); + T result; + try { + result = clazz.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + LOG.error("unable to init beacon", e); + return null; + } + result.readExternal(data); + CorrelatedMarker marker = input.getContext().getCurrentMarker(); + if (marker != null) { + float samplesPerBit = (((float) input.getContext().getCurrentSample().getValue() - marker.getSourceSample()) / raw.length); + int actualBitsCount = (data.length + 3) * 8; // 1 for length, 2 for crc + marker.setEndSample(marker.getSourceSample() + (long) (samplesPerBit * actualBitsCount)); + } + return result; + } +} diff --git a/src/test/java/ru/r2cloud/jradio/openlst/OpenLstBeaconSourceTest.java b/src/test/java/ru/r2cloud/jradio/openlst/OpenLstBeaconSourceTest.java new file mode 100644 index 00000000..e4e44139 --- /dev/null +++ b/src/test/java/ru/r2cloud/jradio/openlst/OpenLstBeaconSourceTest.java @@ -0,0 +1,32 @@ +package ru.r2cloud.jradio.openlst; + +import static org.junit.Assert.assertTrue; + +import org.junit.After; +import org.junit.Test; + +import ru.r2cloud.jradio.AssertJson; +import ru.r2cloud.jradio.demod.FskDemodulator; +import ru.r2cloud.jradio.source.WavFileSource; + +public class OpenLstBeaconSourceTest { + + private OpenLstBeaconSource input; + + @Test + public void testDecodeTelemetry() throws Exception { + WavFileSource source = new WavFileSource(OpenLstBeaconSourceTest.class.getClassLoader().getResourceAsStream("dora.wav")); + FskDemodulator demod = new FskDemodulator(source, 7416); + input = new OpenLstBeaconSource<>(demod, OpenLstBeacon.class); + assertTrue(input.hasNext()); + AssertJson.assertObjectsEqual("Dora.json", input.next()); + } + + @After + public void stop() throws Exception { + if (input != null) { + input.close(); + } + } + +} diff --git a/src/test/java/ru/r2cloud/jradio/openlst/OpenLstTest.java b/src/test/java/ru/r2cloud/jradio/openlst/OpenLstTest.java new file mode 100644 index 00000000..dd5f8d19 --- /dev/null +++ b/src/test/java/ru/r2cloud/jradio/openlst/OpenLstTest.java @@ -0,0 +1,19 @@ +package ru.r2cloud.jradio.openlst; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Test; + +public class OpenLstTest { + + @Test + public void testSuccess() throws Exception { + OpenLst fec = new OpenLst(100); + byte[] input = new byte[] { 1, 2, 3 }; + byte[] encoded = fec.encode(input); + assertArrayEquals(new byte[] { (byte) 0x1a, (byte) 0x1d, (byte) 0xd6, (byte) 0x0b, (byte) 0x0b, (byte) 0x08, (byte) 0x94, (byte) 0xa8, (byte) 0xc7, (byte) 0x95, (byte) 0xa1, (byte) 0x2a, (byte) 0x9e, (byte) 0x70, (byte) 0x47, (byte) 0x06 }, encoded); + byte[] actual = fec.decode(encoded); + assertArrayEquals(input, actual); + } + +} diff --git a/src/test/resources/dora.wav b/src/test/resources/dora.wav new file mode 100644 index 00000000..b6b66ad0 Binary files /dev/null and b/src/test/resources/dora.wav differ diff --git a/src/test/resources/expected/Dora.json b/src/test/resources/expected/Dora.json new file mode 100644 index 00000000..45a0974a --- /dev/null +++ b/src/test/resources/expected/Dora.json @@ -0,0 +1,431 @@ +{ + "flags": 64, + "seqnum": 0, + "hwid": 3328, + "payload": [ + 0, + 75, + 69, + 55, + 68, + 72, + 81, + 53, + 46, + -8, + 83, + 0, + 3, + 111, + 55, + 0, + -67, + 0, + 0, + 0, + 0, + 103, + 8, + -10, + -21, + 0, + 0, + 0, + 0, + 15, + 50, + -116, + 28, + 1, + 0, + 0, + 0, + 33, + 11, + 36, + 0, + 0, + 0, + 11, + -128, + 1, + 70, + 1, + 0, + -31, + -121, + 0, + 48, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 23, + 0, + 0, + 0, + 0, + 0, + 74, + 2, + 74, + 2, + 25, + 2, + 120, + 0, + -85, + 0, + 10, + 0, + -79, + 0, + 19, + 0, + 3, + 110, + 0, + 3, + 0, + 2, + 3, + 37, + 1, + 0, + 3, + 48, + 0, + 68, + 3, + 93, + 0, + 38, + 3, + 6, + 0, + 3, + 0, + 2, + 0, + 2, + 0, + 3, + 0, + 2, + 0, + 3, + 0, + 86, + 0, + 3, + 0, + 2, + 0, + -40, + 0, + 9, + 0, + 2, + 0, + 73, + 0, + 2, + 0, + 2, + 0, + 93, + 0, + 23, + 0, + 3, + 3, + 37, + 3, + 115, + 0, + 12, + 2, + -103, + 2, + -105, + 2, + -100, + 0, + 3, + -39, + 0, + 0, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1 + ], + "rawData": [ + 64, + 0, + 0, + 0, + 75, + 69, + 55, + 68, + 72, + 81, + 53, + 46, + -8, + 83, + 0, + 3, + 111, + 55, + 0, + -67, + 0, + 0, + 0, + 0, + 103, + 8, + -10, + -21, + 0, + 0, + 0, + 0, + 15, + 50, + -116, + 28, + 1, + 0, + 0, + 0, + 33, + 11, + 36, + 0, + 0, + 0, + 11, + -128, + 1, + 70, + 1, + 0, + -31, + -121, + 0, + 48, + 6, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 23, + 0, + 0, + 0, + 0, + 0, + 74, + 2, + 74, + 2, + 25, + 2, + 120, + 0, + -85, + 0, + 10, + 0, + -79, + 0, + 19, + 0, + 3, + 110, + 0, + 3, + 0, + 2, + 3, + 37, + 1, + 0, + 3, + 48, + 0, + 68, + 3, + 93, + 0, + 38, + 3, + 6, + 0, + 3, + 0, + 2, + 0, + 2, + 0, + 3, + 0, + 2, + 0, + 3, + 0, + 86, + 0, + 3, + 0, + 2, + 0, + -40, + 0, + 9, + 0, + 2, + 0, + 73, + 0, + 2, + 0, + 2, + 0, + 93, + 0, + 23, + 0, + 3, + 3, + 37, + 3, + 115, + 0, + 12, + 2, + -103, + 2, + -105, + 2, + -100, + 0, + 3, + -39, + 0, + 0, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 13, + 0 + ], + "beginSample": 19782, + "beginMillis": 0, + "endSample": 30915 +} \ No newline at end of file