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 4e97d0dd..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
@@ -325,6 +328,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..0c680e3b 100644
--- a/res/xml/proto_afsk.xml
+++ b/res/xml/proto_afsk.xml
@@ -38,7 +38,14 @@
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 1be9d4c2..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
@@ -102,6 +103,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..64449580 100644
--- a/src/backend/AprsBackend.scala
+++ b/src/backend/AprsBackend.scala
@@ -2,10 +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
@@ -65,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),
@@ -94,7 +97,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(Manifest.permission.RECORD_AUDIO),
+ CAN_DUPLEX,
+ PASSCODE_NONE
+ )
)
class ProtoInfo(
@@ -108,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"),
@@ -119,7 +129,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
@@ -130,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
new file mode 100644
index 00000000..0afc4614
--- /dev/null
+++ b/src/backend/DigiRig.scala
@@ -0,0 +1,257 @@
+package org.aprsdroid.app
+
+import _root_.android.media.{AudioManager, AudioTrack}
+
+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.media.AudioTrack.OnPlaybackPositionUpdateListener
+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) = {
+ "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 AfskUploader(service, prefs)
+ with PacketHandler with PacketCallback {
+ 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"
+
+ 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, PendingIntent.FLAG_MUTABLE)
+
+ // 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) {
+ 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) {
+ 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()
+
+ 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
+
+ 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()
+
+ 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, "Digirig.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))
+ }
+
+ 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)
+ audioPlaying = true
+ val result = sendMessage(msg)
+ while (audioPlaying) {
+ Thread.sleep(10)
+ }
+ ser.setRTS(false)
+
+ if (result)
+ "AFSK OK"
+ else
+ "AFSK busy"
+ }
+
+ override def stop() {
+ // Stop USB thread
+ 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.interrupt()
+ thread.join(50)
+
+ // 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() 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)
+ ser.setRTS(false)
+
+ // success: remember this for usb-attach launch
+ prefs.prefs.edit().putString(UsbTnc.deviceHandle(dev), prefs.getString("proto", "afsk")).commit()
+
+ log("Opened " + ser.getClass().getSimpleName() + " at " + baudrate + "bd")
+ service.postPosterStarted()
+ while (running) { /* do nothing */ }
+ Log.d(TAG, "terminate()")
+ }
+ }
+}
diff --git a/src/tncproto/AfskProto.scala b/src/tncproto/AfskProto.scala
new file mode 100644
index 00000000..45fe9b39
--- /dev/null
+++ b/src/tncproto/AfskProto.scala
@@ -0,0 +1,17 @@
+package org.aprsdroid.app
+
+import _root_.android.util.Log
+import _root_.java.io.{InputStream, OutputStream}
+
+import _root_.net.ab0oo.aprs.parser._
+
+class AfskProto(service : AprsService, is : InputStream, os : OutputStream) extends TncProto(is, os) {
+ val TAG = "APRSdroid.AfskProto"
+
+ def readPacket() : String = {
+ ""
+ }
+
+ def writePacket(p : APRSPacket) {
+ }
+}