diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 11398d0b..5dff423b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -89,6 +89,12 @@
android:parentActivityName=".PrefsAct"
android:launchMode="singleTop"
/>
+
+
460800
- 921600
+
+ - Off Duty
+ - En Route
+ - In Service
+ - Returning
+ - Committed
+ - Special
+ - Priority
+ - EMERGENCY!
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 4e97d0dd..89989c81 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -249,6 +249,19 @@
Manual, Periodic or SmartBeaconing™
Location Settings
+Normal Compressed Beacons
+Send Compressed Beacons
+Send Uncompressed Beacons
+
+Mic-E Compressed Beacons
+Send Mic-E Beacons
+Send Uncompressed Beacons
+
+Compressed Beacon Settings
+Normal & Mic-E Compression
+
+Mic-E Status
+
SmartBeaconing™
SmartBeaconing™ help
Fast Speed [km/h]
diff --git a/res/xml/compressed.xml b/res/xml/compressed.xml
new file mode 100644
index 00000000..76e63861
--- /dev/null
+++ b/res/xml/compressed.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
index 2f2fff0f..dbc1cda0 100644
--- a/res/xml/preferences.xml
+++ b/res/xml/preferences.xml
@@ -52,6 +52,15 @@
+
+
+
+
(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) = {
+
+ 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 = {
+ 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 + "}"
+ }
+
+ 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)
diff --git a/src/AprsService.scala b/src/AprsService.scala
index 1fb87427..03e31b47 100644
--- a/src/AprsService.scala
+++ b/src/AprsService.scala
@@ -10,6 +10,7 @@ import _root_.android.widget.Toast
import _root_.net.ab0oo.aprs.parser._
+
object AprsService {
val PACKAGE = "org.aprsdroid.app"
// action intents
@@ -230,18 +231,89 @@ class AprsService extends Service {
Digipeater.parseList(digipath, true), payload)
}
+ def newPacketMice(payload: String, destString: String) = {
+ val digipath = prefs.getString("digi_path", "WIDE1-1")
+ val parsedDigipath = Digipeater.parseList(digipath, true)
+ val callsign_ssid = prefs.getCallSsid()
+ Log.d("newPacketMice", s"digipath retrieved: $digipath")
+
+ // Construct the micePacket string
+ val micePacketString = s"$callsign_ssid>$destString${if (digipath.nonEmpty) s",$digipath" else ""}:$payload"
+
+ // Log or return the constructed packet
+ Log.d("newPacketMice", s"Constructed MICE Packet: $micePacketString")
+
+ val micePacketParsed = Parser.parse(micePacketString)
+
+ micePacketParsed
+ }
def formatLoc(symbol : String, status : String, location : Location) = {
val pos = new Position(location.getLatitude, location.getLongitude, 0,
symbol(0), symbol(1))
pos.setPositionAmbiguity(prefs.getStringInt("priv_ambiguity", 0))
- val status_spd = if (prefs.getBoolean("priv_spdbear", true))
- AprsPacket.formatCourseSpeed(location) else ""
+
+ // Calculate status_spd
+ val status_spd = if (prefs.getBoolean("priv_spdbear", true)) {
+ if(prefs.getBoolean("compressed_location", false)) {
+ // Compressed format
+ AprsPacket.formatCourseSpeedCompressed(location)
+ } else {
+ AprsPacket.formatCourseSpeed(location)
+ }
+ } else ""
+ // Log status_spd
+ Log.d("formatLoc", s"Status Speed: $status_spd")
+
+ // Calculate status_freq
val status_freq = AprsPacket.formatFreq(status_spd, prefs.getStringFloat("frequency", 0.0f))
- val status_alt = if (prefs.getBoolean("priv_altitude", true))
- AprsPacket.formatAltitude(location) else ""
- val comment = status_spd + status_freq + status_alt + " " + status;
+
+ // Calculate status_alt
+ val status_alt = if (prefs.getBoolean("priv_altitude", true)) {
+ // if speed is empty then use compressed altitude, otherwise use full length altitude
+ if(prefs.getBoolean("compressed_location", false) && status_spd == "") {
+ // Compressed format
+ AprsPacket.formatAltitudeCompressed(location)
+ } else {
+ AprsPacket.formatAltitude(location)
+ }
+ } else ""
+ // Log status_alt
+ Log.d("formatLoc", s"Status Altitude: $status_alt")
+
+ if(prefs.getBoolean("compressed_location", false)) {
+ if(status_spd == "") {
+ // Speed is empty, so we can use a compressed altitude
+ if(status_alt == "") {
+ // Altitude is empty, so don't send any altitude data
+ pos.setCsTField(" sT")
+ } else {
+ // 3 signifies current GPS fix, GGA altitude, software compressed.
+ pos.setCsTField(status_alt + "3")
+ }
+ val packet = new PositionPacket(
+ pos, status_freq + " " + status, /* messaging = */ true)
+ packet.setCompressedFormat(true)
+ newPacket(packet)
+ } else {
+ // Speed is present, so we need to append the altitude to the end of the packet using the
+ // uncompressed method
+ // Apply the csT field with speed and course
+ // [ signifies current GPS fix, RMC speed, software compressed.
+ pos.setCsTField(status_spd + "[")
+ val packet = new PositionPacket(
+ pos, status_freq + status_alt + " " + status, /* messaging = */ true)
+ packet.setCompressedFormat(true)
+ newPacket(packet)
+ }
+ } else {
+ val packet = new PositionPacket(
+ pos, status_spd + status_freq + status_alt + " " + status, /* messaging = */ true)
+ newPacket(packet)
+ }
+
+ //val comment = status_spd + status_freq + status_alt + " " + status;
// TODO: slice after 43 bytes, not after 43 UTF-8 codepoints
- newPacket(new PositionPacket(pos, comment.slice(0, 43), /* messaging = */ true))
+ //newPacket(new PositionPacket(pos, comment.slice(0, 43), /* messaging = */ true))
}
def sendPacket(packet : APRSPacket, status_postfix : String) {
@@ -263,13 +335,56 @@ class AprsService extends Service {
}
def sendPacket(packet : APRSPacket) { sendPacket(packet, "") }
+ def formatLocMice(symbol : String, status : String, location : Location) = {
+ val privambiguity = 5 - prefs.getStringInt("priv_ambiguity", 0)
+ val ambiguity = if (privambiguity == 5) 0 else privambiguity
+
+ Log.d("MICE", s"Set Ambiguity $ambiguity")
+
+ val miceStatus = prefs.getString("p__location_mice_status", "Off Duty")
+ val (a, b, c) = AprsPacket.statusToBits(miceStatus)
+
+ val status_freq = AprsPacket.formatFreqMice(prefs.getStringFloat("frequency", 0.0f))
+ val (status_spd, course) = AprsPacket.formatCourseSpeedMice(location)
+
+ // Encoding process
+ val (infoString, west, longOffset) = AprsPacket.encodeInfo(location.getLongitude, status_spd, course, symbol)
+ val destString = AprsPacket.encodeDest(location.getLatitude, longOffset, west, a, b, c, ambiguity)
+
+ val altitudeValue = if (prefs.getBoolean("priv_altitude", true)) {
+ AprsPacket.formatAltitudeMice(location)
+ } else {
+ None
+ }
+ val altString = altitudeValue.map(alt => AprsPacket.altitude(alt.toInt)).getOrElse("")
+
+ val formatPayload = infoString +
+ (if (altString.isEmpty) "" else altString) +
+ (if (status.isEmpty) "" else status) +
+ (if (status.nonEmpty && status_freq.nonEmpty) " " else "") +
+ (if (status_freq.isEmpty) "" else status_freq) + "[1"
+
+ Log.d("formatLoc", s"MICE: $infoString $destString $altString")
+
+ val packet = newPacketMice(formatPayload, destString)
+
+ packet
+
+ }
+
def postLocation(location : Location) {
var symbol = prefs.getString("symbol", "")
if (symbol.length != 2)
symbol = getString(R.string.default_symbol)
val status = prefs.getString("status", getString(R.string.default_status))
- val packet = formatLoc(symbol, status, location)
-
+
+ // Use inline prefs.getBoolean to decide the packet format
+ val packet = if (prefs.getBoolean("compressed_mice", false)) {
+ formatLocMice(symbol, status, location)
+ } else {
+ formatLoc(symbol, status, location)
+ }
+
Log.d(TAG, "packet: " + packet)
sendPacket(packet, " (±%dm)".format(location.getAccuracy.asInstanceOf[Int]))
}
diff --git a/src/CompressedPrefs.scala b/src/CompressedPrefs.scala
new file mode 100644
index 00000000..38d87db8
--- /dev/null
+++ b/src/CompressedPrefs.scala
@@ -0,0 +1,77 @@
+package org.aprsdroid.app
+
+import _root_.android.content.SharedPreferences
+import _root_.android.os.Bundle
+import _root_.android.preference.{PreferenceActivity, PreferenceManager, CheckBoxPreference, ListPreference}
+import _root_.android.util.Log
+
+class CompressedPrefs extends PreferenceActivity with SharedPreferences.OnSharedPreferenceChangeListener {
+
+ lazy val prefs = new PrefsWrapper(this)
+
+ def loadXml() {
+ // Load compressed.xml preferences
+ addPreferencesFromResource(R.xml.compressed)
+ }
+
+ override def onCreate(savedInstanceState: Bundle) {
+ super.onCreate(savedInstanceState)
+ loadXml()
+ getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this)
+
+ // Update preferences state on activity creation
+ updateCheckBoxState()
+ }
+
+ override def onDestroy() {
+ super.onDestroy()
+ getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this)
+ }
+
+ override def onSharedPreferenceChanged(sp: SharedPreferences, key: String) {
+ // Handle preference changes for specific keys
+ key match {
+ case "compressed_location" | "compressed_mice" =>
+ // Update checkbox states when either preference changes
+ updateCheckBoxState()
+ case "p__location_mice_status" =>
+ // Handle changes for the p__location_mice_status preference (if needed)
+ updateStatus()
+ case _ => // No action for other preferences
+ }
+ }
+
+ // This method will enable/disable the checkboxes based on their current state
+ private def updateCheckBoxState(): Unit = {
+ val compressedLocationPref = findPreference("compressed_location").asInstanceOf[CheckBoxPreference]
+ val compressedMicePref = findPreference("compressed_mice").asInstanceOf[CheckBoxPreference]
+ val locationMiceStatusPref = findPreference("p__location_mice_status").asInstanceOf[ListPreference]
+
+ // If "compressed_location" is checked, disable "p__location_mice_status"
+ if (compressedLocationPref.isChecked) {
+ locationMiceStatusPref.setEnabled(false)
+ compressedMicePref.setEnabled(false) // Also disable "compressed_mice" when "compressed_location" is checked
+ } else {
+ locationMiceStatusPref.setEnabled(true) // Enable "p__location_mice_status" when "compressed_location" is not checked
+ compressedMicePref.setEnabled(true) // Re-enable "compressed_mice" if "compressed_location" is unchecked
+ }
+
+ // If "compressed_mice" is checked, disable "compressed_location"
+ if (compressedMicePref.isChecked) {
+ compressedLocationPref.setEnabled(false)
+ } else {
+ compressedLocationPref.setEnabled(true)
+ }
+ }
+
+
+ // Method to handle updates related to p__location_mice_status
+ private def updateStatus(): Unit = {
+ val statusPref = findPreference("p__location_mice_status").asInstanceOf[ListPreference]
+ val statusValue = statusPref.getValue
+
+ // Here, you can handle actions based on the selected status.
+ // For example, logging the selected status:
+ Log.d("CompressedPrefs", s"Selected Location Mice Status: $statusValue")
+ }
+}