-
Notifications
You must be signed in to change notification settings - Fork 102
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mic-E & Compressed #397
base: master
Are you sure you want to change the base?
Mic-E & Compressed #397
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -110,4 +110,14 @@ | |
<item>460800</item> | ||
<item>921600</item> | ||
</string-array> | ||
<string-array name="compressed_mice_status"> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [optional] For consistency, the name should be |
||
<item>Off Duty</item> | ||
<item>En Route</item> | ||
<item>In Service</item> | ||
<item>Returning</item> | ||
<item>Committed</item> | ||
<item>Special</item> | ||
<item>Priority</item> | ||
<item>EMERGENCY!</item> | ||
</string-array> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [optional] From APRS12b I take it that the "Custom 0...6" status types do not need to be supported. [important] I'm not sure right now if the standard status type should be translated into the respective user locale or kept in English. In the former case, the array should be moved into the strings.xml file. |
||
</resources> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<PreferenceScreen | ||
xmlns:android="http://schemas.android.com/apk/res/android"> | ||
|
||
<CheckBoxPreference | ||
android:defaultValue="false" | ||
android:key="compressed_location" | ||
android:summaryOff="@string/p__location_compressed_beacons_off" | ||
android:summaryOn="@string/p__location_compressed_beacons_on" | ||
android:title="@string/p__location_compressed_beacons" /> | ||
|
||
<CheckBoxPreference | ||
android:defaultValue="false" | ||
android:key="compressed_mice" | ||
android:summaryOff="@string/p__location_mice_beacons_off" | ||
android:summaryOn="@string/p__location_mice_beacons_on" | ||
android:title="@string/p__location_mice_beacons" /> | ||
|
||
<de.duenndns.ListPreferenceWithValue | ||
android:key="p__location_mice_status" | ||
android:title="@string/p__location_mice_status" | ||
android:entries="@array/compressed_mice_status" | ||
android:entryValues="@array/compressed_mice_status" | ||
android:defaultValue="Off Duty" | ||
android:dialogTitle="@string/p__location_mice_status" /> | ||
|
||
</PreferenceScreen> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,179 @@ import scala.math.abs | |
|
||
object AprsPacket { | ||
val QRG_RE = ".*?(\\d{2,3}[.,]\\d{3,4}).*?".r | ||
val characters = Array( | ||
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", | ||
"E", "F", "G", "H", "I", "J", "K", "L", "P", "Q", "R", "S", "T", "U", "V", | ||
"W", "X", "Y", "Z" | ||
) | ||
|
||
def statusToBits(status: String): (Int, Int, Int) = status match { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [important] the function should be called [optional] But I'd rather use the numeric IDs (position in the array), 0, 1, 2, 3, ... and convert them to the respective bits algorithmically. val a = ((mice_status_id >> 2) & 1) ^ 1;
val b = ((mice_status_id >> 1) & 1) ^ 1;
val c = ((mice_status_id >> 0) & 1) ^ 1; |
||
case "Off Duty" => (1, 1, 1) | ||
case "En Route" => (1, 1, 0) | ||
case "In Service" => (1, 0, 1) | ||
case "Returning" => (1, 0, 0) | ||
case "Committed" => (0, 1, 1) | ||
case "Special" => (0, 1, 0) | ||
case "Priority" => (0, 0, 1) | ||
case "EMERGENCY!" => (0, 0, 0) | ||
case _ => (1, 1, 1) // Default if status is not found | ||
} | ||
|
||
def degreesToDDM(dd: Double): (Int, Double) = { | ||
val degrees = Math.floor(dd).toInt | ||
val minutes = (dd - degrees) * 60 | ||
(degrees, minutes) | ||
} | ||
|
||
def miceLong(dd: Double): (Int, Int, Int) = { | ||
val (degrees, minutes) = degreesToDDM(Math.abs(dd)) | ||
val minutesInt = Math.floor(minutes).toInt | ||
val minutesHundreths = Math.floor(100 * (minutes - minutesInt)).toInt | ||
(degrees, minutesInt, minutesHundreths) | ||
} | ||
|
||
|
||
def encodeDest(dd: Double, longOffset: Int, west: Int, messageA: Int, messageB: Int, messageC: Int, ambiguity: Int): String = { | ||
val north = if (dd < 0) 0 else 1 | ||
|
||
val (degrees, minutes, minutesHundreths) = miceLong(dd) | ||
|
||
val degrees10 = Math.floor(degrees / 10.0).toInt | ||
val degrees1 = degrees - (degrees10 * 10) | ||
|
||
val minutes10 = Math.floor(minutes / 10.0).toInt | ||
val minutes1 = minutes - (minutes10 * 10) | ||
|
||
val minutesHundreths10 = Math.floor(minutesHundreths / 10.0).toInt | ||
val minutesHundreths1 = minutesHundreths - (minutesHundreths10 * 10) | ||
|
||
val sb = new StringBuilder | ||
|
||
if (messageA == 1) sb.append(characters(degrees10 + 22)) else sb.append(characters(degrees10)) | ||
if (messageB == 1) sb.append(characters(degrees1 + 22)) else sb.append(characters(degrees1)) | ||
if (messageC == 1) sb.append(characters(minutes10 + 22)) else sb.append(characters(minutes10)) | ||
|
||
if (north == 1) sb.append(characters(minutes1 + 22)) else sb.append(characters(minutes1)) | ||
if (longOffset == 1) sb.append(characters(minutesHundreths10 + 22)) else sb.append(characters(minutesHundreths10)) | ||
if (west == 1) sb.append(characters(minutesHundreths1 + 22)) else sb.append(characters(minutesHundreths1)) | ||
|
||
val encoded = sb.toString() | ||
|
||
// Replace indices 4 and 5 with 'Z' or 'L', depending on 'west' | ||
val validAmbiguity = ambiguity.max(0).min(4) | ||
val encodedArray = encoded.toCharArray // Convert the encoded string to a char array | ||
|
||
// A map that specifies the modification rules for each index based on ambiguity | ||
val modifyRules = Map( | ||
2 -> (messageC, 'Z', 'L'), | ||
3 -> (north, 'Z', 'L'), | ||
4 -> (longOffset, 'Z', 'L'), | ||
5 -> (west, 'Z', 'L') | ||
) | ||
|
||
// Loop over the indices based on validAmbiguity | ||
for (i <- (6 - validAmbiguity) until 6) { | ||
modifyRules.get(i) match { | ||
case Some((condition, trueChar, falseChar)) => | ||
val charToUse = if (condition == 1) trueChar else falseChar | ||
encodedArray(i) = charToUse | ||
case None => // No modification if the index is not in modifyRules | ||
} | ||
} | ||
|
||
// Return the modified string | ||
val finalEncoded = new String(encodedArray) | ||
|
||
finalEncoded | ||
} | ||
|
||
def encodeInfo(dd: Double, speed: Double, heading: Double, symbol: String): (String, Int, Int) = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [important] should be called |
||
|
||
val (degrees, minutes, minutesHundreths) = miceLong(dd) | ||
|
||
val west = if (dd < 0) 1 else 0 | ||
|
||
val sb = new StringBuilder | ||
sb.append("`") | ||
|
||
val speedHT = Math.floor(speed / 10.0).toInt | ||
val speedUnits = speed - (speedHT * 10) | ||
|
||
val headingHundreds = Math.floor(heading / 100.0).toInt | ||
val headingTensUnits = heading - (headingHundreds * 100) | ||
|
||
var longOffset = 0 | ||
|
||
if (degrees <= 9) { | ||
sb.append((degrees + 118).toChar) | ||
longOffset = 1 | ||
} else if (degrees >= 10 && degrees <= 99) { | ||
sb.append((degrees + 28).toChar) | ||
longOffset = 0 | ||
} else if (degrees >= 100 && degrees <= 109) { | ||
sb.append((degrees + 8).toChar) | ||
longOffset = 1 | ||
} else if (degrees >= 110) { | ||
sb.append((degrees - 72).toChar) | ||
longOffset = 1 | ||
} | ||
|
||
if (minutes <= 9) sb.append((minutes + 88).toChar) else sb.append((minutes + 28).toChar) | ||
sb.append((minutesHundreths + 28).toChar) | ||
|
||
if (speed <= 199) sb.append((speedHT + 108).toChar) else sb.append((speedHT + 28).toChar) | ||
sb.append((Math.floor(speedUnits * 10).toInt + headingHundreds + 32).toChar) | ||
sb.append((headingTensUnits + 28).toChar) | ||
|
||
sb.append(symbol(1)) | ||
sb.append(symbol(0)) | ||
sb.append("`") | ||
|
||
(sb.toString(), west, longOffset) | ||
} | ||
|
||
def altitude(alt: Double): String = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [important] it's not clear what this function does. It should be called |
||
val altM = Math.round(alt * 0.3048).toInt | ||
val relAlt = altM + 10000 | ||
|
||
val val1 = Math.floor(relAlt / 8281.0).toInt | ||
val rem = relAlt % 8281 | ||
val val2 = Math.floor(rem / 91.0).toInt | ||
val val3 = rem % 91 | ||
|
||
// Ensure that the characters are treated as strings and concatenate properly | ||
charFromInt(val1).toString + charFromInt(val2).toString + charFromInt(val3).toString + "}" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [optional] this line's indentation is not at the same level ;) |
||
} | ||
|
||
private def charFromInt(value: Int): Char = (value + 33).toChar | ||
|
||
def formatCourseSpeedMice(location: Location): (Int, Int) = { | ||
// Default values | ||
val status_spd = if (location.hasSpeed && location.getSpeed > 2) { | ||
// Convert speed from m/s to knots, and return as an integer | ||
mps2kt(location.getSpeed).toInt | ||
} else { | ||
0 // If no valid speed or below threshold, set speed to 0 | ||
} | ||
|
||
val course = if (location.hasBearing) { | ||
// Get bearing as an integer (course) | ||
location.getBearing.asInstanceOf[Int] | ||
} else { | ||
0 // If no bearing, set course to 0 | ||
} | ||
|
||
(status_spd, course) | ||
} | ||
|
||
def formatAltitudeMice(location: Location): Option[Int] = { | ||
if (location.hasAltitude) { | ||
// Convert altitude to feet, round to nearest integer, and wrap in Some | ||
Some(math.round(m2ft(location.getAltitude)).toInt) | ||
} else { | ||
None // If no altitude, return None | ||
} | ||
} | ||
|
||
def passcode(callssid : String) : Int = { | ||
// remove ssid, uppercase, add \0 for odd-length calls | ||
|
@@ -44,6 +217,20 @@ object AprsPacket { | |
else | ||
"" | ||
} | ||
|
||
def formatAltitudeCompressed(location : Location) : String = { | ||
if (location.hasAltitude) { | ||
var altitude = m2ft(location.getAltitude) | ||
var compressedAltitude = ((math.log(altitude) / math.log(1.002)) + 0.5).asInstanceOf[Int] | ||
var c = (compressedAltitude / 91).asInstanceOf[Byte] + 33 | ||
var s = (compressedAltitude % 91).asInstanceOf[Byte] + 33 | ||
// Negative altitudes cannot be expressed in base-91 and results in corrupt packets | ||
if(c < 33) c = 33 | ||
if(s < 33) s = 33 | ||
"%c%c".format(c.asInstanceOf[Char], s.asInstanceOf[Char]) | ||
} else | ||
"" | ||
} | ||
|
||
def formatCourseSpeed(location : Location) : String = { | ||
// only report speeds above 2m/s (7.2km/h) | ||
|
@@ -55,12 +242,35 @@ object AprsPacket { | |
"" | ||
} | ||
|
||
def formatCourseSpeedCompressed(location : Location) : String = { | ||
// only report speeds above 2m/s (7.2km/h) | ||
if (location.hasSpeed && location.hasBearing) { | ||
// && location.getSpeed > 2) | ||
var compressedBearing = (location.getBearing.asInstanceOf[Int] / 4).asInstanceOf[Int] | ||
var compressedSpeed = ((math.log(mps2kt(location.getSpeed)) / math.log(1.08)) - 1).asInstanceOf[Int] | ||
var c = compressedBearing.asInstanceOf[Byte] + 33; | ||
var s = compressedSpeed.asInstanceOf[Byte] + 33; | ||
// Negative speeds a courses cannot be expressed in base-91 and results in corrupt packets | ||
if(c < 33) c = 33 | ||
if(s < 33) s = 33 | ||
"%c%c".format(c.asInstanceOf[Char], s.asInstanceOf[Char]) | ||
} else { | ||
"" | ||
} | ||
} | ||
|
||
def formatFreq(csespd : String, freq : Float) : String = { | ||
if (freq == 0) "" else { | ||
val prefix = if (csespd.length() > 0) "/" else "" | ||
prefix + "%07.3fMHz".formatLocal(null, freq) | ||
} | ||
} | ||
|
||
def formatFreqMice(freq : Float) : String = { | ||
if (freq == 0) "" else { | ||
"%07.3fMHz".formatLocal(null, freq) | ||
} | ||
} | ||
|
||
def formatLogin(callsign : String, ssid : String, passcode : String, version : String) : String = { | ||
"user %s pass %s vers %s".format(formatCallSsid(callsign, ssid), passcode, version) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[important] It would be more intuitive to have the compression setting use a
de.duenndns.ListPreferenceWithValue
drop-down with the title "Beacon compression" and the values "uncompressed", "compressed" and "Mic-E".Together with the "Mic-E Status" that's down to two menu items, so they can appear in the main preferences screen and not be hidden in a sub-menu. The "Mic-E Status" menu then can be enabled/disabled based on the value of the tri-state.