-
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 1 commit
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,154 @@ 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 = { | ||
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 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 the last characters with 'Z', ensuring ambiguity is set | ||
val validAmbiguity = ambiguity.max(0).min(4) | ||
encoded.take(6 - validAmbiguity) + "Z" * validAmbiguity | ||
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] this will overwrite up to four characters of the destination callsign, potentially removing the C bit, the north bit, the longitude+100° bit, and the west bit. |
||
} | ||
|
||
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 +192,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 +217,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.