diff --git a/codec2talkie/build.gradle b/codec2talkie/build.gradle index 3c08299..fe1bab7 100644 --- a/codec2talkie/build.gradle +++ b/codec2talkie/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.radio.codec2talkie" minSdkVersion 21 targetSdkVersion 30 - versionCode 132 - versionName "1.32" + versionCode 133 + versionName "1.33" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/MainActivity.java b/codec2talkie/src/main/java/com/radio/codec2talkie/MainActivity.java index 19bc5ff..46ce723 100644 --- a/codec2talkie/src/main/java/com/radio/codec2talkie/MainActivity.java +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/MainActivity.java @@ -110,6 +110,7 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection private boolean _isConnecting = false; private boolean _isAppExit = false; private boolean _isAppRestart = false; + private boolean _isRigCtlUsbConnected = false; private long _backPressedTimestamp; @@ -262,7 +263,11 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection break; case "sound_modem": _textConnInfo.setText(R.string.main_status_sound_modem); - startAppService(TransportFactory.TransportType.SOUND_MODEM); + String rig = _sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_RIG, "Disabled"); + if (rig.equals("Disabled")) + startAppService(TransportFactory.TransportType.SOUND_MODEM); + else + startUsbConnectActivity(); break; case "tcp_ip": startTcpIpConnectActivity(); @@ -284,7 +289,11 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection Intent data = result.getData(); assert data != null; int resultCode = result.getResultCode(); - if (resultCode == RESULT_CANCELED) { + String transportType = _sharedPreferences.getString(PreferenceKeys.PORTS_TYPE, "loopback"); + if (transportType.equals("sound_modem")) { + _isRigCtlUsbConnected = resultCode == RESULT_OK; + startAppService(TransportFactory.TransportType.SOUND_MODEM); + } else if (resultCode == RESULT_CANCELED) { _textConnInfo.setText(R.string.main_status_loopback_test); startAppService(TransportFactory.TransportType.LOOPBACK); } else if (resultCode == RESULT_OK) { @@ -296,6 +305,7 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection protected void startUsbConnectActivity() { _isConnecting = true; + _isRigCtlUsbConnected = false; _usbActivityLauncher.launch(new Intent(this, UsbConnectActivity.class)); } @@ -456,6 +466,12 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection status += getString(R.string.kiss_scrambler_label); } + // rig CAT control + String rigName = _sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_RIG, "Disabled"); + if (!rigName.equals("Disabled") && _isRigCtlUsbConnected) { + status += getString(R.string.ports_sound_modem_rig_label); + } + // aprs boolean aprsEnabled = _sharedPreferences.getBoolean(PreferenceKeys.APRS_ENABLED, false); if (aprsEnabled) { diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/connect/UsbConnectActivity.java b/codec2talkie/src/main/java/com/radio/codec2talkie/connect/UsbConnectActivity.java index 5d86560..ccc06a4 100644 --- a/codec2talkie/src/main/java/com/radio/codec2talkie/connect/UsbConnectActivity.java +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/connect/UsbConnectActivity.java @@ -92,7 +92,7 @@ public class UsbConnectActivity extends AppCompatActivity { customTable.addProduct(0x1b4f, 0x9204, CdcAcmSerialDriver.class); // Arduino Due customTable.addProduct(0x2341, 0x003d, CdcAcmSerialDriver.class); - // STM + // STM, MCHF customTable.addProduct(0x0483, 0x5732, CdcAcmSerialDriver.class); return new UsbSerialProber(customTable); } diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/protocol/Hdlc.java b/codec2talkie/src/main/java/com/radio/codec2talkie/protocol/Hdlc.java index a7742bf..91f66e3 100644 --- a/codec2talkie/src/main/java/com/radio/codec2talkie/protocol/Hdlc.java +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/protocol/Hdlc.java @@ -15,6 +15,7 @@ import com.radio.codec2talkie.transport.SoundModem; import com.radio.codec2talkie.transport.Transport; import java.io.IOException; +import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Arrays; @@ -121,7 +122,12 @@ public class Hdlc implements Protocol { _readByte = 0; _prevHdlc = 0; } else { - _currentFrameBuffer.put(bit); + try { + _currentFrameBuffer.put(bit); + } catch (BufferOverflowException e) { + e.printStackTrace(); + _currentFrameBuffer.clear(); + } _prevHdlc++; } } diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/Disabled.java b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/Disabled.java new file mode 100644 index 0000000..e700b27 --- /dev/null +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/Disabled.java @@ -0,0 +1,21 @@ +package com.radio.codec2talkie.rigctl; + +import android.content.Context; + +import com.radio.codec2talkie.transport.Transport; + +import java.io.IOException; + +public class Disabled implements RigCtl{ + @Override + public void initialize(Transport transport, Context context, RigCtlCallback protocolCallback) throws IOException { + } + + @Override + public void pttOn() { + } + + @Override + public void pttOff() { + } +} diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/Ft817.java b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/Ft817.java new file mode 100644 index 0000000..4e9ad0b --- /dev/null +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/Ft817.java @@ -0,0 +1,51 @@ +package com.radio.codec2talkie.rigctl; + +import android.content.Context; +import android.util.Log; + +import com.radio.codec2talkie.transport.Transport; + +import java.io.IOException; +import java.nio.ByteBuffer; + +// http://www.ka7oei.com/ft817_meow.html +public class Ft817 implements RigCtl { + private static final String TAG = Ft817.class.getSimpleName(); + + private Transport _transport; + + @Override + public void initialize(Transport transport, Context context, RigCtlCallback protocolCallback) throws IOException { + _transport = transport; + } + + @Override + public void pttOn() throws IOException { + // 0x00, 0x00, 0x00, 0x00, 0x08 + // returns 0x00 (was un-keyed), 0xf0 (already keyed) + Log.i(TAG, "PTT ON"); + ByteBuffer cmd = ByteBuffer.allocate(5); + cmd.put((byte)0x00).put((byte)0x00).put((byte)0x00).put((byte)0x00).put((byte)0x08); + _transport.write(cmd.array()); + Log.i(TAG, "PTT ON done"); + + byte[] response = new byte[1]; + int bytesRead = _transport.read(response); + Log.i(TAG, "PTT ON response: " + bytesRead + " " + response[0]); + } + + @Override + public void pttOff() throws IOException { + // 0x00, 0x00, 0x00, 0x00, 0x88 + // returns 0x00 (was keyed), 0xf0 (already un-keyed) + Log.i(TAG, "PTT OFF"); + ByteBuffer cmd = ByteBuffer.allocate(5); + cmd.put((byte)0x00).put((byte)0x00).put((byte)0x00).put((byte)0x00).put((byte)0x88); + _transport.write(cmd.array()); + Log.i(TAG, "PTT OFF done"); + + byte[] response = new byte[1]; + int bytesRead = _transport.read(response); + Log.i(TAG, "PTT OFF response: " + bytesRead + " " + response[0]); + } +} diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtl.java b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtl.java new file mode 100644 index 0000000..31788f3 --- /dev/null +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtl.java @@ -0,0 +1,15 @@ +package com.radio.codec2talkie.rigctl; + +import android.content.Context; + +import com.radio.codec2talkie.protocol.ProtocolCallback; +import com.radio.codec2talkie.transport.Transport; + +import java.io.IOException; + +public interface RigCtl { + void initialize(Transport transport, Context context, RigCtlCallback protocolCallback) throws IOException; + + void pttOn() throws IOException; + void pttOff() throws IOException; +} diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtlCallback.java b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtlCallback.java new file mode 100644 index 0000000..ecc7b06 --- /dev/null +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtlCallback.java @@ -0,0 +1,4 @@ +package com.radio.codec2talkie.rigctl; + +public interface RigCtlCallback { +} diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtlFactory.java b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtlFactory.java new file mode 100644 index 0000000..38af74e --- /dev/null +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/rigctl/RigCtlFactory.java @@ -0,0 +1,29 @@ +package com.radio.codec2talkie.rigctl; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import androidx.preference.PreferenceManager; + +import com.radio.codec2talkie.connect.UsbPortHandler; +import com.radio.codec2talkie.settings.PreferenceKeys; + +public class RigCtlFactory { + private static final String TAG = RigCtlFactory.class.getSimpleName(); + + public static RigCtl create(Context context) { + if (UsbPortHandler.getPort() == null) + return new Disabled(); + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + String rigName = sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_RIG, "Disabled"); + try { + Class loadClass = Class.forName(String.format("com.radio.codec2talkie.rigctl.%s", rigName)); + Log.i(TAG, "Using rig " + rigName); + return (RigCtl)loadClass.newInstance(); + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + e.printStackTrace(); + return new Disabled(); + } + } +} diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/settings/PreferenceKeys.java b/codec2talkie/src/main/java/com/radio/codec2talkie/settings/PreferenceKeys.java index 596a0e5..90cf9b5 100644 --- a/codec2talkie/src/main/java/com/radio/codec2talkie/settings/PreferenceKeys.java +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/settings/PreferenceKeys.java @@ -20,6 +20,8 @@ public final class PreferenceKeys { public static String PORTS_SOUND_MODEM_TYPE = "ports_sound_modem_type"; public static String PORTS_SOUND_MODEM_PREAMBLE = "ports_sound_modem_preamble"; public static String PORTS_SOUND_MODEM_DISABLE_RX = "ports_sound_modem_disable_rx"; + public static String PORTS_SOUND_MODEM_RIG = "ports_sound_modem_rig"; + public static String PORTS_SOUND_MODEM_GAIN ="ports_sound_modem_gain"; public static String CODEC2_MODE = "codec2_mode"; public static String CODEC2_RECORDING_ENABLED = "codec2_recording_enabled"; diff --git a/codec2talkie/src/main/java/com/radio/codec2talkie/transport/SoundModem.java b/codec2talkie/src/main/java/com/radio/codec2talkie/transport/SoundModem.java index 12032c8..705efe4 100644 --- a/codec2talkie/src/main/java/com/radio/codec2talkie/transport/SoundModem.java +++ b/codec2talkie/src/main/java/com/radio/codec2talkie/transport/SoundModem.java @@ -12,6 +12,8 @@ import android.util.Log; import androidx.preference.PreferenceManager; +import com.radio.codec2talkie.rigctl.RigCtl; +import com.radio.codec2talkie.rigctl.RigCtlFactory; import com.radio.codec2talkie.settings.PreferenceKeys; import com.radio.codec2talkie.tools.AudioTools; import com.radio.codec2talkie.tools.BitTools; @@ -22,13 +24,18 @@ import java.io.IOException; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Timer; +import java.util.TimerTask; public class SoundModem implements Transport, Runnable { private static final String TAG = SoundModem.class.getSimpleName(); + private static final int PTT_OFF_DELAY_MS = 1000; + // NOTE, codec2 library requires that sample_rate % bit_rate == 0 public static final int SAMPLE_RATE = 19200; + //public static final int SAMPLE_RATE = 48000; private final String _name; @@ -52,20 +59,25 @@ public class SoundModem implements Transport, Runnable { private final long _fskModem; + private final RigCtl _rigCtl; + private Timer _pttOffTimer; + private boolean _isPttOn = false; + public SoundModem(Context context) { _context = context; _sharedPreferences = PreferenceManager.getDefaultSharedPreferences(_context); boolean disableRx = _sharedPreferences.getBoolean(PreferenceKeys.PORTS_SOUND_MODEM_DISABLE_RX, false); int bitRate = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_TYPE, "1200")); + int gain = Integer.parseInt(_sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_GAIN, "10000")); _name = "SoundModem" + bitRate; if (bitRate == 300) { // <230 spacing for 300 bps does not work with codec2 fsk for receive - _fskModem = Codec2.fskCreate(SAMPLE_RATE, 300, 1600, 200); + _fskModem = Codec2.fskCreate(SAMPLE_RATE, 300, 1600, 200, gain); } else if (bitRate == 1200) { - _fskModem = Codec2.fskCreate(SAMPLE_RATE, 1200, 1200, 1000); + _fskModem = Codec2.fskCreate(SAMPLE_RATE, 1200, 1200, 1000, gain); } else { - _fskModem = Codec2.fskCreate(SAMPLE_RATE, 2400, 2165, 1805); + _fskModem = Codec2.fskCreate(SAMPLE_RATE, 2400, 2165, 1805, gain); } _recordAudioBuffer = new short[Codec2.fskDemodSamplesBufSize(_fskModem)]; @@ -82,6 +94,15 @@ public class SoundModem implements Transport, Runnable { else _sampleBuffer = ByteBuffer.allocate(0); + _rigCtl = RigCtlFactory.create(context); + try { + _rigCtl.initialize(TransportFactory.create(TransportFactory.TransportType.USB, context), context, null); + } catch (IOException e) { + e.printStackTrace(); + } + + _isPttOn = false; + if (!disableRx) new Thread(this).start(); } @@ -147,6 +168,8 @@ public class SoundModem implements Transport, Runnable { @Override public int write(byte[] srcDataBytesAsBits) throws IOException { + pttOn(); + //Log.v(TAG, "write " + DebugTools.byteBitsToFlatString(srcDataBytesAsBits)); byte[] dataBytesAsBits = BitTools.convertToNRZI(srcDataBytesAsBits); //Log.v(TAG, "write NRZ " + DebugTools.byteBitsToFlatString(dataBytesAsBits)); @@ -184,6 +207,7 @@ public class SoundModem implements Transport, Runnable { } else { _systemAudioPlayer.write(_playbackAudioBuffer, 0, bitBufferTail.length * _samplesPerSymbol); } + pttOff(); return 0; } @@ -226,8 +250,10 @@ public class SoundModem implements Transport, Runnable { } } else { int readCnt = _systemAudioRecorder.read(_recordAudioBuffer, 0, nin); + // TODO, read tail if (readCnt != nin) { - Log.e(TAG, "" + readCnt + " != " + nin); + Log.w(TAG, "" + readCnt + " != " + nin); + continue; } //Log.v(TAG, "read samples: " + DebugTools.shortsToHex(_recordAudioBuffer)); } @@ -248,4 +274,36 @@ public class SoundModem implements Transport, Runnable { } } } + + private void pttOn() { + if (_isPttOn) return; + + try { + _rigCtl.pttOn(); + _isPttOn = true; + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void pttOff() { + if (!_isPttOn) return; + if (_pttOffTimer != null) { + _pttOffTimer.cancel(); + _pttOffTimer.purge(); + _pttOffTimer = null; + } + _pttOffTimer = new Timer(); + _pttOffTimer.schedule(new TimerTask() { + @Override + public void run() { + try { + _rigCtl.pttOff(); + _isPttOn = false; + } catch (IOException e) { + e.printStackTrace(); + } + } + }, PTT_OFF_DELAY_MS); + } } diff --git a/codec2talkie/src/main/res/values/arrays.xml b/codec2talkie/src/main/res/values/arrays.xml index 7f31538..569fda1 100644 --- a/codec2talkie/src/main/res/values/arrays.xml +++ b/codec2talkie/src/main/res/values/arrays.xml @@ -352,4 +352,46 @@ usb sound_modem + + + Do not use rig control + FT-817/818/MCHF + + + + Disabled + Ft817 + + + + +0 db + +1 db + +2 db + +3 db + +4 db + +5 db + +6 db + +7 db + +8 db + +9 db + +10 db + +11 db + +12 db + + + + 1000 + 1258 + 1584 + 1995 + 2511 + 3162 + 3981 + 5011 + 6309 + 7943 + 10000 + 12589 + 15848 + \ No newline at end of file diff --git a/codec2talkie/src/main/res/values/strings.xml b/codec2talkie/src/main/res/values/strings.xml index d6a2332..bd09f34 100644 --- a/codec2talkie/src/main/res/values/strings.xml +++ b/codec2talkie/src/main/res/values/strings.xml @@ -302,4 +302,7 @@ TNC transport type Disable receive Run modem in transmit only mode, receive is disabled to save CPU cycles + Select RIG model for CAT PTT control + Set modem audio gain + 🎛 \ No newline at end of file diff --git a/codec2talkie/src/main/res/xml/preferences_sound_modem.xml b/codec2talkie/src/main/res/xml/preferences_sound_modem.xml index ffe11a0..1716424 100644 --- a/codec2talkie/src/main/res/xml/preferences_sound_modem.xml +++ b/codec2talkie/src/main/res/xml/preferences_sound_modem.xml @@ -29,5 +29,23 @@ app:defaultValue="false"> + + + + + + \ No newline at end of file diff --git a/libcodec2-android/src/main/cpp/Codec2JNI.cpp b/libcodec2-android/src/main/cpp/Codec2JNI.cpp index 698a3f3..fe1df4b 100644 --- a/libcodec2-android/src/main/cpp/Codec2JNI.cpp +++ b/libcodec2-android/src/main/cpp/Codec2JNI.cpp @@ -25,6 +25,7 @@ namespace Java_com_ustadmobile_codec2_Codec2 { int Nbits; int N; int Ts; + int gain; }; static Context *getContext(jlong jp) { @@ -56,7 +57,7 @@ namespace Java_com_ustadmobile_codec2_Codec2 { return pv; } - static jlong fskCreate(JNIEnv *env, jclass clazz, int sampleFrequency, int symbolRate, int toneFreq, int toneSpacing) { + static jlong fskCreate(JNIEnv *env, jclass clazz, int sampleFrequency, int symbolRate, int toneFreq, int toneSpacing, int gain) { struct ContextFsk *conFsk; conFsk = (struct ContextFsk *) malloc(sizeof(struct ContextFsk)); struct FSK *fsk; @@ -74,6 +75,8 @@ namespace Java_com_ustadmobile_codec2_Codec2 { conFsk->demodBits = (uint8_t*)malloc(sizeof(uint8_t) * fsk->Nbits); conFsk->demodBuf = (int16_t*)malloc(sizeof(short) * (fsk->N + 2 * fsk->Ts)); + conFsk->gain = gain; + fsk_set_freq_est_limits(fsk, 500, sampleFrequency / 4); fsk_set_freq_est_alg(fsk, 0); @@ -168,7 +171,7 @@ namespace Java_com_ustadmobile_codec2_Codec2 { fsk_mod(conFsk->fsk, conFsk->modBuf, conFsk->modBits, inputBitsSize); jshort *jOutBuf = env->GetShortArrayElements(outputSamples, nullptr); for (int i = 0; i < conFsk->N; i++) { - jOutBuf[i] = (int16_t)(conFsk->modBuf[i] * FDMDV_SCALE); + jOutBuf[i] = (int16_t)(conFsk->modBuf[i] * conFsk->gain); } env->ReleaseShortArrayElements(outputSamples, jOutBuf, 0); return 0; @@ -186,7 +189,7 @@ namespace Java_com_ustadmobile_codec2_Codec2 { ContextFsk *conFsk = getContextFsk(n); env->GetShortArrayRegion(inputSamples, 0, conFsk->N, reinterpret_cast(conFsk->demodBuf)); for(int i = 0; i < fsk_nin(conFsk->fsk); i++){ - conFsk->demodCBuf[i].real = ((float)conFsk->demodBuf[i]) / FDMDV_SCALE; + conFsk->demodCBuf[i].real = ((float)conFsk->demodBuf[i]) / conFsk->gain; conFsk->demodCBuf[i].imag = 0.0; } fsk_demod(conFsk->fsk, conFsk->demodBits, conFsk->demodCBuf); @@ -201,7 +204,7 @@ namespace Java_com_ustadmobile_codec2_Codec2 { {"destroy", "(J)I", (void *) destroy}, {"encode", "(J[S[C)J", (void *) encode}, {"decode", "(J[S[B)J", (void *) decode}, - {"fskCreate", "(IIII)J", (void *) fskCreate}, + {"fskCreate", "(IIIII)J", (void *) fskCreate}, {"fskDestroy", "(J)I", (void *) fskDestroy}, {"fskModulate", "(J[S[B)J", (void *) fskModulate}, {"fskDemodulate", "(J[S[B)J", (void *) fskDemodulate}, diff --git a/libcodec2-android/src/main/java/com/ustadmobile/codec2/Codec2.java b/libcodec2-android/src/main/java/com/ustadmobile/codec2/Codec2.java index 4c8a58d..5de0986 100644 --- a/libcodec2-android/src/main/java/com/ustadmobile/codec2/Codec2.java +++ b/libcodec2-android/src/main/java/com/ustadmobile/codec2/Codec2.java @@ -35,7 +35,7 @@ public class Codec2 { public native static long encode(long con, short[] inputSamples, char[] outputBits); public native static long decode(long con, short[] outputSamples, byte[] inputsBits); - public native static long fskCreate(int sampleFrequency, int symbolRate, int toneFreq, int toneSpacing); + public native static long fskCreate(int sampleFrequency, int symbolRate, int toneFreq, int toneSpacing, int gain); public native static int fskDestroy(long conFsk); public native static int fskDemodBitsBufSize(long conFsk);