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) { + } +}