From 85442a964839640e7875c8d28bac837f42212914 Mon Sep 17 00:00:00 2001 From: Denny Date: Fri, 15 Sep 2017 11:40:23 -0400 Subject: [PATCH] Resolves #269 - creates a generic Viterbi decoder and a Digital Mobile Radio (DMR) 3/4 Rate Viterbi decoder implementation for decoding Trellis Coded Modulation (TCM) encoded DMR Data packets. --- src/bits/BinaryMessage.java | 11 + src/bits/CorrectedBinaryMessage.java | 65 +++++ src/edac/trellis/DMRNode.java | 86 +++++++ src/edac/trellis/Node.java | 77 ++++++ src/edac/trellis/Path.java | 131 ++++++++++ src/edac/trellis/ViterbiDecoder.java | 226 ++++++++++++++++++ src/edac/trellis/ViterbiDecoder_3_4_DMR.java | 159 ++++++++++++ .../radioreference/RadioReferenceService.java | 37 --- 8 files changed, 755 insertions(+), 37 deletions(-) create mode 100644 src/bits/CorrectedBinaryMessage.java create mode 100644 src/edac/trellis/DMRNode.java create mode 100644 src/edac/trellis/Node.java create mode 100644 src/edac/trellis/Path.java create mode 100644 src/edac/trellis/ViterbiDecoder.java create mode 100644 src/edac/trellis/ViterbiDecoder_3_4_DMR.java diff --git a/src/bits/BinaryMessage.java b/src/bits/BinaryMessage.java index 31516602..c3caba05 100644 --- a/src/bits/BinaryMessage.java +++ b/src/bits/BinaryMessage.java @@ -136,6 +136,17 @@ public class BinaryMessage extends BitSet { mCRC = crc; } + + /** + * Indicates the number of bit errors that were corrected in this message. + * + * @see CorrectedBinaryMessage subclass that supports setting this value + * @return number of corrected bits in this message + */ + public int getCorrectedBitCount() + { + return 0; + } /** * Current pointer index diff --git a/src/bits/CorrectedBinaryMessage.java b/src/bits/CorrectedBinaryMessage.java new file mode 100644 index 00000000..597830d3 --- /dev/null +++ b/src/bits/CorrectedBinaryMessage.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package bits; + +import java.util.BitSet; + +public class CorrectedBinaryMessage extends BinaryMessage +{ + private int mCorrectedBitCount; + + /** + * Subclass of binary message class to allow capturing a corrected bits metric. + * @param size of message to create. + */ + public CorrectedBinaryMessage(int size) + { + super(size); + } + + public CorrectedBinaryMessage(int size, boolean[] bitsToPreload) + { + super(size, bitsToPreload); + } + + public CorrectedBinaryMessage(BitSet bitset, int size) + { + super(bitset, size); + } + + public CorrectedBinaryMessage(byte[] data) + { + super(data); + } + + @Override + public int getCorrectedBitCount() + { + return mCorrectedBitCount; + } + + /** + * Sets the number of bits that were corrected in this message + * @param count of corrected bits + */ + public void setCorrectedBitCount(int count) + { + mCorrectedBitCount = count; + } +} diff --git a/src/edac/trellis/DMRNode.java b/src/edac/trellis/DMRNode.java new file mode 100644 index 00000000..4124bd83 --- /dev/null +++ b/src/edac/trellis/DMRNode.java @@ -0,0 +1,86 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package edac.trellis; + +public class DMRNode extends Node +{ + //Hamming error values indicating the number of bits set in each indexed value, 0-15 + public static final int[] HAMMING_ERROR_COUNT = new int[]{0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4}; + + /** + * Transmitted quad-bit value, derived from ETSI TS 102-361-1, Table B.7 Trellis encoder state transition table + * + * Note: the B.7 table contains constellation point ID numbers that have to be converted into actual bit values + * where the bits represent the dibit pair for the constellation point. + */ + public static final int[][] DMR_TRANSITION_MATRIX = new int[][] + { + {2,13,14,1,7,8,11,4}, + {14,1,7,8,11,4,2,13}, + {10,5,6,9,15,0,3,12}, + {6,9,15,0,3,12,10,5}, + {15,0,3,12,10,5,6,9}, + {3,12,10,5,6,9,15,0}, + {7,8,11,4,2,13,14,1}, + {11,4,2,13,14,1,7,8} + }; + + /** + * Trellis Coded Modulation (TCM) 3/4-rate trellis node for decoding DMR + * + * @param inputValue for the trellis time instant + * @param transmittedOutputValue the actual four-bit output value that was transmitted. + */ + public DMRNode(int inputValue, int transmittedOutputValue) + { + super(inputValue, transmittedOutputValue); + } + + /** + * Calculates the error between the preceeding node and this node as the Hamming distance between the expected + * four-bit output value versus the actual transmitted output value. + * + * @param precedingNode that precedes this node + * @return error value for this node relative to the preceeding node argument + */ + @Override + public int getError(Node precedingNode) + { + if(precedingNode != null) + { + int expectedOutputValue = getOutputValue(precedingNode); + int errorMask = expectedOutputValue ^ getTransmittedOutputValue(); + return HAMMING_ERROR_COUNT[errorMask]; + } + + return 0; + } + + /** + * Expected output four-bit value when this node is preceeded by the argument node. + * + * @param precedingNode that precedes this node + * @return expected output value + */ + @Override + public int getOutputValue(Node precedingNode) + { + return DMR_TRANSITION_MATRIX[precedingNode.getInputValue()][getInputValue()]; + } +} diff --git a/src/edac/trellis/Node.java b/src/edac/trellis/Node.java new file mode 100644 index 00000000..3e54ebf0 --- /dev/null +++ b/src/edac/trellis/Node.java @@ -0,0 +1,77 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package edac.trellis; + +public abstract class Node +{ + private int mInputValue; + private int mTransmittedOutputValue; + + /** + * Trellis Coded Modulation (TCM) node. A node represents a time instant on the trellis and contains an input + * value and the corresponding transmitted output value for the time instant. A node does not maintain the + * state at the time instant -- the state is determined by the input value of a preceeding node. + * + * @param inputValue for this node + * @param transmittedOutputValue for this node + */ + public Node(int inputValue, int transmittedOutputValue) + { + mInputValue = inputValue; + mTransmittedOutputValue = transmittedOutputValue; + } + + /** + * Input value for this node. + * + * @return input value + */ + public int getInputValue() + { + return mInputValue; + } + + /** + * Transmitted output value for this node + * @return transmitted output value + */ + public int getTransmittedOutputValue() + { + return mTransmittedOutputValue; + } + + /** + * Calculates the error value for this node (time instant) by determining the expected output value that would have + * been transmitted using the state from the preceding node argument and this node's input value and comparing that + * expected value to the actual transmitted output value. This error is normally represented as the Hamming + * distance between the two values. + * + * @param precedingNode that precedes this node + * @return error value for this node relative to the preceeding node + */ + public abstract int getError(Node precedingNode); + + /** + * Expected output value when this node is preceeded by the node specified in the argument. + * + * @param precedingNode that precedes this node + * @return expected output value + */ + public abstract int getOutputValue(Node precedingNode); +} diff --git a/src/edac/trellis/Path.java b/src/edac/trellis/Path.java new file mode 100644 index 00000000..667bc7eb --- /dev/null +++ b/src/edac/trellis/Path.java @@ -0,0 +1,131 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package edac.trellis; + +import java.util.ArrayList; +import java.util.List; + +public class Path +{ + private List mNodes = new ArrayList<>(); + private int mError = 0; + + /** + * Trellis Coded Modulation (TCM) path. Represents a sequence of nodes that traverse various time instants and + * states across a trellis. + * + * Note: each node can be shared across multiple paths. This class does not assume that any nodes are unique. + * @param startingNode to start the path. This node normally contains an input value of zero, but is implementation + * specific. + */ + public Path(Node startingNode) + { + add(startingNode); + } + + /** + * Trellis Coded Modulation (TCM) path. Represents a sequence of nodes that traverse various time instants and + * states across a trellis. + * + * Note: each node can be shared across multiple paths. This class does not assume that any nodes are unique. + * + * @param original path to create a copy from. Each of the nodes in the original will be added to this path and + * after construction both this path and the original will contain the exact same set of nodes. + */ + private Path(Path original) + { + for(Node node: original.getNodes()) + { + add(node); + } + } + + /** + * Cumulative error across all nodes contained within this path. Error value is updated as each new node is added + * to this path. + * + * @return cumulative error value + */ + public int getError() + { + return mError; + } + + /** + * Length or number of nodes contained within this path. Note: this value includes the starting node and may + * also include a flushing node. The values contained in the starting and flushing nodes are normally not used + * when decoding a path. + * + * @return number of nodes in this path + */ + public int getLength() + { + return mNodes.size() - 1; + } + + /** + * The set of nodes contained in this path. + * @return nodes + */ + public List getNodes() + { + return mNodes; + } + + /** + * Adds the node to this path and updates the cumulative error value for this path by adding the node's error + * value to the running error total. + * + * @param node to add to this path + */ + public void add(Node node) + { + if(!mNodes.isEmpty()) + { + mError += node.getError(getLastNode()); + } + + mNodes.add(node); + } + + /** + * Last node in this path. + * + * @return last node + */ + public Node getLastNode() + { + if(!mNodes.isEmpty()) + { + return mNodes.get(mNodes.size() - 1); + } + + return null; + } + + /** + * Utility method to create an exact copy of this path. + * + * @return a new copy of this path. + */ + public Path copyOf() + { + return new Path(this); + } +} diff --git a/src/edac/trellis/ViterbiDecoder.java b/src/edac/trellis/ViterbiDecoder.java new file mode 100644 index 00000000..6341f1a6 --- /dev/null +++ b/src/edac/trellis/ViterbiDecoder.java @@ -0,0 +1,226 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package edac.trellis; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public abstract class ViterbiDecoder +{ + private int mInputBitLength; + private int mInputValueCount; + private int mOutputBitLength; + private int mOutputValueCount; + + /** + * Viterbi decoder for trellis coded modulation (TCM) encoded binary sequences. + * + * @param inputBitLength for coding (e.g. 3/4 rate input bit length is 3) + * @param outputBitLength for coding (e.g. 3/4 rate output bit length is 4) + */ + public ViterbiDecoder(int inputBitLength, int outputBitLength) + { + mInputBitLength = inputBitLength; + mOutputBitLength = outputBitLength; + + mInputValueCount = (int)Math.pow(2.0, mInputBitLength); + mOutputValueCount = (int)Math.pow(2.0, mOutputBitLength); + } + + /** + * Creates a node that represents an input value and the actual/transmitted output value. + * + * Note: this method must be implemented by a sub-class since the node requires a specific state transition + * table/matrix implementation. + * + * @param inputValue for the specific trellis node + * @param transmittedOutputValue the actual transmitted output value + * @return node for decoding + */ + protected abstract Node createNode(int inputValue, int transmittedOutputValue); + + /** + * Creates a flushing or final node that represents an actual/transmitted output value and a flushing input value. + * + * The final flushing input value is implementation-specific, therefore this will be implemented in any subclass + * of this decoder. A normal flushing input value is zero. + * + * @param transmittedOutputValue the actual transmitted output value + * @return flushing node + */ + protected abstract Node createFlushingNode(int transmittedOutputValue); + + /** + * Creates a starting node that represents a starting state and notional transmitted output value. + * + * The starting input value is implementation-specific, therefore this will be implemented in any subclass + * of this decoder. A normal starting input value is zero. + * + * @return starting node + */ + protected abstract Node createStartingNode(); + + /** + * Size in bits of the input and state values (e.g. 3/4 rate input size is 3) + * + * @return input bit size + */ + public int getInputBitLength() + { + return mInputBitLength; + } + + /** + * Count of input or state values where the set of values is in the range of 0 <> count - 1 + * + * @return input or state values count + */ + public int getInputValueCount() + { + return mInputValueCount; + } + + /** + * Size in bits of the output constellation or transmitted symbol value (e.g. 3/4 rate output size is 4) + * + * @return output size in bits + */ + public int getOutputBitLength() + { + return mOutputBitLength; + } + + /** + * Count of input or state values where the set of values is in the range of 0 <> count - 1 + * + * @return input or state values count + */ + public int getOutputValueCount() + { + return mOutputValueCount; + } + + /** + * Decodes the TCM encoded transmitted output values and returns a path the represents the most likely transmitted + * sequence of nodes. + * + * @param transmittedOutputValues from the encoded message + * @return most likely path representing the transmitted values + */ + public Path decode(int[] transmittedOutputValues) + { + Collection survivingPaths = new ArrayList<>(); + + survivingPaths.add(new Path(createStartingNode())); + + //Add all but the last transmitted value to the starting path, creating a set of survivors at each iteration. + for(int x = 0; x < transmittedOutputValues.length - 1; x++) + { + survivingPaths = add(survivingPaths, transmittedOutputValues[x]); + } + + //Flush the survivors with the final transmitted value and return the lone surviving path + return flush(survivingPaths, transmittedOutputValues[transmittedOutputValues.length - 1]); + } + + /** + * Creates a set of new nodes representing all possible input values and the transmitted output value argument + * and iteratively adds the set of nodes to a copy of each of the surviving paths. A set of surviving paths is + * returned that represents each path that terminates at a distinct input/state value that has the least error + * value. + * + * @param survivingPaths from the previous time instant to evaluate for the next time instant in the trellis + * using the transmitted output value. + * + * @param transmittedOutputValue that will be decoded for this time instant. + * + * @return paths that have survived (ie have the lowest error value) after adding nodes for the transmitted + * output value + * + */ + public Collection add(Collection survivingPaths, int transmittedOutputValue) + { + Map survivingPathMap = new HashMap<>(); + + for(int x = 0; x < getInputValueCount(); x++) + { + //Create a single/unique node for each possible input value combined with the transmitted output value + Node node = createNode(x, transmittedOutputValue); + + //Evaluate each surviving path by creating a copy and adding the node to it and see if it survives + for(Path path: survivingPaths) + { + Path pathToEvaluate = path.copyOf(); + + pathToEvaluate.add(node); + + if(survivingPathMap.containsKey(node.getInputValue())) + { + Path survivingPath = survivingPathMap.get(node.getInputValue()); + + //Replace the current survivor path if this evaluate path has a lower error value + if(pathToEvaluate.getError() < survivingPath.getError()) + { + survivingPathMap.put(node.getInputValue(), pathToEvaluate); + } + } + else + { + //No survivor yet -- set this evaluate path as the first survivor + survivingPathMap.put(node.getInputValue(), pathToEvaluate); + } + } + } + + return survivingPathMap.values(); + } + + /** + * Flushes each of the surviving paths with a final/flushing node using the transmitted symbol. A final flushing + * input value or state is implementation-specific, but normally uses a value of zero. Since all paths will be + * evaluated against each other as terminating at the same state, only the path with the lowest error value will + * be returned from this method. + * + * @param survivingPaths to flush with the final flushing node + * @param transmittedOutputValue to use for the final flushing node + * + * @return the single survivor path that remains after adding the flushing node to each of the candidate argument + * survivor paths. + */ + public Path flush(Collection survivingPaths, int transmittedOutputValue) + { + Node flushingNode = createFlushingNode(transmittedOutputValue); + + Path bestPath = null; + + for(Path survivingPath: survivingPaths) + { + survivingPath.add(flushingNode); + + if(bestPath == null || (survivingPath.getError() < bestPath.getError())) + { + bestPath = survivingPath; + } + } + + return bestPath; + } +} diff --git a/src/edac/trellis/ViterbiDecoder_3_4_DMR.java b/src/edac/trellis/ViterbiDecoder_3_4_DMR.java new file mode 100644 index 00000000..142c7ada --- /dev/null +++ b/src/edac/trellis/ViterbiDecoder_3_4_DMR.java @@ -0,0 +1,159 @@ +/******************************************************************************* + * sdrtrunk + * Copyright (C) 2014-2017 Dennis Sheirer + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see + * + ******************************************************************************/ +package edac.trellis; + +import bits.BinaryMessage; +import bits.CorrectedBinaryMessage; + +import java.util.List; + +public class ViterbiDecoder_3_4_DMR extends ViterbiDecoder +{ + /** + * Viterbi decoder for Digital Mobile Radio (DMR) 3/4 rate Trellis Coded Modulation (TCM) encoded messages. + */ + public ViterbiDecoder_3_4_DMR() + { + super(3,4); + } + + /** + * Decodes a 3/4 rate trellis coded modulation (TCM) encoded DMR binary message containing 196 bits that have + * already been deinterleaved. + * + * @param encodedMessage to decode that has already been deinterleaved. + * @return decoded message + */ + public BinaryMessage decode(BinaryMessage encodedMessage) + { + int[] symbols = getSymbols(encodedMessage); + + Path mostLikelyPath = decode(symbols); + + return getMessage(mostLikelyPath); + } + + /** + * Extracts the decoded/corrected message from the surviving path argument. + * + * @param path to extract a message from + * @return corrected binary message. + */ + private static CorrectedBinaryMessage getMessage(Path path) + { + List mNodes = path.getNodes(); + + //Each node contains an input value excluding the starting(0) and final flushing(0) nodes. + CorrectedBinaryMessage message = new CorrectedBinaryMessage((mNodes.size() - 2) * 3); + + if(mNodes.size() > 2) + { + for(int x = 1; x < mNodes.size() - 1; x++) + { + int inputValue = mNodes.get(x).getInputValue(); + + int messageOffset = 3 * (x - 1); + + if((inputValue & 4) == 4) + { + message.set(messageOffset); + } + + if((inputValue & 2) == 2) + { + message.set(messageOffset + 1); + } + + if((inputValue & 1) == 1) + { + message.set(messageOffset + 2); + } + } + } + + //Transfer the corrected error count to the message + message.setCorrectedBitCount(path.getError()); + + return message; + } + + /** + * Extracts (49) four-bit symbols (196 bits / 4) from the deinterleaved DMR binary message + * + * @param encodedMessage that deinterleaved and contains 196-bits of 3/4 TCM encoding + * @return symbols + */ + public int[] getSymbols(BinaryMessage encodedMessage) + { + //Ensure we have an integral number of transmitted symbols (nibbles) in the message + if(encodedMessage.size() % getOutputBitLength() != 0) + { + throw new IllegalArgumentException("Encoded message must contain an integral number of 4-bit symbols -" + + " message size: " + encodedMessage.size()); + } + + int[] symbols = new int[encodedMessage.size() / getOutputBitLength()]; + + int index = 0; + int pointer = 0; + + while(index < encodedMessage.size()) + { + int symbol = encodedMessage.getInt(index, index + (getOutputBitLength() - 1)); + symbols[pointer++] = symbol; + index += getOutputBitLength(); + } + + return symbols; + } + + /** + * Creates a DMR node + * @param inputValue for the specific trellis node + * @param transmittedOutputValue the actual transmitted output value + * @return created node + */ + @Override + protected Node createNode(int inputValue, int transmittedOutputValue) + { + return new DMRNode(inputValue, transmittedOutputValue); + } + + /** + * Creates a DMR flushing node. DMR uses an input value of zero to flush the final output value out of the + * encoder. + * + * @param transmittedOutputValue the actual transmitted output value + * @return + */ + @Override + protected Node createFlushingNode(int transmittedOutputValue) + { + return new DMRNode(0, transmittedOutputValue); + } + + /** + * Creates a DMR starting node. DMR uses a starting input value of zero for the initial node. + */ + @Override + protected Node createStartingNode() + { + return new DMRNode(0,0); + } +} diff --git a/src/external/radioreference/RadioReferenceService.java b/src/external/radioreference/RadioReferenceService.java index df46e9f0..12eefcea 100644 --- a/src/external/radioreference/RadioReferenceService.java +++ b/src/external/radioreference/RadioReferenceService.java @@ -151,41 +151,4 @@ public class RadioReferenceService return mClient.getUserFeedBroadcasts(mUserAuthorization); } - - - public static void main(String[] args) - { - try - { - RadioReferenceService service = new RadioReferenceService("dsheirer", "1dodgeram"); - - Long expiration = service.getAccountExpirationDate(); - - mLog.debug("Expiration Date:" + (expiration != null ? new Date(expiration).toString() : "unknown")); - - mLog.debug("Is Active: " + service.isAccountActive()); - - UserFeedBroadcast[] feeds = service.getUserFeedBroadcasts(); - - for(UserFeedBroadcast feed: feeds) - { - mLog.debug("Feed ID: " + feed.getFeedId() + " Desc:" + feed.getDescr()); - } - } - catch(AxisFault af) - { - mLog.debug("Hey, I caught an axis fault!"); - mLog.debug("Reason:" + af.getFaultReason()); - } - catch(RemoteException re) - { - re.printStackTrace(); - } - catch(Exception e) - { - e.printStackTrace(); - } - - mLog.debug("Finished!"); - } }