diff --git a/res/values/strings.xml b/res/values/strings.xml index 3ef5884..0961993 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -121,6 +121,12 @@ Enable IGate IGate Settings +Supress APRS-IS Traffic Log +Suppresse all APRS-IS serverside traffic in the log + +Enable Bidirectional IGate +Allows messages to pass to RF + OSM Maps Enable offline mapping Uses locally hosted tile server @@ -438,6 +444,10 @@ Enter a filter for incoming packets Retry interval in seconds +Recently heard station timeout +Timeout to pass messages to recently heard stations to RF +Timeout in minutes + Message filter help Online reference for APRS-IS filters diff --git a/res/xml/igate.xml b/res/xml/igate.xml index e8e4678..2c57a05 100644 --- a/res/xml/igate.xml +++ b/res/xml/igate.xml @@ -28,8 +28,8 @@ + + + + + + diff --git a/src/AprsService.scala b/src/AprsService.scala index 892902a..fe47895 100644 --- a/src/AprsService.scala +++ b/src/AprsService.scala @@ -341,6 +341,9 @@ class AprsService extends Service { val full_status = if (status_postfix == "Digipeated") { addPost(StorageDatabase.Post.TYPE_DIGI, status_postfix, packet.toString) status_postfix + } else if (status_postfix == "APRS-IS > RF") { + addPost(StorageDatabase.Post.TYPE_DIGI, "APRS-IS > RF", packet.toString) + status_postfix } else { val fullStatus = status + status_postfix addPost(StorageDatabase.Post.TYPE_POST, fullStatus, packet.toString) @@ -437,6 +440,25 @@ class AprsService extends Service { } } + def sendThirdPartyPacket(packetString: String): Unit = { + // Parse the incoming string to an APRSPacket object + try { + val igatedPacket = Parser.parse(packetString) + + // Define additional information to be passed as status postfix + val igstatus = "APRS-IS > RF" + + // Send the packet with the additional status postfix + sendPacket(igatedPacket, igstatus) + + 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) diff --git a/src/IgateService.scala b/src/IgateService.scala index 74ff46f..a07f12b 100644 --- a/src/IgateService.scala +++ b/src/IgateService.scala @@ -4,6 +4,9 @@ import _root_.android.content.Context import _root_.java.io.{BufferedReader, InputStreamReader, OutputStream, PrintWriter, IOException} import _root_.java.net.{Socket, SocketException} import _root_.android.util.Log +import _root_.net.ab0oo.aprs.parser._ +import scala.collection.mutable +import java.util.Date // Define a callback interface for connection events trait ConnectionListener { @@ -15,7 +18,7 @@ class IgateService(service: AprsService, prefs: PrefsWrapper) extends Connection val TAG = "IgateService" val hostport = prefs.getString("p.igserver", "rotate.aprs2.net") val (host, port) = parseHostPort(hostport) - val so_timeout = prefs.getStringInt("p.igsotimeout", 30) + val so_timeout = prefs.getStringInt("p.igsotimeout", 120) val connectretryinterval = prefs.getStringInt("p.igconnectretry", 30) var conn: TcpSocketThread = _ var reconnecting = false // Track if we're reconnecting @@ -85,7 +88,7 @@ class IgateService(service: AprsService, prefs: PrefsWrapper) extends Connection // If the service is already running, don't proceed if (!service_running) { - Log.d(TAG, "start() - Service is already running, skipping connection.") + Log.d(TAG, "start() - Service is not running, skipping connection.") reconnecting = false return } @@ -124,6 +127,9 @@ class TcpSocketThread(host: String, port: Int, timeout: Int, service: AprsServic private var reader: BufferedReader = _ private var writer: PrintWriter = _ + // Assuming we have a Map to store the source calls and their last heard timestamps + val lastHeardCalls: mutable.Map[String, Long] = mutable.Map() + override def run(): Unit = { Log.d("IgateService", s"run() - Starting TCP connection to $host with timeout $timeout") service.addPost(StorageDatabase.Post.TYPE_INFO, "APRS-IS", s"Connecting to $host:$port") @@ -145,8 +151,11 @@ class TcpSocketThread(host: String, port: Int, timeout: Int, service: AprsServic val message = reader.readLine() if (message != null) { Log.d("IgateService", s"run() - Received message: $message") - handleMessage(message) - } else { + + handleMessage(message) + handleAprsTrafficPost(message) + + } else { Log.d("IgateService", "run() - Server disconnected. Attempting to reconnect.") running = false listener.onConnectionLost() // Notify listener (IgateService) of failure @@ -221,7 +230,7 @@ class TcpSocketThread(host: String, port: Int, timeout: Int, service: AprsServic // Handle modified data before submitting it to the server def handlePostSubmitData(data: String): Unit = { - Log.d("IgateService", s"handlePostSubmitData() - Received data: $data") + //Log.d("IgateService", s"handlePostSubmitData() - Received data: $data") // Modify the data before sending it to the server val modifiedData = modifyData(data) @@ -232,6 +241,13 @@ class TcpSocketThread(host: String, port: Int, timeout: Int, service: AprsServic return // Stop further processing if the packet contains RFONLY or TCPIP } + // Extract the callsign from the modified data (before the '>' symbol) + val callSign = modifiedData.split(">")(0).trim // Split the string and take the part before '>' + + // Update lastHeardCalls map with the current timestamp for that callsign + lastHeardCalls(callSign) = System.currentTimeMillis() // Use current time in milliseconds + Log.d("IgateService", s"handlePostSubmitData() - Extracted callsign: $callSign, updating last heard time to ${System.currentTimeMillis()} for that callsign.") + // Log the modified data to confirm the change Log.d("IgateService", s"handlePostSubmitData() - Modified data: $modifiedData") @@ -256,12 +272,6 @@ class TcpSocketThread(host: String, port: Int, timeout: Int, service: AprsServic } } - // Handle incoming messages from the server - def handleMessage(message: String): Unit = { - Log.d("IgateService", s"handleMessage() - Handling incoming message: $message") - // Add message handling logic - } - // Clean up resources def shutdown(): Unit = { if (socket != null) { @@ -272,4 +282,95 @@ class TcpSocketThread(host: String, port: Int, timeout: Int, service: AprsServic } } } + + def handleAprsTrafficPost(message: String): Unit = { + val aprsIstrafficEnabled = prefs.getBoolean("p.aprsistraffic", false) + + if (!aprsIstrafficEnabled) { + // If the checkbox is enabled, perform the action + service.addPost(StorageDatabase.Post.TYPE_DIGI, "APRS-IS Received", message) + Log.d("IgateService", s"APRS-IS traffic enabled, post added: $message") + } else { + // If the checkbox is not enabled, skip the action + Log.d("IgateService", "APRS-IS traffic disabled, skipping the post.") + return + } + } + + + def handleMessage(message: String): Unit = { + // Early return if message starts with '#' + if (message.startsWith("#")) { + Log.d("IgateService", "Message starts with '#', skipping processing.") + return + } + Log.d("IgateService", s"handleMessage() - Handling incoming message: $message") + val bidirectionalGate = prefs.getBoolean("p.aprsistorf", false) + val timelastheard = prefs.getStringInt("p.timelastheard", 30) + + if (!bidirectionalGate) { + Log.d("IgateService", "Bidirectional IGate disabled.") + return + } + + // Attempt to parse the message + val packetOpt = try { + val fap = Parser.parse(message) // Attempt initial parsing + + // Check if it's a MessagePacket (i.e., a message type) + fap.getAprsInformation() match { + case msg: MessagePacket => + // Only proceed with further processing if it's a MessagePacket + try { + val callssid = prefs.getCallSsid() // Get the local call sign from preferences + val sourceCall = fap.getSourceCall() // Use fap (parsed packet) + val destinationCall = fap.getDestinationCall() // Use fap + val lastUsedDigi = fap.getDigiString() // Use fap + val payload = fap.getAprsInformation() + val payloadString = if (payload != null) payload.toString else "" + val digipath = prefs.getString("digi_path", "WIDE1-1") + val formattedDigipath = if (digipath.nonEmpty) s",$digipath" else "" + // Extract the targeted callsign by stripping leading and trailing colons and removing spaces + val targetedCallsign = payloadString + .stripPrefix(":") // Remove any leading colon + .takeWhile(_ != ':') // Take everything up to the first colon + .replaceAll("\\s", "") // Remove any spaces + + Log.d("IgateService", s"Targeted Callsign: $targetedCallsign") + + Log.d("IgateService", s"handleMessage() - Parsed Packet Info:") + Log.d("IgateService", s"Source Call: $sourceCall") + Log.d("IgateService", s"Destination Call: $destinationCall") + Log.d("IgateService", s"Last Used Digi: $lastUsedDigi") + Log.d("IgateService", s"Payload: $payloadString") + + // Check if we have seen this source call in the last 30 minutes + val currentTime = System.currentTimeMillis() + val lastHeardTime = lastHeardCalls.getOrElse(targetedCallsign, 0L) + val timeElapsed = currentTime - lastHeardTime + Log.d("IgateService", s"handleMessage() - $targetedCallsign, " + s"last heard at $lastHeardTime, time elapsed: $timeElapsed ms.") + + if (timeElapsed <= timelastheard * 60 * 1000) { // If it was heard within 30 minutes + // Process and send the packet + val igatedPacket = s"$callssid>APDR17$formattedDigipath:}$sourceCall>$destinationCall,TCPIP,$callssid*:$payload" + Log.d("IgateService", igatedPacket) + service.sendThirdPartyPacket(igatedPacket) // Send the packet + } else { + Log.d("IgateService", s"handleMessage() - Source call $sourceCall has not been heard in the last 30 minutes, skipping processing.") + } + + } catch { + case e: Exception => + Log.e("IgateService", s"handleMessage() - Error processing parsed packet: $message", e) + } + case _ => + // If it's not a MessagePacket, don't process further + Log.d("IgateService", s"handleMessage() - Not a MessagePacket, skipping processing.") + } + } catch { + case e: Exception => + Log.e("IgateService", s"handleMessage() - Failed to parse packet: $message", e) + } + } + }