/******************************************************************************* * SDR Trunk * Copyright (C) 2014 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 <http://www.gnu.org/licenses/> ******************************************************************************/ package module.decode.fleetsync2; import java.text.DecimalFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.BitSet; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import map.Plottable; import message.Message; import org.jdesktop.swingx.mapviewer.GeoPosition; import alias.Alias; import alias.AliasList; import bits.BinaryMessage; import edac.CRC; import edac.CRCFleetsync; public class FleetsyncMessage extends Message { private static DecimalFormat mDecimalFormatter = new DecimalFormat("#.#####"); private static SimpleDateFormat mSDF = new SimpleDateFormat( "yyyyMMdd HHmmss" ); //Calendar to use in calculating time hacks Calendar mCalendar = new GregorianCalendar(); //Message parts are identified in big-endian order for correct translation //Message Header private static int[] sRevs = { 4,3,2,1,0 }; private static int[] sSync = { 20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5 }; //Message Block 1 private static int[] sStatusMessage = { 27,26,25,24,23,22,21 }; private static int[] sMessageType = { 33,32,31,30,29 }; private static int sEmergencyFlag = 22; private static int sLoneWorkerFlag = 24; private static int sPagingFlag = 26; private static int sEndOfTransmissionFlag = 27; private static int sManualFlag = 28; private static int sANIFlag = 29; private static int sStatusFlag = 30; private static int sAcknowledgeFlag = 31; //32 - unknown - always 0 //33 - unknown - always 0 //34 - unknown - set for ACKNOWLEDGE private static int sGPSExtensionFlag = 35; private static int sFleetExtensionFlag = 36; private static int[] sFleetFrom = { 44,43,42,41,40,39,38,37 }; private static int[] sIdentFrom = { 56,55,54,53,52,51,50,49,48,47,46,45 }; private static int[] sIdentTo = { 68,67,66,65,64,63,62,61,60,59,58,57 }; private static int[] sCRC1 = { 84,83,82,81,80,79,78,77,76,75,74,73,72,71,70,69 }; //Message Block 2 private static int[] sFleetTo = { 92,91,90,89,88,87,86,85 }; private static int[] sCRC2 = { 148,147,146,145,144,143,142,141,140,139,138,137,136,135,134,132 }; //Message Block 3 private static int[] sGPSHours = { 176,175,174,173,172 }; private static int[] sGPSMinutes = { 182,181,180,179,178,177 }; private static int[] sGPSSeconds = { 188,187,186,185,184,183 }; private static int[] sCRC3 = { 212,211,210,209,208,207,206,205,204,203,202,201,200,199,198,197 }; //Message Block 4 private static int[] sGPSChecksum = { 220,219,218,217,216,215,214,213 }; private static int[] sLatitudeDegreesMinutes = { 236,235,234,233,232,231,230,229,228,227,226,225,224,223,222,221 }; private static int[] sLatitudeDecimalMinutes = { 251,250,249,248,247,246,245,244,243,242,241,240,239,238 }; private static int[] sSpeed = { 276,275,274,273,272,271,270,269,268,267,266,265,264,263,262,261,260,259,258,257,256,255,254,253,252 }; private static int[] sCRC4 = { 276,275,274,273,272,271,270,269,268,267,266,265,264,263,262,261 }; //Message Block 5 private static int[] sGPSCentury = { 284,283,282,281,280,279,278,277 }; private static int[] sGPSYear = { 291,290,289,288,287,286,285 }; private static int[] sGPSMonth = { 295,294,293,292 }; private static int[] sGPSDay = { 300,299,298,297,296 }; private static int[] sLongitudeDegreesMinutes = { 316,315,314,313,312,311,310,309,308,307,306,305,304,303,302,301 }; private static int[] sLongitudeDecimalMinutes = { 331,330,329,328,327,326,325,324,323,322,321,320,319,318 }; private static int[] sCRC5 = { 340,339,338,337,336,335,334,333,332,331,330,329,328,327,326,325 }; //Message Block 6 private static int[] sGPSUnknown1 = { 352,351,350,349 }; private static int[] sGPSHeading = { 365,363,362,361,360,359,358,357,356,355,354,353 }; private static int[] sCRC6 = { 404,403,402,401,400,399,398,397,396,395,394,393,392,391,390,389 }; //Message Block 7 private static int[] sBLK7WhatIsIt = { 444,443,442,441,440,439,438,437,436,435,434,433,432,431,430,429 }; private static int[] sCRC7 = { 468,467,466,465,464,463,462,461,460,459,458,457,456,455,454,453 }; //Message Block 8 private static int[] sGPSSpeed = { 491,490,489,488,487,486,485,484 }; private static int[] sGPSSpeedFractional = { 499,498,497,496,495,494,493,492 }; private static int[] sCRC8 = { 532,531,530,529,528,527,526,525,524,523,522,521,520,519,518,517 }; private BinaryMessage mMessage; private CRC[] mCRC = new CRC[ 8 ]; private AliasList mAliasList; public FleetsyncMessage( BinaryMessage message, AliasList list ) { mMessage = message; mAliasList = list; checkParity(); } private void checkParity() { //Check message block 1 mCRC[ 0 ] = detectAndCorrect( 21, 85 ); //Only check subsequent blocks if we know block 1 is correct if( mCRC[ 0 ] == CRC.PASSED || mCRC[ 0 ] == CRC.CORRECTED ) { if( hasFleetExtensionFlag() ) { //Check message block 2 mCRC[ 1 ] = detectAndCorrect( 85, 149 ); } if( hasGPSFlag() ) { //Check message block 3 mCRC[ 2 ] = detectAndCorrect( 149, 213 ); //Check message block 4 mCRC[ 3 ] = detectAndCorrect( 213, 277 ); //Check message block 5 mCRC[ 4 ] = detectAndCorrect( 277, 341 ); //Check message block 6 mCRC[ 5 ] = detectAndCorrect( 341, 405 ); //Check message block 7 mCRC[ 6 ] = detectAndCorrect( 405, 469 ); //Check message block 8 mCRC[ 7 ] = detectAndCorrect( 469, 533 ); } } } private CRC detectAndCorrect( int start, int end ) { BitSet original = mMessage.get( start, end ); CRC retVal = CRCFleetsync.check( original ); //Attempt to correct single-bit errors if( retVal == CRC.FAILED_PARITY ) { int[] errorBitPositions = CRCFleetsync.findBitErrors( original ); if( errorBitPositions != null ) { for( int errorBitPosition: errorBitPositions ) { mMessage.flip( start + errorBitPosition ); } retVal = CRC.CORRECTED; } } return retVal; } public boolean isValid() { boolean valid = true; for( int x = 0; x < mCRC.length; x++ ) { CRC crc = mCRC[ x ]; if( crc != null ) { if( crc == CRC.FAILED_CRC || crc == CRC.FAILED_PARITY || crc == CRC.UNKNOWN ) { valid = false; } } } return valid; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append( mSDF.format( new Date( System.currentTimeMillis() ) ) ); sb.append( " FSync2 " + getParity() ); sb.append( getMessage() ); sb.append( " [" + mMessage.toString() + "]" ); return sb.toString(); } /** * Pads spaces onto the end of the value to make it 'places' long */ public String pad( String value, int places, String padCharacter ) { StringBuilder sb = new StringBuilder(); sb.append( value ); while( sb.length() < places ) { sb.append( padCharacter ); } return sb.toString(); } /** * Pads an integer value with additional zeroes to make it decimalPlaces long */ public String format( int number, int decimalPlaces ) { StringBuilder sb = new StringBuilder(); int paddingRequired = decimalPlaces - ( String.valueOf( number ).length() ); for( int x = 0; x < paddingRequired; x++) { sb.append( "0" ); } sb.append( number ); return sb.toString(); } public FleetsyncMessageType getMessageType() { if( hasStatusFlag() ) { if( hasAcknowledgeFlag() ) { return FleetsyncMessageType.ACKNOWLEDGE; } if( hasGPSFlag() ) { return FleetsyncMessageType.GPS; } else { return FleetsyncMessageType.STATUS; } } else { if( hasAcknowledgeFlag() ) { return FleetsyncMessageType.ACKNOWLEDGE; } if( hasANIFlag() ) { return FleetsyncMessageType.ANI; } if( hasGPSFlag() ) { return FleetsyncMessageType.GPS; } if( hasPagingFlag() ) { return FleetsyncMessageType.PAGING; } if( hasEmergencyFlag() ) { if( hasLoneWorkerFlag() ) { return FleetsyncMessageType.LONE_WORKER_EMERGENCY; } else return FleetsyncMessageType.EMERGENCY; } } return FleetsyncMessageType.UNKNOWN; } public int getStatus() { return getStatusNumber() + 9; } /** * Returns the RAW status number. The actual status number should be * accessed via the getStatus() method. * @return */ public int getStatusNumber() { return getInt( sStatusMessage ); } /** * @return */ public Alias getStatusAlias() { if( mAliasList != null ) { return mAliasList.getStatus( getStatus() ); } return null; } public int getFleetFrom() { return getInt( sFleetFrom ) + 99; } public int getIdentifierFrom() { return getInt( sIdentFrom ) + 999; } /** * Inverted Flags - 0 = flag is true * @return */ public boolean hasEndOfTransmissionFlag() { return !mMessage.get( sEndOfTransmissionFlag ); } public boolean hasEmergencyFlag() { return !mMessage.get( sEmergencyFlag ); } public boolean hasLoneWorkerFlag() { return !mMessage.get( sLoneWorkerFlag ); } public boolean hasPagingFlag() { return !mMessage.get( sPagingFlag ); } /** * Normal Flags - 1 = flag is true * @return */ public boolean hasANIFlag() { return mMessage.get( sANIFlag ); } public boolean hasAcknowledgeFlag() { return mMessage.get( sAcknowledgeFlag ); } public boolean hasFleetExtensionFlag() { return mMessage.get( sFleetExtensionFlag ); } public boolean hasGPSFlag() { return mMessage.get( sGPSExtensionFlag ); } public boolean hasStatusFlag() { return mMessage.get( sStatusFlag ); } public int getFleetTo() { if( hasFleetExtensionFlag() ) { return getInt( sFleetTo ) + 99; } else { return getInt( sFleetFrom ) + 99; } } public int getIdentifierTo() { return getInt( sIdentTo ) + 999; } /** * GPS Heading * @return */ public double getHeading() { double retVal = 0.0; int heading = getInt( sGPSHeading ); if( heading != 4095 ) { retVal = (double)(heading / 10.0 ); } return retVal; } public int getBlock7WhatIsIt() { int value = getInt( sBLK7WhatIsIt ); if( value == 65535 ) { value = -1; } return value; } /** * Speed in Kph - whole numbers and three decimal digits of precision * @return */ public double getSpeed() { double retVal = 0.0; int temp = getInt( sSpeed ); if( temp != 0 ) { retVal = (double)temp / 1000.0D; } return retVal; } public double getLatitude() { //TODO: determine the correct hemisphere indicator and replace this //hardcoded "0" with the correct hemisphere value return convertDDMToDD( 0, getInt( sLatitudeDegreesMinutes ), getInt( sLatitudeDecimalMinutes ) ); } public double getLongitude() { //TODO: determine the correct hemisphere indicator and replace this //hardcoded "1" with the correct hemisphere value return convertDDMToDD( 1, getInt( sLongitudeDegreesMinutes ), getInt( sLongitudeDecimalMinutes ) ); } /** * Converts Degrees Decimal Minutes to Decimal Degrees * * Latitude and Longitude values are represented by: * * @param hemisphere - 0=North & East, 1=South & West * @param degreesMinutes - an integer value with the first 2-3 digits representing * the degrees and the last two digits representing the minutes * * @param decimalDegrees - an integer value representing the fractional * minutes * * @return - decimal degrees formatted value */ public double convertDDMToDD( int hemisphere, int degreesMinutes, int decimalDegrees ) { double retVal = 0.0; if( degreesMinutes != 0 ) { //Degrees - divide value by 100 and retain the whole number value (ie degrees) retVal += (double)( degreesMinutes / 100 ); //Minutes - modulus by 100 to get the whole minutes value int wholeMinutes = degreesMinutes % 100; if( wholeMinutes != 0 ) { retVal += (double)( wholeMinutes / 60.0D ); } } if( decimalDegrees != 0 ) { //Fractional Minutes - divide by 10,000 to get the decimal place correct //then divide by 60 (minutes) to get the decimal value //10,000 * 60 = 600,000 retVal += (double)( decimalDegrees / 600000.0D ); } //Adjust the value +/- for the hemisphere if( hemisphere == 1 ) //South and West values { retVal = -retVal; } return retVal; } public String getGPSTime() { StringBuilder sb = new StringBuilder(); sb.append( format( getInt( sGPSHours ), 2 ) ); sb.append( ":" ); sb.append( format( getInt( sGPSMinutes ), 2 ) ); sb.append( ":" ); sb.append( format( getInt( sGPSSeconds ), 2 ) ); sb.append( "z" ); return sb.toString(); } /** * 7 bit checksum that is part of the GPGGA message, I think * @return */ public int getGPSChecksum() { return getInt( sGPSChecksum ); } public String getGPSChecksumString() { String sum = Integer.toHexString( getGPSChecksum() ); if( sum.length() == 1 ) { return "*0" + sum; } else { return "*" + sum; } } /** * Returns 1-based calendar day of month * 1 = 1st day of month */ public int getGPSDay() { return getInt( sGPSDay ) + 1; } /** * Returns 0-based GPS Month * 0 = January * * Note: actual day of month value is 1-based, so we subtract 1 to get * the actual day of month value */ public int getGPSMonth() { return getInt( sGPSMonth ); } /** * Returns year, which is a combination of the century bit field + 1, times * 100, plus the year bit field */ public int getGPSYear() { // return ( ( getInt( sGPSCentury ) + 1 ) * 100 ) + getInt( sGPSYear ); return ( 2000 ) + getInt( sGPSYear ); } public String getGPSLocation() { StringBuilder sb = new StringBuilder(); sb.append( pad( String.valueOf( mDecimalFormatter.format( getLatitude() ) ), 8, "0" ) + " " ); sb.append( pad( String.valueOf( mDecimalFormatter.format( getLongitude() ) ), 10, "0" ) ); return sb.toString(); } public String getGPSDate() { StringBuilder sb = new StringBuilder(); sb.append( getGPSYear() + "-" ); int month = getGPSMonth(); if( month < 10 ) { sb.append( "0" ); } sb.append( month + "-" ); int day = getGPSDay(); if( day < 10 ) { sb.append( "0" ); } sb.append( day ); return sb.toString(); } private int getInt( int[] bits ) { int retVal = 0; for( int x = 0; x < bits.length; x++ ) { if( mMessage.get( bits[ x ] ) ) { retVal += 1<<x; } } return retVal; } @Override public String getBinaryMessage() { return mMessage.toString(); } /** * String representing results of the parity check * * [P] = passes parity check * [f] = fails parity check * [C] = corrected message * [-] = message section not present */ public String getParity() { return "[" + CRC.format( mCRC ) + "]"; } @Override public String getProtocol() { return "Fleetsync II"; } @Override public String getEventType() { return getMessageType().toString(); } @Override public String getFromID() { StringBuilder sb = new StringBuilder(); sb.append( getFleetFrom() ); sb.append( "-" ); sb.append( getIdentifierFrom() ); return sb.toString(); } @Override public Alias getFromIDAlias() { if( mAliasList != null ) { return mAliasList.getFleetsyncAlias( getFromID() ); } return null; } @Override public String getToID() { StringBuilder sb = new StringBuilder(); sb.append( getFleetTo() ); sb.append( "-" ); sb.append( getIdentifierTo() ); return sb.toString(); } @Override public Alias getToIDAlias() { if( mAliasList != null ) { return mAliasList.getFleetsyncAlias( getToID() ); } return null; } private String getFromTo( boolean includeFrom, boolean includeTo ) { StringBuilder sb = new StringBuilder(); if( includeFrom ) { sb.append( "FM:" ); sb.append( getFromID() ); Alias from = getFromIDAlias(); if( from != null ) { sb.append( " " ); sb.append( from.getName() ); } } if( includeFrom && includeTo ) { sb.append( " " ); } if( includeTo ) { sb.append( "TO:" ); sb.append( getToID() ); Alias to = getToIDAlias(); if( to != null ) { sb.append( " " ); sb.append( to.getName() ); } } return sb.toString(); } @Override public String getMessage() { StringBuilder sb = new StringBuilder(); FleetsyncMessageType type = getMessageType(); switch( type ) { case ACKNOWLEDGE: sb.append( "ACKNOWLEDGE " ); sb.append( getFromTo( true, true ) ); break; case ANI: sb.append( "ANI " ); sb.append( getFromTo( true, false ) ); break; case EMERGENCY: sb.append( "**EMERGENCY** " ); sb.append( getFromTo( true, true ) ); break; case GPS: sb.append( "GPS [" ); sb.append( getGPSLocation() ); sb.append( "] " ); sb.append( getFromTo( true, true ) ); sb.append( " HDG:" ); sb.append( pad( String.valueOf( getHeading() ), 5, " " ) ); sb.append( " GPSDate[" ); sb.append( getGPSDate() ); sb.append( " " ); sb.append( getGPSTime() ); sb.append( "]" ); break; case LONE_WORKER_EMERGENCY: sb.append( "**LONE WORKER EMERGENCY** " ); sb.append( getFromTo( true, true ) ); break; case PAGING: sb.append( "PAGING " ); sb.append( getFromTo( true, true ) ); break; case STATUS: sb.append( "STATUS: " + format( getStatus(), 2 ) ); Alias status = getStatusAlias(); if( status != null ) { sb.append( "/" ); sb.append( status.getName() ); } sb.append( getFromTo( true, true ) ); break; case UNKNOWN: default: sb.append( "*** UNKNOWN *** " ); sb.append( getFromTo( true, true ) ); break; } return sb.toString(); } @Override public String getErrorStatus() { return CRC.format( mCRC ); } @Override public Plottable getPlottable() { if( isValid() && getMessageType() == FleetsyncMessageType.GPS ) { GeoPosition position = new GeoPosition( getLatitude(), getLongitude() ); Alias alias = mAliasList.getFleetsyncAlias( getFromID() ); return new Plottable( mTimeReceived, position, getFromID(), alias ); } else { return null; } } /** * Provides a listing of aliases contained in the message. */ public List<Alias> getAliases() { List<Alias> aliases = new ArrayList<Alias>(); Alias from = getFromIDAlias(); if( from != null ) { aliases.add( from ); } Alias to = getToIDAlias(); if( to != null ) { aliases.add( to ); } Alias status = getStatusAlias(); if( status != null ) { aliases.add( status ); } return aliases; } }