From f2a3cbdda10cce4b908bbac4e529d322eed86fb9 Mon Sep 17 00:00:00 2001 From: Charlie Burrows Date: Tue, 14 Mar 2023 00:51:10 -0400 Subject: [PATCH 1/7] Added DigiRig backend and some UI elements in the AFSK config screen. --- res/values/strings.xml | 2 + res/xml/backend_digirig.xml | 19 ++++ res/xml/proto_afsk.xml | 16 ++- src/PrefsWrapper.scala | 2 + src/backend/AprsBackend.scala | 12 ++- src/backend/DigiRig.scala | 193 ++++++++++++++++++++++++++++++++++ 6 files changed, 240 insertions(+), 4 deletions(-) create mode 100644 res/xml/backend_digirig.xml create mode 100644 src/backend/DigiRig.scala diff --git a/res/values/strings.xml b/res/values/strings.xml index 4e97d0dd..3cc4f625 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -325,6 +325,8 @@ Bluetooth Headset Use Bluetooth (SCO) headset for AFSK Audio Output +Use Push-to-Talk +Push-to-Talk Port Voice Call Ringtone diff --git a/res/xml/backend_digirig.xml b/res/xml/backend_digirig.xml new file mode 100644 index 00000000..72d957ac --- /dev/null +++ b/res/xml/backend_digirig.xml @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/res/xml/proto_afsk.xml b/res/xml/proto_afsk.xml index 80a8a324..bba1381c 100644 --- a/res/xml/proto_afsk.xml +++ b/res/xml/proto_afsk.xml @@ -37,8 +37,20 @@ android:title="@string/p_afsk_prefix" android:summary="@string/p_afsk_prefix_summary" android:dialogTitle="@string/p_afsk_prefix_entry" /> - - + + + + diff --git a/src/PrefsWrapper.scala b/src/PrefsWrapper.scala index 1be9d4c2..fb3269cd 100644 --- a/src/PrefsWrapper.scala +++ b/src/PrefsWrapper.scala @@ -102,6 +102,8 @@ class PrefsWrapper(val context : Context) { def getProto() = getString("proto", "aprsis") def getAfskHQ() = getBoolean("afsk.hqdemod", true) + def getAfskRTS() = getBoolean("afsk.ptt", false) + def getPTTPort() = getString("afsk.pttport", "") def getAfskBluetooth() = getBoolean("afsk.btsco", false) && getAfskHQ() def getAfskOutput() = if (getAfskBluetooth()) AudioManager.STREAM_VOICE_CALL else getStringInt("afsk.output", 0) } diff --git a/src/backend/AprsBackend.scala b/src/backend/AprsBackend.scala index 64b33d71..e9e5e186 100644 --- a/src/backend/AprsBackend.scala +++ b/src/backend/AprsBackend.scala @@ -3,6 +3,7 @@ package org.aprsdroid.app import android.Manifest import android.os.Build import _root_.net.ab0oo.aprs.parser.APRSPacket + import _root_.java.io.{InputStream, OutputStream} object AprsBackend { @@ -94,7 +95,14 @@ object AprsBackend { R.xml.backend_usb, Set(), CAN_DUPLEX, - PASSCODE_NONE) + PASSCODE_NONE), + "digirig" -> new BackendInfo( + (s, p) => new DigiRig(s, p), + R.xml.backend_digirig, + Set(), + CAN_DUPLEX, + PASSCODE_NONE + ) ) class ProtoInfo( @@ -119,7 +127,7 @@ object AprsBackend { "kenwood" -> new ProtoInfo( (s, is, os) => new KenwoodProto(s, is, os), R.xml.proto_kenwood, "link") - ); + ) def defaultProtoInfo(p : String) : ProtoInfo = { proto_collection.get(p) match { case Some(pi) => pi diff --git a/src/backend/DigiRig.scala b/src/backend/DigiRig.scala new file mode 100644 index 00000000..0050ec1b --- /dev/null +++ b/src/backend/DigiRig.scala @@ -0,0 +1,193 @@ +package org.aprsdroid.app + +import android.app.PendingIntent +import android.app.Service +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.content.SharedPreferences +import android.hardware.usb.UsbManager +import android.hardware.usb.UsbDevice +import android.hardware.usb.UsbDeviceConnection +import android.util.Log +import java.io.{InputStream, OutputStream} + +import net.ab0oo.aprs.parser._ + +import com.felhr.usbserial._ + +object DigiRig { + def deviceHandle(dev : UsbDevice) = { + "usb_%04x_%04x_%s".format(dev.getVendorId(), dev.getProductId(), dev.getDeviceName()) + } + def checkDeviceHandle(prefs : SharedPreferences, dev_p : android.os.Parcelable) : Boolean = { + if (dev_p == null) + return false + val dev = dev_p.asInstanceOf[UsbDevice] + val last_use = prefs.getString(deviceHandle(dev), null) + if (last_use == null) + return false + prefs.edit().putString("proto", last_use) + .putString("link", "usb").commit() + true + } +} + +class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AprsBackend(prefs) { + val TAG = "APRSdroid.Usb" + + val USB_PERM_ACTION = "org.aprsdroid.app.DigiRig.PERM" + val ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED" + val ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED" + + val usbManager = service.getSystemService(Context.USB_SERVICE).asInstanceOf[UsbManager]; + var thread : UsbThread = null + var dev : UsbDevice = null + var con : UsbDeviceConnection = null + var ser : UsbSerialInterface = null + var alreadyRunning = false + + val intent = new Intent(USB_PERM_ACTION) + val pendingIntent = PendingIntent.getBroadcast(service, 0, intent, 0) + + val receiver = new BroadcastReceiver() { + override def onReceive(ctx : Context, i : Intent) { + Log.d(TAG, "onReceive: " + i) + if (i.getAction() == ACTION_USB_DETACHED) { + log("USB device detached.") + ctx.stopService(AprsService.intent(ctx, AprsService.SERVICE)) + return + } + val granted = i.getExtras().getBoolean(UsbManager.EXTRA_PERMISSION_GRANTED) + if (!granted) { + service.postAbort(service.getString(R.string.p_serial_noperm)) + return + } + log("Obtained USB permissions.") + thread = new UsbThread() + thread.start() + } + } + + var proto : TncProto = null + var sis : SerialInputStream = null + + def start() = { + val filter = new IntentFilter(USB_PERM_ACTION) + filter.addAction(ACTION_USB_DETACHED) + service.registerReceiver(receiver, filter) + alreadyRunning = true + if (ser == null) + requestPermissions() + false + } + + def log(s : String) { + service.postAddPost(StorageDatabase.Post.TYPE_INFO, R.string.post_info, s) + } + + def requestPermissions() { + Log.d(TAG, "UsbTnc.requestPermissions"); + val dl = usbManager.getDeviceList(); + var requested = false + import scala.collection.JavaConversions._ + for ((name, dev) <- dl) { + val deviceVID = dev.getVendorId() + val devicePID = dev.getProductId() + if (UsbSerialDevice.isSupported(dev)) { + // this is not a USB Hub + log("Found USB device %04x:%04x, requesting permissions.".format(deviceVID, devicePID)) + this.dev = dev + usbManager.requestPermission(dev, pendingIntent) + return + } else + log("Unsupported USB device %04x:%04x.".format(deviceVID, devicePID)) + } + service.postAbort(service.getString(R.string.p_serial_notfound)) + } + + def update(packet : APRSPacket) : String = { + proto.writePacket(packet) + "USB OK" + } + + def stop() { + if (alreadyRunning) + service.unregisterReceiver(receiver) + alreadyRunning = false + if (ser != null) + ser.close() + if (sis != null) + sis.close() + if (con != null) + con.close() + if (thread == null) + return + thread.synchronized { + thread.running = false + } + //thread.shutdown() + thread.interrupt() + thread.join(50) + if (proto != null) + proto.stop() + } + + class UsbThread() + extends Thread("APRSdroid USB connection") { + val TAG = "UsbThread" + var running = true + + def log(s : String) { + service.postAddPost(StorageDatabase.Post.TYPE_INFO, R.string.post_info, s) + } + + override def run() { + val con = usbManager.openDevice(dev) + ser = UsbSerialDevice.createUsbSerialDevice(dev, con) + if (ser == null || !ser.syncOpen()) { + con.close() + service.postAbort(service.getString(R.string.p_serial_unsupported)) + return + } + val baudrate = prefs.getStringInt("baudrate", 115200) + ser.setBaudRate(baudrate) + ser.setDataBits(UsbSerialInterface.DATA_BITS_8) + ser.setStopBits(UsbSerialInterface.STOP_BITS_1) + ser.setParity(UsbSerialInterface.PARITY_NONE) + ser.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF) + + // success: remember this for usb-attach launch + prefs.prefs.edit().putString(UsbTnc.deviceHandle(dev), prefs.getString("proto", "kiss")).commit() + + log("Opened " + ser.getClass().getSimpleName() + " at " + baudrate + "bd") + sis = new SerialInputStream(ser) + try { + proto = AprsBackend.instanciateProto(service, sis, new SerialOutputStream(ser)) + } catch { + case e : IllegalArgumentException => + service.postAbort(e.getMessage()); running = false + return + } + service.postPosterStarted() + while (running) { + try { + val line = proto.readPacket() + Log.d(TAG, "recv: " + line) + service.postSubmit(line) + } catch { + case e : Exception => + Log.d(TAG, "readPacket exception: " + e.toString()) + if (running) { + service.postAbort(e.toString()); running = false + } + } + } + Log.d(TAG, "terminate()") + } + + + } + +} From 7e7d018cbb3307c5c7b3094ba0e69ff6a7fd5dd5 Mon Sep 17 00:00:00 2001 From: Alex Thorlton Date: Sat, 3 Feb 2024 16:15:07 -0600 Subject: [PATCH 2/7] AfskProto.scala: Make a copy of KissProto.scala I'm going to base the new AfskProto protocol implementation on the existing KissProto. Make a copy of KissProto and commit it separately to ensure that it's clear what I've changed. --- src/tncproto/AfskProto.scala | 91 ++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/tncproto/AfskProto.scala diff --git a/src/tncproto/AfskProto.scala b/src/tncproto/AfskProto.scala new file mode 100644 index 00000000..7cc4df95 --- /dev/null +++ b/src/tncproto/AfskProto.scala @@ -0,0 +1,91 @@ +package org.aprsdroid.app + +import _root_.android.util.Log +import _root_.java.io.{InputStream, OutputStream} + +import _root_.net.ab0oo.aprs.parser._ + +class KissProto(service : AprsService, is : InputStream, os : OutputStream) extends TncProto(is, os) { + val TAG = "APRSdroid.KissProto" + + object Kiss { + // escape sequences + val FEND = 0xC0 + val FESC = 0xDB + val TFEND = 0xDC + val TFESC = 0xDD + + // commands + val CMD_DATA = 0x00 + } + + val initstring = java.net.URLDecoder.decode(service.prefs.getString("kiss.init", ""), "UTF-8") + val initdelay = service.prefs.getStringInt("kiss.delay", 300) + if (initstring != null && initstring != "") { + for (line <- initstring.split("\n")) { + service.postAddPost(StorageDatabase.Post.TYPE_TX, + R.string.p_tnc_init, line) + os.write(line.getBytes()) + os.write('\r') + os.write('\n') + Thread.sleep(initdelay) + } + } + + if (service.prefs.getCallsign().length() > 6) { + throw new IllegalArgumentException(service.getString(R.string.e_toolong_callsign)) + } + + def readPacket() : String = { + import Kiss._ + val buf = scala.collection.mutable.ListBuffer[Byte]() + do { + var ch = is.read() + if (ch >= 0) + Log.d(TAG, "readPacket: %02X '%c'".format(ch, ch)) + ch match { + case FEND => + if (buf.length > 0) { + Log.d(TAG, "readPacket: sending back %s".format(new String(buf.toArray))) + try { + return Parser.parseAX25(buf.toArray).toString().trim() + } catch { + case e : Exception => buf.clear() + } + } + case FESC => is.read() match { + case TFEND => buf.append(FEND.toByte) + case TFESC => buf.append(FESC.toByte) + case _ => + } + case -1 => throw new java.io.IOException("KissReader out of data") + case 0 => + // hack: ignore 0x00 byte at start of frame, this is the command + if (buf.length != 0) + buf.append(ch.toByte) + else + Log.d(TAG, "readPacket: ignoring command byte") + case 10 => + // heuristic for ASCII strings: + // * non-empty (including CRLF) + // * starts with ASCII character (KISS starts with >=0x82) + // (buf(0) > 0) does this check, as byte is [-128..127] + // * ends in CRLF + if (buf.length > 1 && (buf(0) > 0) && buf(buf.length-1)==13) + return new String(buf.toArray).trim() + case _ => + buf.append(ch.toByte) + } + } while (true) + "" + } + + def writePacket(p : APRSPacket) { + Log.d(TAG, "writePacket: " + p) + os.write(Kiss.FEND) + os.write(Kiss.CMD_DATA) + os.write(p.toAX25Frame()) + os.write(Kiss.FEND) + os.flush() + } +} From a0e8d45c4f290f5cb923db4d558d86b3295d736a Mon Sep 17 00:00:00 2001 From: Alex Thorlton Date: Sat, 3 Feb 2024 18:25:38 -0600 Subject: [PATCH 3/7] digirig: Basic implementation of AfskProto and Digirig This includes the changes that I needed to make to Charlie's original commit, as well as the changes I made to the copied KissProto implementation. The UI stuff on the app looks mostly correct at this point, and I believe the bones are there to get packet tx/rx working. --- res/values/arrays.xml | 8 ++ res/values/strings.xml | 3 + res/xml/proto_afsk.xml | 19 ++--- src/BackendPrefs.scala | 4 +- src/PrefsWrapper.scala | 1 + src/backend/AprsBackend.scala | 20 +++-- src/backend/DigiRig.scala | 150 ++++++++++++++++++++++++---------- src/tncproto/AfskProto.scala | 78 +----------------- 8 files changed, 146 insertions(+), 137 deletions(-) diff --git a/res/values/arrays.xml b/res/values/arrays.xml index a94ee1a3..4a81d4df 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -34,6 +34,14 @@ usb tcpip + + @string/p_afsk_vox + @string/p_afsk_digirig + + + vox + digirig + 0 2 diff --git a/res/values/strings.xml b/res/values/strings.xml index 3cc4f625..f6550225 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -208,6 +208,9 @@ Bluetooth SPP TCP/IP USB Serial + +VOX +Digirig Manual Position Periodic GPS/Network Position diff --git a/res/xml/proto_afsk.xml b/res/xml/proto_afsk.xml index bba1381c..b2ddb544 100644 --- a/res/xml/proto_afsk.xml +++ b/res/xml/proto_afsk.xml @@ -37,19 +37,14 @@ android:title="@string/p_afsk_prefix" android:summary="@string/p_afsk_prefix_summary" android:dialogTitle="@string/p_afsk_prefix_entry" /> - - + diff --git a/src/BackendPrefs.scala b/src/BackendPrefs.scala index 319f6a3c..4ffa04db 100644 --- a/src/BackendPrefs.scala +++ b/src/BackendPrefs.scala @@ -5,6 +5,7 @@ import _root_.android.os.Bundle import _root_.android.content.{Context, Intent, SharedPreferences} import _root_.android.content.SharedPreferences.OnSharedPreferenceChangeListener import _root_.android.preference.{CheckBoxPreference, Preference, PreferenceActivity, PreferenceManager} +import _root_.android.util.Log import android.location.LocationManager import android.preference.Preference.OnPreferenceClickListener import android.widget.Toast @@ -20,6 +21,7 @@ class BackendPrefs extends PreferenceActivity addPreferencesFromResource(R.xml.backend) addPreferencesFromResource(AprsBackend.prefxml_proto(prefs)) val additional_xml = AprsBackend.prefxml_backend(prefs) + Log.d("BackendPrefs", "DEBUG: prefs add xml " + additional_xml) if (additional_xml != 0) { addPreferencesFromResource(additional_xml) hookPasscode() @@ -67,7 +69,7 @@ class BackendPrefs extends PreferenceActivity } override def onSharedPreferenceChanged(sp: SharedPreferences, key : String) { - if (key == "proto" || key == "link" || key == "aprsis") { + if (key == "proto" || key == "link" || key == "aprsis" || key == "afsk") { setPreferenceScreen(null) loadXml() } diff --git a/src/PrefsWrapper.scala b/src/PrefsWrapper.scala index fb3269cd..a9cf087f 100644 --- a/src/PrefsWrapper.scala +++ b/src/PrefsWrapper.scala @@ -74,6 +74,7 @@ class PrefsWrapper(val context : Context) { R.array.p_conntype_ev, R.array.p_conntype_e) val link = AprsBackend.defaultProtoInfo(this).link link match { + case "afsk" => "%s, %s".format(proto, getListItemName(link, AprsBackend.DEFAULT_CONNTYPE, R.array.p_afsk_ev, R.array.p_afsk_e)) case "aprsis" => "%s, %s".format(proto, getListItemName(link, AprsBackend.DEFAULT_CONNTYPE, R.array.p_aprsis_ev, R.array.p_aprsis_e)) case "link" => "%s, %s".format(proto, getListItemName(link, AprsBackend.DEFAULT_CONNTYPE, R.array.p_link_ev, R.array.p_link_e)) case _ => proto diff --git a/src/backend/AprsBackend.scala b/src/backend/AprsBackend.scala index e9e5e186..64449580 100644 --- a/src/backend/AprsBackend.scala +++ b/src/backend/AprsBackend.scala @@ -2,11 +2,13 @@ package org.aprsdroid.app import android.Manifest import android.os.Build +import _root_.android.util.Log import _root_.net.ab0oo.aprs.parser.APRSPacket import _root_.java.io.{InputStream, OutputStream} object AprsBackend { + val TAG = "AprsBackend" /** "Modular" system to connect to an APRS backend. * The backend config consists of three items backed by prefs values: * - *proto* inside the connection ("aprsis", "afsk", "kiss", "tnc2", "kenwood") - ProtoInfo class @@ -66,7 +68,7 @@ object AprsBackend { Set(), CAN_XMIT, PASSCODE_REQUIRED), - "afsk" -> new BackendInfo( + "vox" -> new BackendInfo( (s, p) => new AfskUploader(s, p), 0, Set(Manifest.permission.RECORD_AUDIO), @@ -99,7 +101,7 @@ object AprsBackend { "digirig" -> new BackendInfo( (s, p) => new DigiRig(s, p), R.xml.backend_digirig, - Set(), + Set(Manifest.permission.RECORD_AUDIO), CAN_DUPLEX, PASSCODE_NONE ) @@ -116,8 +118,8 @@ object AprsBackend { (s, is, os) => new AprsIsProto(s, is, os), R.xml.proto_aprsis, "aprsis"), "afsk" -> new ProtoInfo( - null, - R.xml.proto_afsk, null), + (s, is, os) => new AfskProto(s, is, os), + R.xml.proto_afsk, "afsk"), "kiss" -> new ProtoInfo( (s, is, os) => new KissProto(s, is, os), R.xml.proto_kiss, "link"), @@ -138,7 +140,15 @@ object AprsBackend { def defaultBackendInfo(prefs : PrefsWrapper) : BackendInfo = { val pi = defaultProtoInfo(prefs) - val link = if (pi.link != null) { prefs.getString(pi.link, DEFAULT_LINK) } else { prefs.getProto() } + var link = "" + if (pi.link != null) { + link = prefs.getString(pi.link, DEFAULT_LINK) + Log.d(TAG, "DEBUG: pi.link (" + pi.link + ") != null : " + link) + } else { + link = prefs.getProto() + Log.d(TAG, "DEBUG: pi.link == null : " + link) + } + backend_collection.get(link) match { case Some(bi) => bi case None => backend_collection(DEFAULT_CONNTYPE) diff --git a/src/backend/DigiRig.scala b/src/backend/DigiRig.scala index 0050ec1b..83966534 100644 --- a/src/backend/DigiRig.scala +++ b/src/backend/DigiRig.scala @@ -1,5 +1,7 @@ package org.aprsdroid.app +import _root_.android.media.{AudioManager, AudioTrack} + import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver @@ -14,8 +16,10 @@ import android.util.Log import java.io.{InputStream, OutputStream} import net.ab0oo.aprs.parser._ - +import com.nogy.afu.soundmodem.{Message, APRSFrame, Afsk} import com.felhr.usbserial._ +import com.jazzido.PacketDroid.{AudioBufferProcessor, PacketCallback} +import sivantoledo.ax25.PacketHandler object DigiRig { def deviceHandle(dev : UsbDevice) = { @@ -34,24 +38,50 @@ object DigiRig { } } -class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AprsBackend(prefs) { - val TAG = "APRSdroid.Usb" +class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader(service, prefs) + with PacketHandler with PacketCallback { + + override val TAG = "APRSdroid.Digirig" val USB_PERM_ACTION = "org.aprsdroid.app.DigiRig.PERM" val ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED" val ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED" - val usbManager = service.getSystemService(Context.USB_SERVICE).asInstanceOf[UsbManager]; - var thread : UsbThread = null - var dev : UsbDevice = null - var con : UsbDeviceConnection = null - var ser : UsbSerialInterface = null - var alreadyRunning = false + // USB stuff + + val usbManager = service.getSystemService(Context.USB_SERVICE).asInstanceOf[UsbManager]; + var thread : UsbThread = null + var dev : UsbDevice = null + var con : UsbDeviceConnection = null + var ser : UsbSerialInterface = null + var alreadyRunning = false val intent = new Intent(USB_PERM_ACTION) val pendingIntent = PendingIntent.getBroadcast(service, 0, intent, 0) + // Audio stuff + output.setVolume(AudioTrack.getMaxVolume()) + val receiver = new BroadcastReceiver() { + override def onReceive(ctx: Context, i: Intent) { + Log.d(TAG, "onReceive: " + i) + if (i.getAction() == ACTION_USB_DETACHED) { + log("USB device detached.") + ctx.stopService(AprsService.intent(ctx, AprsService.SERVICE)) + return + } + val granted = i.getExtras().getBoolean(UsbManager.EXTRA_PERMISSION_GRANTED) + if (!granted) { + service.postAbort(service.getString(R.string.p_serial_noperm)) + return + } + log("Obtained USB permissions.") + thread = new UsbThread() + thread.start() + } + } + + override val btScoReceiver = new BroadcastReceiver() { override def onReceive(ctx : Context, i : Intent) { Log.d(TAG, "onReceive: " + i) if (i.getAction() == ACTION_USB_DETACHED) { @@ -67,28 +97,49 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AprsBackend(p log("Obtained USB permissions.") thread = new UsbThread() thread.start() + + val state = i.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1) + Log.d(TAG, "AudioManager SCO event: " + state) + if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { + // we are connected, perform actual start + log(service.getString(R.string.afsk_info_sco_est)) + aw.start() + service.unregisterReceiver(this) + service.postPosterStarted() + } } } var proto : TncProto = null var sis : SerialInputStream = null - def start() = { + override def start() = { val filter = new IntentFilter(USB_PERM_ACTION) filter.addAction(ACTION_USB_DETACHED) service.registerReceiver(receiver, filter) alreadyRunning = true if (ser == null) requestPermissions() - false - } - def log(s : String) { - service.postAddPost(StorageDatabase.Post.TYPE_INFO, R.string.post_info, s) + if (!isCallsignAX25Valid()) + false + + if (use_bt) { + log(service.getString(R.string.afsk_info_sco_req)) + service.getSystemService(Context.AUDIO_SERVICE) + .asInstanceOf[AudioManager].startBluetoothSco() + service.registerReceiver(btScoReceiver, new IntentFilter(AudioManager.ACTION_SCO_AUDIO_STATE_CHANGED)) + false + } else { + aw.start() + true + } + + false } def requestPermissions() { - Log.d(TAG, "UsbTnc.requestPermissions"); + Log.d(TAG, "Digirig.requestPermissions"); val dl = usbManager.getDeviceList(); var requested = false import scala.collection.JavaConversions._ @@ -107,12 +158,34 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AprsBackend(p service.postAbort(service.getString(R.string.p_serial_notfound)) } - def update(packet : APRSPacket) : String = { - proto.writePacket(packet) - "USB OK" + override def update(packet: APRSPacket): String = { + // Need to "parse" the packet in order to replace the Digipeaters + packet.setDigipeaters(Digipeater.parseList(Digis, true)) + val from = packet.getSourceCall() + val to = packet.getDestinationCall() + val data = packet.getAprsInformation().toString() + val msg = new APRSFrame(from, to, Digis, data, FrameLength).getMessage() + Log.d(TAG, "update(): From: " + from + " To: " + to + " Via: " + Digis + " telling " + data) + + ser.setRTS(true) + val bits_per_byte = 8 + val bits_in_frame = packet.toAX25Frame().length / bits_per_byte + val ms_per_s = 1000 + val sleep_ms = bits_in_frame * ms_per_s / 1200 // aprs is 1200 baud + val sleep_pad_ms = 1500 + Thread.sleep(sleep_ms + sleep_pad_ms) + val result = sendMessage(msg) + Thread.sleep(sleep_ms + sleep_pad_ms) + ser.setRTS(false) + + if (result) + "AFSK OK" + else + "AFSK busy" } - def stop() { + override def stop() { + // Stop USB thread if (alreadyRunning) service.unregisterReceiver(receiver) alreadyRunning = false @@ -130,8 +203,18 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AprsBackend(p //thread.shutdown() thread.interrupt() thread.join(50) - if (proto != null) - proto.stop() + + // Stop AFSK Demodulator + aw.close() + if (use_bt) { + service.getSystemService(Context.AUDIO_SERVICE) + .asInstanceOf[AudioManager].stopBluetoothSco() + try { + service.unregisterReceiver(btScoReceiver) + } catch { + case e : RuntimeException => // ignore, receiver already unregistered + } + } } class UsbThread() @@ -157,33 +240,14 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AprsBackend(p ser.setStopBits(UsbSerialInterface.STOP_BITS_1) ser.setParity(UsbSerialInterface.PARITY_NONE) ser.setFlowControl(UsbSerialInterface.FLOW_CONTROL_OFF) + ser.setRTS(false) // success: remember this for usb-attach launch - prefs.prefs.edit().putString(UsbTnc.deviceHandle(dev), prefs.getString("proto", "kiss")).commit() + prefs.prefs.edit().putString(UsbTnc.deviceHandle(dev), prefs.getString("proto", "afsk")).commit() log("Opened " + ser.getClass().getSimpleName() + " at " + baudrate + "bd") - sis = new SerialInputStream(ser) - try { - proto = AprsBackend.instanciateProto(service, sis, new SerialOutputStream(ser)) - } catch { - case e : IllegalArgumentException => - service.postAbort(e.getMessage()); running = false - return - } service.postPosterStarted() - while (running) { - try { - val line = proto.readPacket() - Log.d(TAG, "recv: " + line) - service.postSubmit(line) - } catch { - case e : Exception => - Log.d(TAG, "readPacket exception: " + e.toString()) - if (running) { - service.postAbort(e.toString()); running = false - } - } - } + while (running) { /* do nothing */ } Log.d(TAG, "terminate()") } diff --git a/src/tncproto/AfskProto.scala b/src/tncproto/AfskProto.scala index 7cc4df95..45fe9b39 100644 --- a/src/tncproto/AfskProto.scala +++ b/src/tncproto/AfskProto.scala @@ -5,87 +5,13 @@ import _root_.java.io.{InputStream, OutputStream} import _root_.net.ab0oo.aprs.parser._ -class KissProto(service : AprsService, is : InputStream, os : OutputStream) extends TncProto(is, os) { - val TAG = "APRSdroid.KissProto" - - object Kiss { - // escape sequences - val FEND = 0xC0 - val FESC = 0xDB - val TFEND = 0xDC - val TFESC = 0xDD - - // commands - val CMD_DATA = 0x00 - } - - val initstring = java.net.URLDecoder.decode(service.prefs.getString("kiss.init", ""), "UTF-8") - val initdelay = service.prefs.getStringInt("kiss.delay", 300) - if (initstring != null && initstring != "") { - for (line <- initstring.split("\n")) { - service.postAddPost(StorageDatabase.Post.TYPE_TX, - R.string.p_tnc_init, line) - os.write(line.getBytes()) - os.write('\r') - os.write('\n') - Thread.sleep(initdelay) - } - } - - if (service.prefs.getCallsign().length() > 6) { - throw new IllegalArgumentException(service.getString(R.string.e_toolong_callsign)) - } +class AfskProto(service : AprsService, is : InputStream, os : OutputStream) extends TncProto(is, os) { + val TAG = "APRSdroid.AfskProto" def readPacket() : String = { - import Kiss._ - val buf = scala.collection.mutable.ListBuffer[Byte]() - do { - var ch = is.read() - if (ch >= 0) - Log.d(TAG, "readPacket: %02X '%c'".format(ch, ch)) - ch match { - case FEND => - if (buf.length > 0) { - Log.d(TAG, "readPacket: sending back %s".format(new String(buf.toArray))) - try { - return Parser.parseAX25(buf.toArray).toString().trim() - } catch { - case e : Exception => buf.clear() - } - } - case FESC => is.read() match { - case TFEND => buf.append(FEND.toByte) - case TFESC => buf.append(FESC.toByte) - case _ => - } - case -1 => throw new java.io.IOException("KissReader out of data") - case 0 => - // hack: ignore 0x00 byte at start of frame, this is the command - if (buf.length != 0) - buf.append(ch.toByte) - else - Log.d(TAG, "readPacket: ignoring command byte") - case 10 => - // heuristic for ASCII strings: - // * non-empty (including CRLF) - // * starts with ASCII character (KISS starts with >=0x82) - // (buf(0) > 0) does this check, as byte is [-128..127] - // * ends in CRLF - if (buf.length > 1 && (buf(0) > 0) && buf(buf.length-1)==13) - return new String(buf.toArray).trim() - case _ => - buf.append(ch.toByte) - } - } while (true) "" } def writePacket(p : APRSPacket) { - Log.d(TAG, "writePacket: " + p) - os.write(Kiss.FEND) - os.write(Kiss.CMD_DATA) - os.write(p.toAX25Frame()) - os.write(Kiss.FEND) - os.flush() } } From b78a1028c45f2edfbcd4a3e4f63028a739812fea Mon Sep 17 00:00:00 2001 From: Alex Thorlton Date: Sun, 28 Jul 2024 20:59:06 -0500 Subject: [PATCH 4/7] backend/DigiRig: General code cleanup --- src/backend/DigiRig.scala | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/backend/DigiRig.scala b/src/backend/DigiRig.scala index 83966534..35a021d9 100644 --- a/src/backend/DigiRig.scala +++ b/src/backend/DigiRig.scala @@ -25,6 +25,7 @@ object DigiRig { def deviceHandle(dev : UsbDevice) = { "usb_%04x_%04x_%s".format(dev.getVendorId(), dev.getProductId(), dev.getDeviceName()) } + def checkDeviceHandle(prefs : SharedPreferences, dev_p : android.os.Parcelable) : Boolean = { if (dev_p == null) return false @@ -40,26 +41,24 @@ object DigiRig { class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader(service, prefs) with PacketHandler with PacketCallback { + override val TAG = "APRSdroid.Digirig" - override val TAG = "APRSdroid.Digirig" - + // USB stuff val USB_PERM_ACTION = "org.aprsdroid.app.DigiRig.PERM" val ACTION_USB_ATTACHED = "android.hardware.usb.action.USB_DEVICE_ATTACHED" val ACTION_USB_DETACHED = "android.hardware.usb.action.USB_DEVICE_DETACHED" - // USB stuff - - val usbManager = service.getSystemService(Context.USB_SERVICE).asInstanceOf[UsbManager]; - var thread : UsbThread = null - var dev : UsbDevice = null - var con : UsbDeviceConnection = null - var ser : UsbSerialInterface = null - var alreadyRunning = false + val usbManager = service.getSystemService(Context.USB_SERVICE).asInstanceOf[UsbManager]; + var thread : UsbThread = null + var dev : UsbDevice = null + var con : UsbDeviceConnection = null + var ser : UsbSerialInterface = null + var alreadyRunning = false val intent = new Intent(USB_PERM_ACTION) val pendingIntent = PendingIntent.getBroadcast(service, 0, intent, 0) - // Audio stuff + // Audio stuff output.setVolume(AudioTrack.getMaxVolume()) val receiver = new BroadcastReceiver() { @@ -200,7 +199,6 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader( thread.synchronized { thread.running = false } - //thread.shutdown() thread.interrupt() thread.join(50) @@ -217,8 +215,7 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader( } } - class UsbThread() - extends Thread("APRSdroid USB connection") { + class UsbThread() extends Thread("APRSdroid USB connection") { val TAG = "UsbThread" var running = true @@ -250,8 +247,5 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader( while (running) { /* do nothing */ } Log.d(TAG, "terminate()") } - - } - } From 69f6cc33f95d343a3e97b2fc6a7998f93f66cb44 Mon Sep 17 00:00:00 2001 From: Alex Thorlton Date: Sun, 28 Jul 2024 21:41:49 -0500 Subject: [PATCH 5/7] res/xml/proto_afsk.xml: Whitespace cleanup --- res/xml/proto_afsk.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/res/xml/proto_afsk.xml b/res/xml/proto_afsk.xml index b2ddb544..0c680e3b 100644 --- a/res/xml/proto_afsk.xml +++ b/res/xml/proto_afsk.xml @@ -38,13 +38,13 @@ android:summary="@string/p_afsk_prefix_summary" android:dialogTitle="@string/p_afsk_prefix_entry" /> - + From 29abb67892e45cfc4c146de6ac995c9b4c6c8625 Mon Sep 17 00:00:00 2001 From: Alex Thorlton Date: Sun, 28 Jul 2024 22:52:37 -0500 Subject: [PATCH 6/7] backend/DigiRig: Sync RTS signal to audio playback I originally had some hacky code in here to _hopfeully_ ensure that the RTS signal was raise while packets were being transmitted, but it was only a best-guess attempt. This commit implements an OnPlaybackPositionUpdateListener to tell us exactly when the audio for a packet has finished playing, so we know exactly when to clear the RTS signal. --- src/backend/DigiRig.scala | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/backend/DigiRig.scala b/src/backend/DigiRig.scala index 35a021d9..a848378e 100644 --- a/src/backend/DigiRig.scala +++ b/src/backend/DigiRig.scala @@ -12,6 +12,7 @@ import android.content.SharedPreferences import android.hardware.usb.UsbManager import android.hardware.usb.UsbDevice import android.hardware.usb.UsbDeviceConnection +import android.media.AudioTrack.OnPlaybackPositionUpdateListener import android.util.Log import java.io.{InputStream, OutputStream} @@ -59,7 +60,15 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader( val pendingIntent = PendingIntent.getBroadcast(service, 0, intent, 0) // Audio stuff + var audioPlaying = false output.setVolume(AudioTrack.getMaxVolume()) + output.setPlaybackPositionUpdateListener(new OnPlaybackPositionUpdateListener { + override def onMarkerReached(audioTrack: AudioTrack): Unit = { + DigiRig.this.audioPlaying = false + } + + override def onPeriodicNotification(audioTrack: AudioTrack): Unit = {} + }) val receiver = new BroadcastReceiver() { override def onReceive(ctx: Context, i: Intent) { @@ -167,14 +176,11 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader( Log.d(TAG, "update(): From: " + from + " To: " + to + " Via: " + Digis + " telling " + data) ser.setRTS(true) - val bits_per_byte = 8 - val bits_in_frame = packet.toAX25Frame().length / bits_per_byte - val ms_per_s = 1000 - val sleep_ms = bits_in_frame * ms_per_s / 1200 // aprs is 1200 baud - val sleep_pad_ms = 1500 - Thread.sleep(sleep_ms + sleep_pad_ms) + audioPlaying = true val result = sendMessage(msg) - Thread.sleep(sleep_ms + sleep_pad_ms) + while (audioPlaying) { + Thread.sleep(10) + } ser.setRTS(false) if (result) From d308e42617c0af09da8fcf963866b594dbda9f69 Mon Sep 17 00:00:00 2001 From: "Loren M. Lang" Date: Sun, 4 Aug 2024 00:15:56 -0700 Subject: [PATCH 7/7] Fix PendingIntent for Android 12 Starting with API level 31, a flag for mutable or immutable intent is now required or an exception will be generated crashing the app. This fixes a crash seen in PR ge0rg/aprsdroid#382 --- src/backend/DigiRig.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/DigiRig.scala b/src/backend/DigiRig.scala index a848378e..0afc4614 100644 --- a/src/backend/DigiRig.scala +++ b/src/backend/DigiRig.scala @@ -57,7 +57,7 @@ class DigiRig(service : AprsService, prefs : PrefsWrapper) extends AfskUploader( var alreadyRunning = false val intent = new Intent(USB_PERM_ACTION) - val pendingIntent = PendingIntent.getBroadcast(service, 0, intent, 0) + val pendingIntent = PendingIntent.getBroadcast(service, 0, intent, PendingIntent.FLAG_MUTABLE) // Audio stuff var audioPlaying = false