From dda032c3e5f7a0dbcbc805cc3707daf3ea1f1b12 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 27 Oct 2024 09:59:05 -0700 Subject: [PATCH] Digipeater Updates & Other Features --- build.gradle | 8 -- res/values/strings.xml | 28 +++++ res/xml/backend_tcp.xml | 8 ++ res/xml/backend_tcptnc.xml | 9 ++ res/xml/preferences.xml | 59 ++++++++++ src/AprsService.scala | 215 +++++++++++++++++++++++++++++++++++ src/MessageListAdapter.scala | 6 +- src/MessageService.scala | 9 +- src/PrefsWrapper.scala | 7 ++ 9 files changed, 337 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index 0c002ec..88177c3 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,6 @@ plugins { // obtain revision from git id 'org.ajoberstar.grgit' version '1.6.0' // gradle-amazon-app-store-publisher - id "app.brant.amazonappstorepublisher" version "0.1.0" } allprojects { @@ -69,13 +68,6 @@ def mapsApiKey() { properties.getProperty('mapsApiKey', "AIzaSyA12R_iI_upYQ33FWnPU_8GlMKrEmjDxiQ") } -amazon { - securityProfile = file("amazon-publish-credentials.json") - applicationId = "amzn1.devportal.mobileapp.90ffde1571a347f8a100e1083c64812e" - pathToApks = [ file("build/outputs/apk/release/aprsdroid-release.apk") ] - replaceEdit = true -} - android { compileSdkVersion 33 buildToolsVersion "33.0.2" diff --git a/res/values/strings.xml b/res/values/strings.xml index 4e97d0d..25d1736 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -108,6 +108,21 @@ \n\nTranslation: Home Page +Regenerate Packets +Dangerously regenerates heard packets with NO filter!! + +Digipeating Functions +Digipeater +Enable Digipeater + +Dedupe Timeout +Timeout in seconds +Digipeat timeout for duplicate packets + +Digipeater Path +Set Digi Path +Path to digipeat + Overlays Google: Map @@ -312,6 +327,15 @@ Error: %s Connected: %s +Messaging +Message Retries +Number of messages to retry +Message retry limit + +Retry Interval +Retry interval start rate +Rate doubles each retry + APRS digi path hop 1, hop 2, ... @@ -349,6 +373,10 @@ Time before resetting the connection Timeout value in seconds (0 = disable) +TCP reconnect timeout +Time before reconnecting +Timeout value in seconds + Map file name MapsForge map file for APRSdroid Choose map file diff --git a/res/xml/backend_tcp.xml b/res/xml/backend_tcp.xml index 1fa2069..83c801b 100644 --- a/res/xml/backend_tcp.xml +++ b/res/xml/backend_tcp.xml @@ -45,6 +45,14 @@ android:defaultValue="120" android:dialogTitle="@string/p_sotimeout_entry" /> + + diff --git a/res/xml/backend_tcptnc.xml b/res/xml/backend_tcptnc.xml index ebc2d82..01ca37d 100644 --- a/res/xml/backend_tcptnc.xml +++ b/res/xml/backend_tcptnc.xml @@ -22,6 +22,15 @@ android:defaultValue="120" android:dialogTitle="@string/p_sotimeout_entry" /> + + + diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index 2f2fff0..5660332 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -224,6 +224,65 @@ android:title="@string/p_themefile" android:summary="@string/p_themefile_summary" /> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/AprsService.scala b/src/AprsService.scala index 1fb8742..d4563b4 100644 --- a/src/AprsService.scala +++ b/src/AprsService.scala @@ -9,6 +9,8 @@ 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" @@ -73,6 +75,10 @@ class AprsService extends Service { 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", "WIDE") + val handler = new Handler() lazy val db = StorageDatabase.open(this) @@ -284,6 +290,21 @@ class AprsService extends Service { } } + def sendTestPacket(packetString: String): Unit = { + // Parse the incoming string to an APRSPacket object + try { + val testPacket = Parser.parse(packetString) + + // Send the packet with an empty status postfix + sendPacket(testPacket) + + 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) @@ -372,7 +393,201 @@ class AprsService extends Service { } } 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) { + + val packet = Parser.parse(post) // Parse the incoming post to an APRSPacket + // 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 + } + + + // 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 { + logReceivedPacket(packet) // Log the received packet + + // 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) + + // 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 && component.startsWith(digipeaterpath)) { + // Handle the first unused WIDE path + component match { + case w if w.endsWith("-2") => + // Change -2 to -1 and insert callssid* before it + (acc :+ s"$callssid*" :+ w.stripSuffix("-2") + "-1", true) + case w if w.endsWith("-1") => + // Remove the WIDE component entirely and insert callssid* + (acc :+ s"$callssid*", true) + case _ => + // Leave unchanged if there's no -1 or -2 + (acc :+ component, hasModified) + } + } else if (component.startsWith(callssid) && !component.endsWith("*")) { + // Replace callssid with callssid* only if it hasn't been modified + if (!hasModified) { + (acc :+ s"$callssid*", true) + } else { + (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) { diff --git a/src/MessageListAdapter.scala b/src/MessageListAdapter.scala index 0a8257b..9b60dca 100644 --- a/src/MessageListAdapter.scala +++ b/src/MessageListAdapter.scala @@ -14,7 +14,6 @@ object MessageListAdapter { val LIST_FROM = Array("TSS", CALL, TEXT) val LIST_TO = Array(R.id.listts, R.id.liststatus, R.id.listmessage) - val NUM_OF_RETRIES = 7 // null, incoming, out-new, out-acked, out-rejected, out-aborted val COLORS = Array(0, 0xff8080b0, 0xff80a080, 0xff30b030, 0xffb03030, 0xffa08080) } @@ -25,6 +24,9 @@ class MessageListAdapter(context : Context, prefs : PrefsWrapper, lazy val storage = StorageDatabase.open(context) + lazy val NUM_OF_RETRIES = prefs.getStringInt("p.messaging", 7) // Fetch NUM_OF_RETRIES from prefs, defaulting to 7 if not found + + reload() lazy val locReceiver = new LocationReceiver2(load_cursor, @@ -45,7 +47,7 @@ class MessageListAdapter(context : Context, prefs : PrefsWrapper, case TYPE_INCOMING => targetcall case TYPE_OUT_NEW => - "%s %d/%d".format(mycall, retrycnt, MessageListAdapter.NUM_OF_RETRIES) + "%s %d/%d".format(mycall, retrycnt, NUM_OF_RETRIES) case TYPE_OUT_ACKED => mycall case TYPE_OUT_REJECTED => diff --git a/src/MessageService.scala b/src/MessageService.scala index 93a0b63..2dd5d38 100644 --- a/src/MessageService.scala +++ b/src/MessageService.scala @@ -9,7 +9,11 @@ import _root_.net.ab0oo.aprs.parser._ class MessageService(s : AprsService) { val TAG = "APRSdroid.MsgService" - val NUM_OF_RETRIES = 7 + val NUM_OF_RETRIES = s.prefs.getStringInt("p.messaging", 7) + + val RETRY_INTERVAL = s.prefs.getStringInt("p.retry", 30) + + val pendingSender = new Runnable() { override def run() { sendPendingMessages() } } def createMessageNotifier() = new BroadcastReceiver() { @@ -59,7 +63,8 @@ class MessageService(s : AprsService) { } // return 2^n * 30s, at most 32min - def getRetryDelayMS(retrycnt : Int) = 30000 * (1 << math.min(retrycnt - 1, 6)) + def getRetryDelayMS(retrycnt : Int) = (RETRY_INTERVAL * 1000) * (1 << math.min(retrycnt - 1, NUM_OF_RETRIES)) + def scheduleNextSend(delay : Long) { // add some time to prevent fast looping diff --git a/src/PrefsWrapper.scala b/src/PrefsWrapper.scala index 1be9d4c..fcdd987 100644 --- a/src/PrefsWrapper.scala +++ b/src/PrefsWrapper.scala @@ -12,6 +12,13 @@ class PrefsWrapper(val context : Context) { def getString(key : String, defValue : String) = prefs.getString(key, defValue) def getBoolean(key : String, defValue : Boolean) = prefs.getBoolean(key, defValue) + def isDigipeaterEnabled(): Boolean = { + prefs.getBoolean("p.digipeating", false) + } + def isRegenerateEnabled(): Boolean = { + prefs.getBoolean("p.regenerate", false) + } + // safely read integers def getStringInt(key : String, defValue : Int) = { try { prefs.getString(key, null).trim.toInt } catch { case _ : Throwable => defValue }