kopia lustrzana https://github.com/ge0rg/aprsdroid
688 wiersze
23 KiB
Scala
688 wiersze
23 KiB
Scala
package org.aprsdroid.app
|
|
|
|
import _root_.android.app.Service
|
|
import _root_.android.content.{Context, Intent, IntentFilter}
|
|
import _root_.android.location._
|
|
import _root_.android.os.{Bundle, IBinder, Handler}
|
|
import _root_.android.preference.PreferenceManager
|
|
import _root_.android.util.Log
|
|
import _root_.android.widget.Toast
|
|
|
|
import _root_.net.ab0oo.aprs.parser._
|
|
import scala.collection.mutable
|
|
import java.time.Instant
|
|
|
|
object AprsService {
|
|
val PACKAGE = "org.aprsdroid.app"
|
|
// action intents
|
|
val SERVICE = PACKAGE + ".SERVICE"
|
|
val SERVICE_ONCE = PACKAGE + ".ONCE"
|
|
val SERVICE_SEND_PACKET = PACKAGE + ".SEND_PACKET"
|
|
val SERVICE_FREQUENCY = PACKAGE + ".FREQUENCY"
|
|
val SERVICE_STOP = PACKAGE + ".SERVICE_STOP"
|
|
// event intents
|
|
val SERVICE_STARTED = PACKAGE + ".SERVICE_STARTED"
|
|
val SERVICE_STOPPED = PACKAGE + ".SERVICE_STOPPED"
|
|
val POSITION = PACKAGE + ".POSITION"
|
|
val MICLEVEL = PACKAGE + ".MICLEVEL" // internal volume event intent
|
|
val LINK_ON = PACKAGE + ".LINK_ON"
|
|
val LINK_OFF = PACKAGE + ".LINK_OFF"
|
|
val LINK_INFO = PACKAGE + ".LINK_INFO"
|
|
// broadcast actions
|
|
val UPDATE = PACKAGE + ".UPDATE" // something added to the log
|
|
val MESSAGE = PACKAGE + ".MESSAGE" // we received a message/ack
|
|
val MESSAGETX = PACKAGE + ".MESSAGETX" // we created a message for TX
|
|
// broadcast intent extras
|
|
// SERVICE_STARTED
|
|
val API_VERSION = "api_version" // API version
|
|
val CALLSIGN = "callsign" // callsign + ssid of the user
|
|
// UPDATE
|
|
val TYPE = "type" // type
|
|
val STATUS = "status" // content
|
|
// POSITION
|
|
val LOCATION = "location" // Location object
|
|
val SOURCE = "source" // sender callsign
|
|
val PACKET = "packet" // raw packet content
|
|
// MESSAGE
|
|
// +- SOURCE
|
|
val DEST = "dest" // destination callsign
|
|
val BODY = "body" // body of the message
|
|
|
|
// APRSdroid API version
|
|
val API_VERSION_CODE = 1
|
|
|
|
// private intents for message handling
|
|
lazy val MSG_PRIV_INTENT = new Intent(MESSAGE).setPackage("org.aprsdroid.app")
|
|
lazy val MSG_TX_PRIV_INTENT = new Intent(MESSAGETX).setPackage("org.aprsdroid.app")
|
|
|
|
def intent(ctx : Context, action : String) : Intent = {
|
|
new Intent(action, null, ctx, classOf[AprsService])
|
|
}
|
|
|
|
var running = false
|
|
var link_error = 0
|
|
|
|
implicit def block2runnable[F](f: => F) = new Runnable() { def run() { f } }
|
|
}
|
|
|
|
class AprsService extends Service {
|
|
import AprsService._
|
|
val TAG = "APRSdroid.Service"
|
|
|
|
lazy val APP_VERSION = "APDR%s".format(
|
|
getPackageManager().getPackageInfo(getPackageName(), 0).versionName
|
|
filter (_.isDigit) take 2)
|
|
|
|
lazy val prefs = new PrefsWrapper(this)
|
|
|
|
lazy val dedupeTime = prefs.getStringInt("p.dedupe", 30) // Fetch NUM_OF_RETRIES from prefs, defaulting to 7 if not found
|
|
|
|
lazy val digipeaterpath = prefs.getString("digipeater_path", "WIDE1,WIDE2")
|
|
|
|
val handler = new Handler()
|
|
|
|
lazy val db = StorageDatabase.open(this)
|
|
|
|
lazy val msgService = new MessageService(this)
|
|
lazy val locSource = LocationSource.instanciateLocation(this, prefs)
|
|
lazy val msgNotifier = msgService.createMessageNotifier()
|
|
|
|
var poster : AprsBackend = null
|
|
|
|
var singleShot = false
|
|
|
|
override def onStart(i : Intent, startId : Int) {
|
|
Log.d(TAG, "onStart: " + i + ", " + startId);
|
|
super.onStart(i, startId)
|
|
handleStart(i)
|
|
}
|
|
|
|
override def onStartCommand(i : Intent, flags : Int, startId : Int) : Int = {
|
|
Log.d(TAG, "onStartCommand: " + i + ", " + flags + ", " + startId);
|
|
handleStart(i)
|
|
Service.START_REDELIVER_INTENT
|
|
}
|
|
|
|
def handleStart(i : Intent) {
|
|
if (i.getAction() == SERVICE_STOP) {
|
|
// explicitly disabled, remember this
|
|
prefs.setBoolean("service_running", false)
|
|
if (running)
|
|
stopSelf()
|
|
return
|
|
} else
|
|
if (i.getAction() == SERVICE_SEND_PACKET) {
|
|
if (!running) {
|
|
Log.d(TAG, "SEND_PACKET ignored, service not running.")
|
|
return
|
|
}
|
|
val data_field = i.getStringExtra("data")
|
|
if (data_field == null) {
|
|
Log.d(TAG, "SEND_PACKET ignored, data extra is empty.")
|
|
return
|
|
}
|
|
val p = Parser.parseBody(prefs.getCallSsid(), APP_VERSION, null,
|
|
data_field)
|
|
sendPacket(p)
|
|
return
|
|
} else
|
|
if (i.getAction() == SERVICE_FREQUENCY) {
|
|
val data_field = i.getStringExtra("frequency")
|
|
if (data_field == null) {
|
|
Log.d(TAG, "FREQUENCY ignored, 'frequency' extra is empty.")
|
|
return
|
|
}
|
|
val freq_cleaned = data_field.replace("MHz", "").trim
|
|
val freq = try { freq_cleaned.toFloat; freq_cleaned } catch { case _ : Throwable => "" }
|
|
if (prefs.getString("frequency", null) != freq) {
|
|
prefs.set("frequency", freq)
|
|
if (!running) return
|
|
// XXX: fall through into SERVICE_ONCE
|
|
} else return
|
|
}
|
|
|
|
|
|
// display notification (even though we are not actually started yet,
|
|
// but we need this to prevent error message reordering)
|
|
val toastString = if (i.getAction() == SERVICE_ONCE) {
|
|
// if already running, we want to send immediately and continue;
|
|
// otherwise, we finish after a single position report
|
|
// set to true if not yet running or already running singleShot
|
|
singleShot = !running || singleShot
|
|
if (singleShot)
|
|
getString(R.string.service_once)
|
|
else null
|
|
} else {
|
|
getString(R.string.service_start)
|
|
}
|
|
// only show toast on newly started service
|
|
if (toastString != null)
|
|
showToast(toastString.format(
|
|
prefs.getLocationSourceName(),
|
|
prefs.getBackendName()))
|
|
|
|
val callssid = prefs.getCallSsid()
|
|
ServiceNotifier.instance.start(this, callssid)
|
|
|
|
// the poster needs to be running before location updates come in
|
|
if (!running) {
|
|
running = true
|
|
startPoster()
|
|
|
|
// register for outgoing message notifications
|
|
registerReceiver(msgNotifier, new IntentFilter(AprsService.MESSAGETX))
|
|
} else
|
|
onPosterStarted()
|
|
}
|
|
|
|
def startPoster() {
|
|
if (poster != null)
|
|
poster.stop()
|
|
poster = AprsBackend.instanciateUploader(this, prefs)
|
|
if (poster.start())
|
|
onPosterStarted()
|
|
}
|
|
|
|
def onPosterStarted() {
|
|
Log.d(TAG, "onPosterStarted")
|
|
// (re)start location source, get location source name
|
|
val loc_info = locSource.start(singleShot)
|
|
|
|
val callssid = prefs.getCallSsid()
|
|
val message = "%s: %s".format(callssid, loc_info)
|
|
ServiceNotifier.instance.start(this, message)
|
|
|
|
msgService.sendPendingMessages()
|
|
|
|
sendBroadcast(new Intent(SERVICE_STARTED)
|
|
.putExtra(API_VERSION, API_VERSION_CODE)
|
|
.putExtra(CALLSIGN, callssid))
|
|
|
|
// startup completed, remember state
|
|
if (!singleShot)
|
|
prefs.setBoolean("service_running", true)
|
|
}
|
|
|
|
override def onBind(i : Intent) : IBinder = null
|
|
|
|
override def onUnbind(i : Intent) : Boolean = false
|
|
|
|
def showToast(msg : String) {
|
|
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
|
addPost(StorageDatabase.Post.TYPE_INFO, null, msg)
|
|
}
|
|
|
|
override def onDestroy() {
|
|
running = false
|
|
link_error = 0
|
|
// catch FC when service is killed from outside
|
|
if (poster != null) {
|
|
poster.stop()
|
|
showToast(getString(R.string.service_stop))
|
|
|
|
sendBroadcast(new Intent(SERVICE_STOPPED))
|
|
}
|
|
msgService.stop()
|
|
locSource.stop()
|
|
scala.util.control.Exception.ignoring(classOf[IllegalArgumentException]) {
|
|
unregisterReceiver(msgNotifier)
|
|
}
|
|
ServiceNotifier.instance.stop(this)
|
|
}
|
|
|
|
def newPacket(payload : InformationField) = {
|
|
val digipath = prefs.getString("digi_path", "WIDE1-1")
|
|
new APRSPacket(prefs.getCallSsid(), APP_VERSION,
|
|
Digipeater.parseList(digipath, true), payload)
|
|
}
|
|
|
|
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)) {
|
|
if(prefs.getBoolean("compressed_location", false)) {
|
|
// Compressed format
|
|
AprsPacket.formatCourseSpeedCompressed(location)
|
|
} else {
|
|
AprsPacket.formatCourseSpeed(location)
|
|
}
|
|
} else ""
|
|
val status_freq = AprsPacket.formatFreq(status_spd, prefs.getStringFloat("frequency", 0.0f))
|
|
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 ""
|
|
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))
|
|
}
|
|
|
|
def sendPacket(packet : APRSPacket, status_postfix : String) {
|
|
implicit val ec = scala.concurrent.ExecutionContext.global
|
|
scala.concurrent.Future {
|
|
val status = try {
|
|
val status = poster.update(packet)
|
|
val full_status = status + status_postfix
|
|
addPost(StorageDatabase.Post.TYPE_POST, full_status, packet.toString)
|
|
full_status
|
|
} catch {
|
|
case e : Exception =>
|
|
addPost(StorageDatabase.Post.TYPE_ERROR, "Error", e.toString())
|
|
e.printStackTrace()
|
|
e.toString()
|
|
}
|
|
handler.post { sendPacketFinished(status) }
|
|
}
|
|
}
|
|
def sendPacket(packet : APRSPacket) { sendPacket(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)
|
|
|
|
Log.d(TAG, "packet: " + packet)
|
|
sendPacket(packet, " (±%dm)".format(location.getAccuracy.asInstanceOf[Int]))
|
|
}
|
|
|
|
def sendPacketFinished(result : String) {
|
|
if (singleShot) {
|
|
singleShot = false
|
|
stopSelf()
|
|
} else {
|
|
val message = "%s: %s".format(prefs.getCallSsid(), result)
|
|
ServiceNotifier.instance.notifyPosition(this, prefs, message)
|
|
}
|
|
}
|
|
|
|
def sendTestPacket(packetString: String): Unit = {
|
|
// Parse the incoming string to an APRSPacket object
|
|
try {
|
|
val testPacket = Parser.parse(packetString)
|
|
|
|
// Define additional information to be passed as status postfix
|
|
val digistatus = " - Digipeated"
|
|
|
|
// Send the packet with the additional status postfix
|
|
sendPacket(testPacket, digistatus)
|
|
|
|
Log.d("APRSdroid.Service", s"Successfully sent packet: $packetString")
|
|
} catch {
|
|
case e: Exception =>
|
|
Log.e("APRSdroid.Service", s"Failed to send packet: $packetString", e)
|
|
}
|
|
}
|
|
|
|
def parsePacket(ts : Long, message : String, source : Int) {
|
|
try {
|
|
var fap = Parser.parse(message)
|
|
if (fap.getType() == APRSTypes.T_THIRDPARTY) {
|
|
Log.d(TAG, "parsePacket: third-party packet from " + fap.getSourceCall())
|
|
val inner = fap.getAprsInformation().toString()
|
|
// strip away leading "}"
|
|
fap = Parser.parse(inner.substring(1, inner.length()))
|
|
}
|
|
|
|
val callssid = prefs.getCallSsid()
|
|
if (source == StorageDatabase.Post.TYPE_INCMG &&
|
|
fap.getSourceCall().equalsIgnoreCase(callssid) &&
|
|
fap.getLastUsedDigi() != null) {
|
|
Log.i(TAG, "got digipeated own packet")
|
|
val message = getString(R.string.got_digipeated, fap.getLastUsedDigi(),
|
|
fap.getAprsInformation().toString())
|
|
ServiceNotifier.instance.notifyPosition(this, prefs, message, "dgp_")
|
|
return
|
|
}
|
|
|
|
if (fap.getAprsInformation() == null) {
|
|
Log.d(TAG, "parsePacket() misses payload: " + message)
|
|
return
|
|
}
|
|
if (fap.hasFault())
|
|
throw new Exception("FAP fault")
|
|
fap.getAprsInformation() match {
|
|
case pp : PositionPacket => addPosition(ts, fap, pp, pp.getPosition(), null)
|
|
case op : ObjectPacket => addPosition(ts, fap, op, op.getPosition(), op.getObjectName())
|
|
case msg : MessagePacket => msgService.handleMessage(ts, fap, msg)
|
|
}
|
|
} catch {
|
|
case e : Exception =>
|
|
Log.d(TAG, "parsePacket() unsupported packet: " + message)
|
|
e.printStackTrace()
|
|
}
|
|
}
|
|
|
|
def getCSE(field : InformationField) : CourseAndSpeedExtension = {
|
|
field.getExtension() match {
|
|
case cse : CourseAndSpeedExtension => cse
|
|
case _ => null
|
|
}
|
|
}
|
|
def addPosition(ts : Long, ap : APRSPacket, field : InformationField, pos : Position, objectname : String) {
|
|
val cse = getCSE(field)
|
|
db.addPosition(ts, ap, pos, cse, objectname)
|
|
|
|
sendBroadcast(new Intent(POSITION)
|
|
.putExtra(SOURCE, ap.getSourceCall())
|
|
.putExtra(LOCATION, AprsPacket.position2location(ts, pos, cse))
|
|
.putExtra(CALLSIGN, if (objectname != null) objectname else ap.getSourceCall())
|
|
.putExtra(PACKET, ap.toString())
|
|
)
|
|
}
|
|
|
|
def addPost(t : Int, status : String, message : String) {
|
|
val ts = System.currentTimeMillis()
|
|
db.addPost(ts, t, status, message)
|
|
if (t == StorageDatabase.Post.TYPE_POST || t == StorageDatabase.Post.TYPE_INCMG) {
|
|
parsePacket(ts, message, t)
|
|
} else {
|
|
// only log status messages
|
|
Log.d(TAG, "addPost: " + status + " - " + message)
|
|
}
|
|
sendBroadcast(new Intent(UPDATE)
|
|
.putExtra(TYPE, t)
|
|
.putExtra(STATUS, message))
|
|
}
|
|
// support for translated IDs
|
|
def addPost(t : Int, status_id : Int, message : String) {
|
|
addPost(t, getString(status_id), message)
|
|
}
|
|
|
|
def postAddPost(t : Int, status_id : Int, message : String) {
|
|
// only log "info" if enabled in prefs
|
|
if (t == StorageDatabase.Post.TYPE_INFO && prefs.getBoolean("conn_log", false) == false)
|
|
return
|
|
handler.post {
|
|
addPost(t, status_id, message)
|
|
if (t == StorageDatabase.Post.TYPE_INCMG)
|
|
msgService.sendPendingMessages()
|
|
else if (t == StorageDatabase.Post.TYPE_ERROR)
|
|
stopSelf()
|
|
}
|
|
}
|
|
def postSubmit(post : String) {
|
|
// Log the incoming post message for debugging
|
|
Log.d("APRSdroid.Service", s"Incoming post: $post")
|
|
postAddPost(StorageDatabase.Post.TYPE_INCMG, R.string.post_incmg, post)
|
|
|
|
// Process the incoming post
|
|
processIncomingPost(post)
|
|
}
|
|
|
|
// Map to store recent digipeats with their timestamps
|
|
val recentDigipeats: mutable.Map[String, Instant] = mutable.Map()
|
|
|
|
// Function to add or update the digipeat
|
|
def storeDigipeat(sourceCall: String, destinationCall: String, payload: String): Unit = {
|
|
// Unique identifier using source call, destination call, and payload
|
|
val key = s"$sourceCall>$destinationCall:$payload"
|
|
recentDigipeats(key) = Instant.now() // Store the current timestamp
|
|
}
|
|
|
|
// Function to filter digipeats that are older than dedupeTime seconds
|
|
def isDigipeatRecent(sourceCall: String, destinationCall: String, payload: String): Boolean = {
|
|
// Unique identifier using source call, destination call, and payload
|
|
val key = s"$sourceCall>$destinationCall:$payload"
|
|
recentDigipeats.get(key) match {
|
|
case Some(timestamp) =>
|
|
// Check if the packet was heard within the last 30 seconds
|
|
Instant.now().isBefore(timestamp.plusSeconds(dedupeTime))
|
|
case None =>
|
|
false // Not found in recent digipeats
|
|
}
|
|
}
|
|
|
|
// Function to clean up old entries
|
|
def cleanupOldDigipeats(): Unit = {
|
|
val now = Instant.now()
|
|
// Retain only those digipeats that are within the last 30 seconds
|
|
recentDigipeats.retain { case (_, timestamp) =>
|
|
now.isBefore(timestamp.plusSeconds(dedupeTime))
|
|
}
|
|
}
|
|
|
|
def processIncomingPost(post: String) {
|
|
Log.d(TAG, "POST STRING TEST: " + post) // Log the incoming post for debugging
|
|
|
|
// Check if backendName contains "KISS" or "AFSK"
|
|
if (prefs.getBackendName().contains("KISS") || prefs.getBackendName().contains("AFSK")) {
|
|
android.util.Log.d("PrefsAct", "Backend contains KISS or AFSK")
|
|
} else {
|
|
android.util.Log.d("PrefsAct", "Backend does not contain KISS or AFSK")
|
|
return
|
|
}
|
|
//TODO, Add workaround for unsupported formats.
|
|
// Attempt to parse the incoming post to an APRSPacket.
|
|
val packet = try {
|
|
Parser.parse(post) // Attempt to parse
|
|
} catch {
|
|
case e: Exception =>
|
|
Log.e("Parsing FAILED!", s"Failed to parse packet: $post", e)
|
|
return // Exit the function if parsing fails
|
|
}
|
|
|
|
// Check if both digipeating and regeneration are enabled. Temp fix until re-implementation. Remove later on.
|
|
if (prefs.isDigipeaterEnabled() && prefs.isRegenerateEnabled()) {
|
|
Log.d("APRSdroid.Service", "Both Digipeating and Regeneration are enabled; Set Regen to false.")
|
|
prefs.setBoolean("p.regenerate", false) // Disable regeneration
|
|
|
|
}
|
|
|
|
// New regen
|
|
if (!prefs.isDigipeaterEnabled() && prefs.isRegenerateEnabled()) {
|
|
Log.d("APRSdroid.Service", "Regen enabled")
|
|
sendTestPacket(packet.toString)
|
|
return // Exit if both digipeating and regeneration are enabled
|
|
}
|
|
|
|
// Check if the digipeating setting is enabled
|
|
if (!prefs.isDigipeaterEnabled()) {
|
|
Log.d("APRSdroid.Service", "Digipeating is disabled; skipping processing.")
|
|
return // Exit if digipeating is not enabled
|
|
}
|
|
|
|
cleanupOldDigipeats() // Clean up old digipeats before processing
|
|
|
|
// Try to parse the incoming post to an APRSPacket
|
|
try {
|
|
// Now you can access the source call from the packet
|
|
val callssid = prefs.getCallSsid()
|
|
val sourceCall = packet.getSourceCall()
|
|
val destinationCall = packet.getDestinationCall();
|
|
val lastUsedDigi = packet.getDigiString()
|
|
val payload = packet.getAprsInformation()
|
|
|
|
val payloadString = packet.getAprsInformation().toString() // Ensure payload is a String
|
|
|
|
|
|
// Check if callssid matches sourceCall; if they match, do not digipeat
|
|
if (callssid == sourceCall) {
|
|
Log.d("APRSdroid.Service", s"No digipeat: callssid ($callssid) matches source call ($sourceCall).")
|
|
return // Exit if no digipeating is needed
|
|
}
|
|
|
|
// Check if this packet has been digipeated recently
|
|
if (isDigipeatRecent(sourceCall, destinationCall, payloadString)) {
|
|
Log.d("APRSdroid.Service", s"Packet from $sourceCall to $destinationCall and $payload has been heard recently, skipping digipeating.")
|
|
return // Skip processing this packet
|
|
}
|
|
|
|
|
|
val (modifiedDigiPath, digipeatOccurred) = processDigiPath(lastUsedDigi, callssid)
|
|
|
|
|
|
Log.d("APRSdroid.Service", s"Source: $sourceCall")
|
|
Log.d("APRSdroid.Service", s"Destination: $destinationCall")
|
|
Log.d("APRSdroid.Service", s"Digi: $lastUsedDigi")
|
|
Log.d("APRSdroid.Service", s"Modified Digi Path: $modifiedDigiPath")
|
|
|
|
Log.d("APRSdroid.Service", s"Payload: $payload")
|
|
|
|
// Format the string for sending
|
|
val testPacket = s"$sourceCall>$destinationCall,$modifiedDigiPath:$payload"
|
|
|
|
// Optionally, send a test packet with the formatted string only if a digipeat occurred
|
|
if (digipeatOccurred) {
|
|
sendTestPacket(testPacket)
|
|
|
|
// Store the digipeat to the recent list
|
|
storeDigipeat(sourceCall, destinationCall, payloadString)
|
|
|
|
} else {
|
|
Log.d("APRSdroid.Service", "No digipeat occurred, not sending a test packet.")
|
|
}
|
|
|
|
} catch {
|
|
case e: Exception =>
|
|
Log.e("APRSdroid.Service", s"Failed to parse packet: $post", e)
|
|
}
|
|
}
|
|
|
|
def processDigiPath(lastUsedDigi: String, callssid: String): (String, Boolean) = {
|
|
// Log the input Digi path
|
|
Log.d("APRSdroid.Service", s"Original Digi Path: '$lastUsedDigi'")
|
|
|
|
// If lastUsedDigi is empty, return it unchanged
|
|
if (lastUsedDigi.trim.isEmpty) {
|
|
Log.d("APRSdroid.Service", "LastUsedDigi is empty, returning unchanged.")
|
|
return (lastUsedDigi, false)
|
|
}
|
|
|
|
// Remove leading comma for easier processing
|
|
val trimmedPath = lastUsedDigi.stripPrefix(",")
|
|
|
|
// Split the path into components, avoiding empty strings
|
|
val pathComponents = trimmedPath.split(",").toList.filter(_.nonEmpty)
|
|
val digipeaterPaths = digipeaterpath.split(",").toList.filter(_.nonEmpty)
|
|
|
|
// Create a new list of components with modifications
|
|
val (modifiedPath, modified) = pathComponents.foldLeft((List.empty[String], false)) {
|
|
case ((acc, hasModified), component) =>
|
|
// Check if callssid* is in the path and skip if found
|
|
if (component == s"$callssid*") {
|
|
// Skip digipeating if callssid* is found
|
|
return (lastUsedDigi, false) // Return the original path, do not modify
|
|
} else if (!hasModified && (digipeaterPaths.exists(path => component.split("-")(0) == path) || digipeaterPaths.contains(component)) || component == callssid) {
|
|
// We need to check if the first unused component matches digipeaterpath
|
|
if (acc.isEmpty || acc.last.endsWith("*")) {
|
|
// This is the first unused component
|
|
component match {
|
|
|
|
case w if w == callssid =>
|
|
// If `callssid` is found (without *), replace with `callssid*`
|
|
if (!hasModified) {
|
|
(acc :+ s"$callssid*", true)
|
|
} else {
|
|
(acc :+ w, hasModified) // If already modified, keep `callssid` as-is
|
|
}
|
|
|
|
case w if w.matches(".*-(\\d+)$") =>
|
|
// Extract the number from the suffix
|
|
val number = w.split("-").last.toInt
|
|
// Decrement the number
|
|
val newNumber = number - 1
|
|
|
|
if (newNumber == 0) {
|
|
// If the number is decremented to 0, remove the component and insert callssid*
|
|
(acc :+ s"$callssid*", true)
|
|
} else {
|
|
// Otherwise, decrement the number and keep the component
|
|
val newComponent = w.stripSuffix(s"-$number") + s"-$newNumber"
|
|
(acc :+ s"$callssid*" :+ newComponent, true)
|
|
}
|
|
|
|
case _ =>
|
|
// Leave unchanged if there's no -N suffix
|
|
(acc :+ component, hasModified)
|
|
}
|
|
|
|
} else {
|
|
// If the first unused component doesn't match digipeaterpath, keep unchanged
|
|
(acc :+ component, hasModified)
|
|
}
|
|
|
|
} else {
|
|
// Keep the component as it is
|
|
(acc :+ component, hasModified)
|
|
}
|
|
}
|
|
|
|
// Rebuild the modified path
|
|
val resultPath = modifiedPath.mkString(",")
|
|
|
|
// Log the modified path before returning
|
|
Log.d("APRSdroid.Service", s"Modified Digi Path: '$resultPath'")
|
|
|
|
// If no modification occurred, return the original lastUsedDigi
|
|
if (resultPath == trimmedPath) {
|
|
Log.d("APRSdroid.Service", "No modifications were made; returning the original path.")
|
|
return (lastUsedDigi, false)
|
|
}
|
|
|
|
// Return the modified path with a leading comma
|
|
(s"$resultPath", true)
|
|
}
|
|
|
|
def postAbort(post : String) {
|
|
postAddPost(StorageDatabase.Post.TYPE_ERROR, R.string.post_error, post)
|
|
}
|
|
def postPosterStarted() {
|
|
handler.post {
|
|
onPosterStarted()
|
|
}
|
|
}
|
|
|
|
def postLinkOn(link : Int) {
|
|
link_error = 0
|
|
sendBroadcast(new Intent(LINK_ON).putExtra(LINK_INFO, link))
|
|
val message = getString(R.string.status_linkon, getString(link))
|
|
ServiceNotifier.instance.start(this, message)
|
|
}
|
|
|
|
def postLinkOff(link : Int) {
|
|
link_error = link
|
|
sendBroadcast(new Intent(LINK_OFF).putExtra(LINK_INFO, link))
|
|
val message = getString(R.string.status_linkoff, getString(link))
|
|
ServiceNotifier.instance.start(this, message)
|
|
}
|
|
}
|
|
|