Skip to content
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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@
android:parentActivityName=".PrefsAct"
android:launchMode="singleTop"
/>
<activity android:name=".CompressedPrefs"
android:label="@string/p__location_compressed_settings"
android:parentActivityName=".PrefsAct"
android:launchMode="singleTop"
android:configChanges="orientation|keyboardHidden|screenSize" />

Copy link
Owner

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.

<activity android:name=".GoogleMapAct" android:label="@string/app_map"
android:launchMode="singleTop"
android:parentActivityName=".HubActivity"
Expand Down
10 changes: 10 additions & 0 deletions res/values/arrays.xml
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,14 @@
<item>460800</item>
<item>921600</item>
</string-array>
<string-array name="compressed_mice_status">
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[optional] For consistency, the name should be p_mice_status_e (preferences, Mic-E status, entries)

<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>
Copy link
Owner

Choose a reason for hiding this comment

The 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>
13 changes: 13 additions & 0 deletions res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,19 @@
<string name="p_locsource_summary">Manual, Periodic or SmartBeaconing™</string>
<string name="p__location">Location Settings</string>

<string name="p__location_compressed_beacons">Normal Compressed Beacons</string>
<string name="p__location_compressed_beacons_on">Send Compressed Beacons</string>
<string name="p__location_compressed_beacons_off">Send Uncompressed Beacons</string>

<string name="p__location_mice_beacons">Mic-E Compressed Beacons</string>
<string name="p__location_mice_beacons_on">Send Mic-E Beacons</string>
<string name="p__location_mice_beacons_off">Send Uncompressed Beacons</string>

<string name="p__location_compressed_settings">Compressed Beacon Settings</string>
<string name="p__location_compressed_summary">Normal &amp; Mic-E Compression</string>

<string name="p__location_mice_status">Mic-E Status</string>

<string name="p_smartbeaconing">SmartBeaconing™</string>
<string name="p_sb_help">SmartBeaconing™ help</string>
<string name="p_sb_fast_speed">Fast Speed [km/h]</string>
Expand Down
27 changes: 27 additions & 0 deletions res/xml/compressed.xml
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>
9 changes: 9 additions & 0 deletions res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@

<PreferenceCategory
android:title="@string/p__position">
<PreferenceScreen
android:key="compressed_location"
android:title="@string/p__location_compressed_settings"
android:summary="@string/p__location_compressed_summary">

<intent android:action="android.intent.action.MAIN"
android:targetPackage="org.aprsdroid.app"
android:targetClass="org.aprsdroid.app.CompressedPrefs" />
</PreferenceScreen>
<PreferenceScreen
android:key="p_symbol"
android:title="@string/p_symbol"
Expand Down
210 changes: 210 additions & 0 deletions src/AprsPacket.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important] the function should be called miceStatusToBits() for consistency, also using the English string value is not a robust way to obtain the value. You should add a android:entryValues= property to the drop-down, and then you can encode the bit values ("111", "110", ...) in the compressed_mice_status_ev array.

[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. PrefsWrapper.getListItemIndex() will return the numeric ID, and the bit-ops should be:

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) = {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important] should be called encodeMiceInfo(). Also you are passing the latitude (as the dd parameter, but you are not encoding the ambiguity here. The code should also treat location ambiguity for latitude correctly.


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 = {
Copy link
Owner

Choose a reason for hiding this comment

The 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 formatAltitude<something> where something tells the format. I guess it's formatAltitudeMice() but I'm not quite sure.

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 + "}"
Copy link
Owner

Choose a reason for hiding this comment

The 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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
Loading