Mike 2025-02-08 17:38:52 -05:00 zatwierdzone przez GitHub
commit 001e474ec5
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
8 zmienionych plików z 475 dodań i 8 usunięć

Wyświetl plik

@ -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" />
<activity android:name=".GoogleMapAct" android:label="@string/app_map"
android:launchMode="singleTop"
android:parentActivityName=".HubActivity"

Wyświetl plik

@ -110,4 +110,14 @@
<item>460800</item>
<item>921600</item>
</string-array>
<string-array name="compressed_mice_status">
<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>
</resources>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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"

Wyświetl plik

@ -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 {
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) = {
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)

Wyświetl plik

@ -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]))
}

Wyświetl plik

@ -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")
}
}