kopia lustrzana https://github.com/sh123/codec2_talkie
Porównaj commity
155 Commity
Autor | SHA1 | Data |
---|---|---|
![]() |
1b15e9a029 | |
![]() |
f6bfff12a2 | |
![]() |
cdb787d922 | |
![]() |
8edf786869 | |
![]() |
579c27f931 | |
![]() |
fb809eb47f | |
![]() |
ad6e79044b | |
![]() |
83cffeaeea | |
![]() |
532686c3cf | |
![]() |
dda3677b00 | |
![]() |
b19dcb8ca3 | |
![]() |
aa1087cd9e | |
![]() |
4d0eb259b3 | |
![]() |
4e10b023dc | |
![]() |
49f8060635 | |
![]() |
4b8321e425 | |
![]() |
800af35e8f | |
![]() |
d2711d807b | |
![]() |
cac9a17885 | |
![]() |
e88a94e9be | |
![]() |
f5ce9e7bbd | |
![]() |
083e996ae6 | |
![]() |
6fcf2f38ff | |
![]() |
cc83f5add8 | |
![]() |
2f949aa501 | |
![]() |
4d56b32c55 | |
![]() |
52c41ec2a0 | |
![]() |
d8eb26d6fe | |
![]() |
5128480b6c | |
![]() |
bb9abbc379 | |
![]() |
4d8ec2998b | |
![]() |
b53785f261 | |
![]() |
2c7eb04b69 | |
![]() |
df80a982de | |
![]() |
9886bccaf4 | |
![]() |
3346003d2d | |
![]() |
9dab9acad0 | |
![]() |
561325757b | |
![]() |
3a4438e80c | |
![]() |
64e6b7f265 | |
![]() |
854bd629da | |
![]() |
1edf1c4c51 | |
![]() |
e6a05091d9 | |
![]() |
6c3700809a | |
![]() |
d79c24f602 | |
![]() |
7e6e0bc43e | |
![]() |
968a213bc6 | |
![]() |
2c7e3e80b5 | |
![]() |
ef7f4a8dcd | |
![]() |
8499df6e18 | |
![]() |
e842032cad | |
![]() |
dee32ec97f | |
![]() |
09d231e118 | |
![]() |
710afa0b82 | |
![]() |
45af1ff8c3 | |
![]() |
19d5d8adbd | |
![]() |
93f98cc26b | |
![]() |
2db9c0154e | |
![]() |
55997a703e | |
![]() |
837dcc2e4c | |
![]() |
8ecc9bafe5 | |
![]() |
dea6304258 | |
![]() |
32d6b92960 | |
![]() |
a80af0fad9 | |
![]() |
2fe55655b8 | |
![]() |
4bfc4532a9 | |
![]() |
7588f6b0ba | |
![]() |
d697657684 | |
![]() |
a59a908c88 | |
![]() |
77c307ca3c | |
![]() |
2ed8a4074d | |
![]() |
a9ee325c47 | |
![]() |
8bc35767fd | |
![]() |
28146a9ff7 | |
![]() |
aca71d54d5 | |
![]() |
95bc0fb812 | |
![]() |
970ca74b92 | |
![]() |
2062e2ee19 | |
![]() |
084af64d50 | |
![]() |
6780839fa7 | |
![]() |
bb82360e3f | |
![]() |
f6334c580f | |
![]() |
169eaf1000 | |
![]() |
0c70d6353c | |
![]() |
19eb6fcc09 | |
![]() |
1b3beb9f04 | |
![]() |
bb0b7d0e89 | |
![]() |
12193f5f8e | |
![]() |
4e2b8bbc17 | |
![]() |
03be0feb2d | |
![]() |
ad6aa8c0f8 | |
![]() |
332f4627e1 | |
![]() |
1995051f89 | |
![]() |
ec9dee33c7 | |
![]() |
93846cb976 | |
![]() |
59b309e4bb | |
![]() |
74f782feb5 | |
![]() |
eba020cbfe | |
![]() |
ce069d92f2 | |
![]() |
6b119b892d | |
![]() |
370ef3f2db | |
![]() |
69c09be2aa | |
![]() |
febe7a5fdb | |
![]() |
9ff42c834d | |
![]() |
64a8e18ce1 | |
![]() |
c2768dbd76 | |
![]() |
ab54a49d00 | |
![]() |
7968612cf2 | |
![]() |
4dcd4c5c4c | |
![]() |
f0aa85cefd | |
![]() |
0025b790b6 | |
![]() |
4530c58811 | |
![]() |
6428b8263b | |
![]() |
024697ff04 | |
![]() |
20aab8d253 | |
![]() |
e0fd23d401 | |
![]() |
f6613bfb6e | |
![]() |
48a9ac86d2 | |
![]() |
bbf4000579 | |
![]() |
5cee15769f | |
![]() |
b5ed6a011f | |
![]() |
df999f0e58 | |
![]() |
d463c2e767 | |
![]() |
a47801fae6 | |
![]() |
373fec8c91 | |
![]() |
60bab818f0 | |
![]() |
ec2ac02d84 | |
![]() |
90ddf20a7e | |
![]() |
30cda1b159 | |
![]() |
4f92c99139 | |
![]() |
5951a7c0f6 | |
![]() |
6f09a0b0e6 | |
![]() |
19c2937ced | |
![]() |
701fe71af5 | |
![]() |
fad1c6654c | |
![]() |
eef3a0c469 | |
![]() |
974e6cf9ee | |
![]() |
83a87fa6e9 | |
![]() |
d77f79f89a | |
![]() |
a91a11a999 | |
![]() |
50450ac0af | |
![]() |
09b38e65f7 | |
![]() |
4ce3820baa | |
![]() |
b67d298587 | |
![]() |
0470cc317a | |
![]() |
47782cb1dc | |
![]() |
1c25d65d59 | |
![]() |
9d0f06a6af | |
![]() |
742ca8184d | |
![]() |
6d01acbc0f | |
![]() |
9669142de8 | |
![]() |
3c2bdb6128 | |
![]() |
424e3c87fa | |
![]() |
9656016a29 | |
![]() |
067ff7318b |
|
@ -2,11 +2,15 @@
|
|||
![APK](https://img.shields.io/endpoint?url=https://apt.izzysoft.de/fdroid/api/v1/shield/com.radio.codec2talkie)
|
||||
|
||||
# Introduction
|
||||
**Turn your Android phone into real Amateur Radio HF/VHF/UHF APRS enabled Codec2 DV (digital voice) and/or FreeDV handheld transceiver.**
|
||||
**Turn your Android phone into real Amateur Radio HF/VHF/UHF APRS enabled Codec2/OPUS DV (digital voice) and/or FreeDV handheld transceiver.**
|
||||
|
||||
**Requires additional hardware (e.g. AFSK/LoRa), software (e.g. Direwolf) radio modem or analog transceiver with USB audio + VOX/USB CAT PTT control, such as MCHF or ICOM**
|
||||
|
||||
For more information visit project [Wiki](https://github.com/sh123/codec2_talkie/wiki)
|
||||
For more information about FreeDV and Codec2 visit https://github.com/drowe67/codec2
|
||||
|
||||
For more information about OPUS codec visit https://opus-codec.org/
|
||||
|
||||
For detailed information about project [Wiki](https://github.com/sh123/codec2_talkie/wiki)
|
||||
|
||||
![alt text](images/diagram.png)
|
||||
|
||||
|
@ -16,14 +20,20 @@ For more information visit project [Wiki](https://github.com/sh123/codec2_talkie
|
|||
# Short Description
|
||||
What you can do with this app:
|
||||
- Voice communication:
|
||||
- Send and receive voice over FreeDV modes
|
||||
- Send and receive Codec2 voice over KISS
|
||||
- Send and receive Codec2 voice encapsulated into APRS UI frames
|
||||
- Send and receive Codec2 voice over FreeDV modes
|
||||
- Send and receive Codec2/OPUS voice over KISS
|
||||
- Send and receive Codec2/OPUS voice encapsulated into APRS UI frames
|
||||
- Data communication
|
||||
- Send and receive APRS position reports over FSK 300 (HF, TX only), AFSK1200 (VHF), FreeDV OFDM (HF)
|
||||
- Use APRS over FSK 300 (HF, TX only), AFSK1200 (VHF), FreeDV OFDM (HF) or APRS-IS (Internet)
|
||||
- Send and receive APRS position reports
|
||||
- Send and receive APRS messages
|
||||
- APRS log with raw APRS data
|
||||
- APRS station hub with stations grouped by callsign and their log
|
||||
- APRS map with ability to see station info, station track with information about each geo position
|
||||
- Use application as APRS digirepeater
|
||||
- Use application as APRS-IS RX/TX iGate
|
||||
- Use application as APRS-IS RX/TX iGate to forward packets to/from APRS-IS (Internet)
|
||||
- Use application as APRS-IS internet tracker to send/receive APRS data over Internet
|
||||
- Send and receive text packets in lora-aprs format over KISS
|
||||
- Integrate with hardware/software
|
||||
- Use it with your KISS Bluetooth/BLE/USB/TCPIP hardware modem, such as LoRa/FSK/AFSK/etc, control its parameters by using "set hardware" KISS command
|
||||
- Use it with KISS software modem using TCPIP, such as Direwolf
|
||||
|
@ -35,8 +45,6 @@ What you can do with this app:
|
|||
# Requirements
|
||||
- Android 7.0 (API 24) or higher
|
||||
- Application could also be used with your Android network radio, such as Inrico TM-7, apk just needs to be installed over USB, see [Discussion](https://github.com/sh123/codec2_talkie/issues/4)
|
||||
- Android 5.0, 5.1, 6.0 (API 21, 22, 23)
|
||||
- Separate apk package is released with "legacy" suffix from legacy branch
|
||||
- Modem, radio module or transceiver which supports [KISS protocol](https://en.wikipedia.org/wiki/KISS_(TNC)) or can process KISS or raw Codec2 audio frames over serial Bluetooth, BLE, USB or TCP/IP
|
||||
- Analog transceiver with built-in or external USB audio adapter and VOX or USB CAT PTT control (such as MCHF or iCom IC-7x00 series)
|
||||
|
||||
|
@ -44,12 +52,16 @@ What you can do with this app:
|
|||
- Source code is integrated into this project for easier building and customization:
|
||||
- Codec2 codec: https://github.com/drowe67/codec2
|
||||
- Android Codec2 wrapper code: https://github.com/UstadMobile/Codec2-Android
|
||||
- OPUS codec: https://opus-codec.org
|
||||
- Fetched with gradle as dependency:
|
||||
- Android USB serial: https://github.com/mik3y/usb-serial-for-android
|
||||
|
||||
# Other similar or related projects
|
||||
- Hardware
|
||||
- ESP32 LoRa APRS modem (used with this application for testing): https://github.com/sh123/esp32_loraprs
|
||||
- ESP32 LoRa DV transceiver: https://github.com/sh123/esp32_loradv
|
||||
- Arduno Micro KISS modem and APRS AX.25 digirepeater: https://github.com/sh123/micro_loraprs
|
||||
- ESP32 Arduino Codec2 library (ESP32 i2s walkie talkie example interoperable with this application): https://github.com/sh123/esp32_codec2_arduino
|
||||
- Minimal Arduino LoRa KISS modem: https://github.com/sh123/lora_arduino_kiss_modem
|
||||
- Minimal Arduino NRF24 KISS modem: https://github.com/sh123/nrf24l01_arduino_kiss_modem
|
||||
- Software:
|
||||
|
@ -60,3 +72,11 @@ What you can do with this app:
|
|||
- Codec2 iOS wrapper: https://github.com/Beartooth/codec2-ios
|
||||
- Other interesting projects:
|
||||
- LoRa mesh text GPS communicator: https://github.com/meshtastic/Meshtastic-device
|
||||
|
||||
# Prototypes used with this application
|
||||
- ESP32 LoRa Bluetooth headless APRS modem (no screen and external controls): https://github.com/sh123/esp32_loraprs
|
||||
- ESP32 LoRa DV handheld transceiver (with screen and controls): https://github.com/sh123/esp32_loradv
|
||||
- Arduno Micro USB KISS modem (no screen and external controls): https://github.com/sh123/micro_loraprs
|
||||
|
||||
![alt text](images/modems.png)
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
android.ndkVersion "21.4.7075529"
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
|
@ -10,8 +11,8 @@ android {
|
|||
applicationId "com.radio.codec2talkie"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 30
|
||||
versionCode 142
|
||||
versionName "1.42"
|
||||
versionCode 172
|
||||
versionName "1.72"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
@ -31,6 +32,7 @@ android {
|
|||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation project(":libcodec2-android")
|
||||
implementation project(":libopus-android")
|
||||
|
||||
implementation 'androidx.preference:preference:1.2.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
|
@ -38,12 +40,15 @@ dependencies {
|
|||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'com.github.mik3y:usb-serial-for-android:3.4.3'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.2.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata:2.5.1'
|
||||
|
||||
testImplementation 'junit:junit:4.13.1'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
||||
implementation "androidx.room:room-runtime:2.4.2"
|
||||
annotationProcessor "androidx.room:room-compiler:2.4.2"
|
||||
implementation "androidx.room:room-runtime:2.4.3"
|
||||
annotationProcessor "androidx.room:room-compiler:2.4.3"
|
||||
|
||||
implementation "org.osmdroid:osmdroid-android:6.1.13"
|
||||
}
|
||||
|
|
|
@ -13,12 +13,15 @@
|
|||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-feature android:name="android.hardware.bluetooth_le"
|
||||
android:required="true" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:fullBackupOnly="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
@ -56,6 +59,9 @@
|
|||
<activity
|
||||
android:name=".storage.message.MessageItemActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".maps.MapActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|screenSize">
|
||||
|
|
|
@ -53,6 +53,7 @@ import com.radio.codec2talkie.connect.BleConnectActivity;
|
|||
import com.radio.codec2talkie.connect.BluetoothConnectActivity;
|
||||
import com.radio.codec2talkie.connect.BluetoothSocketHandler;
|
||||
import com.radio.codec2talkie.connect.TcpIpConnectActivity;
|
||||
import com.radio.codec2talkie.maps.MapActivity;
|
||||
import com.radio.codec2talkie.settings.SettingsWrapper;
|
||||
import com.radio.codec2talkie.storage.log.LogItemActivity;
|
||||
import com.radio.codec2talkie.protocol.ProtocolFactory;
|
||||
|
@ -88,7 +89,8 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
Manifest.permission.BLUETOOTH_ADMIN,
|
||||
Manifest.permission.RECORD_AUDIO,
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
};
|
||||
|
||||
private AppService _appService;
|
||||
|
@ -100,6 +102,7 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
|
||||
private TextView _textConnInfo;
|
||||
private TextView _textStatus;
|
||||
private TextView _textTelemetry;
|
||||
private TextView _textCodecMode;
|
||||
private TextView _textRssi;
|
||||
private ProgressBar _progressAudioLevel;
|
||||
|
@ -131,6 +134,7 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
|
||||
_textConnInfo = findViewById(R.id.textBtName);
|
||||
_textStatus = findViewById(R.id.textStatus);
|
||||
_textTelemetry = findViewById(R.id.textTelemetry);
|
||||
_textRssi = findViewById(R.id.textRssi);
|
||||
|
||||
// UV bar
|
||||
|
@ -378,6 +382,9 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
_logViewActivityLauncher.launch(new Intent(this, LogItemActivity.class));
|
||||
}
|
||||
|
||||
protected void startMapViewActivity() {
|
||||
_logViewActivityLauncher.launch(new Intent(this, MapActivity.class));
|
||||
}
|
||||
private final ActivityResultLauncher<Intent> _settingsActivityLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(), result -> restartApplication());
|
||||
|
||||
|
@ -450,8 +457,6 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
}
|
||||
|
||||
private void updateStatusText(ProtocolFactory.ProtocolType protocolType) {
|
||||
String codec2ModeName = _sharedPreferences.getString(PreferenceKeys.CODEC2_MODE, getResources().getStringArray(R.array.codec2_modes)[0]);
|
||||
|
||||
// protocol
|
||||
String status = "";
|
||||
|
||||
|
@ -484,12 +489,18 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
status += getString(R.string.voax25_label);
|
||||
}
|
||||
|
||||
// Lora aprs text packets
|
||||
boolean textPacketsEnabled = SettingsWrapper.isTextPacketsEnabled(_sharedPreferences);
|
||||
if (textPacketsEnabled) {
|
||||
status += getString(R.string.text_packets_label);
|
||||
}
|
||||
// Digirepeater
|
||||
boolean isDigirepeaterEnabled = _sharedPreferences.getBoolean(PreferenceKeys.AX25_DIGIREPEATER_ENABLED, false);
|
||||
if (isDigirepeaterEnabled) {
|
||||
status += getString(R.string.digirepeater_label);
|
||||
}
|
||||
|
||||
// APRSIS
|
||||
boolean aprsisEnabled = SettingsWrapper.isAprsIsEnabled(_sharedPreferences);
|
||||
if (aprsisEnabled) {
|
||||
status += getString(R.string.aprsis_label);
|
||||
|
@ -505,7 +516,7 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
|
||||
status = status.length() == 0 ? protocolType.toString() : protocolType.toString() + " " + status;
|
||||
|
||||
String statusLine = AudioTools.getSpeedStatusText(codec2ModeName, _sharedPreferences) + ", " + status;
|
||||
String statusLine = AudioTools.getSpeedStatusText(_sharedPreferences, getResources()) + ", " + status;
|
||||
_textCodecMode.setText(statusLine);
|
||||
}
|
||||
|
||||
|
@ -604,6 +615,10 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
startLogViewActivity();
|
||||
return true;
|
||||
}
|
||||
else if (itemId == R.id.aprs_map) {
|
||||
startMapViewActivity();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
|
@ -803,6 +818,12 @@ public class MainActivity extends AppCompatActivity implements ServiceConnection
|
|||
_progressRssi.setProgress(msg.arg1 - S_METER_S0_VALUE_DB);
|
||||
}
|
||||
break;
|
||||
case EV_TELEMETRY:
|
||||
if (msg.arg1 > 0) {
|
||||
// NOTE, reuse status indicator for voltage
|
||||
_textTelemetry.setText(String.format(Locale.getDefault(), "%2.2fV", (double)msg.arg1 / 100.0));
|
||||
}
|
||||
break;
|
||||
// same progress bar is reused for rx and tx levels
|
||||
case EV_RX_LEVEL:
|
||||
case EV_TX_LEVEL:
|
||||
|
|
|
@ -18,16 +18,17 @@ public enum AppMessage {
|
|||
EV_RX_ERROR(13),
|
||||
EV_TX_ERROR(14),
|
||||
EV_RX_RADIO_LEVEL(15),
|
||||
EV_STARTED_TRACKING(16),
|
||||
EV_STOPPED_TRACKING(17),
|
||||
EV_TELEMETRY(16),
|
||||
EV_STARTED_TRACKING(17),
|
||||
EV_STOPPED_TRACKING(18),
|
||||
// commands
|
||||
CMD_SEND_LOCATION_TO_TNC(18),
|
||||
CMD_PROCESS(19),
|
||||
CMD_QUIT(20),
|
||||
CMD_START_TRACKING(21),
|
||||
CMD_STOP_TRACKING(22),
|
||||
CMD_SEND_SINGLE_TRACKING(23),
|
||||
CMD_SEND_MESSAGE(24);
|
||||
CMD_SEND_LOCATION_TO_TNC(19),
|
||||
CMD_PROCESS(20),
|
||||
CMD_QUIT(21),
|
||||
CMD_START_TRACKING(22),
|
||||
CMD_STOP_TRACKING(23),
|
||||
CMD_SEND_SINGLE_TRACKING(24),
|
||||
CMD_SEND_MESSAGE(25);
|
||||
|
||||
private final int _value;
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ import java.io.IOException;
|
|||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsIsData;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.storage.log.LogItem;
|
||||
import com.radio.codec2talkie.storage.log.LogItemRepository;
|
||||
|
@ -32,6 +32,7 @@ import com.radio.codec2talkie.protocol.position.Position;
|
|||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.storage.message.MessageItemRepository;
|
||||
import com.radio.codec2talkie.storage.position.PositionItemRepository;
|
||||
import com.radio.codec2talkie.storage.station.StationItemRepository;
|
||||
import com.radio.codec2talkie.tools.AudioTools;
|
||||
import com.radio.codec2talkie.transport.Transport;
|
||||
import com.radio.codec2talkie.transport.TransportFactory;
|
||||
|
@ -44,7 +45,7 @@ public class AppWorker extends Thread {
|
|||
private static final int AUDIO_MAX_LEVEL = 0;
|
||||
private static final int AUDIO_SAMPLE_SIZE = 8000;
|
||||
|
||||
private static final int PROCESS_INTERVAL_MS = 20;
|
||||
private static final int PROCESS_INTERVAL_MS = 10;
|
||||
private static final int LISTEN_AFTER_MS = 1500;
|
||||
|
||||
private boolean _needTransmission = false;
|
||||
|
@ -53,8 +54,6 @@ public class AppWorker extends Thread {
|
|||
private final Protocol _protocol;
|
||||
private final Transport _transport;
|
||||
|
||||
private final int _codec2Mode;
|
||||
|
||||
// input data, bt -> audio
|
||||
private AudioTrack _systemAudioPlayer;
|
||||
|
||||
|
@ -74,6 +73,7 @@ public class AppWorker extends Thread {
|
|||
private final LogItemRepository _logItemRepository;
|
||||
private final MessageItemRepository _messageItemRepository;
|
||||
private final PositionItemRepository _positionItemRepository;
|
||||
private final StationItemRepository _stationItemRepository;
|
||||
|
||||
private final Context _context;
|
||||
private final SharedPreferences _sharedPreferences;
|
||||
|
@ -85,15 +85,13 @@ public class AppWorker extends Thread {
|
|||
_context = context;
|
||||
_sharedPreferences = PreferenceManager.getDefaultSharedPreferences(_context);
|
||||
|
||||
String codec2ModeName = _sharedPreferences.getString(PreferenceKeys.CODEC2_MODE, _context.getResources().getStringArray(R.array.codec2_modes)[0]);
|
||||
_codec2Mode = AudioTools.extractCodec2ModeId(codec2ModeName);
|
||||
|
||||
_logItemRepository = new LogItemRepository((Application)context);
|
||||
_messageItemRepository = new MessageItemRepository((Application)context);
|
||||
_positionItemRepository = new PositionItemRepository((Application)context);
|
||||
_stationItemRepository = new StationItemRepository((Application)context);
|
||||
|
||||
_transport = TransportFactory.create(transportType, context);
|
||||
_protocol = ProtocolFactory.create(_codec2Mode, context);
|
||||
_protocol = ProtocolFactory.create(context);
|
||||
|
||||
_processPeriodicTimer = new Timer();
|
||||
|
||||
|
@ -234,6 +232,12 @@ public class AppWorker extends Thread {
|
|||
_onWorkerStateChanged.sendMessage(msg);
|
||||
}
|
||||
|
||||
private void sendTelemetryUpdate(int batVoltage) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = AppMessage.EV_TELEMETRY.toInt();
|
||||
msg.arg1 = batVoltage;
|
||||
_onWorkerStateChanged.sendMessage(msg);
|
||||
}
|
||||
private void sendRxAudioLevelUpdate(short [] pcmAudioSamples) {
|
||||
Message msg = Message.obtain();
|
||||
msg.what = AppMessage.EV_RX_LEVEL.toInt();
|
||||
|
@ -250,17 +254,18 @@ public class AppWorker extends Thread {
|
|||
|
||||
private void recordAndSendAudioFrame() throws IOException {
|
||||
_systemAudioRecorder.read(_recordAudioBuffer, 0, _recordAudioBuffer.length);
|
||||
_protocol.sendPcmAudio(null, null, _codec2Mode, _recordAudioBuffer);
|
||||
_protocol.sendPcmAudio(null, null, _recordAudioBuffer);
|
||||
}
|
||||
|
||||
private final ProtocolCallback _protocolCallback = new ProtocolCallback() {
|
||||
@Override
|
||||
protected void onReceivePosition(Position position) {
|
||||
Log.i(TAG, String.format("Position received: %s, lat: %f, lon: %f, course: %f, speed: %f, alt: %f, sym: %s, status: %s, comment: %s",
|
||||
position.maidenHead, position.latitude, position.longitude,
|
||||
Log.i(TAG, String.format("Position received: %s→%s, %s, lat: %f, lon: %f, course: %f, speed: %f, alt: %f, sym: %s, range: %.2f, status: %s, comment: %s",
|
||||
position.srcCallsign, position.dstCallsign, position.maidenHead, position.latitude, position.longitude,
|
||||
position.bearingDegrees, position.speedMetersPerSecond, position.altitudeMeters,
|
||||
position.symbolCode, position.status, position.comment));
|
||||
_positionItemRepository.insertPositionItem(position.toPositionItem(false));
|
||||
position.symbolCode, position.rangeMiles, position.status, position.comment));
|
||||
_positionItemRepository.upsertPositionItem(position.toPositionItem(false));
|
||||
_stationItemRepository.upsertStationItem(position.toStationItem());
|
||||
|
||||
String note = (position.srcCallsign == null ? "UNK" : position.srcCallsign) + "→" +
|
||||
(position.dstCallsign == null ? "UNK" : position.dstCallsign);
|
||||
|
@ -268,7 +273,7 @@ public class AppWorker extends Thread {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
String note = (src == null ? "UNK" : src) + "→" + (dst == null ? "UNK" : dst);
|
||||
sendStatusUpdate(AppMessage.EV_VOICE_RECEIVED, note);
|
||||
sendRxAudioLevelUpdate(pcmFrame);
|
||||
|
@ -279,7 +284,7 @@ public class AppWorker extends Thread {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrame) {
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrame) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -288,7 +293,11 @@ public class AppWorker extends Thread {
|
|||
String note = (textMessage.src == null ? "UNK" : textMessage.src) + "→" +
|
||||
(textMessage.dst == null ? "UNK" : textMessage.dst);
|
||||
sendStatusUpdate(AppMessage.EV_TEXT_MESSAGE_RECEIVED, note + ": " + textMessage.text);
|
||||
_messageItemRepository.insertMessageItem(textMessage.toMessageItem(false));
|
||||
if (textMessage.isAutoReply()) {
|
||||
// TODO, acknowledge or reject message with the given (src, dst, ackId)
|
||||
} else {
|
||||
_messageItemRepository.insertMessageItem(textMessage.toMessageItem(false));
|
||||
}
|
||||
Log.i(TAG, "message received: " + textMessage.text);
|
||||
}
|
||||
|
||||
|
@ -303,6 +312,11 @@ public class AppWorker extends Thread {
|
|||
sendRxRadioLevelUpdate(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
sendTelemetryUpdate(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
Log.i(TAG, "RX-LOG: " + logData);
|
||||
|
@ -310,14 +324,14 @@ public class AppWorker extends Thread {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
String note = (src == null ? "UNK" : src) + "→" + (dst == null ? "UNK" : dst);
|
||||
sendStatusUpdate(AppMessage.EV_TRANSMITTED_VOICE, note);
|
||||
sendTxAudioLevelUpdate(frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -326,12 +340,15 @@ public class AppWorker extends Thread {
|
|||
String note = (textMessage.src == null ? "UNK" : textMessage.src) + "→" +
|
||||
(textMessage.dst == null ? "UNK" : textMessage.dst);
|
||||
sendStatusUpdate(AppMessage.EV_TEXT_MESSAGE_TRANSMITTED, note);
|
||||
_messageItemRepository.insertMessageItem(textMessage.toMessageItem(true));
|
||||
if (!textMessage.isAutoReply()) {
|
||||
_messageItemRepository.insertMessageItem(textMessage.toMessageItem(true));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPosition(Position position) {
|
||||
_positionItemRepository.insertPositionItem(position.toPositionItem(true));
|
||||
_positionItemRepository.upsertPositionItem(position.toPositionItem(true));
|
||||
_stationItemRepository.upsertStationItem(position.toStationItem());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -359,16 +376,25 @@ public class AppWorker extends Thread {
|
|||
}
|
||||
};
|
||||
|
||||
void storeLogData(String logData, boolean isTransmit) {
|
||||
// TODO, parse through aprs data
|
||||
String[] callsignData = logData.split(">");
|
||||
if (callsignData.length >= 2) {
|
||||
private void storeLogData(String logData, boolean isTransmit) {
|
||||
AprsIsData aprsIsData = AprsIsData.fromString(logData);
|
||||
if (aprsIsData != null) {
|
||||
LogItem logItem = new LogItem();
|
||||
logItem.setTimestampEpoch(System.currentTimeMillis());
|
||||
logItem.setSrcCallsign(callsignData[0]);
|
||||
logItem.setLogLine(logData);
|
||||
logItem.setIsTransmit(isTransmit);
|
||||
logItem.setSrcCallsign(aprsIsData.src);
|
||||
logItem.setLogLine(logData);
|
||||
_logItemRepository.insertLogItem(logItem);
|
||||
_stationItemRepository.upsertStationItem(logItem.toStationItem());
|
||||
if (aprsIsData.hasThirdParty()) {
|
||||
LogItem logItemThirdParty = new LogItem();
|
||||
logItemThirdParty.setTimestampEpoch(System.currentTimeMillis());
|
||||
logItemThirdParty.setIsTransmit(isTransmit);
|
||||
logItemThirdParty.setSrcCallsign(aprsIsData.thirdParty.src);
|
||||
logItemThirdParty.setLogLine(aprsIsData.thirdParty.convertToString(true));
|
||||
_logItemRepository.insertLogItem(logItemThirdParty);
|
||||
_stationItemRepository.upsertStationItem(logItemThirdParty.toStationItem());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -425,10 +451,14 @@ public class AppWorker extends Thread {
|
|||
|
||||
private void cleanup() {
|
||||
Log.i(TAG, "cleanup() started");
|
||||
_systemAudioRecorder.stop();
|
||||
try {
|
||||
_systemAudioRecorder.stop();
|
||||
} catch (IllegalStateException ignored) {}
|
||||
_systemAudioRecorder.release();
|
||||
|
||||
_systemAudioPlayer.stop();
|
||||
try {
|
||||
_systemAudioPlayer.stop();
|
||||
} catch (IllegalStateException ignored) {}
|
||||
_systemAudioPlayer.release();
|
||||
|
||||
try {
|
||||
|
|
|
@ -26,6 +26,8 @@ public class BleGattWrapper extends BluetoothGattCallback {
|
|||
public static final UUID BT_KISS_CHARACTERISTIC_RX_UUID = UUID.fromString("00000003-ba2a-46c9-ae49-01b0961f68bb");
|
||||
public static final UUID BT_NOTIFY_UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb");
|
||||
|
||||
private static final int DefaultMtuSize = 16;
|
||||
|
||||
private BluetoothGatt _gatt;
|
||||
private BluetoothGattCharacteristic _rxCharacteristic;
|
||||
private BluetoothGattCharacteristic _txCharacteristic;
|
||||
|
@ -63,9 +65,7 @@ public class BleGattWrapper extends BluetoothGattCallback {
|
|||
synchronized (_readBuffer) {
|
||||
// nothing to read
|
||||
if (_readBuffer.position() == 0) return 0;
|
||||
|
||||
_readBuffer.flip();
|
||||
|
||||
int countRead = 0;
|
||||
try {
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
|
@ -85,27 +85,34 @@ public class BleGattWrapper extends BluetoothGattCallback {
|
|||
|
||||
public int write(byte[] data) throws IOException {
|
||||
if (!_isConnected) throw new IOException();
|
||||
|
||||
synchronized (_writeBuffer) {
|
||||
_writeBuffer.put(data);
|
||||
_writeBuffer.flip();
|
||||
|
||||
byte[] arr = new byte[_writeBuffer.limit()];
|
||||
_writeBuffer.get(arr);
|
||||
_txCharacteristic.setValue(arr);
|
||||
|
||||
if (_gatt.writeCharacteristic(_txCharacteristic)) {
|
||||
// written successfully
|
||||
_writeBuffer.clear();
|
||||
} else {
|
||||
// redo
|
||||
_writeBuffer.position(_writeBuffer.limit());
|
||||
_writeBuffer.limit(_writeBuffer.capacity());
|
||||
}
|
||||
writeCharacteristic(_txCharacteristic);
|
||||
return data.length;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeCharacteristic(BluetoothGattCharacteristic characteristic) {
|
||||
_writeBuffer.flip();
|
||||
int cntRemaining = _writeBuffer.remaining();
|
||||
if (cntRemaining > 0) {
|
||||
int cntWrite = Math.min(cntRemaining, DefaultMtuSize);
|
||||
byte[] arr = new byte[cntWrite];
|
||||
_writeBuffer.get(arr);
|
||||
characteristic.setValue(arr);
|
||||
if (!_gatt.writeCharacteristic(characteristic)) {
|
||||
Log.d(TAG, "GATT write delayed");
|
||||
_writeBuffer.position(_writeBuffer.position() - cntWrite);
|
||||
}
|
||||
}
|
||||
_writeBuffer.compact();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
super.onMtuChanged(gatt, mtu, status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
super.onCharacteristicRead(gatt, characteristic, status);
|
||||
|
@ -129,18 +136,12 @@ public class BleGattWrapper extends BluetoothGattCallback {
|
|||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
super.onCharacteristicWrite(gatt, characteristic, status);
|
||||
|
||||
if (status == BluetoothGatt.GATT_SUCCESS && _writeBuffer.position() > 0) {
|
||||
if (status == BluetoothGatt.GATT_SUCCESS) {
|
||||
synchronized (_writeBuffer) {
|
||||
_writeBuffer.flip();
|
||||
|
||||
byte[] arr = new byte[_writeBuffer.limit()];
|
||||
_writeBuffer.get(arr);
|
||||
characteristic.setValue(arr);
|
||||
|
||||
_gatt.writeCharacteristic(characteristic);
|
||||
_writeBuffer.clear();
|
||||
writeCharacteristic(_txCharacteristic);
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "GATT write failure " + status);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,9 @@ import androidx.appcompat.app.AppCompatActivity;
|
|||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver;
|
||||
import com.hoho.android.usbserial.driver.Ch34xSerialDriver;
|
||||
import com.hoho.android.usbserial.driver.Cp21xxSerialDriver;
|
||||
import com.hoho.android.usbserial.driver.FtdiSerialDriver;
|
||||
import com.hoho.android.usbserial.driver.ProbeTable;
|
||||
import com.hoho.android.usbserial.driver.UsbSerialDriver;
|
||||
import com.hoho.android.usbserial.driver.UsbSerialPort;
|
||||
|
@ -92,13 +94,33 @@ public class UsbConnectActivity extends AppCompatActivity {
|
|||
private UsbSerialProber getCustomProber() {
|
||||
ProbeTable customTable = new ProbeTable();
|
||||
// Spark Fun
|
||||
customTable.addProduct(0x1b4f, 0x9203, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x1b4f, 0x9204, CdcAcmSerialDriver.class);
|
||||
// Arduino Due
|
||||
customTable.addProduct(0x2341, 0x003d, CdcAcmSerialDriver.class);
|
||||
// Arduino Uno/Nano (CH34x)
|
||||
customTable.addProduct(0x1a86, 0x5523, Ch34xSerialDriver.class);
|
||||
customTable.addProduct(0x1a86, 0x7523, Ch34xSerialDriver.class);
|
||||
// STM, MCHF
|
||||
customTable.addProduct(0x0483, 0x5732, CdcAcmSerialDriver.class);
|
||||
// CP2102/2109, iCom
|
||||
customTable.addProduct(0x10c4, 0xea60, Cp21xxSerialDriver.class);
|
||||
customTable.addProduct(0x10c4, 0xea70, Cp21xxSerialDriver.class);
|
||||
customTable.addProduct(0x10c4, 0xea71, Cp21xxSerialDriver.class);
|
||||
// FTDI
|
||||
customTable.addProduct(0x0403, 0x6001, FtdiSerialDriver.class);
|
||||
customTable.addProduct(0x0403, 0x6010, FtdiSerialDriver.class);
|
||||
customTable.addProduct(0x0403, 0x6011, FtdiSerialDriver.class);
|
||||
customTable.addProduct(0x0403, 0x6014, FtdiSerialDriver.class);
|
||||
customTable.addProduct(0x0403, 0x6015, FtdiSerialDriver.class);
|
||||
// Raspberry PI Pico
|
||||
customTable.addProduct(0x2e8a, 0x0004, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x2e8a, 0x0005, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x2e8a, 0x000a, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x2e8a, 0x000b, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x2e8a, 0x000c, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x2e8a, 0x000d, CdcAcmSerialDriver.class);
|
||||
customTable.addProduct(0x2e8a, 0x000e, CdcAcmSerialDriver.class);
|
||||
return new UsbSerialProber(customTable);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,256 @@
|
|||
package com.radio.codec2talkie.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.radio.codec2talkie.BuildConfig;
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.Aprs;
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsSymbolTable;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.tools.UnitTools;
|
||||
|
||||
import org.osmdroid.api.IMapController;
|
||||
import org.osmdroid.config.Configuration;
|
||||
import org.osmdroid.tileprovider.modules.SqlTileWriter;
|
||||
import org.osmdroid.tileprovider.tilesource.TileSourceFactory;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.compass.CompassOverlay;
|
||||
import org.osmdroid.views.overlay.compass.InternalCompassOrientationProvider;
|
||||
import org.osmdroid.views.overlay.mylocation.GpsMyLocationProvider;
|
||||
import org.osmdroid.views.overlay.mylocation.IMyLocationProvider;
|
||||
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MapActivity extends AppCompatActivity {
|
||||
private static final String TAG = MapActivity.class.getSimpleName();
|
||||
|
||||
private static final double MAP_STARTUP_ZOOM = 5.0;
|
||||
private static final double MAP_FOLLOW_ZOOM = 14.0;
|
||||
|
||||
private MapView _mapView;
|
||||
private IMapController _mapController;
|
||||
private MyLocationNewOverlay _myLocationNewOverlay;
|
||||
|
||||
private MapStations _mapStations;
|
||||
private boolean _rotateMap = false;
|
||||
private boolean _shouldFollowLocation = false;
|
||||
|
||||
private String _positionInfo;
|
||||
private double _prevBearing = 0.0;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.menu_aprs_map);
|
||||
setContentView(R.layout.activity_map_view);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
Context context = getApplicationContext();
|
||||
Configuration.getInstance().setUserAgentValue(Aprs.APRS_ID + " " + BuildConfig.VERSION_NAME);
|
||||
|
||||
// map
|
||||
_mapView = findViewById(R.id.map);
|
||||
_mapView.setTileSource(TileSourceFactory.MAPNIK);
|
||||
_mapView.setMultiTouchControls(true);
|
||||
|
||||
// controller
|
||||
_mapController = _mapView.getController();
|
||||
_mapController.zoomTo(MAP_STARTUP_ZOOM);
|
||||
|
||||
// compass
|
||||
InternalCompassOrientationProvider compassOrientationProvider = new InternalCompassOrientationProvider(context) {
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent sensorEvent) {
|
||||
if (_rotateMap) {
|
||||
_mapView.setMapOrientation(-sensorEvent.values[0]);
|
||||
}
|
||||
super.onSensorChanged(sensorEvent);
|
||||
}
|
||||
};
|
||||
CompassOverlay compassOverlay = new CompassOverlay(context, compassOrientationProvider, _mapView);
|
||||
compassOverlay.enableCompass();
|
||||
_mapView.getOverlays().add(compassOverlay);
|
||||
|
||||
// my location
|
||||
_myLocationNewOverlay = new MyLocationNewOverlay(new GpsMyLocationProvider(context), _mapView) {
|
||||
@Override
|
||||
public void onLocationChanged(Location location, IMyLocationProvider source) {
|
||||
super.onLocationChanged(location, source);
|
||||
_positionInfo = String.format(Locale.US, "%dkm/h, %d°, %dm, %s, %f, %f",
|
||||
UnitTools.metersPerSecondToKilometersPerHour((int)location.getSpeed()),
|
||||
(int)location.getBearing(),
|
||||
(int)location.getAltitude(),
|
||||
UnitTools.decimalToMaidenhead(location.getLatitude(), location.getLongitude()),
|
||||
location.getLatitude(),
|
||||
location.getLongitude()
|
||||
);
|
||||
|
||||
double currentBearing = location.getBearing();
|
||||
|
||||
if (_prevBearing > 180 && currentBearing <= 180)
|
||||
updateMyIcon(false);
|
||||
else if (_prevBearing <= 180 && currentBearing > 180)
|
||||
updateMyIcon(true);
|
||||
|
||||
_prevBearing = currentBearing;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas pCanvas, MapView pMapView, boolean pShadow) {
|
||||
super.draw(pCanvas, pMapView, pShadow);
|
||||
if (_positionInfo == null || !_shouldFollowLocation) return;
|
||||
|
||||
// create paint
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setTextSize(24);
|
||||
|
||||
// query bounds from text
|
||||
Rect bounds = new Rect();
|
||||
paint.getTextBounds(_positionInfo, 0, _positionInfo.length(), bounds);
|
||||
|
||||
// draw background
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setAlpha(200);
|
||||
pCanvas.drawRect(pCanvas.getWidth() - bounds.width(),
|
||||
0,
|
||||
pCanvas.getWidth(),
|
||||
bounds.height(),
|
||||
paint);
|
||||
|
||||
// draw text
|
||||
paint.setColor(Color.BLACK);
|
||||
paint.setAlpha(255);
|
||||
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
|
||||
pCanvas.drawText(_positionInfo, pCanvas.getWidth() - bounds.width(), bounds.height(), paint);
|
||||
}
|
||||
};
|
||||
|
||||
// set own icon
|
||||
updateMyIcon(false);
|
||||
|
||||
// my location overlay
|
||||
_myLocationNewOverlay.enableMyLocation();
|
||||
_myLocationNewOverlay.runOnFirstFix(() -> runOnUiThread(() -> {
|
||||
_mapController.setCenter(_myLocationNewOverlay.getMyLocation());
|
||||
_mapController.animateTo(_myLocationNewOverlay.getMyLocation());
|
||||
}));
|
||||
_mapView.getOverlays().add(_myLocationNewOverlay);
|
||||
|
||||
// stations
|
||||
_mapStations = new MapStations(context, _mapView, this);
|
||||
}
|
||||
|
||||
public void updateMyIcon(boolean shouldFlip) {
|
||||
Context context = getApplicationContext();
|
||||
Configuration.getInstance().setUserAgentValue(Aprs.APRS_ID + " " + BuildConfig.VERSION_NAME);
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
// my symbol
|
||||
AprsSymbolTable aprsSymbolTable = AprsSymbolTable.getInstance(context);
|
||||
String mySymbolCode = sharedPreferences.getString(PreferenceKeys.APRS_SYMBOL, "/[");
|
||||
|
||||
Bitmap myBitmapIcon = aprsSymbolTable.bitmapFromSymbol(mySymbolCode, true);
|
||||
if (AprsSymbolTable.needsRotation(mySymbolCode)) {
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postRotate(-90);
|
||||
if (shouldFlip) {
|
||||
matrix.postScale(-1, 1, myBitmapIcon.getWidth() / 2f, myBitmapIcon.getHeight() / 2f);
|
||||
}
|
||||
myBitmapIcon = Bitmap.createBitmap(myBitmapIcon, 0, 0, myBitmapIcon.getWidth(), myBitmapIcon.getHeight(), matrix, true);
|
||||
}
|
||||
_myLocationNewOverlay.setDirectionIcon(myBitmapIcon);
|
||||
_myLocationNewOverlay.setPersonIcon(myBitmapIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.map_menu, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
|
||||
if (itemId == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
} else if (itemId == R.id.map_menu_clear_cache) {
|
||||
new Thread(() -> {
|
||||
_mapView.getTileProvider().clearTileCache();
|
||||
SqlTileWriter sqlTileWriter = new SqlTileWriter();
|
||||
boolean isCleared = sqlTileWriter.purgeCache(_mapView.getTileProvider().getTileSource().name());
|
||||
if (isCleared)
|
||||
Log.i(TAG, "Cleanup completed");
|
||||
else
|
||||
Log.e(TAG, "Cache was not cleared");
|
||||
}).start();
|
||||
return true;
|
||||
} else if (itemId == R.id.map_menu_rotate_map) {
|
||||
if (item.isChecked()) {
|
||||
item.setChecked(false);
|
||||
_rotateMap = false;
|
||||
_mapView.setMapOrientation(0);
|
||||
} else {
|
||||
item.setChecked(true);
|
||||
_rotateMap = true;
|
||||
}
|
||||
return true;
|
||||
} else if (itemId == R.id.map_menu_show_range) {
|
||||
boolean showCircles = false;
|
||||
if (item.isChecked()) {
|
||||
item.setChecked(false);
|
||||
} else {
|
||||
item.setChecked(true);
|
||||
showCircles = true;
|
||||
}
|
||||
_mapStations.showRangeCircles(showCircles);
|
||||
return true;
|
||||
} else if (itemId == R.id.map_menu_show_moving) {
|
||||
boolean showMoving = false;
|
||||
if (item.isChecked()) {
|
||||
item.setChecked(false);
|
||||
} else {
|
||||
item.setChecked(true);
|
||||
showMoving = true;
|
||||
}
|
||||
_mapStations.showMovingStations(showMoving);
|
||||
return true;
|
||||
} else if (itemId == R.id.map_menu_move_map) {
|
||||
if (item.isChecked()) {
|
||||
item.setChecked(false);
|
||||
_shouldFollowLocation = false;
|
||||
_myLocationNewOverlay.disableFollowLocation();
|
||||
} else {
|
||||
item.setChecked(true);
|
||||
_shouldFollowLocation = true;
|
||||
_myLocationNewOverlay.enableFollowLocation();
|
||||
_mapController.zoomTo(MAP_FOLLOW_ZOOM);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package com.radio.codec2talkie.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelStoreOwner;
|
||||
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsSymbolTable;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.storage.position.PositionItemViewModel;
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
import com.radio.codec2talkie.storage.station.StationItemViewModel;
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
import com.radio.codec2talkie.tools.UnitTools;
|
||||
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.Marker;
|
||||
import org.osmdroid.views.overlay.Polygon;
|
||||
import org.osmdroid.views.overlay.infowindow.MarkerInfoWindow;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MapStations {
|
||||
private static final String TAG = MapTrack.class.getSimpleName();
|
||||
|
||||
private final Context _context;
|
||||
|
||||
private final AprsSymbolTable _aprsSymbolTable;
|
||||
|
||||
private final PositionItemViewModel _positionItemViewModel;
|
||||
private final ViewModelStoreOwner _owner;
|
||||
private final MapView _mapView;
|
||||
|
||||
private final MarkerInfoWindow _infoWindow;
|
||||
|
||||
private LiveData<List<StationItem>> _stationItemLiveData;
|
||||
private final StationItemViewModel _stationItemViewModel;
|
||||
|
||||
private final HashMap<String, Marker> _objectOverlayItems = new HashMap<>();
|
||||
private final HashMap<String, Polygon> _objectOverlayRangeCircles = new HashMap<>();
|
||||
|
||||
private boolean _showCircles = false;
|
||||
private boolean _showMoving = false;
|
||||
|
||||
private final MapTrack _activeTrack;
|
||||
|
||||
public MapStations(Context context, MapView mapView, ViewModelStoreOwner owner) {
|
||||
_context = context;
|
||||
_owner = owner;
|
||||
_mapView = mapView;
|
||||
_positionItemViewModel = new ViewModelProvider(_owner).get(PositionItemViewModel.class);
|
||||
|
||||
_aprsSymbolTable = AprsSymbolTable.getInstance(context);
|
||||
_infoWindow = new MarkerInfoWindow(R.layout.bonuspack_bubble, _mapView);
|
||||
_activeTrack = new MapTrack(_context, _mapView, _owner);
|
||||
|
||||
_stationItemViewModel = new ViewModelProvider(_owner).get(StationItemViewModel.class);
|
||||
loadStations(_showMoving);
|
||||
}
|
||||
|
||||
private void loadStations(boolean movingOnly) {
|
||||
if (_stationItemLiveData != null)
|
||||
_stationItemLiveData.removeObservers((LifecycleOwner) _owner);
|
||||
removePositionMarkers();
|
||||
_stationItemLiveData = _stationItemViewModel.getAllStationItems(movingOnly);
|
||||
// FIXME, room livedata sends all list if one item changed event with distinctUntilChanged
|
||||
_stationItemLiveData.observe((LifecycleOwner) _owner, allStations -> {
|
||||
Log.i(TAG, "add stations " + allStations.size());
|
||||
for (StationItem station : allStations) {
|
||||
//Log.i(TAG, "new position " + station.getSrcCallsign() + ">" +
|
||||
// station.getDstCallsign() + " " + station.getLatitude() + " " + station.getLongitude());
|
||||
// do not add items without coordinate
|
||||
if (station.getMaidenHead() == null) continue;
|
||||
if (addStationPositionIcon(station)) {
|
||||
addRangeCircle(station);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void removePositionMarkers() {
|
||||
for (Marker marker : _objectOverlayItems.values()) {
|
||||
marker.remove(_mapView);
|
||||
}
|
||||
_objectOverlayItems.clear();
|
||||
for (Polygon circle : _objectOverlayRangeCircles.values()) {
|
||||
_mapView.getOverlays().remove(circle);
|
||||
}
|
||||
_objectOverlayRangeCircles.clear();
|
||||
}
|
||||
|
||||
public void showMovingStations(boolean isMoving) {
|
||||
_showMoving = isMoving;
|
||||
loadStations(_showMoving);
|
||||
}
|
||||
|
||||
public void showRangeCircles(boolean isVisible) {
|
||||
_showCircles = isVisible;
|
||||
for (Polygon polygon : _objectOverlayRangeCircles.values()) {
|
||||
polygon.setVisible(isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
private void addRangeCircle(StationItem group) {
|
||||
if (group.getRangeMiles() == 0) return;
|
||||
String callsign = group.getSrcCallsign();
|
||||
Polygon polygon = null;
|
||||
|
||||
if (_objectOverlayRangeCircles.containsKey(callsign)) {
|
||||
polygon = _objectOverlayRangeCircles.get(callsign);
|
||||
assert polygon != null;
|
||||
}
|
||||
|
||||
if (polygon == null) {
|
||||
polygon = new Polygon();
|
||||
polygon.setVisible(_showCircles);
|
||||
|
||||
Paint p = polygon.getOutlinePaint();
|
||||
p.setStrokeWidth(1);
|
||||
|
||||
_mapView.getOverlayManager().add(0, polygon);
|
||||
_objectOverlayRangeCircles.put(callsign, polygon);
|
||||
}
|
||||
ArrayList<GeoPoint> circlePoints = new ArrayList<>();
|
||||
for (float f = 0; f < 360; f += 6) {
|
||||
circlePoints.add(new GeoPoint(group.getLatitude(), group.getLongitude()).destinationPoint(1000 * UnitTools.milesToKilometers(group.getRangeMiles()), f));
|
||||
}
|
||||
polygon.setPoints(circlePoints);
|
||||
}
|
||||
|
||||
private boolean addStationPositionIcon(StationItem group) {
|
||||
String callsign = group.getSrcCallsign();
|
||||
Marker marker = null;
|
||||
|
||||
String newTitle = DateTools.epochToIso8601(group.getTimestampEpoch()) + " " + callsign;
|
||||
String newSnippet = getStatus(group);
|
||||
|
||||
// find old marker
|
||||
if (_objectOverlayItems.containsKey(callsign)) {
|
||||
marker = _objectOverlayItems.get(callsign);
|
||||
assert marker != null;
|
||||
|
||||
// skip if unchanged
|
||||
GeoPoint oldPosition = marker.getPosition();
|
||||
if (oldPosition.getLatitude() == group.getLatitude() &&
|
||||
oldPosition.getLongitude() == group.getLongitude() &&
|
||||
marker.getTitle().equals(newTitle) &&
|
||||
marker.getSnippet().equals(newSnippet)) {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// create new marker
|
||||
if (marker == null) {
|
||||
Bitmap bitmapInfoIcon = _aprsSymbolTable.bitmapFromSymbol(group.getSymbolCode(), true);
|
||||
if (bitmapInfoIcon == null) return false;
|
||||
|
||||
// add marker
|
||||
BitmapDrawable drawableText = group.drawLabelWithIcon(_context, 12);
|
||||
BitmapDrawable drawableInfoIcon = new BitmapDrawable(_context.getResources(), bitmapInfoIcon);
|
||||
marker = new Marker(_mapView);
|
||||
marker.setId(callsign);
|
||||
if (drawableText == null)
|
||||
Log.e(TAG, "Cannot load icon for " + callsign);
|
||||
else
|
||||
marker.setIcon(drawableText);
|
||||
marker.setImage(drawableInfoIcon);
|
||||
marker.setOnMarkerClickListener((monitoredStationMarker, mapView) -> {
|
||||
GeoPoint markerPoint = monitoredStationMarker.getPosition();
|
||||
_infoWindow.open(monitoredStationMarker, new GeoPoint(markerPoint.getLatitude(), markerPoint.getLongitude()), 0, -64);
|
||||
_activeTrack.drawForStationMarker(monitoredStationMarker);
|
||||
return false;
|
||||
});
|
||||
_mapView.getOverlays().add(marker);
|
||||
_objectOverlayItems.put(callsign, marker);
|
||||
}
|
||||
|
||||
marker.setPosition(new GeoPoint(group.getLatitude(), group.getLongitude()));
|
||||
marker.setTitle(newTitle);
|
||||
marker.setSnippet(newSnippet);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private String getStatus(StationItem station) {
|
||||
double range = UnitTools.milesToKilometers(station.getRangeMiles());
|
||||
return String.format(Locale.US, "%s %s<br>%s %f %f<br>%03d° %03dkm/h %04dm %.2fkm<br>%s %s",
|
||||
station.getDstCallsign(),
|
||||
station.getDigipath(),
|
||||
station.getMaidenHead(), station.getLatitude(), station.getLongitude(),
|
||||
(int)station.getBearingDegrees(),
|
||||
UnitTools.metersPerSecondToKilometersPerHour((int)station.getSpeedMetersPerSecond()),
|
||||
(int)station.getAltitudeMeters(),
|
||||
range == 0 ? UnitTools.milesToKilometers(Position.DEFAULT_RANGE_MILES): range,
|
||||
station.getStatus(),
|
||||
station.getComment());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package com.radio.codec2talkie.maps;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelStoreOwner;
|
||||
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.storage.position.PositionItem;
|
||||
import com.radio.codec2talkie.storage.position.PositionItemViewModel;
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
import com.radio.codec2talkie.tools.BitmapTools;
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
import com.radio.codec2talkie.tools.UnitTools;
|
||||
|
||||
import org.osmdroid.util.GeoPoint;
|
||||
import org.osmdroid.views.MapView;
|
||||
import org.osmdroid.views.overlay.Marker;
|
||||
import org.osmdroid.views.overlay.Polyline;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MapTrack {
|
||||
private static final String TAG = MapTrack.class.getSimpleName();
|
||||
|
||||
private final PositionItemViewModel _positionItemViewModel;
|
||||
private final ViewModelStoreOwner _owner;
|
||||
private final MapView _mapView;
|
||||
private final Context _context;
|
||||
|
||||
// track db data
|
||||
private LiveData<List<PositionItem>> _activeTrackLiveData;
|
||||
|
||||
// track data
|
||||
private final HashSet<Long> _activeTrackTimestamps = new HashSet<>();
|
||||
private final List<GeoPoint> _activeTrackPoints = new ArrayList<>();
|
||||
private final Polyline _activeTrackLine = new Polyline();
|
||||
|
||||
// track points
|
||||
private final HashSet<Marker> _trackMarkers = new HashSet<>();
|
||||
|
||||
public MapTrack(Context context, MapView mapView, ViewModelStoreOwner owner) {
|
||||
_owner = owner;
|
||||
_mapView = mapView;
|
||||
_context = context;
|
||||
_positionItemViewModel = new ViewModelProvider(_owner).get(PositionItemViewModel.class);
|
||||
|
||||
// initialize track
|
||||
Paint p = _activeTrackLine.getOutlinePaint();
|
||||
p.setStrokeWidth(8);
|
||||
p.setColor(Color.RED);
|
||||
p.setStyle(Paint.Style.STROKE);
|
||||
p.setPathEffect(new DashPathEffect(new float[] {10f, 10f}, 0f));
|
||||
_mapView.getOverlayManager().add(_activeTrackLine);
|
||||
}
|
||||
|
||||
public void drawForStationMarker(Marker marker) {
|
||||
if (_activeTrackLiveData != null)
|
||||
_activeTrackLiveData.removeObservers((LifecycleOwner) _owner);
|
||||
|
||||
for (Marker trackMarker : _trackMarkers) {
|
||||
_mapView.getOverlays().remove(trackMarker);
|
||||
}
|
||||
_mapView.getOverlays().remove(_activeTrackLine);
|
||||
|
||||
_activeTrackPoints.clear();
|
||||
_activeTrackTimestamps.clear();
|
||||
_trackMarkers.clear();
|
||||
|
||||
_activeTrackLine.setPoints(_activeTrackPoints);
|
||||
_activeTrackLine.setVisible(false);
|
||||
_mapView.getOverlays().add(_activeTrackLine);
|
||||
|
||||
// FIXME, room livedata sends all list if one item changed event with distinctUntilChanged
|
||||
_activeTrackLiveData = _positionItemViewModel.getPositionItems(marker.getId());
|
||||
_activeTrackLiveData.observe((LifecycleOwner) _owner, this::addTrack);
|
||||
}
|
||||
|
||||
private void addTrack(List<PositionItem> positions) {
|
||||
boolean shouldSet = false;
|
||||
for (PositionItem trackPoint : positions) {
|
||||
if (!_activeTrackTimestamps.contains(trackPoint.getTimestampEpoch())) {
|
||||
long pointTimestamp = trackPoint.getTimestampEpoch();
|
||||
Log.i(TAG, "addPoint " + trackPoint.getTimestampEpoch() + " " + trackPoint.getLatitude() + " " + trackPoint.getLongitude());
|
||||
|
||||
// add point into the line
|
||||
GeoPoint point = new GeoPoint(trackPoint.getLatitude(), trackPoint.getLongitude());
|
||||
_activeTrackPoints.add(point);
|
||||
_activeTrackTimestamps.add(pointTimestamp);
|
||||
if (_activeTrackPoints.size() > 1)
|
||||
_activeTrackLine.setVisible(true);
|
||||
|
||||
// draw point marker with time
|
||||
Marker marker = new Marker(_mapView);
|
||||
marker.setIcon(BitmapTools.drawLabel(_context, DateTools.epochToIso8601Time(pointTimestamp), 11));
|
||||
marker.setTitle(DateTools.epochToIso8601(pointTimestamp) + " " + trackPoint.getSrcCallsign());
|
||||
marker.setSnippet(getStatus(trackPoint));
|
||||
marker.setPosition(point);
|
||||
_trackMarkers.add(marker);
|
||||
_mapView.getOverlays().add(marker);
|
||||
|
||||
shouldSet = true;
|
||||
}
|
||||
}
|
||||
if (shouldSet)
|
||||
_activeTrackLine.setPoints(_activeTrackPoints);
|
||||
}
|
||||
|
||||
private String getStatus(PositionItem position) {
|
||||
return String.format(Locale.US, "%s<br>%s %f %f<br>%03d° %03dkm/h %04dm<br>%s",
|
||||
position.getDigipath(),
|
||||
position.getMaidenHead(), position.getLatitude(), position.getLongitude(),
|
||||
(int)position.getBearingDegrees(),
|
||||
UnitTools.metersPerSecondToKilometersPerHour((int)position.getSpeedMetersPerSecond()),
|
||||
(int)position.getAltitudeMeters(),
|
||||
position.getComment());
|
||||
}
|
||||
}
|
|
@ -20,6 +20,8 @@ import com.radio.codec2talkie.transport.Transport;
|
|||
import java.io.IOException;
|
||||
|
||||
public class Aprs implements Protocol {
|
||||
public static final String APRS_ID = "APCTLK";
|
||||
|
||||
private static final String TAG = Aprs.class.getSimpleName();
|
||||
|
||||
private final Protocol _childProtocol;
|
||||
|
@ -55,7 +57,7 @@ public class Aprs implements Protocol {
|
|||
_srcCallsign = AX25Callsign.formatCallsign(
|
||||
sharedPreferences.getString(PreferenceKeys.AX25_CALLSIGN, "NOCALL").toUpperCase(),
|
||||
sharedPreferences.getString(PreferenceKeys.AX25_SSID, "0"));
|
||||
_dstCallsign = "APZMDM";
|
||||
_dstCallsign = APRS_ID;
|
||||
|
||||
_symbolCode = sharedPreferences.getString(PreferenceKeys.APRS_SYMBOL, "/[");
|
||||
String packetFormat = sharedPreferences.getString(PreferenceKeys.APRS_LOCATION_PACKET_FORMAT, "uncompressed");
|
||||
|
@ -78,8 +80,8 @@ public class Aprs implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame);
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -99,11 +101,11 @@ public class Aprs implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec2Mode, short[] pcmFrame) throws IOException {
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
if (_isVoax25Enabled) {
|
||||
_childProtocol.sendPcmAudio(src == null ? _srcCallsign : src, dst == null ? _dstCallsign : dst, codec2Mode, pcmFrame);
|
||||
_childProtocol.sendPcmAudio(src == null ? _srcCallsign : src, dst == null ? _dstCallsign : dst, pcmFrame);
|
||||
} else {
|
||||
_childProtocol.sendPcmAudio(src, dst, codec2Mode, pcmFrame);
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,14 +126,14 @@ public class Aprs implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
String dstCallsign = new AprsCallsign(dst).isSoftware() ? "*" : dst;
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dstCallsign, codec, pcmFrame);
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dstCallsign, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrame) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrame);
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrame) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -142,14 +144,13 @@ public class Aprs implements Protocol {
|
|||
@Override
|
||||
protected void onReceiveData(String src, String dst, String path, byte[] data) {
|
||||
if (data.length == 0) return;
|
||||
AprsDataType dataType = new AprsDataType((char)data[0]);
|
||||
AprsData aprsData = AprsDataFactory.fromBinary(src, dst, path, data);
|
||||
if (aprsData != null && aprsData.isValid()) {
|
||||
if (dataType.isTextMessage()) {
|
||||
if (aprsData.isTextMessage()) {
|
||||
TextMessage textMessage = aprsData.toTextMessage();
|
||||
_parentProtocolCallback.onReceiveTextMessage(textMessage);
|
||||
return;
|
||||
} else if (dataType.isPositionReport()) {
|
||||
} else if (aprsData.isPositionReport()) {
|
||||
Position position = aprsData.toPosition();
|
||||
if (position != null) {
|
||||
_parentProtocolCallback.onReceivePosition(position);
|
||||
|
@ -165,20 +166,25 @@ public class Aprs implements Protocol {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
String dstCallsign = new AprsCallsign(dst).isSoftware() ? "*" : dst;
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dstCallsign, codec, frame);
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dstCallsign, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,18 +4,20 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.util.Xml;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.radio.codec2talkie.MainActivity;
|
||||
import com.radio.codec2talkie.BuildConfig;
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.aprs.AprsCallsign;
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsHeardList;
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsIsData;
|
||||
import com.radio.codec2talkie.protocol.ax25.AX25Callsign;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.settings.SettingsWrapper;
|
||||
import com.radio.codec2talkie.tools.DebugTools;
|
||||
import com.radio.codec2talkie.tools.TextTools;
|
||||
import com.radio.codec2talkie.transport.TcpIp;
|
||||
|
@ -27,8 +29,8 @@ import java.net.InetSocketAddress;
|
|||
import java.net.Socket;
|
||||
import java.nio.BufferOverflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import kotlin.text.MatchGroup;
|
||||
import kotlin.text.MatchResult;
|
||||
|
@ -38,7 +40,8 @@ public class AprsIs implements Protocol, Runnable {
|
|||
private static final String TAG = AprsIs.class.getSimpleName();
|
||||
|
||||
private static final int APRSIS_RETRY_WAIT_MS = 10000;
|
||||
private static final int APRSIS_DEFAULT_PORT = 14580;
|
||||
|
||||
private static final int HEARD_LIST_DURATION_SECONDS = 60;
|
||||
|
||||
private final Protocol _childProtocol;
|
||||
private Context _context;
|
||||
|
@ -50,23 +53,28 @@ public class AprsIs implements Protocol, Runnable {
|
|||
private boolean _isSelfEnabled;
|
||||
private boolean _isRxGateEnabled;
|
||||
private boolean _isTxGateEnabled;
|
||||
private boolean _isLoopbackTransport;
|
||||
|
||||
private String _callsign;
|
||||
private String _digipath;
|
||||
private String _ssid;
|
||||
private int _filterRadius;
|
||||
private String _filter;
|
||||
private int _defaultPort;
|
||||
|
||||
private final ByteBuffer _rxQueue;
|
||||
private final ByteBuffer _txQueue;
|
||||
private final ByteBuffer _fromAprsIsQueue;
|
||||
private final ByteBuffer _toAprsIsQueue;
|
||||
private final byte[] _rxBuf;
|
||||
|
||||
private final AprsHeardList _rfHeardList = new AprsHeardList(HEARD_LIST_DURATION_SECONDS);
|
||||
|
||||
protected boolean _isRunning = true;
|
||||
private boolean _isConnected = false;
|
||||
|
||||
public AprsIs(Protocol childProtocol) {
|
||||
_childProtocol = childProtocol;
|
||||
_rxQueue = ByteBuffer.allocate(4096);
|
||||
_txQueue = ByteBuffer.allocate(4096);
|
||||
_fromAprsIsQueue = ByteBuffer.allocate(4096);
|
||||
_toAprsIsQueue = ByteBuffer.allocate(4096);
|
||||
_rxBuf = new byte[4096];
|
||||
}
|
||||
|
||||
|
@ -80,12 +88,15 @@ public class AprsIs implements Protocol, Runnable {
|
|||
_isRxGateEnabled = sharedPreferences.getBoolean(PreferenceKeys.APRS_IS_ENABLE_RX_GATE, false);
|
||||
_isTxGateEnabled = sharedPreferences.getBoolean(PreferenceKeys.APRS_IS_ENABLE_TX_GATE, false);
|
||||
_isSelfEnabled = sharedPreferences.getBoolean(PreferenceKeys.APRS_IS_ENABLE_SELF, false);
|
||||
_callsign = sharedPreferences.getString(PreferenceKeys.AX25_CALLSIGN, "N0CALL");
|
||||
_callsign = sharedPreferences.getString(PreferenceKeys.AX25_CALLSIGN, "N0CALL").toUpperCase(Locale.ROOT);
|
||||
_digipath = sharedPreferences.getString(PreferenceKeys.AX25_DIGIPATH, "").toUpperCase();
|
||||
_ssid = sharedPreferences.getString(PreferenceKeys.AX25_SSID, "0");
|
||||
_passcode = sharedPreferences.getString(PreferenceKeys.APRS_IS_CODE, "");
|
||||
_server = sharedPreferences.getString(PreferenceKeys.APRS_IS_TCPIP_SERVER, "euro.aprs2.net");
|
||||
_filterRadius = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.APRS_IS_RADIUS, "10"));
|
||||
_filter = sharedPreferences.getString(PreferenceKeys.APRS_IS_FILTER, "");
|
||||
_isLoopbackTransport = SettingsWrapper.isLoopbackTransport(sharedPreferences);
|
||||
_defaultPort = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.APRS_IS_TCPIP_SERVER_PORT, "14580"));
|
||||
|
||||
Log.i(TAG, "AprsIs RX gate: " + _isTxGateEnabled + ", TX gate: " + _isTxGateEnabled + ", server: " + _server);
|
||||
|
||||
|
@ -98,8 +109,8 @@ public class AprsIs implements Protocol, Runnable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame);
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -108,34 +119,51 @@ public class AprsIs implements Protocol, Runnable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec2Mode, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, codec2Mode, pcmFrame);
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(String src, String dst, String path, byte[] data) throws IOException {
|
||||
if (_isSelfEnabled) {
|
||||
AprsIsData aprsIsData = new AprsIsData(src, dst, path, new String(data));
|
||||
synchronized (_txQueue) {
|
||||
_txQueue.put(aprsIsData.toString().getBytes());
|
||||
synchronized (_toAprsIsQueue) {
|
||||
String rawData = aprsIsData.convertToString(false) + "\n";
|
||||
_toAprsIsQueue.put(rawData.getBytes());
|
||||
}
|
||||
}
|
||||
_childProtocol.sendData(src, dst, path, data);
|
||||
}
|
||||
|
||||
private boolean isEligibleForTxGate(AprsIsData aprsIsData) {
|
||||
AprsCallsign aprsCallsign = new AprsCallsign(aprsIsData.src);
|
||||
return _isTxGateEnabled &&
|
||||
aprsCallsign.isValid &&
|
||||
!_isLoopbackTransport &&
|
||||
!_rfHeardList.contains(aprsIsData.src) &&
|
||||
aprsIsData.isEligibleForTxGate();
|
||||
}
|
||||
|
||||
private byte[] thirdPartyWrap(AprsIsData aprsIsData) {
|
||||
// wrap into third party, https://aprs-is.net/IGateDetails.aspx
|
||||
aprsIsData.digipath = "TCPIP," + _callsign + "*";
|
||||
String txData = "}" + aprsIsData.convertToString(false);
|
||||
return txData.getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receive() throws IOException {
|
||||
String line;
|
||||
synchronized (_rxQueue) {
|
||||
line = TextTools.getString(_rxQueue);
|
||||
synchronized (_fromAprsIsQueue) {
|
||||
line = TextTools.getString(_fromAprsIsQueue);
|
||||
}
|
||||
if (line.length() > 0) {
|
||||
Log.d(TAG, "APRS-RX: " + DebugTools.bytesToDebugString(line.getBytes()));
|
||||
AprsIsData aprsIsData = AprsIsData.fromString(line);
|
||||
if (aprsIsData != null) {
|
||||
_parentProtocolCallback.onReceiveData(aprsIsData.src, aprsIsData.dst, aprsIsData.path, aprsIsData.data.getBytes());
|
||||
if (_isTxGateEnabled && new AprsCallsign(aprsIsData.src).isValid) {
|
||||
sendData(aprsIsData.src, aprsIsData.dst, aprsIsData.path, aprsIsData.data.getBytes());
|
||||
_parentProtocolCallback.onReceiveData(aprsIsData.src, aprsIsData.dst, aprsIsData.rawDigipath, aprsIsData.data.getBytes());
|
||||
if (isEligibleForTxGate(aprsIsData)) {
|
||||
_childProtocol.sendData(new AX25Callsign(_callsign, _ssid).toString(), Aprs.APRS_ID, _digipath, thirdPartyWrap(aprsIsData));
|
||||
}
|
||||
}
|
||||
_parentProtocolCallback.onReceiveLog(line);
|
||||
|
@ -150,13 +178,13 @@ public class AprsIs implements Protocol, Runnable {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame);
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrame) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrame);
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrame) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -166,10 +194,19 @@ public class AprsIs implements Protocol, Runnable {
|
|||
|
||||
@Override
|
||||
protected void onReceiveData(String src, String dst, String path, byte[] data) {
|
||||
if (_isRxGateEnabled) {
|
||||
_rfHeardList.add(src);
|
||||
if (_isRxGateEnabled && !_isLoopbackTransport) {
|
||||
// NOTE, https://aprs-is.net/IGateDetails.aspx
|
||||
AprsIsData aprsIsData = new AprsIsData(src, dst, path, new String(data));
|
||||
synchronized (_txQueue) {
|
||||
_txQueue.put(aprsIsData.toString().getBytes());
|
||||
if (aprsIsData.isEligibleForRxGate()) {
|
||||
// strip "rf header" for third party packets before gating
|
||||
if (aprsIsData.hasThirdParty()) {
|
||||
aprsIsData = aprsIsData.thirdParty;
|
||||
}
|
||||
String rawData = aprsIsData.convertToString(false) + "\n";
|
||||
synchronized (_toAprsIsQueue) {
|
||||
_toAprsIsQueue.put(rawData.getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
_parentProtocolCallback.onReceiveData(src, dst, path, data);
|
||||
|
@ -180,19 +217,24 @@ public class AprsIs implements Protocol, Runnable {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame);
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -271,7 +313,7 @@ public class AprsIs implements Protocol, Runnable {
|
|||
}
|
||||
|
||||
private String getLoginCommand() {
|
||||
String cmd = "user " + _callsign + "-" + _ssid + " pass " + _passcode + " vers " + "C2T 1.0";
|
||||
String cmd = "user " + new AX25Callsign(_callsign, _ssid).toString() + " pass " + _passcode + " vers " + Aprs.APRS_ID + " " + BuildConfig.VERSION_NAME;
|
||||
if (_filterRadius > 0) {
|
||||
cmd += " filter m/" + _filterRadius;
|
||||
}
|
||||
|
@ -288,7 +330,7 @@ public class AprsIs implements Protocol, Runnable {
|
|||
private TcpIp runConnect() {
|
||||
Socket socket = new Socket();
|
||||
try {
|
||||
socket.connect(new InetSocketAddress(_server, APRSIS_DEFAULT_PORT));
|
||||
socket.connect(new InetSocketAddress(_server, _defaultPort));
|
||||
TcpIp tcpIp = new TcpIp(socket, "aprsis");
|
||||
String loginCmd = getLoginCommand();
|
||||
Log.i(TAG, "Login command " + loginCmd);
|
||||
|
@ -312,11 +354,12 @@ public class AprsIs implements Protocol, Runnable {
|
|||
}
|
||||
|
||||
private void runWrite(TcpIp tcpIp) {
|
||||
synchronized (_txQueue) {
|
||||
String line = TextTools.getString(_txQueue);
|
||||
synchronized (_toAprsIsQueue) {
|
||||
String line = TextTools.getString(_toAprsIsQueue);
|
||||
if (line.length() > 0) {
|
||||
Log.d(TAG, "APRS-IS TX: " + DebugTools.bytesToDebugString(line.getBytes()));
|
||||
try {
|
||||
line += "\n";
|
||||
tcpIp.write(line.getBytes());
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Lost connection on transmit");
|
||||
|
@ -368,12 +411,12 @@ public class AprsIs implements Protocol, Runnable {
|
|||
}
|
||||
// data
|
||||
} else {
|
||||
synchronized (_rxQueue) {
|
||||
synchronized (_fromAprsIsQueue) {
|
||||
try {
|
||||
_rxQueue.put(Arrays.copyOf(_rxBuf, bytesRead));
|
||||
_fromAprsIsQueue.put(Arrays.copyOf(_rxBuf, bytesRead));
|
||||
} catch (BufferOverflowException e) {
|
||||
e.printStackTrace();
|
||||
_rxQueue.clear();
|
||||
_fromAprsIsQueue.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
package com.radio.codec2talkie.protocol;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.tools.AudioTools;
|
||||
import com.radio.codec2talkie.transport.Transport;
|
||||
import com.ustadmobile.codec2.Codec2;
|
||||
|
||||
|
@ -19,18 +23,23 @@ public class AudioCodec2 implements Protocol {
|
|||
private char[] _recordAudioEncodedBuffer;
|
||||
private short[] _playbackAudioBuffer;
|
||||
|
||||
private final SharedPreferences _sharedPreferences;
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
|
||||
public AudioCodec2(Protocol childProtocol, int codec2ModeId) {
|
||||
public AudioCodec2(Protocol childProtocol, SharedPreferences sharedPreferences) {
|
||||
_childProtocol = childProtocol;
|
||||
_codec2Con = 0;
|
||||
constructCodec2(codec2ModeId);
|
||||
_sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Transport transport, Context context, ProtocolCallback protocolCallback) throws IOException {
|
||||
_parentProtocolCallback = protocolCallback;
|
||||
_childProtocol.initialize(transport, context, _protocolCallback);
|
||||
|
||||
String codec2ModeName = _sharedPreferences.getString(PreferenceKeys.CODEC2_MODE, context.getResources().getStringArray(R.array.codec2_modes)[0]);
|
||||
int codec2ModeId = AudioTools.extractCodec2ModeId(codec2ModeName);
|
||||
constructCodec2(codec2ModeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -39,8 +48,8 @@ public class AudioCodec2 implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame);
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -49,8 +58,8 @@ public class AudioCodec2 implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec2Mode, short[] pcmFrame) throws IOException {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec2Mode, pcmFrame);
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, pcmFrame);
|
||||
|
||||
Codec2.encode(_codec2Con, pcmFrame, _recordAudioEncodedBuffer);
|
||||
|
||||
|
@ -59,7 +68,7 @@ public class AudioCodec2 implements Protocol {
|
|||
for (int i = 0; i < _recordAudioEncodedBuffer.length; i++) {
|
||||
frame[i] = (byte)_recordAudioEncodedBuffer[i];
|
||||
}
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame);
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -79,14 +88,14 @@ public class AudioCodec2 implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame);
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrame) {
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrame) {
|
||||
Codec2.decode(_codec2Con, _playbackAudioBuffer, audioFrame);
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec2Mode, _playbackAudioBuffer);
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, _playbackAudioBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -104,19 +113,24 @@ public class AudioCodec2 implements Protocol {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame);
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -6,18 +6,20 @@ import android.util.Log;
|
|||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.tools.AudioTools;
|
||||
import com.radio.codec2talkie.transport.Transport;
|
||||
import com.ustadmobile.codec2.Codec2;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class AudioFrameAggregator implements Protocol {
|
||||
public class AudioCodec2FrameAggregator implements Protocol {
|
||||
|
||||
private static final String TAG = AudioFrameAggregator.class.getSimpleName();
|
||||
private static final String TAG = AudioCodec2FrameAggregator.class.getSimpleName();
|
||||
|
||||
private final Protocol _childProtocol;
|
||||
|
||||
|
@ -25,19 +27,17 @@ public class AudioFrameAggregator implements Protocol {
|
|||
private int _outputBufferPos;
|
||||
private byte[] _outputBuffer;
|
||||
|
||||
private final int _codec2FrameSize;
|
||||
private final int _codec2ModeId;
|
||||
private int _codec2FrameSize;
|
||||
|
||||
private String _lastSrc;
|
||||
private String _lastDst;
|
||||
private int _lastCodec2Mode;
|
||||
|
||||
private final SharedPreferences _sharedPreferences;
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
|
||||
public AudioFrameAggregator(Protocol childProtocol, int codec2ModeId) {
|
||||
public AudioCodec2FrameAggregator(Protocol childProtocol, SharedPreferences sharedPreferences) {
|
||||
_childProtocol = childProtocol;
|
||||
_codec2ModeId = codec2ModeId;
|
||||
_codec2FrameSize = getPcmAudioBufferSize(codec2ModeId);
|
||||
_sharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -48,6 +48,10 @@ public class AudioFrameAggregator implements Protocol {
|
|||
_outputBuffer = new byte[_outputBufferSize];
|
||||
_outputBufferPos = 0;
|
||||
_childProtocol.initialize(transport, context, _protocolCallback);
|
||||
|
||||
String codec2ModeName = _sharedPreferences.getString(PreferenceKeys.CODEC2_MODE, context.getResources().getStringArray(R.array.codec2_modes)[0]);
|
||||
int codec2ModeId = AudioTools.extractCodec2ModeId(codec2ModeName);
|
||||
_codec2FrameSize = getPcmAudioBufferSize(codec2ModeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -63,14 +67,13 @@ public class AudioFrameAggregator implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
if ( _outputBufferPos + frame.length >= _outputBufferSize) {
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, Arrays.copyOf(_outputBuffer, _outputBufferPos));
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
if ( _outputBufferPos + frame.length > _outputBufferSize) {
|
||||
_childProtocol.sendCompressedAudio(src, dst, Arrays.copyOf(_outputBuffer, _outputBufferPos));
|
||||
_outputBufferPos = 0;
|
||||
}
|
||||
_lastSrc = src;
|
||||
_lastDst = dst;
|
||||
_lastCodec2Mode = codec2Mode;
|
||||
System.arraycopy(frame, 0, _outputBuffer, _outputBufferPos, frame.length);
|
||||
_outputBufferPos += frame.length;
|
||||
}
|
||||
|
@ -81,8 +84,8 @@ public class AudioFrameAggregator implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, codec, pcmFrame);
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -102,12 +105,12 @@ public class AudioFrameAggregator implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame);
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrames) {
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrames) {
|
||||
if (audioFrames.length % _codec2FrameSize != 0) {
|
||||
Log.e(TAG, "Ignoring audio frame of wrong size: " + audioFrames.length);
|
||||
_parentProtocolCallback.onProtocolRxError();
|
||||
|
@ -118,7 +121,7 @@ public class AudioFrameAggregator implements Protocol {
|
|||
for (int j = 0; j < _codec2FrameSize && (j + i) < audioFrames.length; j++) {
|
||||
audioFrame[j] = audioFrames[i + j];
|
||||
}
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrame);
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -138,19 +141,24 @@ public class AudioFrameAggregator implements Protocol {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame);
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -192,7 +200,7 @@ public class AudioFrameAggregator implements Protocol {
|
|||
@Override
|
||||
public void flush() throws IOException {
|
||||
if (_outputBufferPos > 0) {
|
||||
_childProtocol.sendCompressedAudio(_lastSrc, _lastDst, _lastCodec2Mode, Arrays.copyOf(_outputBuffer, _outputBufferPos));
|
||||
_childProtocol.sendCompressedAudio(_lastSrc, _lastDst, Arrays.copyOf(_outputBuffer, _outputBufferPos));
|
||||
_outputBufferPos = 0;
|
||||
}
|
||||
_childProtocol.flush();
|
|
@ -0,0 +1,216 @@
|
|||
package com.radio.codec2talkie.protocol;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.transport.Transport;
|
||||
import com.radio.opus.Opus;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AudioOpus implements Protocol {
|
||||
|
||||
private static final String TAG = AudioOpus.class.getSimpleName();
|
||||
|
||||
private final Protocol _childProtocol;
|
||||
|
||||
private static final int SAMPLE_RATE = 8000;
|
||||
|
||||
private int _pcmFrameSize;
|
||||
|
||||
private byte[] _recordAudioEncodedBuffer;
|
||||
private short[] _playbackAudioBuffer;
|
||||
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
|
||||
private long _opusCon;
|
||||
private int _audioBufferSize;
|
||||
|
||||
public AudioOpus(Protocol childProtocol) {
|
||||
_childProtocol = childProtocol;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Transport transport, Context context, ProtocolCallback protocolCallback) throws IOException {
|
||||
_parentProtocolCallback = protocolCallback;
|
||||
_childProtocol.initialize(transport, context, _protocolCallback);
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
int bitRate = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.OPUS_BIT_RATE, "3200"));
|
||||
int complexity = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.OPUS_COMPLEXITY, "5"));
|
||||
float pcmFrameDuration = Float.parseFloat(sharedPreferences.getString(PreferenceKeys.OPUS_FRAME_SIZE, "40"));
|
||||
|
||||
_pcmFrameSize = (int)(SAMPLE_RATE / 1000 * pcmFrameDuration);
|
||||
_audioBufferSize = 10*_pcmFrameSize;
|
||||
|
||||
_playbackAudioBuffer = new short[_audioBufferSize];
|
||||
_recordAudioEncodedBuffer = new byte[_audioBufferSize];
|
||||
|
||||
_opusCon = Opus.create(SAMPLE_RATE, 1, Opus.OPUS_APPLICATION_VOIP, bitRate, complexity);
|
||||
if (_opusCon == 0) {
|
||||
Log.e(TAG, "Failed to create opus");
|
||||
}
|
||||
Log.i(TAG, "Opus is initialized, pcm frame size: " + _pcmFrameSize + ", buffer size: " + _audioBufferSize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPcmAudioRecordBufferSize() {
|
||||
return _pcmFrameSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendTextMessage(TextMessage textMessage) throws IOException {
|
||||
_childProtocol.sendTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, pcmFrame);
|
||||
int encodedBytesCnt = Opus.encode(_opusCon, pcmFrame, _pcmFrameSize, _recordAudioEncodedBuffer);
|
||||
if (encodedBytesCnt == 0) {
|
||||
Log.w(TAG, "Nothing was encoded");
|
||||
return;
|
||||
}
|
||||
if (encodedBytesCnt > 0) {
|
||||
byte[] frame = new byte[encodedBytesCnt];
|
||||
System.arraycopy(_recordAudioEncodedBuffer, 0, frame, 0, encodedBytesCnt);
|
||||
Log.v(TAG, "pcm count: " + pcmFrame.length + ", encoded bytes count: " + encodedBytesCnt);
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
} else {
|
||||
Log.e(TAG, "Encode error: " + encodedBytesCnt);
|
||||
_parentProtocolCallback.onProtocolTxError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(String src, String dst, String path, byte[] dataPacket) throws IOException {
|
||||
_childProtocol.sendData(src, dst, path, dataPacket);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receive() throws IOException {
|
||||
return _childProtocol.receive();
|
||||
}
|
||||
|
||||
ProtocolCallback _protocolCallback = new ProtocolCallback() {
|
||||
@Override
|
||||
protected void onReceivePosition(Position position) {
|
||||
_parentProtocolCallback.onReceivePosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioEncodedFrame) {
|
||||
int decodedSamplesCnt = Opus.decode(_opusCon, audioEncodedFrame, _playbackAudioBuffer, _audioBufferSize);
|
||||
Log.v(TAG, "encoded frame size: " + audioEncodedFrame.length + ", decoded samples count:" + decodedSamplesCnt);
|
||||
if (decodedSamplesCnt == 0) {
|
||||
Log.w(TAG, "Nothing was decoded");
|
||||
return;
|
||||
}
|
||||
short [] decodedSamples = new short[decodedSamplesCnt];
|
||||
if (decodedSamplesCnt > 0) {
|
||||
System.arraycopy(_playbackAudioBuffer, 0, decodedSamples, 0, decodedSamplesCnt);
|
||||
} else {
|
||||
Log.e(TAG, "Decode error: " + decodedSamplesCnt);
|
||||
_parentProtocolCallback.onProtocolRxError();
|
||||
}
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, decodedSamples);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTextMessage(TextMessage textMessage) {
|
||||
_parentProtocolCallback.onReceiveTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveData(String src, String dst, String path, byte[] data) {
|
||||
_parentProtocolCallback.onReceiveData(src, dst, path, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveSignalLevel(short rssi, short snr) {
|
||||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitTextMessage(TextMessage textMessage) {
|
||||
_parentProtocolCallback.onTransmitTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPosition(Position position) {
|
||||
_parentProtocolCallback.onTransmitPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitData(String src, String dst, String path, byte[] data) {
|
||||
_parentProtocolCallback.onTransmitData(src, dst, path, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitLog(String logData) {
|
||||
_parentProtocolCallback.onTransmitLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProtocolRxError() {
|
||||
_parentProtocolCallback.onProtocolRxError();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProtocolTxError() {
|
||||
_parentProtocolCallback.onProtocolTxError();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void sendPosition(Position position) throws IOException {
|
||||
_childProtocol.sendPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
_childProtocol.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Opus.destroy(_opusCon);
|
||||
_childProtocol.close();
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ public class Ax25 implements Protocol {
|
|||
private String _digipath;
|
||||
private boolean _isVoax25Enabled;
|
||||
private boolean _isDigiRepeaterEnabled;
|
||||
private boolean _useTextPackets;
|
||||
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
|
||||
|
@ -44,6 +45,7 @@ public class Ax25 implements Protocol {
|
|||
// NOTE, may need to pass through sendData/sendAudio
|
||||
_digipath = sharedPreferences.getString(PreferenceKeys.AX25_DIGIPATH, "").toUpperCase();
|
||||
_isVoax25Enabled = SettingsWrapper.isVoax25Enabled(sharedPreferences);
|
||||
_useTextPackets = SettingsWrapper.isTextPacketsEnabled(sharedPreferences);
|
||||
_isDigiRepeaterEnabled = sharedPreferences.getBoolean(PreferenceKeys.AX25_DIGIREPEATER_ENABLED, false);
|
||||
}
|
||||
|
||||
|
@ -53,13 +55,12 @@ public class Ax25 implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
if (_isVoax25Enabled) {
|
||||
AX25Packet ax25Packet = new AX25Packet();
|
||||
ax25Packet.src = src;
|
||||
ax25Packet.dst = dst;
|
||||
ax25Packet.digipath = _digipath;
|
||||
ax25Packet.codec2Mode = codec2Mode;
|
||||
ax25Packet.isAudio = true;
|
||||
ax25Packet.rawData = frame;
|
||||
byte[] ax25Frame = ax25Packet.toBinary();
|
||||
|
@ -67,10 +68,10 @@ public class Ax25 implements Protocol {
|
|||
Log.e(TAG, "Cannot convert AX.25 voice packet to binary");
|
||||
_parentProtocolCallback.onProtocolTxError();
|
||||
} else {
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, ax25Frame);
|
||||
_childProtocol.sendCompressedAudio(src, dst, ax25Frame);
|
||||
}
|
||||
} else {
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame);
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,8 +81,8 @@ public class Ax25 implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, codec, pcmFrame);
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -93,7 +94,7 @@ public class Ax25 implements Protocol {
|
|||
ax25Packet.digipath = path == null ? _digipath : path;
|
||||
ax25Packet.isAudio = false;
|
||||
ax25Packet.rawData = dataPacket;
|
||||
byte[] ax25Frame = ax25Packet.toBinary();
|
||||
byte[] ax25Frame = _useTextPackets ? ax25Packet.toTextBinary() : ax25Packet.toBinary();
|
||||
if (ax25Frame == null) {
|
||||
Log.e(TAG, "Cannot convert AX.25 data packet to binary");
|
||||
_parentProtocolCallback.onProtocolTxError();
|
||||
|
@ -115,25 +116,25 @@ public class Ax25 implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame);
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrames) {
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrames) {
|
||||
AX25Packet ax25Data = new AX25Packet();
|
||||
ax25Data.fromBinary(audioFrames);
|
||||
if (ax25Data.isValid) {
|
||||
if (ax25Data.isAudio) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(ax25Data.src, ax25Data.dst, ax25Data.codec2Mode, ax25Data.rawData);
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(ax25Data.src, ax25Data.dst, ax25Data.rawData);
|
||||
} else {
|
||||
_parentProtocolCallback.onReceiveData(ax25Data.src, ax25Data.dst, ax25Data.digipath, ax25Data.rawData);
|
||||
_parentProtocolCallback.onReceiveLog(ax25Data.toString());
|
||||
_parentProtocolCallback.onReceiveData(ax25Data.src, ax25Data.dst, ax25Data.digipath, ax25Data.rawData);
|
||||
if (_isDigiRepeaterEnabled) digiRepeat(ax25Data);
|
||||
}
|
||||
} else {
|
||||
// fallback to raw audio if ax25 frame is invalid
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrames);
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrames);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,19 +153,24 @@ public class Ax25 implements Protocol {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame);
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
package com.radio.codec2talkie.protocol;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.tools.TextTools;
|
||||
import com.radio.codec2talkie.transport.Transport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class CustomDataPrefix implements Protocol {
|
||||
|
||||
private final Protocol _childProtocol;
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
private final byte[] _bytePrefix;
|
||||
|
||||
public CustomDataPrefix(Protocol childProtocol, SharedPreferences sharedPreferences) {
|
||||
_childProtocol = childProtocol;
|
||||
String prefix = sharedPreferences.getString(PreferenceKeys.CUSTOM_PREFIX, "");
|
||||
_bytePrefix = TextTools.hexStringToByteArray(prefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(Transport transport, Context context, ProtocolCallback protocolCallback) throws IOException {
|
||||
_parentProtocolCallback = protocolCallback;
|
||||
_childProtocol.initialize(transport, context, _protocolCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPcmAudioRecordBufferSize() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
byte[] prefixedData = ByteBuffer.allocate(_bytePrefix.length + frame.length)
|
||||
.put(_bytePrefix)
|
||||
.put(frame)
|
||||
.array();
|
||||
_childProtocol.sendCompressedAudio(src, dst, prefixedData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendTextMessage(TextMessage textMessage) throws IOException {
|
||||
_childProtocol.sendTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(String src, String dst, String path, byte[] dataPacket) throws IOException {
|
||||
byte[] prefixedData = ByteBuffer.allocate(_bytePrefix.length + dataPacket.length)
|
||||
.put(_bytePrefix)
|
||||
.put(dataPacket)
|
||||
.array();
|
||||
_childProtocol.sendData(src, dst, path, prefixedData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean receive() throws IOException {
|
||||
return _childProtocol.receive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPosition(Position position) throws IOException {
|
||||
_childProtocol.sendPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
_childProtocol.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
_childProtocol.close();
|
||||
}
|
||||
|
||||
ProtocolCallback _protocolCallback = new ProtocolCallback() {
|
||||
@Override
|
||||
protected void onReceivePosition(Position position) {
|
||||
_parentProtocolCallback.onReceivePosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrame) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTextMessage(TextMessage textMessage) {
|
||||
_parentProtocolCallback.onReceiveTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveData(String src, String dst, String path, byte[] data) {
|
||||
_parentProtocolCallback.onReceiveData(src, dst, path, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveSignalLevel(short rssi, short snr) {
|
||||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitTextMessage(TextMessage textMessage) {
|
||||
_parentProtocolCallback.onTransmitTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPosition(Position position) {
|
||||
_parentProtocolCallback.onTransmitPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitData(String src, String dst, String path, byte[] data) {
|
||||
_parentProtocolCallback.onTransmitData(src, dst, path, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitLog(String logData) {
|
||||
_parentProtocolCallback.onTransmitLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProtocolRxError() {
|
||||
_parentProtocolCallback.onProtocolRxError();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProtocolTxError() {
|
||||
_parentProtocolCallback.onProtocolTxError();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -22,6 +22,9 @@ import java.util.Arrays;
|
|||
public class Freedv implements Protocol {
|
||||
private static final String TAG = Freedv.class.getSimpleName();
|
||||
|
||||
private static final int CRC_LENGTH = 2;
|
||||
private static final int PKT_SIZE_LENGTH = 2;
|
||||
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
private Transport _transport;
|
||||
|
||||
|
@ -70,15 +73,15 @@ public class Freedv implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException {
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
Codec2.freedvTx(_freedv, _modemTxBuffer, pcmFrame);
|
||||
//Log.i(TAG, "send pcm " + _modemTxBuffer.length);
|
||||
_transport.write(_modemTxBuffer);
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, pcmFrame);
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec, byte[] frame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
Log.w(TAG, "sendCompressedAudio() is not supported");
|
||||
}
|
||||
|
||||
|
@ -89,7 +92,7 @@ public class Freedv implements Protocol {
|
|||
|
||||
@Override
|
||||
public void sendData(String src, String dst, String path, byte[] dataPacket) throws IOException {
|
||||
if (dataPacket.length > _dataBuffer.length - 2) {
|
||||
if (dataPacket.length > _dataBuffer.length - CRC_LENGTH - PKT_SIZE_LENGTH) {
|
||||
Log.e(TAG, "Too large packet " + dataPacket.length + " > " + _dataBuffer.length);
|
||||
return;
|
||||
}
|
||||
|
@ -97,7 +100,10 @@ public class Freedv implements Protocol {
|
|||
_transport.write(Arrays.copyOf(_dataSamplesBuffer, (int) cnt));
|
||||
|
||||
Arrays.fill(_dataBuffer, (byte) 0);
|
||||
System.arraycopy(dataPacket, 0, _dataBuffer, 0, dataPacket.length);
|
||||
// transmit packet size first
|
||||
_dataBuffer[0] = (byte)((dataPacket.length >> 8) & 0xff);
|
||||
_dataBuffer[1] = (byte)(dataPacket.length & 0xff);
|
||||
System.arraycopy(dataPacket, 0, _dataBuffer, 2, dataPacket.length);
|
||||
Codec2.freedvRawDataTx(_freedvData, _dataSamplesBuffer, _dataBuffer);
|
||||
_transport.write(_dataSamplesBuffer);
|
||||
|
||||
|
@ -134,7 +140,7 @@ public class Freedv implements Protocol {
|
|||
long cntRead = Codec2.freedvRx(_freedv, _speechRxBuffer, samplesSpeech);
|
||||
if (cntRead > 0) {
|
||||
//Log.i(TAG, "receive " + cntRead);
|
||||
_parentProtocolCallback.onReceivePcmAudio(null, null, -1, Arrays.copyOf(_speechRxBuffer, (int) cntRead));
|
||||
_parentProtocolCallback.onReceivePcmAudio(null, null, Arrays.copyOf(_speechRxBuffer, (int) cntRead));
|
||||
float snr = Codec2.freedvGetModemStat(_freedv);
|
||||
_parentProtocolCallback.onReceiveSignalLevel((short) 0, (short)(100 * snr));
|
||||
isRead = true;
|
||||
|
@ -151,8 +157,12 @@ public class Freedv implements Protocol {
|
|||
long cntRead = Codec2.freedvRawDataRx(_freedvData, _dataBuffer, samplesData);
|
||||
if (cntRead > 0) {
|
||||
Log.i(TAG, "receive " + cntRead);
|
||||
// TODO, refactor, use onReceiveData
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(null, null, -1, _dataBuffer);
|
||||
// extract packet length
|
||||
int pktLen = (((int)_dataBuffer[0] & 0xff) << 8) | ((int)_dataBuffer[1] & 0xff);
|
||||
byte [] pkt = new byte[pktLen];
|
||||
System.arraycopy(_dataBuffer, 2, pkt, 0, pktLen);
|
||||
// NOTE, default data is compressed audio, upper layer should distinguish
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(null, null, pkt);
|
||||
float snr = Codec2.freedvGetModemStat(_freedvData);
|
||||
_parentProtocolCallback.onReceiveSignalLevel((short) 0, (short)(100 * snr));
|
||||
isRead = true;
|
||||
|
@ -170,7 +180,8 @@ public class Freedv implements Protocol {
|
|||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
// TODO, check if need to flush buffers
|
||||
_speechSamples.clear();
|
||||
_dataSamples.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -55,12 +55,12 @@ public class Hdlc implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException {
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
Log.w(TAG, "sendPcmAudio() is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec, byte[] frame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
_transport.write(hdlcEncode(frame));
|
||||
}
|
||||
|
||||
|
@ -111,7 +111,8 @@ public class Hdlc implements Protocol {
|
|||
if (calculatedCrc == packetCrc) {
|
||||
//Log.v(TAG, DebugTools.byteBitsToString(packetBits));
|
||||
//Log.i(TAG, "RX: " + DebugTools.bytesToHex(packetBytes));
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(null, null, -1, contentBytes);
|
||||
// NOTE, default data is compressed audio, upper layer should distinguish
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(null, null, contentBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ public class Kiss implements Protocol {
|
|||
private static final int FRAME_OUTPUT_BUFFER_SIZE = 1024;
|
||||
private static final int KISS_CMD_BUFFER_SIZE = 128;
|
||||
|
||||
private static final int KISS_RADIO_CONTROL_COMMAND_SIZE = 17;
|
||||
private static final int KISS_RADIO_CONTROL_COMMAND_SIZE = 34;
|
||||
|
||||
private static final byte KISS_FEND = (byte)0xc0;
|
||||
private static final byte KISS_FESC = (byte)0xdb;
|
||||
|
@ -46,6 +46,7 @@ public class Kiss implements Protocol {
|
|||
private static final byte KISS_CMD_SET_HARDWARE = (byte)0x06;
|
||||
private static final byte KISS_CMD_SIGNAL_REPORT = (byte)0x07;
|
||||
private static final byte KISS_CMD_REBOOT = (byte)0x08;
|
||||
private static final byte KISS_CMD_TELEMETRY = (byte)0x09;
|
||||
private static final byte KISS_CMD_NOCMD = (byte)0x80;
|
||||
|
||||
private static final byte CSMA_PERSISTENCE = (byte)0xff;
|
||||
|
@ -54,6 +55,7 @@ public class Kiss implements Protocol {
|
|||
private static final byte TX_TAIL_10MS_UNITS = (byte)(500 / 10);
|
||||
|
||||
private static final int SIGNAL_LEVEL_EVENT_SIZE = 4;
|
||||
private static final int TELEMETRY_EVENT_SIZE = 2;
|
||||
|
||||
private enum State {
|
||||
GET_START,
|
||||
|
@ -65,7 +67,8 @@ public class Kiss implements Protocol {
|
|||
|
||||
private enum DataType {
|
||||
RAW,
|
||||
SIGNAL_REPORT
|
||||
SIGNAL_REPORT,
|
||||
TELEMETRY
|
||||
}
|
||||
|
||||
private DataType _kissDataType = DataType.RAW;
|
||||
|
@ -152,41 +155,52 @@ public class Kiss implements Protocol {
|
|||
|
||||
private void initializeExtended() throws IOException {
|
||||
/*
|
||||
struct LoraControlCommand {
|
||||
uint32_t freq;
|
||||
struct SetHardware {
|
||||
uint32_t freqRx;
|
||||
uint32_t freqTx;
|
||||
uint8_t modType;
|
||||
uint16_t pwr;
|
||||
uint32_t bw;
|
||||
uint16_t sf;
|
||||
uint16_t cr;
|
||||
uint16_t pwr;
|
||||
uint16_t sync;
|
||||
uint8_t crc;
|
||||
} __attribute__((packed));
|
||||
uint32_t fskBitRate;
|
||||
uint32_t fskFreqDev;
|
||||
uint32_t fskRxBw;
|
||||
} __attribute__((packed));
|
||||
*/
|
||||
String freq = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_FREQUENCY, "433775000");
|
||||
String freqTx = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_FREQUENCY_TX, "433775000");
|
||||
if (!_sharedPreferences.getBoolean(PreferenceKeys.KISS_EXTENSIONS_RADIO_SPLIT_FREQ, false)) freqTx = freq;
|
||||
String modType = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_MOD, "0");
|
||||
String bw = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_BANDWIDTH, "125000");
|
||||
String sf = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_SF, "7");
|
||||
String cr = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_CR, "6");
|
||||
String pwr = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_POWER, "20");
|
||||
String sync = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_SYNC, "34");
|
||||
byte crc = (byte)(_sharedPreferences.getBoolean(PreferenceKeys.KISS_EXTENSIONS_RADIO_CRC, true) ? 1 : 0);
|
||||
String fskBitRate = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_FSK_BIT_RATE, "4800");
|
||||
String fskFreqDev = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_FSK_FREQ_DEV, "1200");
|
||||
String fskRxBw = _sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_FSK_RX_BW, "9700");
|
||||
|
||||
ByteBuffer rawBuffer = ByteBuffer.allocate(KISS_RADIO_CONTROL_COMMAND_SIZE);
|
||||
|
||||
rawBuffer.putInt(Integer.parseInt(freq))
|
||||
.putInt(Integer.parseInt(freqTx))
|
||||
.put(Byte.parseByte(modType))
|
||||
.putShort(Short.parseShort(pwr))
|
||||
.putInt(Integer.parseInt(bw))
|
||||
.putShort(Short.parseShort(sf))
|
||||
.putShort(Short.parseShort(cr))
|
||||
.putShort(Short.parseShort(pwr))
|
||||
.putShort(Short.parseShort(sync, 16))
|
||||
.put(crc)
|
||||
.putInt(Integer.parseInt(fskBitRate))
|
||||
.putInt(Integer.parseInt(fskFreqDev))
|
||||
.putInt(Integer.parseInt(fskRxBw))
|
||||
.rewind();
|
||||
|
||||
startKissPacket(KISS_CMD_SET_HARDWARE);
|
||||
for (byte b: rawBuffer.array()) {
|
||||
sendKissByte(b);
|
||||
}
|
||||
completeKissPacket();
|
||||
|
||||
send(KISS_CMD_SET_HARDWARE, rawBuffer.array());
|
||||
_context.registerReceiver(onModemRebootRequested, new IntentFilter(PreferenceKeys.KISS_EXTENSIONS_ACTION_REBOOT_REQUESTED));
|
||||
}
|
||||
|
||||
|
@ -205,9 +219,9 @@ public class Kiss implements Protocol {
|
|||
};
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec, byte[] frame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
// NOTE, KISS does not distinguish between audio and data packet, upper layer should decide
|
||||
send(frame);
|
||||
send(KISS_CMD_DATA, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -216,14 +230,14 @@ public class Kiss implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
Log.w(TAG, "sendPcmAudio() is not supported");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendData(String src, String dst, String path, byte[] dataPacket) throws IOException {
|
||||
// NOTE, KISS does not distinguish between audio and data packet, upper layer should decide
|
||||
send(dataPacket);
|
||||
send(KISS_CMD_DATA, dataPacket);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -255,14 +269,14 @@ public class Kiss implements Protocol {
|
|||
}
|
||||
}
|
||||
|
||||
private void send(byte[] data) throws IOException {
|
||||
private void send(byte commandCode, byte[] data) throws IOException {
|
||||
// escape
|
||||
ByteBuffer escapedFrame = escape(data);
|
||||
int escapedFrameSize = escapedFrame.position();
|
||||
escapedFrame.rewind();
|
||||
|
||||
// send
|
||||
startKissPacket(KISS_CMD_DATA);
|
||||
startKissPacket(commandCode);
|
||||
while (escapedFrame.position() < escapedFrameSize) {
|
||||
sendKissByte(escapedFrame.get());
|
||||
}
|
||||
|
@ -280,6 +294,11 @@ public class Kiss implements Protocol {
|
|||
_kissDataType = DataType.SIGNAL_REPORT;
|
||||
_kissCmdBufferPos = 0;
|
||||
break;
|
||||
case KISS_CMD_TELEMETRY:
|
||||
_kissState = State.GET_DATA;
|
||||
_kissDataType = DataType.TELEMETRY;
|
||||
_kissCmdBufferPos = 0;
|
||||
break;
|
||||
case KISS_FEND:
|
||||
break;
|
||||
default:
|
||||
|
@ -296,9 +315,8 @@ public class Kiss implements Protocol {
|
|||
break;
|
||||
case KISS_FEND:
|
||||
if (_kissDataType == DataType.RAW) {
|
||||
// NOTE, KISS does not distinguish between audio and data packets, upper layer should decide
|
||||
// TODO, refactor, use onReceiveData
|
||||
protocolCallback.onReceiveCompressedAudio(null, null, -1, Arrays.copyOf(_frameOutputBuffer, _frameOutputBufferPos));
|
||||
// NOTE, default data is compressed audio, KISS does not distinguish between audio and data packets, upper layer should decide
|
||||
protocolCallback.onReceiveCompressedAudio(null, null, Arrays.copyOf(_frameOutputBuffer, _frameOutputBufferPos));
|
||||
} else if (_kissDataType == DataType.SIGNAL_REPORT && _isExtendedMode) {
|
||||
byte[] signalLevelRaw = Arrays.copyOf(_kissCmdBuffer, _kissCmdBufferPos);
|
||||
ByteBuffer data = ByteBuffer.wrap(signalLevelRaw);
|
||||
|
@ -311,13 +329,24 @@ public class Kiss implements Protocol {
|
|||
Log.e(TAG, "Signal event of wrong size");
|
||||
}
|
||||
_kissCmdBufferPos = 0;
|
||||
} else if (_kissDataType == DataType.TELEMETRY && _isExtendedMode) {
|
||||
byte[] telemetryRaw = Arrays.copyOf(_kissCmdBuffer, _kissCmdBufferPos);
|
||||
ByteBuffer data = ByteBuffer.wrap(telemetryRaw);
|
||||
if (telemetryRaw.length == TELEMETRY_EVENT_SIZE) {
|
||||
short batVoltage = data.getShort();
|
||||
protocolCallback.onReceiveTelemetry(batVoltage);
|
||||
} else {
|
||||
protocolCallback.onProtocolRxError();
|
||||
Log.e(TAG, "Telemetry event of wrong size " + telemetryRaw.length + " vs " + TELEMETRY_EVENT_SIZE);
|
||||
}
|
||||
_kissCmdBufferPos = 0;
|
||||
}
|
||||
resetState();
|
||||
break;
|
||||
default:
|
||||
if (_kissDataType == DataType.RAW) {
|
||||
receiveFrameByte(b);
|
||||
} else if (_kissDataType == DataType.SIGNAL_REPORT) {
|
||||
} else if (_kissDataType == DataType.SIGNAL_REPORT || _kissDataType == DataType.TELEMETRY) {
|
||||
_kissCmdBuffer[_kissCmdBufferPos++] = b;
|
||||
}
|
||||
break;
|
||||
|
@ -393,6 +422,8 @@ public class Kiss implements Protocol {
|
|||
private void completeKissPacket() throws IOException {
|
||||
if (_transportOutputBufferPos > 0) {
|
||||
sendKissByte(KISS_FEND);
|
||||
//byte[] d = Arrays.copyOf(_transportOutputBuffer, _transportOutputBufferPos);
|
||||
//Log.i(TAG, DebugTools.bytesToHex(d));
|
||||
_transport.write(Arrays.copyOf(_transportOutputBuffer, _transportOutputBufferPos));
|
||||
_transportOutputBufferPos = 0;
|
||||
}
|
||||
|
|
|
@ -13,8 +13,8 @@ public interface Protocol {
|
|||
void initialize(Transport transport, Context context, ProtocolCallback protocolCallback) throws IOException;
|
||||
// audio
|
||||
int getPcmAudioRecordBufferSize();
|
||||
void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException;
|
||||
void sendCompressedAudio(String src, String dst, int codec, byte[] frame) throws IOException;
|
||||
void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException;
|
||||
void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException;
|
||||
// messaging
|
||||
void sendTextMessage(TextMessage textMessage) throws IOException;
|
||||
// data
|
||||
|
|
|
@ -6,16 +6,17 @@ import com.radio.codec2talkie.protocol.position.Position;
|
|||
public abstract class ProtocolCallback {
|
||||
// receive
|
||||
abstract protected void onReceivePosition(Position position);
|
||||
abstract protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame);
|
||||
abstract protected void onReceiveCompressedAudio(String src, String dst, int codec, byte[] frame);
|
||||
abstract protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame);
|
||||
abstract protected void onReceiveCompressedAudio(String src, String dst, byte[] frame);
|
||||
abstract protected void onReceiveTextMessage(TextMessage textMessage);
|
||||
abstract protected void onReceiveData(String src, String dst, String path, byte[] data);
|
||||
abstract protected void onReceiveSignalLevel(short rssi, short snr);
|
||||
abstract protected void onReceiveTelemetry(int batVoltage);
|
||||
abstract protected void onReceiveLog(String logData);
|
||||
|
||||
// transmit
|
||||
abstract protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame);
|
||||
abstract protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame);
|
||||
abstract protected void onTransmitPcmAudio(String src, String dst, short[] frame);
|
||||
abstract protected void onTransmitCompressedAudio(String src, String dst, byte[] frame);
|
||||
abstract protected void onTransmitTextMessage(TextMessage textMessage);
|
||||
abstract protected void onTransmitPosition(Position position);
|
||||
abstract protected void onTransmitData(String src, String dst, String path, byte[] data);
|
||||
|
|
|
@ -3,9 +3,9 @@ package com.radio.codec2talkie.protocol;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.settings.SettingsWrapper;
|
||||
|
||||
public class ProtocolFactory {
|
||||
|
@ -24,6 +24,7 @@ public class ProtocolFactory {
|
|||
_name = name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return _name;
|
||||
|
@ -58,7 +59,7 @@ public class ProtocolFactory {
|
|||
return protocolType;
|
||||
}
|
||||
|
||||
public static Protocol create(int codec2ModeId, Context context) {
|
||||
public static Protocol create(Context context) {
|
||||
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
|
@ -70,6 +71,8 @@ public class ProtocolFactory {
|
|||
boolean aprsEnabled = SettingsWrapper.isAprsEnabled(sharedPreferences);
|
||||
boolean aprsIsEnabled = SettingsWrapper.isAprsIsEnabled(sharedPreferences);
|
||||
boolean freedvEnabled = SettingsWrapper.isFreeDvSoundModemModulation(sharedPreferences);
|
||||
boolean codec2Enabled = SettingsWrapper.isCodec2Enabled(sharedPreferences);
|
||||
boolean isCustomPrefixEnabled = SettingsWrapper.isCustomPrefixEnabled(sharedPreferences);
|
||||
|
||||
// "root" protocol
|
||||
Protocol proto;
|
||||
|
@ -95,6 +98,9 @@ public class ProtocolFactory {
|
|||
break;
|
||||
}
|
||||
|
||||
if (isCustomPrefixEnabled) {
|
||||
proto = new CustomDataPrefix(proto, sharedPreferences);
|
||||
}
|
||||
if (scramblingEnabled) {
|
||||
proto = new Scrambler(proto, scramblingKey);
|
||||
}
|
||||
|
@ -102,12 +108,15 @@ public class ProtocolFactory {
|
|||
proto = new Ax25(proto);
|
||||
}
|
||||
if (!freedvEnabled) {
|
||||
if (recordingEnabled) {
|
||||
proto = new Recorder(proto, codec2ModeId);
|
||||
if (codec2Enabled) {
|
||||
if (recordingEnabled) {
|
||||
proto = new Recorder(proto, sharedPreferences);
|
||||
}
|
||||
proto = new AudioCodec2FrameAggregator(proto, sharedPreferences);
|
||||
proto = new AudioCodec2(proto, sharedPreferences);
|
||||
} else {
|
||||
proto = new AudioOpus(proto);
|
||||
}
|
||||
|
||||
proto = new AudioFrameAggregator(proto, codec2ModeId);
|
||||
proto = new AudioCodec2(proto, codec2ModeId);
|
||||
}
|
||||
|
||||
if (aprsEnabled) {
|
||||
|
|
|
@ -37,7 +37,7 @@ public class Raw implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
_transport.write(Arrays.copyOf(frame, frame.length));
|
||||
}
|
||||
|
||||
|
@ -47,7 +47,7 @@ public class Raw implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
Log.w(TAG, "sendPcmAudio() is not supported");
|
||||
}
|
||||
|
||||
|
@ -60,7 +60,8 @@ public class Raw implements Protocol {
|
|||
public boolean receive() throws IOException {
|
||||
int bytesRead = _transport.read(_rxDataBuffer);
|
||||
if (bytesRead > 0) {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(null, null, -1, Arrays.copyOf(_rxDataBuffer, bytesRead));
|
||||
// NOTE, default data is compressed audio, upper layer should distinguish
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(null, null, Arrays.copyOf(_rxDataBuffer, bytesRead));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
package com.radio.codec2talkie.protocol;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import com.radio.codec2talkie.MainActivity;
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.tools.AudioTools;
|
||||
import com.radio.codec2talkie.tools.StorageTools;
|
||||
import com.radio.codec2talkie.transport.Transport;
|
||||
|
||||
|
@ -31,16 +35,17 @@ public class Recorder implements Protocol {
|
|||
private Timer _fileRotationTimer;
|
||||
|
||||
final Protocol _childProtocol;
|
||||
final int _codec2ModeId;
|
||||
int _codec2ModeId;
|
||||
|
||||
private String _prevSrcCallsign;
|
||||
private String _prevDstCallsign;
|
||||
|
||||
private final SharedPreferences _sharedPreferences;
|
||||
private ProtocolCallback _parentProtocolCallback;
|
||||
|
||||
public Recorder(Protocol childProtocol, int codec2ModeId) {
|
||||
public Recorder(Protocol childProtocol, SharedPreferences sharedPreferences) {
|
||||
_childProtocol = childProtocol;
|
||||
_codec2ModeId = codec2ModeId;
|
||||
_sharedPreferences = sharedPreferences;
|
||||
|
||||
_prevSrcCallsign = null;
|
||||
_prevDstCallsign = null;
|
||||
|
@ -51,6 +56,9 @@ public class Recorder implements Protocol {
|
|||
_parentProtocolCallback = protocolCallback;
|
||||
_storage = StorageTools.getStorage(context);
|
||||
_childProtocol.initialize(transport, context, _protocolCallback);
|
||||
|
||||
String codec2ModeName = _sharedPreferences.getString(PreferenceKeys.CODEC2_MODE, context.getResources().getStringArray(R.array.codec2_modes)[0]);
|
||||
_codec2ModeId = AudioTools.extractCodec2ModeId(codec2ModeName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,10 +67,10 @@ public class Recorder implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] frame) throws IOException {
|
||||
rotateIfNewSrcOrDstCallsign(src, dst);
|
||||
writeToFile(src, dst, codec2Mode, frame);
|
||||
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame);
|
||||
writeToFile(src, dst, frame);
|
||||
_childProtocol.sendCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -71,8 +79,8 @@ public class Recorder implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, codec, pcmFrame);
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -92,15 +100,15 @@ public class Recorder implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame);
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrames) {
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] audioFrames) {
|
||||
rotateIfNewSrcOrDstCallsign(src, dst);
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrames);
|
||||
writeToFile(src, dst, codec2Mode, audioFrames);
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrames);
|
||||
writeToFile(src, dst, audioFrames);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -118,19 +126,24 @@ public class Recorder implements Protocol {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame);
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -179,9 +192,9 @@ public class Recorder implements Protocol {
|
|||
_childProtocol.close();
|
||||
}
|
||||
|
||||
private void writeToFile(String src, String dst, int codec2Mode, byte[] rawData) {
|
||||
private void writeToFile(String src, String dst, byte[] rawData) {
|
||||
stopRotationTimer();
|
||||
createStreamIfNotExists(src, dst, codec2Mode);
|
||||
createStreamIfNotExists(src, dst);
|
||||
writeToStream(rawData);
|
||||
startRotationTimer();
|
||||
}
|
||||
|
@ -196,7 +209,7 @@ public class Recorder implements Protocol {
|
|||
}
|
||||
}
|
||||
|
||||
private void createStreamIfNotExists(String src, String dst, int codec2Mode) {
|
||||
private void createStreamIfNotExists(String src, String dst) {
|
||||
if (_activeStream == null) {
|
||||
try {
|
||||
Date date = new Date();
|
||||
|
@ -204,7 +217,7 @@ public class Recorder implements Protocol {
|
|||
if (!newDirectory.exists() && !newDirectory.mkdirs()) {
|
||||
Log.e(TAG, "Failed to create directory for voicemails");
|
||||
}
|
||||
File newAudioFile = new File(newDirectory, getNewFileName(date, src, dst, codec2Mode));
|
||||
File newAudioFile = new File(newDirectory, getNewFileName(date, src, dst));
|
||||
_activeStream = new FileOutputStream(newAudioFile);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
|
@ -217,13 +230,9 @@ public class Recorder implements Protocol {
|
|||
return df.format(date);
|
||||
}
|
||||
|
||||
private String getNewFileName(Date date, String src, String dst, int codec2Mode) {
|
||||
int mode = codec2Mode;
|
||||
if (mode == -1) {
|
||||
mode = _codec2ModeId;
|
||||
}
|
||||
private String getNewFileName(Date date, String src, String dst) {
|
||||
SimpleDateFormat tf = new SimpleDateFormat("HHmmss", Locale.ENGLISH);
|
||||
String codec2mode = String.format(Locale.ENGLISH, "%02d", mode);
|
||||
String codec2mode = String.format(Locale.ENGLISH, "%02d", _codec2ModeId);
|
||||
String fileName = codec2mode + "_" + tf.format(date);
|
||||
if (src != null && dst != null) {
|
||||
fileName += "_" + src + "_" + dst;
|
||||
|
|
|
@ -51,7 +51,7 @@ public class Scrambler implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrame) throws IOException {
|
||||
public void sendCompressedAudio(String src, String dst, byte[] audioFrame) throws IOException {
|
||||
byte[] result = scramble(audioFrame);
|
||||
if (result == null) {
|
||||
_parentProtocolCallback.onProtocolTxError();
|
||||
|
@ -66,8 +66,8 @@ public class Scrambler implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void sendPcmAudio(String src, String dst, int codec, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, codec, pcmFrame);
|
||||
public void sendPcmAudio(String src, String dst, short[] pcmFrame) throws IOException {
|
||||
_childProtocol.sendPcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -92,18 +92,18 @@ public class Scrambler implements Protocol {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame);
|
||||
protected void onReceivePcmAudio(String src, String dst, short[] pcmFrame) {
|
||||
_parentProtocolCallback.onReceivePcmAudio(src, dst, pcmFrame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] scrambledFrame) {
|
||||
protected void onReceiveCompressedAudio(String src, String dst, byte[] scrambledFrame) {
|
||||
|
||||
byte[] audioFrames = unscramble(scrambledFrame);
|
||||
if (audioFrames == null) {
|
||||
_parentProtocolCallback.onProtocolRxError();
|
||||
} else {
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrames);
|
||||
_parentProtocolCallback.onReceiveCompressedAudio(src, dst, audioFrames);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,19 +127,24 @@ public class Scrambler implements Protocol {
|
|||
_parentProtocolCallback.onReceiveSignalLevel(rssi, snr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveTelemetry(int batVoltage) {
|
||||
_parentProtocolCallback.onReceiveTelemetry(batVoltage);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReceiveLog(String logData) {
|
||||
_parentProtocolCallback.onReceiveLog(logData);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitPcmAudio(String src, String dst, int codec, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame);
|
||||
protected void onTransmitPcmAudio(String src, String dst, short[] frame) {
|
||||
_parentProtocolCallback.onTransmitPcmAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, frame);
|
||||
protected void onTransmitCompressedAudio(String src, String dst, byte[] frame) {
|
||||
_parentProtocolCallback.onTransmitCompressedAudio(src, dst, frame);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -4,6 +4,8 @@ import com.radio.codec2talkie.protocol.message.TextMessage;
|
|||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
|
||||
public interface AprsData {
|
||||
boolean isPositionReport();
|
||||
boolean isTextMessage();
|
||||
void fromPosition(Position position);
|
||||
void fromTextMessage(TextMessage textMessage);
|
||||
Position toPosition();
|
||||
|
|
|
@ -16,6 +16,12 @@ public class AprsDataFactory {
|
|||
return new AprsDataPositionReport();
|
||||
case MESSAGE:
|
||||
return new AprsDataTextMessage();
|
||||
case OBJECT:
|
||||
return new AprsObject();
|
||||
case ITEM:
|
||||
return new AprsItem();
|
||||
case THIRD_PARTY:
|
||||
return new AprsThirdParty();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.radio.codec2talkie.tools.TextTools;
|
|||
import com.radio.codec2talkie.tools.UnitTools;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -22,6 +23,16 @@ public class AprsDataPositionReport implements AprsData {
|
|||
private byte[] _binary;
|
||||
private boolean _isValid;
|
||||
|
||||
@Override
|
||||
public boolean isPositionReport() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextMessage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromPosition(Position position) {
|
||||
_isValid = false;
|
||||
|
@ -143,7 +154,7 @@ public class AprsDataPositionReport implements AprsData {
|
|||
|
||||
byte[] tail = new byte[buffer.remaining()];
|
||||
buffer.get(tail);
|
||||
String strTail = new String(tail);
|
||||
String strTail = new String(tail, StandardCharsets.UTF_8);
|
||||
Pattern latLonPattern = Pattern.compile("^([\\\\/])(\\S{4})(\\S{4})(\\S)(.\\S)?(\\S)?(.*)$", Pattern.DOTALL);
|
||||
Matcher latLonMatcher = latLonPattern.matcher(strTail);
|
||||
if (!latLonMatcher.matches()) {
|
||||
|
@ -215,7 +226,7 @@ public class AprsDataPositionReport implements AprsData {
|
|||
// read latitude/symbol_table/longitude/symbol
|
||||
byte[] tail = new byte[buffer.remaining()];
|
||||
buffer.get(tail);
|
||||
String strTail = new String(tail);
|
||||
String strTail = new String(tail, StandardCharsets.UTF_8);
|
||||
Pattern latLonPattern = Pattern.compile(
|
||||
"^" +
|
||||
"(?:.*)?" + // optional timestamp
|
||||
|
@ -284,6 +295,31 @@ public class AprsDataPositionReport implements AprsData {
|
|||
_position.isAltitudeEnabled = false;
|
||||
_position.hasAltitude = false;
|
||||
}
|
||||
|
||||
// read PHG range
|
||||
Pattern phgPattern = Pattern.compile("^.*(PHG\\d{4}).*$", Pattern.DOTALL);
|
||||
Matcher phgMatcher = phgPattern.matcher(strTail);
|
||||
if (phgMatcher.matches()) {
|
||||
String phg = phgMatcher.group(1);
|
||||
if (phg != null) {
|
||||
strTail = strTail.replaceAll(phg, "");
|
||||
_position.directivityDeg = AprsTools.phgToDirectivityDegrees(phg);
|
||||
_position.rangeMiles = AprsTools.phgToRangeMiles(phg);
|
||||
}
|
||||
}
|
||||
|
||||
// read RNG range
|
||||
Pattern rngPattern = Pattern.compile("^.*(RNG\\d{4}).*$", Pattern.DOTALL);
|
||||
Matcher rngMatcher = rngPattern.matcher(strTail);
|
||||
if (rngMatcher.matches()) {
|
||||
String rng = rngMatcher.group(1);
|
||||
if (rng != null) {
|
||||
strTail = strTail.replaceAll(rng, "");
|
||||
_position.rangeMiles = Double.parseDouble(rng.substring(3));
|
||||
_position.directivityDeg = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// read comment until the end
|
||||
_position.comment = TextTools.stripNulls(strTail);
|
||||
return true;
|
||||
|
|
|
@ -7,6 +7,7 @@ import com.radio.codec2talkie.tools.TextTools;
|
|||
import com.radio.codec2talkie.tools.UnitTools;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
|
@ -62,6 +63,16 @@ public class AprsDataPositionReportMicE implements AprsData {
|
|||
put(0b001, "custom_6");
|
||||
}};
|
||||
|
||||
@Override
|
||||
public boolean isPositionReport() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextMessage() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromPosition(Position position) {
|
||||
_isValid = false;
|
||||
|
@ -222,9 +233,9 @@ public class AprsDataPositionReportMicE implements AprsData {
|
|||
if (infoData.length > 11 && infoData[11] == '}') {
|
||||
_position.hasAltitude = true;
|
||||
_position.altitudeMeters = ((infoData[8] - 33) * 91 * 91 + (infoData[9] - 33) * 91 + (infoData[10] - 33)) - 10000;
|
||||
_position.comment = TextTools.stripNulls(new String(Arrays.copyOfRange(infoData, 12, infoData.length)));
|
||||
_position.comment = TextTools.stripNulls(new String(Arrays.copyOfRange(infoData, 12, infoData.length), StandardCharsets.UTF_8));
|
||||
} else {
|
||||
_position.comment = TextTools.stripNulls(new String(Arrays.copyOfRange(infoData, 8, infoData.length)));
|
||||
_position.comment = TextTools.stripNulls(new String(Arrays.copyOfRange(infoData, 8, infoData.length), StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
_position.maidenHead = UnitTools.decimalToMaidenhead(_position.latitude, _position.longitude);
|
||||
|
|
|
@ -4,6 +4,10 @@ import com.radio.codec2talkie.protocol.message.TextMessage;
|
|||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AprsDataTextMessage implements AprsData {
|
||||
|
||||
|
@ -11,9 +15,20 @@ public class AprsDataTextMessage implements AprsData {
|
|||
public String dstCallsign;
|
||||
public String digipath;
|
||||
public String textMessage;
|
||||
public Integer ackId;
|
||||
|
||||
private boolean _isValid;
|
||||
|
||||
@Override
|
||||
public boolean isPositionReport() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextMessage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromPosition(Position position) {
|
||||
_isValid = false;
|
||||
|
@ -24,6 +39,7 @@ public class AprsDataTextMessage implements AprsData {
|
|||
this.dstCallsign = textMessage.dst;
|
||||
this.textMessage = textMessage.text;
|
||||
this.digipath = textMessage.digipath;
|
||||
this.ackId = textMessage.ackId;
|
||||
_isValid = true;
|
||||
}
|
||||
|
||||
|
@ -39,6 +55,7 @@ public class AprsDataTextMessage implements AprsData {
|
|||
textMessage.dst = this.dstCallsign;
|
||||
textMessage.digipath = this.digipath;
|
||||
textMessage.text = this.textMessage;
|
||||
textMessage.ackId = this.ackId;
|
||||
return textMessage;
|
||||
}
|
||||
|
||||
|
@ -49,28 +66,66 @@ public class AprsDataTextMessage implements AprsData {
|
|||
this.digipath = digipath;
|
||||
this.srcCallsign = srcCallsign;
|
||||
ByteBuffer buffer = ByteBuffer.wrap(infoData);
|
||||
|
||||
// callsign, trim ending spaces
|
||||
byte[] callsign = new byte[9];
|
||||
buffer.get(callsign);
|
||||
this.dstCallsign = new String(callsign).replaceAll("\\s+$", "");
|
||||
|
||||
// ':' separator
|
||||
byte b = buffer.get();
|
||||
if (b != ':') return;
|
||||
|
||||
// message
|
||||
byte[] message = new byte[buffer.remaining()];
|
||||
buffer.get(message);
|
||||
textMessage = new String(message);
|
||||
// TODO, message id: {xxxxx
|
||||
_isValid = true;
|
||||
String stringMessage = new String(message, StandardCharsets.UTF_8);
|
||||
|
||||
// ack/rej message
|
||||
this.ackId = 0;
|
||||
Pattern p = Pattern.compile("^(ack|rej)(\n+){1,5}$", Pattern.DOTALL);
|
||||
Matcher m = p.matcher(stringMessage);
|
||||
if (m.find()) {
|
||||
String type = m.group(1);
|
||||
if (type != null) {
|
||||
String ackIdStr = m.group(2);
|
||||
if (ackIdStr != null)
|
||||
this.ackId = Integer.parseInt(ackIdStr);
|
||||
}
|
||||
} else {
|
||||
// message requires acknowledge {xxxxx (for auto ack)
|
||||
p = Pattern.compile("^.+[{](\\d+){1,5}$", Pattern.DOTALL);
|
||||
m = p.matcher(stringMessage);
|
||||
if (m.find()) {
|
||||
this.textMessage = m.group(1);
|
||||
String ackNumStr = m.group(2);
|
||||
if (ackNumStr != null)
|
||||
this.ackId = Integer.parseInt(ackNumStr);
|
||||
} else {
|
||||
this.textMessage = stringMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO, telemetry, make subclass from message, extend and extract values
|
||||
if (this.textMessage != null)
|
||||
_isValid = !isTelemetry(this.textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary() {
|
||||
return String.format(":%-9s:%s", dstCallsign, textMessage).getBytes();
|
||||
return (ackId > 0)
|
||||
? String.format(Locale.US, ":%-9s:%s{%d", dstCallsign, textMessage, ackId).getBytes()
|
||||
: String.format(":%-9s:%s", dstCallsign, textMessage).getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
return _isValid;
|
||||
}
|
||||
|
||||
private boolean isTelemetry(String textMessage) {
|
||||
Pattern p = Pattern.compile("^(EQNS|PARM|UNIT|BITS)[.].+$", Pattern.DOTALL);
|
||||
Matcher m = p.matcher(textMessage);
|
||||
return m.matches();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ public class AprsDataType {
|
|||
POSITION_WITH_TIMESTAMP_NO_MSG,
|
||||
POSITION_WITHOUT_TIMESTAMP_NO_MSG,
|
||||
OBJECT,
|
||||
STATUS
|
||||
ITEM,
|
||||
STATUS,
|
||||
THIRD_PARTY
|
||||
}
|
||||
|
||||
private final DataType _dataType;
|
||||
|
@ -40,7 +42,12 @@ public class AprsDataType {
|
|||
_dataType == DataType.POSITION_WITH_TIMESTAMP_MSG ||
|
||||
_dataType == DataType.POSITION_WITHOUT_TIMESTAMP_MSG ||
|
||||
_dataType == DataType.POSITION_WITH_TIMESTAMP_NO_MSG ||
|
||||
_dataType == DataType.POSITION_WITHOUT_TIMESTAMP_NO_MSG);
|
||||
_dataType == DataType.POSITION_WITHOUT_TIMESTAMP_NO_MSG ||
|
||||
_dataType == DataType.OBJECT);
|
||||
}
|
||||
|
||||
public boolean isObject() {
|
||||
return _dataType == DataType.OBJECT;
|
||||
}
|
||||
|
||||
public boolean isTextMessage() {
|
||||
|
@ -62,8 +69,12 @@ public class AprsDataType {
|
|||
return DataType.POSITION_WITH_TIMESTAMP_NO_MSG;
|
||||
} else if (ident == ';') {
|
||||
return DataType.OBJECT;
|
||||
} else if (ident == ')') {
|
||||
return DataType.ITEM;
|
||||
} else if (ident == '>') {
|
||||
return DataType.STATUS;
|
||||
} else if (ident == '}') {
|
||||
return DataType.THIRD_PARTY;
|
||||
} else {
|
||||
return DataType.UNKNOWN;
|
||||
}
|
||||
|
@ -84,8 +95,12 @@ public class AprsDataType {
|
|||
return '/';
|
||||
} else if (dataType == DataType.OBJECT) {
|
||||
return ';';
|
||||
} else if (dataType == DataType.ITEM) {
|
||||
return ')';
|
||||
} else if (dataType == DataType.STATUS) {
|
||||
return '>';
|
||||
} else if (dataType == DataType.THIRD_PARTY) {
|
||||
return '}';
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package com.radio.codec2talkie.protocol.aprs;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AprsItem extends AprsDataPositionReport {
|
||||
private static final String TAG = AprsItem.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void fromBinary(String srcCallsign, String dstCallsign, String digipath, byte[] infoData) {
|
||||
String info = new String(infoData);
|
||||
|
||||
Pattern itemPattern = Pattern.compile("^([^_!]{3,9})(!)(.+)$", Pattern.DOTALL);
|
||||
Matcher itemMatcher = itemPattern.matcher(info);
|
||||
if (!itemMatcher.matches()) return;
|
||||
|
||||
String posSrcCallsign = itemMatcher.group(1);
|
||||
if (posSrcCallsign == null) return;
|
||||
String itemState = itemMatcher.group(2);
|
||||
if (itemState == null) return;
|
||||
String positionInfoData = itemMatcher.group(3);
|
||||
if (positionInfoData == null) return;
|
||||
|
||||
super.fromBinary(posSrcCallsign, dstCallsign, digipath, positionInfoData.getBytes());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.radio.codec2talkie.protocol.aprs;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class AprsObject extends AprsDataPositionReport {
|
||||
private static final String TAG = AprsObject.class.getSimpleName();
|
||||
|
||||
@Override
|
||||
public void fromBinary(String srcCallsign, String dstCallsign, String digipath, byte[] infoData) {
|
||||
if (infoData.length <= 9) return;
|
||||
ByteBuffer buffer = ByteBuffer.wrap(infoData);
|
||||
|
||||
// callsign
|
||||
byte[] objectCallsign = new byte[9];
|
||||
buffer.get(objectCallsign, 0, 9);
|
||||
|
||||
// process only live objects
|
||||
byte isLive = buffer.get();
|
||||
if (isLive != '*') return;
|
||||
|
||||
byte[] positionInfoData = new byte[buffer.remaining()];
|
||||
buffer.get(positionInfoData);
|
||||
|
||||
String positionSrcCallsign = new String(objectCallsign).trim();
|
||||
|
||||
super.fromBinary(positionSrcCallsign, dstCallsign, digipath, positionInfoData);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package com.radio.codec2talkie.protocol.aprs;
|
||||
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsIsData;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
|
||||
public class AprsThirdParty implements AprsData {
|
||||
|
||||
private AprsData _aprsData;
|
||||
|
||||
@Override
|
||||
public boolean isPositionReport() {
|
||||
return _aprsData.isPositionReport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTextMessage() {
|
||||
return _aprsData.isTextMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromPosition(Position position) {
|
||||
_aprsData.fromPosition(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromTextMessage(TextMessage textMessage) {
|
||||
_aprsData.fromTextMessage(textMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Position toPosition() {
|
||||
return _aprsData.toPosition();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextMessage toTextMessage() {
|
||||
return _aprsData.toTextMessage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fromBinary(String srcCallsign, String dstCallsign, String digipath, byte[] infoData) {
|
||||
AprsIsData data = AprsIsData.fromString(new String(infoData));
|
||||
if (data == null) return;
|
||||
AprsDataType aprsDataType = new AprsDataType((char)data.data.charAt(0));
|
||||
_aprsData = AprsDataFactory.create(aprsDataType);
|
||||
if (_aprsData == null) return;
|
||||
_aprsData.fromBinary(data.src, data.dst, data.rawDigipath, data.data.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] toBinary() {
|
||||
return _aprsData.toBinary();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid() {
|
||||
if (_aprsData == null) return false;
|
||||
return _aprsData.isValid();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.radio.codec2talkie.protocol.aprs.tools;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class AprsHeardList {
|
||||
|
||||
private static class AprsHeardListItem {
|
||||
public long timestamp;
|
||||
public String callsign;
|
||||
|
||||
public AprsHeardListItem(long timestamp, String callsign) {
|
||||
this.timestamp = timestamp;
|
||||
this.callsign = callsign;
|
||||
}
|
||||
}
|
||||
|
||||
private final int _keepSeconds;
|
||||
private final TreeMap<String, AprsHeardListItem> _data = new TreeMap<>();
|
||||
|
||||
public AprsHeardList(int keepSeconds) {
|
||||
_keepSeconds = keepSeconds;
|
||||
}
|
||||
|
||||
public void add(String callsign) {
|
||||
synchronized (_data) {
|
||||
AprsHeardListItem heardItem = _data.get(callsign);
|
||||
if (heardItem == null) {
|
||||
AprsHeardListItem newHeardItem = new AprsHeardListItem(System.currentTimeMillis(), callsign);
|
||||
_data.put(callsign, newHeardItem);
|
||||
} else {
|
||||
heardItem.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains(String callsign) {
|
||||
synchronized (_data) {
|
||||
cleanup();
|
||||
return _data.containsKey(callsign);
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
long removeOlderThan = System.currentTimeMillis() - _keepSeconds * 1000L;
|
||||
for (Map.Entry<String, AprsHeardListItem> entryElement : _data.entrySet()) {
|
||||
if (entryElement.getValue().timestamp < removeOlderThan) {
|
||||
_data.remove(entryElement.getKey());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package com.radio.codec2talkie.protocol.aprs.tools;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import kotlin.text.Regex;
|
||||
|
@ -10,8 +8,10 @@ import kotlin.text.RegexOption;
|
|||
public class AprsIsData {
|
||||
public String src;
|
||||
public String dst;
|
||||
public String path;
|
||||
public String digipath;
|
||||
public String rawDigipath;
|
||||
public String data;
|
||||
public AprsIsData thirdParty;
|
||||
|
||||
public AprsIsData() {
|
||||
}
|
||||
|
@ -19,21 +19,51 @@ public class AprsIsData {
|
|||
public AprsIsData(String src, String dst, String path, String data) {
|
||||
this.src = src;
|
||||
this.dst = dst;
|
||||
this.path = path;
|
||||
this.digipath = path;
|
||||
this.data = data;
|
||||
// handle third party packet
|
||||
if (data.length() > 10 && data.startsWith("}")) {
|
||||
thirdParty = AprsIsData.fromString(data.substring(1));
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasThirdParty() {
|
||||
return thirdParty != null;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String toString() {
|
||||
public String convertToString(boolean useRawPath) {
|
||||
String result = src + ">";
|
||||
if (dst != null && dst.length() > 0)
|
||||
result += dst;
|
||||
if (path != null && path.length() > 0)
|
||||
result += "," + path;
|
||||
if (useRawPath && rawDigipath != null && rawDigipath.length() > 0)
|
||||
result += "," + rawDigipath;
|
||||
else if (digipath != null && digipath.length() > 0)
|
||||
result += "," + digipath;
|
||||
result += ":" + data;
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean isEligibleForRxGate() {
|
||||
boolean hasNoGate = rawDigipath.contains("TCPIP") ||
|
||||
rawDigipath.contains("TCPXX") ||
|
||||
rawDigipath.contains("NOGATE") ||
|
||||
rawDigipath.contains("RFONLY");
|
||||
|
||||
boolean thirdPartyHasNoGate = thirdParty != null &&
|
||||
(thirdParty.rawDigipath.contains("TCPIP") ||
|
||||
thirdParty.rawDigipath.contains("TCPXX"));
|
||||
|
||||
// do not gate TCPIP/NOGATE, queries and third party tcp ip packets
|
||||
return !hasNoGate && !data.startsWith("?") && !thirdPartyHasNoGate;
|
||||
}
|
||||
|
||||
public boolean isEligibleForTxGate() {
|
||||
return !(rawDigipath.contains("TCPXX") ||
|
||||
rawDigipath.contains("NOGATE") ||
|
||||
rawDigipath.contains("RFONLY"));
|
||||
}
|
||||
|
||||
public static AprsIsData fromString(String textData) {
|
||||
AprsIsData aprsIsData = new AprsIsData();
|
||||
// N0CALL>PATH:DATA
|
||||
|
@ -47,8 +77,12 @@ public class AprsIsData {
|
|||
String[] path = digipathData[0].split(",");
|
||||
if (path.length == 0) return null;
|
||||
aprsIsData.dst = path[0];
|
||||
aprsIsData.path = joinTail(path, ",", "^WIDE.+$");
|
||||
aprsIsData.digipath = joinTail(path, ",", "^WIDE.+$");
|
||||
aprsIsData.rawDigipath = joinTail(path, ",", ".*");
|
||||
aprsIsData.data = joinTail(digipathData, ":", ".*");
|
||||
if (aprsIsData.data.length() > 10 && aprsIsData.data.startsWith("}")) {
|
||||
aprsIsData.thirdParty = AprsIsData.fromString(aprsIsData.data.substring(1));
|
||||
}
|
||||
return aprsIsData;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.graphics.Bitmap;
|
|||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.media.audiofx.DynamicsProcessing;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.widget.ImageView;
|
||||
|
||||
|
@ -12,6 +13,8 @@ import com.radio.codec2talkie.R;
|
|||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class AprsSymbolTable {
|
||||
|
@ -24,11 +27,11 @@ public class AprsSymbolTable {
|
|||
private final ArrayList<Bitmap> _secondaryTableIconsLarge;
|
||||
private final ArrayList<Bitmap> _overlayTableIconsLarge;
|
||||
|
||||
private final int _cellWidth = 24;
|
||||
private final int _cellHeight = 24;
|
||||
private static final int _cellWidth = 24;
|
||||
private static final int _cellHeight = 24;
|
||||
|
||||
private final int _cellWidthLarge = 64;
|
||||
private final int _cellHeightLarge = 64;
|
||||
private static final int _cellWidthLarge = 64;
|
||||
private static final int _cellHeightLarge = 64;
|
||||
|
||||
private static final int _cntWidth = 16;
|
||||
private static final int _cntHeight = 6;
|
||||
|
@ -39,6 +42,10 @@ public class AprsSymbolTable {
|
|||
|
||||
private static AprsSymbolTable _symbolTable;
|
||||
|
||||
private static final List<String> _symbolsToRotate = Arrays.asList("/'", "/(", "/*", "/<", "/=",
|
||||
"/C", "/F", "/P", "/U", "/X", "/Y", "/[", "/^", "/a", "/b", "/e", "/f", "/g", "/j",
|
||||
"/k", "/p", "/s", "/u", "/v", "/>", "\\k", "\\u", "\\v", "\\>");
|
||||
|
||||
public static AprsSymbolTable getInstance(Context context) {
|
||||
if (_symbolTable == null) {
|
||||
synchronized (AprsSymbolTable.class) {
|
||||
|
@ -89,17 +96,22 @@ public class AprsSymbolTable {
|
|||
|
||||
if (symbolIconIndex < 0 || symbolIconIndex >= (_cntWidth * _cntHeight)) return null;
|
||||
|
||||
if (table == '/') {
|
||||
return _primaryTable.get(symbolIconIndex);
|
||||
} else if (table == '\\') {
|
||||
return _secondaryTable.get(symbolIconIndex);
|
||||
try {
|
||||
if (table == '/') {
|
||||
return _primaryTable.get(symbolIconIndex);
|
||||
} else if (table == '\\') {
|
||||
return _secondaryTable.get(symbolIconIndex);
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (overlayIconIndex < 0 || overlayIconIndex >= (_cntWidth * _cntHeight)) return null;
|
||||
|
||||
Bitmap icon = _secondaryTable.get(symbolIconIndex);
|
||||
Bitmap overlayIcon = _overlayTable.get(overlayIconIndex);
|
||||
Bitmap bmOverlay = Bitmap.createBitmap(icon.getWidth(), icon.getHeight(), null);
|
||||
Bitmap bmOverlay = Bitmap.createBitmap(icon.getWidth(), icon.getHeight(), Bitmap.Config.ARGB_8888);
|
||||
bmOverlay.setDensity(DisplayMetrics.DENSITY_DEFAULT);
|
||||
|
||||
Canvas canvas = new Canvas(bmOverlay);
|
||||
|
@ -154,7 +166,7 @@ public class AprsSymbolTable {
|
|||
ArrayList<Bitmap> secondaryTableIcons = Load(imageViewSecondary, _selectorIconDim, _selectorIconDim, _cntWidth, _cntHeight);
|
||||
primaryTableIcons.addAll(secondaryTableIcons);
|
||||
|
||||
Bitmap bmOverlay = Bitmap.createBitmap(_selectorIconDim*cntX, _selectorIconDim*cntY*2, null);
|
||||
Bitmap bmOverlay = Bitmap.createBitmap(_selectorIconDim*cntX, _selectorIconDim*cntY*2, Bitmap.Config.ARGB_8888);
|
||||
bmOverlay.setDensity(DisplayMetrics.DENSITY_DEFAULT);
|
||||
Canvas canvas = new Canvas(bmOverlay);
|
||||
Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||
|
@ -169,4 +181,8 @@ public class AprsSymbolTable {
|
|||
}
|
||||
return bmOverlay;
|
||||
}
|
||||
|
||||
public static boolean needsRotation(String symbol) {
|
||||
return _symbolsToRotate.contains(symbol);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,4 +11,20 @@ public class AprsTools {
|
|||
}
|
||||
return new String(buffer);
|
||||
}
|
||||
|
||||
public static int phgToDirectivityDegrees(String phg) {
|
||||
int d = phg.charAt(6) - '0';
|
||||
if (d > 8) return 0;
|
||||
return 45*d;
|
||||
}
|
||||
|
||||
public static double phgToRangeMiles(String phg) {
|
||||
int p = phg.charAt(3) - '0';
|
||||
double power = p*p;
|
||||
int h = phg.charAt(4) - '0';
|
||||
double haat = 10.0 * Math.pow(2.0, h);
|
||||
int g = phg.charAt(5) - '0';
|
||||
double gain = Math.pow(10.0, g / 10.0);
|
||||
return Math.sqrt(2.0 * haat * Math.sqrt((power / 10.0) * (gain / 2.0)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,13 @@ public class AX25Callsign {
|
|||
public boolean isValid;
|
||||
public boolean isLast = false;
|
||||
|
||||
public AX25Callsign() {}
|
||||
|
||||
public AX25Callsign(String callsign, String ssid) {
|
||||
this.callsign = callsign;
|
||||
this.ssid = Integer.parseInt(ssid);
|
||||
}
|
||||
|
||||
public static String formatCallsign(String callsign, String ssid) {
|
||||
return String.format("%s-%s", callsign, ssid);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,14 @@ package com.radio.codec2talkie.protocol.ax25;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsIsData;
|
||||
import com.radio.codec2talkie.tools.DebugTools;
|
||||
import com.radio.codec2talkie.tools.TextTools;
|
||||
|
||||
import java.nio.BufferUnderflowException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class AX25Packet {
|
||||
|
||||
|
@ -16,7 +19,6 @@ public class AX25Packet {
|
|||
public String src;
|
||||
public String dst;
|
||||
public String digipath;
|
||||
public int codec2Mode;
|
||||
public boolean isAudio;
|
||||
public byte[] rawData;
|
||||
public boolean isValid;
|
||||
|
@ -28,6 +30,21 @@ public class AX25Packet {
|
|||
public void fromBinary(byte[] data) {
|
||||
isValid = false;
|
||||
if (data == null) return;
|
||||
// lora text packet with 0x3c,0xff,0x01 prefix
|
||||
if (data.length > 3 && data[0] == (byte)0x3c && data[1] == (byte)0xff && data[2] == (byte)0x01) {
|
||||
String rawText = new String(Arrays.copyOfRange(data, 3, data.length), StandardCharsets.US_ASCII);
|
||||
AprsIsData textPacket = AprsIsData.fromString(rawText);
|
||||
if (textPacket != null) {
|
||||
src = textPacket.src;
|
||||
dst = textPacket.dst;
|
||||
digipath = textPacket.rawDigipath;
|
||||
rawData = textPacket.data.getBytes(StandardCharsets.US_ASCII);
|
||||
isAudio = false;
|
||||
isValid = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// binary packet
|
||||
ByteBuffer buffer = ByteBuffer.wrap(data);
|
||||
try {
|
||||
// dst
|
||||
|
@ -66,7 +83,6 @@ public class AX25Packet {
|
|||
byte ax25Pid = buffer.get();
|
||||
if (ax25Pid == AX25PID_AUDIO) {
|
||||
isAudio = true;
|
||||
codec2Mode = buffer.get();
|
||||
} else if (ax25Pid == AX25PID_NO_LAYER3) {
|
||||
isAudio = false;
|
||||
} else {
|
||||
|
@ -81,6 +97,18 @@ public class AX25Packet {
|
|||
}
|
||||
}
|
||||
|
||||
public byte[] toTextBinary() {
|
||||
byte[] packetContent = toString().getBytes(StandardCharsets.US_ASCII);
|
||||
// lora aprs prefix 0x3c,0xff,0x01
|
||||
ByteBuffer textPacketBuffer = ByteBuffer.allocateDirect(packetContent.length + 3);
|
||||
textPacketBuffer.put((byte)0x3c).put((byte)0xff).put((byte)0x01);
|
||||
textPacketBuffer.put(packetContent);
|
||||
textPacketBuffer.flip();
|
||||
byte[] ax25Frame = new byte[textPacketBuffer.remaining()];
|
||||
textPacketBuffer.get(ax25Frame);
|
||||
return ax25Frame;
|
||||
}
|
||||
|
||||
public byte[] toBinary() {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(MaximumSize);
|
||||
String[] rptCallsigns = new String[] {};
|
||||
|
@ -113,7 +141,6 @@ public class AX25Packet {
|
|||
buffer.put(AX25CTRL_UI);
|
||||
if (isAudio) {
|
||||
buffer.put(AX25PID_AUDIO);
|
||||
buffer.put((byte)codec2Mode);
|
||||
} else {
|
||||
buffer.put(AX25PID_NO_LAYER3);
|
||||
}
|
||||
|
|
|
@ -2,20 +2,41 @@ package com.radio.codec2talkie.protocol.message;
|
|||
|
||||
import com.radio.codec2talkie.storage.message.MessageItem;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class TextMessage {
|
||||
public String src;
|
||||
public String dst;
|
||||
public String digipath;
|
||||
public String text;
|
||||
public Integer ackId;
|
||||
|
||||
public MessageItem toMessageItem(boolean isTransmit) {
|
||||
MessageItem messageItem = new MessageItem();
|
||||
messageItem.setTimestampEpoch(System.currentTimeMillis());
|
||||
messageItem.setNeedsAck(false); // TODO
|
||||
messageItem.setIsTransmit(isTransmit);
|
||||
messageItem.setSrcCallsign(this.src);
|
||||
messageItem.setDstCallsign(this.dst);
|
||||
messageItem.setMessage(this.text);
|
||||
messageItem.setAckId(this.ackId);
|
||||
messageItem.setIsAcknowledged(false);
|
||||
messageItem.setRetryCnt(0);
|
||||
return messageItem;
|
||||
}
|
||||
|
||||
public boolean isAck() {
|
||||
return this.text != null &&
|
||||
this.text.toLowerCase(Locale.ROOT).equals("ack") &&
|
||||
this.ackId > 0;
|
||||
}
|
||||
|
||||
public boolean isRej() {
|
||||
return this.text != null &&
|
||||
this.text.toLowerCase(Locale.ROOT).equals("rej") &&
|
||||
this.ackId > 0;
|
||||
}
|
||||
|
||||
public boolean isAutoReply() {
|
||||
return isAck() || isRej();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,9 +3,13 @@ package com.radio.codec2talkie.protocol.position;
|
|||
import android.location.Location;
|
||||
|
||||
import com.radio.codec2talkie.storage.position.PositionItem;
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
import com.radio.codec2talkie.tools.UnitTools;
|
||||
|
||||
public class Position {
|
||||
|
||||
public final static double DEFAULT_RANGE_MILES = 6.0;
|
||||
|
||||
public long timestampEpochMs;
|
||||
public String srcCallsign;
|
||||
public String dstCallsign;
|
||||
|
@ -26,6 +30,8 @@ public class Position {
|
|||
public boolean isAltitudeEnabled;
|
||||
public boolean hasBearing;
|
||||
public boolean hasAltitude;
|
||||
public double rangeMiles;
|
||||
public int directivityDeg;
|
||||
public boolean hasSpeed;
|
||||
|
||||
public static Position fromLocation(Location location) {
|
||||
|
@ -40,6 +46,8 @@ public class Position {
|
|||
position.hasAltitude = location.hasAltitude();
|
||||
position.hasSpeed = location.hasSpeed();
|
||||
position.maidenHead = UnitTools.decimalToMaidenhead(position.latitude, position.longitude);
|
||||
position.rangeMiles = 0.0;
|
||||
position.directivityDeg = 0; // 0 - omni
|
||||
return position;
|
||||
}
|
||||
|
||||
|
@ -92,6 +100,7 @@ public class Position {
|
|||
positionItem.setIsTransmit(isTransmit);
|
||||
positionItem.setSrcCallsign(srcCallsign);
|
||||
positionItem.setDstCallsign(dstCallsign);
|
||||
positionItem.setDigipath(digipath);
|
||||
positionItem.setLatitude(latitude);
|
||||
positionItem.setLongitude(longitude);
|
||||
positionItem.setMaidenHead(maidenHead);
|
||||
|
@ -102,6 +111,28 @@ public class Position {
|
|||
positionItem.setComment(comment);
|
||||
positionItem.setSymbolCode(symbolCode);
|
||||
positionItem.setPrivacyLevel(privacyLevel);
|
||||
positionItem.setDirectivityDeg(directivityDeg);
|
||||
positionItem.setRangeMiles(rangeMiles);
|
||||
return positionItem;
|
||||
}
|
||||
|
||||
public StationItem toStationItem() {
|
||||
StationItem stationItem = new StationItem(srcCallsign);
|
||||
stationItem.setTimestampEpoch(System.currentTimeMillis());
|
||||
stationItem.setDstCallsign(dstCallsign);
|
||||
stationItem.setDigipath(digipath);
|
||||
stationItem.setLatitude(latitude);
|
||||
stationItem.setLongitude(longitude);
|
||||
stationItem.setMaidenHead(maidenHead);
|
||||
stationItem.setAltitudeMeters(altitudeMeters);
|
||||
stationItem.setBearingDegrees(bearingDegrees);
|
||||
stationItem.setSpeedMetersPerSecond(speedMetersPerSecond);
|
||||
stationItem.setStatus(status);
|
||||
stationItem.setComment(comment);
|
||||
stationItem.setSymbolCode(symbolCode);
|
||||
stationItem.setPrivacyLevel(privacyLevel);
|
||||
stationItem.setDirectivityDeg(directivityDeg);
|
||||
stationItem.setRangeMiles(rangeMiles);
|
||||
return stationItem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import com.radio.codec2talkie.protocol.aprs.tools.AprsSymbolTable;
|
|||
|
||||
public class AprsSymbolSelectionActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = AprsSymbolSelectionActivity.class.getSimpleName();
|
||||
|
||||
private SharedPreferences _sharedPreferences;
|
||||
|
||||
private ImageView _currentSelectionView;
|
||||
|
@ -68,9 +70,12 @@ public class AprsSymbolSelectionActivity extends AppCompatActivity {
|
|||
float y = event.getY();
|
||||
_currentSymbolCode = AprsSymbolTable.getSymbolFromCoordinate(x, y, v.getWidth(), v.getHeight());
|
||||
_currentSelectionText.setText(_currentSymbolCode);
|
||||
Log.i("---", _currentSymbolCode);
|
||||
Log.i(TAG, "Selected symbol: " + _currentSymbolCode);
|
||||
Bitmap currentSymbolBitmap = AprsSymbolTable.getInstance(this).bitmapFromSymbol(_currentSymbolCode, true);
|
||||
_currentSelectionView.setImageBitmap(currentSymbolBitmap);
|
||||
if (currentSymbolBitmap == null)
|
||||
Log.e(TAG, "Cannot select symbol");
|
||||
else
|
||||
_currentSelectionView.setImageBitmap(currentSymbolBitmap);
|
||||
v.performClick();
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -9,7 +9,6 @@ public final class PreferenceKeys {
|
|||
public static String PORTS_USB_PARITY = "ports_usb_parity";
|
||||
public static String PORTS_USB_DTR = "ports_usb_dtr";
|
||||
public static String PORTS_USB_RTS = "ports_usb_rts";
|
||||
|
||||
public static String PORTS_BT_CLIENT_NAME = "ports_bt_client_name";
|
||||
|
||||
public static String PORTS_TCP_IP_ADDRESS = "ports_tcp_ip_address";
|
||||
|
@ -30,10 +29,19 @@ public final class PreferenceKeys {
|
|||
public static String PORTS_SOUND_MODEM_FREEDV_SQUELCH_SNR="ports_sound_modem_freedv_squelch_snr";
|
||||
public static String PORTS_SOUND_MODEM_FREEDV_DATA_MODE="ports_sound_modem_freedv_data_mode";
|
||||
|
||||
public static String CODEC2_MODE = "codec2_mode";
|
||||
public static String CUSTOM_PREFIX_ENABLED = "custom_prefix_enabled";
|
||||
public static String CUSTOM_PREFIX = "custom_prefix";
|
||||
|
||||
public static String CODEC_TYPE = "codec_type";
|
||||
public static String CODEC2_RECORDING_ENABLED = "codec2_recording_enabled";
|
||||
|
||||
public static String CODEC2_MODE = "codec2_mode";
|
||||
public static String CODEC2_TX_FRAME_MAX_SIZE = "codec2_tx_frame_max_size";
|
||||
|
||||
public static String OPUS_BIT_RATE = "opus_bit_rate";
|
||||
public static String OPUS_FRAME_SIZE = "opus_frame_size";
|
||||
public static String OPUS_COMPLEXITY = "opus_complexity";
|
||||
|
||||
public static String KISS_ENABLED = "kiss_enable";
|
||||
public static String KISS_BUFFERED_ENABLED = "kiss_buffered_enable";
|
||||
public static String KISS_PARROT = "kiss_parrot_enable";
|
||||
|
@ -47,13 +55,19 @@ public final class PreferenceKeys {
|
|||
public static String KISS_SCRAMBLER_ITERATIONS = "kiss_scrambler_iterations";
|
||||
|
||||
public static String KISS_EXTENSIONS_ENABLED = "kiss_extensions_enable";
|
||||
public static String KISS_EXTENSIONS_RADIO_MOD = "kiss_extension_radio_mod";
|
||||
public static String KISS_EXTENSIONS_RADIO_SPLIT_FREQ = "kiss_extension_radio_split_freq";
|
||||
public static String KISS_EXTENSIONS_RADIO_FREQUENCY = "kiss_extension_radio_frequency";
|
||||
public static String KISS_EXTENSIONS_RADIO_FREQUENCY_TX = "kiss_extension_radio_frequency_tx";
|
||||
public static String KISS_EXTENSIONS_RADIO_BANDWIDTH = "kiss_extension_radio_bandwidth";
|
||||
public static String KISS_EXTENSIONS_RADIO_POWER = "kiss_extension_radio_power";
|
||||
public static String KISS_EXTENSIONS_RADIO_SF = "kiss_extension_radio_sf";
|
||||
public static String KISS_EXTENSIONS_RADIO_CR = "kiss_extension_radio_cr";
|
||||
public static String KISS_EXTENSIONS_RADIO_SYNC = "kiss_extension_radio_sync";
|
||||
public static String KISS_EXTENSIONS_RADIO_CRC = "kiss_extension_radio_crc";
|
||||
public static String KISS_EXTENSIONS_RADIO_FSK_BIT_RATE = "kiss_extension_radio_fsk_bit_rate";
|
||||
public static String KISS_EXTENSIONS_RADIO_FSK_FREQ_DEV = "kiss_extension_radio_fsk_freq_dev";
|
||||
public static String KISS_EXTENSIONS_RADIO_FSK_RX_BW = "kiss_extension_radio_fsk_rx_bw";
|
||||
|
||||
public static String KISS_EXTENSIONS_ACTION_REBOOT_REQUESTED = "com.radio.codec2talkie.MODEM_REBOOT";
|
||||
|
||||
|
@ -67,6 +81,7 @@ public final class PreferenceKeys {
|
|||
public static String APP_AUDIO_DESTINATION = "app_audio_destination";
|
||||
|
||||
public static String AX25_VOAX25_ENABLE = "aprs_voax25_enable";
|
||||
public static String AX25_TEXT_PACKETS_ENABLE = "aprs_text_packets_enable";
|
||||
public static String AX25_CALLSIGN = "aprs_callsign";
|
||||
public static String AX25_SSID = "aprs_ssid";
|
||||
public static String AX25_DIGIPATH = "aprs_digipath";
|
||||
|
@ -100,6 +115,7 @@ public final class PreferenceKeys {
|
|||
public static String APRS_IS_ENABLE="aprs_is_enable";
|
||||
public static String APRS_IS_CODE = "aprs_is_code";
|
||||
public static String APRS_IS_TCPIP_SERVER = "aprs_is_tcpip_server";
|
||||
public static String APRS_IS_TCPIP_SERVER_PORT = "aprs_is_tcpip_server_port";
|
||||
public static String APRS_IS_ENABLE_RX_GATE = "aprs_is_enable_rx_gate";
|
||||
public static String APRS_IS_ENABLE_TX_GATE = "aprs_is_enable_tx_gate";
|
||||
public static String APRS_IS_ENABLE_SELF = "aprs_is_enable_self";
|
||||
|
|
|
@ -38,7 +38,9 @@ public class SettingsActivity extends AppCompatActivity
|
|||
"ports_tcp_ip_retry_count",
|
||||
"ports_tcp_ip_retry_delay",
|
||||
"ports_sound_modem_preamble",
|
||||
"ports_sound_modem_ptt_off_delay_ms"
|
||||
"ports_sound_modem_ptt_off_delay_ms",
|
||||
"aprs_is_tcpip_server_port",
|
||||
"opus_bit_rate"
|
||||
};
|
||||
|
||||
private static final String[] _signedDecimalSettings = {
|
||||
|
@ -115,6 +117,15 @@ public class SettingsActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
public static class SettingsTncExtendedFragment extends PreferenceFragmentCompat
|
||||
{
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.preferences_tnc_extended, null);
|
||||
setNumberInputType(getPreferenceManager());
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsTcpIpFragment extends PreferenceFragmentCompat
|
||||
{
|
||||
@Override
|
||||
|
@ -142,6 +153,15 @@ public class SettingsActivity extends AppCompatActivity
|
|||
}
|
||||
}
|
||||
|
||||
public static class SettingsCodecFragment extends PreferenceFragmentCompat
|
||||
{
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(R.xml.preferences_codec, null);
|
||||
setNumberInputType(getPreferenceManager());
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsAprsLocationFragment extends PreferenceFragmentCompat
|
||||
{
|
||||
@Override
|
||||
|
|
|
@ -40,6 +40,14 @@ public class SettingsWrapper {
|
|||
sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_TYPE, "1200").startsWith("F");
|
||||
}
|
||||
|
||||
public static boolean isCodec2Enabled(SharedPreferences sharedPreferences) {
|
||||
return sharedPreferences.getString(PreferenceKeys.CODEC_TYPE, "Codec2").equals("Codec2");
|
||||
}
|
||||
|
||||
public static boolean isCustomPrefixEnabled(SharedPreferences sharedPreferences) {
|
||||
return sharedPreferences.getBoolean(PreferenceKeys.CUSTOM_PREFIX_ENABLED, false);
|
||||
}
|
||||
|
||||
public static int getFreeDvSoundModemModulation(SharedPreferences sharedPreferences) {
|
||||
String modemType = sharedPreferences.getString(PreferenceKeys.PORTS_SOUND_MODEM_TYPE, "1200");
|
||||
if (modemType.startsWith("F")) {
|
||||
|
@ -93,6 +101,10 @@ public class SettingsWrapper {
|
|||
!isFreeDvSoundModemModulation(sharedPreferences); // no voax25 in freedv
|
||||
}
|
||||
|
||||
public static boolean isTextPacketsEnabled(SharedPreferences sharedPreferences) {
|
||||
return sharedPreferences.getBoolean(PreferenceKeys.AX25_TEXT_PACKETS_ENABLE, false);
|
||||
}
|
||||
|
||||
public static boolean isAprsIsEnabled(SharedPreferences sharedPreferences) {
|
||||
return sharedPreferences.getBoolean(PreferenceKeys.APRS_IS_ENABLE, false);
|
||||
}
|
||||
|
|
|
@ -13,13 +13,15 @@ import com.radio.codec2talkie.storage.message.MessageItem;
|
|||
import com.radio.codec2talkie.storage.message.MessageItemDao;
|
||||
import com.radio.codec2talkie.storage.position.PositionItem;
|
||||
import com.radio.codec2talkie.storage.position.PositionItemDao;
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
import com.radio.codec2talkie.storage.station.StationItemDao;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@androidx.room.Database(
|
||||
version = 4,
|
||||
entities = {LogItem.class, MessageItem.class, PositionItem.class},
|
||||
version = 13,
|
||||
entities = {LogItem.class, MessageItem.class, PositionItem.class, StationItem.class},
|
||||
exportSchema = false
|
||||
)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
@ -29,6 +31,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
|||
public abstract LogItemDao logItemDao();
|
||||
public abstract MessageItemDao messageItemDao();
|
||||
public abstract PositionItemDao positionItemDao();
|
||||
public abstract StationItemDao stationitemDao();
|
||||
|
||||
private static AppDatabase _db;
|
||||
private static ExecutorService _executor;
|
||||
|
|
|
@ -4,7 +4,9 @@ import androidx.room.Entity;
|
|||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
|
||||
@Entity(indices = {@Index(value = {"id", "srcCallsign"}, unique = true)})
|
||||
public class LogItem {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -45,4 +47,12 @@ public class LogItem {
|
|||
public void setLogLine(String logLine) { this.logLine = logLine; }
|
||||
|
||||
public void setIsTransmit(boolean isTransmit) { this.isTransmit = isTransmit; }
|
||||
|
||||
public StationItem toStationItem() {
|
||||
StationItem stationItem = new StationItem(srcCallsign);
|
||||
stationItem.setTimestampEpoch(System.currentTimeMillis());
|
||||
stationItem.setDstCallsign(stationItem.dstCallsign);
|
||||
stationItem.setLogLine(logLine);
|
||||
return stationItem;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package com.radio.codec2talkie.storage.log;
|
|||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
@ -18,19 +17,20 @@ import androidx.recyclerview.widget.DividerItemDecoration;
|
|||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.radio.codec2talkie.MainActivity;
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.storage.log.group.LogItemGroupAdapter;
|
||||
import com.radio.codec2talkie.storage.station.StationItemAdapter;
|
||||
import com.radio.codec2talkie.storage.position.PositionItemViewModel;
|
||||
import com.radio.codec2talkie.storage.station.StationItemViewModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class LogItemActivity extends AppCompatActivity {
|
||||
private static final String TAG = LogItemActivity.class.getSimpleName();
|
||||
|
||||
private String _groupName;
|
||||
private String _stationName;
|
||||
private LogItemViewModel _logItemViewModel;
|
||||
private PositionItemViewModel _positionItemViewModel;
|
||||
private StationItemViewModel _stationItemViewModel;
|
||||
|
||||
private LiveData<List<LogItem>> _logItemLiveData;
|
||||
|
||||
|
@ -38,66 +38,67 @@ public class LogItemActivity extends AppCompatActivity {
|
|||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_log_view);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
// get group name to decide if filtering should be enabled
|
||||
// get station name to decide if filtering should be enabled
|
||||
Bundle bundle = getIntent().getExtras();
|
||||
_groupName = null;
|
||||
_stationName = null;
|
||||
if (bundle != null) {
|
||||
_groupName = (String)bundle.get("groupName");
|
||||
_stationName = (String)bundle.get("stationName");
|
||||
}
|
||||
|
||||
// view models
|
||||
_logItemViewModel = new ViewModelProvider(this).get(LogItemViewModel.class);
|
||||
_positionItemViewModel = new ViewModelProvider(this).get(PositionItemViewModel.class);
|
||||
_stationItemViewModel = new ViewModelProvider(this).get(StationItemViewModel.class);
|
||||
|
||||
// log items
|
||||
RecyclerView logItemRecyclerView = findViewById(R.id.log_item_recyclerview);
|
||||
logItemRecyclerView.setHasFixedSize(true);
|
||||
|
||||
// log lines list adapter
|
||||
final LogItemAdapter adapter = new LogItemAdapter(new LogItemAdapter.LogItemDiff(), _groupName == null);
|
||||
final LogItemAdapter adapter = new LogItemAdapter(new LogItemAdapter.LogItemDiff(), _stationName == null);
|
||||
logItemRecyclerView.setAdapter(adapter);
|
||||
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
|
||||
linearLayoutManager.setReverseLayout(true);
|
||||
logItemRecyclerView.setLayoutManager(linearLayoutManager);
|
||||
logItemRecyclerView.addItemDecoration(new DividerItemDecoration(logItemRecyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||
|
||||
// log groups
|
||||
RecyclerView logItemGroupRecyclerView = findViewById(R.id.log_item_group_recyclerview);
|
||||
logItemGroupRecyclerView.setHasFixedSize(true);
|
||||
// stations
|
||||
RecyclerView stationsRecyclerView = findViewById(R.id.log_item_group_recyclerview);
|
||||
stationsRecyclerView.setHasFixedSize(true);
|
||||
|
||||
// groups adapter
|
||||
final LogItemGroupAdapter adapterGroup = new LogItemGroupAdapter(new LogItemGroupAdapter.LogItemGroupDiff());
|
||||
adapterGroup.setClickListener(v -> {
|
||||
// stations adapter
|
||||
final StationItemAdapter stationsAdapter = new StationItemAdapter(new StationItemAdapter.StationItemDiff());
|
||||
stationsAdapter.setClickListener(v -> {
|
||||
TextView itemView = v.findViewById(R.id.log_view_group_item_title);
|
||||
//_logItemLiveData.removeObserver(adapter::submitList);
|
||||
_logItemLiveData.removeObservers(this);
|
||||
_groupName = itemView.getText().toString();
|
||||
_logItemLiveData = _logItemViewModel.getData(_groupName);
|
||||
_stationName = itemView.getText().toString();
|
||||
_logItemLiveData = _logItemViewModel.getData(_stationName);
|
||||
_logItemLiveData.observe(this, adapter::submitList);
|
||||
setTitle(_groupName);
|
||||
setTitle(_stationName);
|
||||
});
|
||||
logItemGroupRecyclerView.setAdapter(adapterGroup);
|
||||
LinearLayoutManager linearLayoutManagerGroup = new LinearLayoutManager(this);
|
||||
logItemGroupRecyclerView.setLayoutManager(linearLayoutManagerGroup);
|
||||
logItemGroupRecyclerView.addItemDecoration(new DividerItemDecoration(logItemGroupRecyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||
stationsRecyclerView.setAdapter(stationsAdapter);
|
||||
LinearLayoutManager linearLayoutManagerStations = new LinearLayoutManager(this);
|
||||
stationsRecyclerView.setLayoutManager(linearLayoutManagerStations);
|
||||
stationsRecyclerView.addItemDecoration(new DividerItemDecoration(stationsRecyclerView.getContext(), DividerItemDecoration.VERTICAL));
|
||||
|
||||
_logItemViewModel.getGroups().observe(this, adapterGroup::submitList);
|
||||
_stationItemViewModel.getAllStationItems(false).observe(this, stationsAdapter::submitList);
|
||||
|
||||
// launch with filter if group name is provided
|
||||
if (_groupName == null) {
|
||||
logItemGroupRecyclerView.setVisibility(View.GONE);
|
||||
// launch with filter if station name is provided
|
||||
if (_stationName == null) {
|
||||
stationsRecyclerView.setVisibility(View.GONE);
|
||||
findViewById(R.id.log_item_textview).setVisibility(View.GONE);
|
||||
findViewById(R.id.log_item_group_textview).setVisibility(View.GONE);
|
||||
_logItemLiveData = _logItemViewModel.getAllData();
|
||||
_logItemLiveData.observe(this, adapter::submitList);
|
||||
setTitle(R.string.aprs_log_view_title);
|
||||
} else {
|
||||
_logItemLiveData = _logItemViewModel.getData(_groupName);
|
||||
_logItemLiveData = _logItemViewModel.getData(_stationName);
|
||||
_logItemLiveData.observe(this, adapter::submitList);
|
||||
setTitle(_groupName);
|
||||
setTitle(_stationName);
|
||||
}
|
||||
|
||||
// register live scroll
|
||||
|
@ -106,8 +107,9 @@ public class LogItemActivity extends AppCompatActivity {
|
|||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
int msgCount = adapter.getItemCount();
|
||||
int lastVisiblePosition = linearLayoutManager.findLastCompletelyVisibleItemPosition();
|
||||
if (lastVisiblePosition == RecyclerView.NO_POSITION || positionStart == msgCount - 1 && lastVisiblePosition == positionStart - 1) {
|
||||
logItemRecyclerView.scrollToPosition(positionStart);
|
||||
//Log.i(TAG, " " + positionStart + " " + itemCount + " " + lastVisiblePosition + " " + msgCount);
|
||||
if (lastVisiblePosition == RecyclerView.NO_POSITION || (positionStart == msgCount - itemCount && lastVisiblePosition == positionStart - 1)) {
|
||||
logItemRecyclerView.scrollToPosition(msgCount - 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -121,7 +123,7 @@ public class LogItemActivity extends AppCompatActivity {
|
|||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
if (_groupName != null) {
|
||||
if (_stationName != null) {
|
||||
menu.findItem(R.id.log_view_menu_stations).setVisible(false);
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
|
@ -135,35 +137,45 @@ public class LogItemActivity extends AppCompatActivity {
|
|||
if (itemId == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
else if (itemId == R.id.log_view_menu_clear) {
|
||||
deleteAll();
|
||||
} else if (itemId == R.id.log_view_menu_clear_all) {
|
||||
deleteLogItems(-1);
|
||||
return true;
|
||||
} else if (itemId == R.id.log_view_menu_clear_1h) {
|
||||
deleteLogItems(1);
|
||||
return true;
|
||||
} else if (itemId == R.id.log_view_menu_clear_12h) {
|
||||
deleteLogItems(12);
|
||||
return true;
|
||||
} else if (itemId == R.id.log_view_menu_clear_1d) {
|
||||
deleteLogItems(24);
|
||||
return true;
|
||||
} else if (itemId == R.id.log_view_menu_clear_7d) {
|
||||
deleteLogItems(24*7);
|
||||
return true;
|
||||
} else if (itemId == R.id.log_view_menu_stations) {
|
||||
Intent logItemIntent = new Intent(this, LogItemActivity.class);
|
||||
logItemIntent.putExtra("groupName", getString(R.string.log_view_station_history));
|
||||
logItemIntent.putExtra("stationName", getString(R.string.log_view_station_history));
|
||||
startActivity(logItemIntent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void deleteAll() {
|
||||
private void deleteLogItems(int hours) {
|
||||
DialogInterface.OnClickListener deleteAllDialogClickListener = (dialog, which) -> {
|
||||
if (which == DialogInterface.BUTTON_POSITIVE) {
|
||||
if (_groupName == null) {
|
||||
_logItemViewModel.deleteAllLogItems();
|
||||
_positionItemViewModel.deleteAllPositionItems();
|
||||
} else {
|
||||
_logItemViewModel.deleteLogItems(_groupName);
|
||||
_positionItemViewModel.deletePositionItems(_groupName);
|
||||
}
|
||||
_logItemViewModel.deleteLogItems(_stationName, hours);
|
||||
_positionItemViewModel.deletePositionItems(_stationName, hours);
|
||||
_stationItemViewModel.deleteStationItems(_stationName, hours);
|
||||
}
|
||||
};
|
||||
String alertMessage = getString(R.string.log_item_activity_delete_all_title);
|
||||
if (_groupName != null) {
|
||||
if (hours != -1) {
|
||||
alertMessage = String.format(getString(R.string.log_item_activity_delete_hours_title), hours);
|
||||
}
|
||||
if (_stationName != null) {
|
||||
alertMessage = getString(R.string.log_item_activity_delete_group_title);
|
||||
alertMessage = String.format(alertMessage, _groupName);
|
||||
alertMessage = String.format(alertMessage, _stationName);
|
||||
}
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setMessage(alertMessage)
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.room.Insert;
|
|||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
|
||||
import com.radio.codec2talkie.storage.log.group.LogItemGroup;
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -16,25 +16,6 @@ public interface LogItemDao {
|
|||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
void insertLogItem(LogItem logItem);
|
||||
|
||||
@Query("SELECT pos.timestampEpoch AS timestampEpoch, " +
|
||||
"log.srcCallsign AS srcCallsign, " +
|
||||
"pos.dstCallsign AS dstCallsign, " +
|
||||
"pos.latitude AS latitude, " +
|
||||
"pos.longitude AS longitude, " +
|
||||
"pos.maidenHead AS maidenHead, " +
|
||||
"pos.altitudeMeters AS altitudeMeters, " +
|
||||
"pos.bearingDegrees AS bearingDegrees, " +
|
||||
"pos.speedMetersPerSecond AS speedMetersPerSecond, " +
|
||||
"pos.status AS status, " +
|
||||
"pos.comment AS comment, " +
|
||||
"pos.symbolCode AS symbolCode, " +
|
||||
"pos.privacyLevel AS privacyLevel, " +
|
||||
"MAX(pos.timestampEpoch)" +
|
||||
"FROM LogItem log " +
|
||||
"LEFT OUTER JOIN PositionItem pos ON (log.srcCallsign = pos.srcCallsign)" +
|
||||
"GROUP BY log.srcCallsign")
|
||||
LiveData<List<LogItemGroup>> getGroups();
|
||||
|
||||
@Query("SELECT * FROM LogItem ORDER by timestampEpoch ASC")
|
||||
LiveData<List<LogItem>> getAllLogItems();
|
||||
|
||||
|
@ -42,8 +23,14 @@ public interface LogItemDao {
|
|||
LiveData<List<LogItem>> getLogItems(String srcCallsign);
|
||||
|
||||
@Query("DELETE FROM LogItem WHERE srcCallsign = :srcCallsign")
|
||||
void deleteLogItems(String srcCallsign);
|
||||
void deleteLogItemsFromCallsign(String srcCallsign);
|
||||
|
||||
@Query("DELETE FROM LogItem")
|
||||
void deleteAllLogItems();
|
||||
|
||||
@Query("DELETE FROM LogItem WHERE timestampEpoch < :timestampEpoch")
|
||||
void deleteLogItemsOlderThanTimestamp(long timestampEpoch);
|
||||
|
||||
@Query("DELETE FROM LogItem WHERE srcCallsign = :srcCallsign AND timestampEpoch < :timestampEpoch")
|
||||
void deleteLogItems(String srcCallsign, long timestampEpoch);
|
||||
}
|
||||
|
|
|
@ -33,7 +33,8 @@ public class LogItemHolder extends RecyclerView.ViewHolder implements View.OnCli
|
|||
DateTools.epochToIso8601(timestamp),
|
||||
isTransmitting ? "→" : "←",
|
||||
srcCallsign));
|
||||
_logItemViewMessage.setText(TextTools.addZeroWidthSpaces(text));
|
||||
if (text != null)
|
||||
_logItemViewMessage.setText(TextTools.addZeroWidthSpaces(text));
|
||||
}
|
||||
|
||||
static LogItemHolder create(ViewGroup parent, boolean isClickable) {
|
||||
|
@ -46,7 +47,7 @@ public class LogItemHolder extends RecyclerView.ViewHolder implements View.OnCli
|
|||
public void onClick(View v) {
|
||||
if (!_isClickable) return;
|
||||
Intent logItemIntent = new Intent(v.getContext(), LogItemActivity.class);
|
||||
logItemIntent.putExtra("groupName", _srcCallsign);
|
||||
logItemIntent.putExtra("stationName", _srcCallsign);
|
||||
v.getContext().startActivity(logItemIntent);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.app.Application;
|
|||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.radio.codec2talkie.storage.AppDatabase;
|
||||
import com.radio.codec2talkie.storage.log.group.LogItemGroup;
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -13,22 +13,17 @@ public class LogItemRepository {
|
|||
|
||||
private final LogItemDao _logItemDao;
|
||||
private final LiveData<List<LogItem>> _logItemLiveData;
|
||||
private LiveData<List<LogItem>> _logItemGroupLiveData;
|
||||
private final LiveData<List<LogItemGroup>> _logItemGroups;
|
||||
|
||||
public LogItemRepository(Application application) {
|
||||
AppDatabase appDatabase = AppDatabase.getDatabase(application);
|
||||
_logItemDao = appDatabase.logItemDao();
|
||||
_logItemLiveData = _logItemDao.getAllLogItems();
|
||||
_logItemGroups = _logItemDao.getGroups();
|
||||
}
|
||||
|
||||
public LiveData<List<LogItem>> getAllLogItems() {
|
||||
return _logItemLiveData;
|
||||
}
|
||||
|
||||
public LiveData<List<LogItemGroup>> getGroups() { return _logItemGroups; }
|
||||
|
||||
public LiveData<List<LogItem>> getLogItems(String groupName) {
|
||||
return _logItemDao.getLogItems(groupName);
|
||||
}
|
||||
|
@ -37,11 +32,16 @@ public class LogItemRepository {
|
|||
AppDatabase.getDatabaseExecutor().execute(() -> _logItemDao.insertLogItem(logItem));
|
||||
}
|
||||
|
||||
public void deleteAllLogItems() {
|
||||
AppDatabase.getDatabaseExecutor().execute(_logItemDao::deleteAllLogItems);
|
||||
}
|
||||
|
||||
public void deleteLogItems(String groupName) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> _logItemDao.deleteLogItems(groupName));
|
||||
public void deleteLogItems(String srcCallsign, int hours) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> {
|
||||
if (srcCallsign == null && hours == -1)
|
||||
_logItemDao.deleteAllLogItems();
|
||||
else if (srcCallsign == null)
|
||||
_logItemDao.deleteLogItemsOlderThanTimestamp(DateTools.currentTimestampMinusHours(hours));
|
||||
else if (hours == -1)
|
||||
_logItemDao.deleteLogItemsFromCallsign(srcCallsign);
|
||||
else
|
||||
_logItemDao.deleteLogItems(srcCallsign, DateTools.currentTimestampMinusHours(hours));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.radio.codec2talkie.storage.log.group.LogItemGroup;
|
||||
import com.radio.codec2talkie.storage.station.StationItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -14,14 +14,11 @@ public class LogItemViewModel extends AndroidViewModel {
|
|||
|
||||
private final LogItemRepository _logItemRepository;
|
||||
private final LiveData<List<LogItem>> _logItemLiveData;
|
||||
private LiveData<List<LogItem>> _logItemGroupLiveData;
|
||||
private final LiveData<List<LogItemGroup>> _logItemGroups;
|
||||
|
||||
public LogItemViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
_logItemRepository = new LogItemRepository(application);
|
||||
_logItemLiveData = _logItemRepository.getAllLogItems();
|
||||
_logItemGroups = _logItemRepository.getGroups();
|
||||
}
|
||||
|
||||
public LiveData<List<LogItem>> getAllData() {
|
||||
|
@ -32,11 +29,7 @@ public class LogItemViewModel extends AndroidViewModel {
|
|||
return _logItemRepository.getLogItems(groupName);
|
||||
}
|
||||
|
||||
public LiveData<List<LogItemGroup>> getGroups() { return _logItemGroups; }
|
||||
|
||||
public void deleteAllLogItems() { _logItemRepository.deleteAllLogItems(); }
|
||||
|
||||
public void deleteLogItems(String groupName) {
|
||||
_logItemRepository.deleteLogItems(groupName);
|
||||
public void deleteLogItems(String srcCallsign, int hours) {
|
||||
_logItemRepository.deleteLogItems(srcCallsign, hours);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +0,0 @@
|
|||
package com.radio.codec2talkie.storage.log.group;
|
||||
|
||||
public class LogItemGroup {
|
||||
private long timestampEpoch;
|
||||
private String srcCallsign;
|
||||
public String dstCallsign;
|
||||
private String maidenHead;
|
||||
public double latitude;
|
||||
public double longitude;
|
||||
public double altitudeMeters;
|
||||
public double bearingDegrees;
|
||||
public double speedMetersPerSecond;
|
||||
public String status;
|
||||
public String comment;
|
||||
public String symbolCode;
|
||||
public int privacyLevel;
|
||||
|
||||
public long getTimestampEpoch() { return timestampEpoch; }
|
||||
|
||||
public String getSrcCallsign() { return srcCallsign; }
|
||||
|
||||
public String getDstCallsign() { return dstCallsign; }
|
||||
|
||||
public double getLatitude() { return latitude; }
|
||||
|
||||
public double getLongitude() { return longitude; }
|
||||
|
||||
public String getMaidenHead() { return maidenHead; }
|
||||
|
||||
public double getAltitudeMeters() { return altitudeMeters; }
|
||||
|
||||
public double getBearingDegrees() { return bearingDegrees; }
|
||||
|
||||
public double getSpeedMetersPerSecond() { return speedMetersPerSecond; };
|
||||
|
||||
public String getStatus() { return status; }
|
||||
|
||||
public String getComment() { return comment; };
|
||||
|
||||
public String getSymbolCode() { return symbolCode; }
|
||||
|
||||
public int getPrivacyLevel() { return privacyLevel; }
|
||||
|
||||
public void setTimestampEpoch(long timestampEpoch) { this.timestampEpoch = timestampEpoch; }
|
||||
|
||||
public void setSrcCallsign(String srcCallsign) { this.srcCallsign = srcCallsign; }
|
||||
|
||||
public void setMaidenHead(String maidenHead) { this.maidenHead = maidenHead; }
|
||||
|
||||
public void setDstCallsign(String dstCallsign) { this.dstCallsign = dstCallsign; }
|
||||
|
||||
public void setLatitude(double latitude) { this.latitude = latitude; }
|
||||
|
||||
public void setLongitude(double longitude) { this.longitude = longitude; }
|
||||
|
||||
public void setAltitudeMeters(double altitudeMeters) { this.altitudeMeters = altitudeMeters; }
|
||||
|
||||
public void setBearingDegrees(double bearingDegrees) { this.bearingDegrees = bearingDegrees; }
|
||||
|
||||
public void setSpeedMetersPerSecond(double speedMetersPerSecond) { this.speedMetersPerSecond = speedMetersPerSecond; }
|
||||
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public void setComment(String comment) { this.comment = comment; }
|
||||
|
||||
public void setSymbolCode(String symbolCode) { this.symbolCode = symbolCode; }
|
||||
|
||||
public void setPrivacyLevel(int privacyLevel) { this.privacyLevel = privacyLevel; }
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package com.radio.codec2talkie.storage.log.group;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
public class LogItemGroupAdapter extends ListAdapter<LogItemGroup, LogItemGroupHolder> {
|
||||
|
||||
private View.OnClickListener _clickListener;
|
||||
|
||||
public LogItemGroupAdapter(@NonNull DiffUtil.ItemCallback<LogItemGroup> diffCallback) {
|
||||
super(diffCallback);
|
||||
}
|
||||
|
||||
public void setClickListener(View.OnClickListener clickListener) {
|
||||
_clickListener = clickListener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public LogItemGroupHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return LogItemGroupHolder.create(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(LogItemGroupHolder holder, int position) {
|
||||
LogItemGroup current = getItem(position);
|
||||
holder.itemView.setOnClickListener(_clickListener);
|
||||
holder.bind(current);
|
||||
}
|
||||
|
||||
public static class LogItemGroupDiff extends DiffUtil.ItemCallback<LogItemGroup> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull LogItemGroup oldItem, @NonNull LogItemGroup newItem) {
|
||||
return oldItem.getSrcCallsign().equals(newItem.getSrcCallsign());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull LogItemGroup oldItem, @NonNull LogItemGroup newItem) {
|
||||
return oldItem.getSrcCallsign().equals(newItem.getSrcCallsign());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package com.radio.codec2talkie.storage.message;
|
||||
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity
|
||||
@Entity(indices = {@Index(value = {"id", "srcCallsign", "dstCallsign", "ackId"}, unique = true)})
|
||||
public class MessageItem {
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
|
@ -13,7 +14,9 @@ public class MessageItem {
|
|||
private String dstCallsign;
|
||||
private String message;
|
||||
private boolean needsAck;
|
||||
private int ackNum;
|
||||
private boolean isAcknowledged;
|
||||
private int ackId;
|
||||
private int retryCnt;
|
||||
private boolean isTransmit;
|
||||
|
||||
public long getId() {
|
||||
|
@ -34,7 +37,11 @@ public class MessageItem {
|
|||
|
||||
public boolean getNeedsAck() { return needsAck; }
|
||||
|
||||
public int getAckNum() { return ackNum; }
|
||||
public int getAckId() { return ackId; }
|
||||
|
||||
public int getRetryCnt() { return this.retryCnt; }
|
||||
|
||||
public boolean getIsAcknowledged() { return this.isAcknowledged; }
|
||||
|
||||
public boolean getIsTransmit() { return isTransmit; }
|
||||
|
||||
|
@ -42,6 +49,8 @@ public class MessageItem {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
public void setRetryCnt(int retryCnt) { this.retryCnt = retryCnt; }
|
||||
|
||||
public void setTimestampEpoch(long timestampEpoch) {
|
||||
this.timestampEpoch = timestampEpoch;
|
||||
}
|
||||
|
@ -56,8 +65,10 @@ public class MessageItem {
|
|||
|
||||
public void setNeedsAck(boolean needsAck) { this.needsAck = needsAck; }
|
||||
|
||||
public void setAckNum(int ackNum) { this.ackNum = ackNum; }
|
||||
public void setAckId(int ackId) { this.ackId = ackId; }
|
||||
|
||||
public void setIsTransmit(boolean isTransmit) { this.isTransmit = isTransmit; }
|
||||
|
||||
public void setIsAcknowledged(boolean isAcknowledged) { this.isAcknowledged = isAcknowledged; }
|
||||
}
|
||||
|
||||
|
|
|
@ -75,6 +75,7 @@ public class MessageItemActivity extends AppCompatActivityWithServiceConnection
|
|||
TextMessage textMessage = new TextMessage();
|
||||
textMessage.dst = _groupName;
|
||||
textMessage.text = messageEdit.getText().toString();
|
||||
textMessage.ackId = 0;
|
||||
getService().sendTextMessage(textMessage);
|
||||
messageEdit.setText("");
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import com.radio.codec2talkie.R;
|
|||
import com.radio.codec2talkie.app.AppService;
|
||||
import com.radio.codec2talkie.protocol.message.TextMessage;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MessageGroupDialogSendTo extends AlertDialog implements View.OnClickListener {
|
||||
|
||||
private final AppService _appService;
|
||||
|
@ -44,8 +46,9 @@ public class MessageGroupDialogSendTo extends AlertDialog implements View.OnClic
|
|||
assert targetEdit != null;
|
||||
assert messageEdit != null;
|
||||
TextMessage textMessage = new TextMessage();
|
||||
textMessage.dst = targetEdit.getText().toString();
|
||||
textMessage.dst = targetEdit.getText().toString().toUpperCase(Locale.ROOT);
|
||||
textMessage.text = messageEdit.getText().toString();
|
||||
textMessage.ackId = 0;
|
||||
_appService.sendTextMessage(textMessage);
|
||||
dismiss();
|
||||
} else if (id == R.id.send_message_to_btn_cancel) {
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
package com.radio.codec2talkie.storage.position;
|
||||
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity
|
||||
import com.radio.codec2talkie.protocol.position.Position;
|
||||
|
||||
@Entity(indices = {@Index(value = {"id", "srcCallsign"}, unique = true)})
|
||||
public class PositionItem {
|
||||
|
||||
private static final double MIN_COORDINATE_CHANGE_DELTA = 0.003;
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
private long timestampEpoch;
|
||||
private boolean isTransmit;
|
||||
public String srcCallsign;
|
||||
public String dstCallsign;
|
||||
public String digipath;
|
||||
public double latitude;
|
||||
public double longitude;
|
||||
public String maidenHead;
|
||||
|
@ -22,6 +28,8 @@ public class PositionItem {
|
|||
public String comment;
|
||||
public String symbolCode;
|
||||
public int privacyLevel;
|
||||
public double rangeMiles;
|
||||
public int directivityDeg;
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
|
@ -37,6 +45,8 @@ public class PositionItem {
|
|||
|
||||
public String getDstCallsign() { return dstCallsign; }
|
||||
|
||||
public String getDigipath() { return digipath; }
|
||||
|
||||
public double getLatitude() { return latitude; }
|
||||
|
||||
public double getLongitude() { return longitude; }
|
||||
|
@ -59,6 +69,10 @@ public class PositionItem {
|
|||
|
||||
public boolean getIsTransmit() { return isTransmit; }
|
||||
|
||||
public int getDirectivityDeg() { return directivityDeg; }
|
||||
|
||||
public double getRangeMiles() { return rangeMiles; }
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
@ -73,6 +87,8 @@ public class PositionItem {
|
|||
|
||||
public void setDstCallsign(String dstCallsign) { this.dstCallsign = dstCallsign; }
|
||||
|
||||
public void setDigipath(String digipath) { this.digipath = digipath; }
|
||||
|
||||
public void setLatitude(double latitude) { this.latitude = latitude; }
|
||||
|
||||
public void setLongitude(double longitude) { this.longitude = longitude; }
|
||||
|
@ -92,4 +108,17 @@ public class PositionItem {
|
|||
public void setSymbolCode(String symbolCode) { this.symbolCode = symbolCode; }
|
||||
|
||||
public void setPrivacyLevel(int privacyLevel) { this.privacyLevel = privacyLevel; }
|
||||
|
||||
public void setDirectivityDeg(int directivityDeg) { this.directivityDeg = directivityDeg; }
|
||||
|
||||
public void setRangeMiles(double rangeMiles) { this.rangeMiles = rangeMiles; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
PositionItem positionItem = (PositionItem) o;
|
||||
return getSrcCallsign().equals(positionItem.getSrcCallsign()) &
|
||||
getIsTransmit() == positionItem.getIsTransmit() &&
|
||||
Math.abs(getLongitude() - positionItem.getLongitude()) <= MIN_COORDINATE_CHANGE_DELTA &
|
||||
Math.abs(getLatitude() - positionItem.getLatitude()) <= MIN_COORDINATE_CHANGE_DELTA;
|
||||
}
|
||||
}
|
|
@ -5,29 +5,59 @@ import androidx.room.Dao;
|
|||
import androidx.room.Insert;
|
||||
import androidx.room.OnConflictStrategy;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
import androidx.room.Update;
|
||||
|
||||
import com.radio.codec2talkie.storage.message.MessageItem;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public interface PositionItemDao {
|
||||
public abstract class PositionItemDao {
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
void insertPositionItem(PositionItem logItem);
|
||||
@Insert
|
||||
public abstract void insertPositionItem(PositionItem positionItem);
|
||||
|
||||
@Update
|
||||
public abstract void updatePositionItem(PositionItem positionItem);
|
||||
|
||||
@Transaction
|
||||
public void upsertPositionItem(PositionItem positionItem) {
|
||||
PositionItem oldPosition = getLastPositionItem(positionItem.getSrcCallsign());
|
||||
if (oldPosition != null && oldPosition.equals(positionItem)) {
|
||||
// update id and coordinates from existing position
|
||||
positionItem.setId(oldPosition.getId());
|
||||
positionItem.setLatitude(oldPosition.getLatitude());
|
||||
positionItem.setLongitude(oldPosition.getLongitude());
|
||||
//Log.i(TAG, "UPDATE " + positionItem.getSrcCallsign());
|
||||
updatePositionItem(positionItem);
|
||||
} else {
|
||||
//Log.i(TAG, "INSERT " + positionItem.getSrcCallsign());
|
||||
insertPositionItem(positionItem);
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM PositionItem WHERE srcCallsign = :srcCallsign ORDER BY timestampEpoch DESC LIMIT 1")
|
||||
public abstract PositionItem getLastPositionItem(String srcCallsign);
|
||||
|
||||
@Query("SELECT srcCallsign from PositionItem GROUP BY srcCallsign")
|
||||
LiveData<List<String>> getGroups();
|
||||
public abstract LiveData<List<String>> getStationNames();
|
||||
|
||||
@Query("SELECT * FROM PositionItem ORDER by timestampEpoch DESC")
|
||||
LiveData<List<PositionItem>> getAllPositionItems();
|
||||
public abstract LiveData<List<PositionItem>> getAllPositionItems();
|
||||
|
||||
@Query("SELECT * FROM PositionItem WHERE srcCallsign = :srcCallsign ORDER BY timestampEpoch DESC")
|
||||
LiveData<List<PositionItem>> getPositionItems(String srcCallsign);
|
||||
@Query("SELECT * FROM PositionItem WHERE srcCallsign = :srcCallsign ORDER BY timestampEpoch ASC")
|
||||
public abstract LiveData<List<PositionItem>> getPositionItems(String srcCallsign);
|
||||
|
||||
@Query("DELETE FROM PositionItem WHERE srcCallsign = :srcCallsign")
|
||||
void deletePositionItems(String srcCallsign);
|
||||
public abstract void deletePositionItemsFromCallsign(String srcCallsign);
|
||||
|
||||
@Query("DELETE FROM PositionItem WHERE timestampEpoch < :timestamp")
|
||||
public abstract void deletePositionItemsOlderThanTimestamp(long timestamp);
|
||||
|
||||
@Query("DELETE FROM PositionItem WHERE timestampEpoch < :timestamp AND srcCallsign = :srcCallsign")
|
||||
public abstract void deletePositionItems(String srcCallsign, long timestamp);
|
||||
|
||||
@Query("DELETE FROM PositionItem")
|
||||
void deleteAllPositionItems();
|
||||
public abstract void deleteAllPositionItems();
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
package com.radio.codec2talkie.storage.position;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import android.util.Log;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import com.radio.codec2talkie.storage.AppDatabase;
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PositionItemRepository {
|
||||
private static final String TAG = PositionItemRepository.class.getSimpleName();
|
||||
|
||||
private final PositionItemDao _positionItemDao;
|
||||
|
||||
|
@ -17,15 +21,26 @@ public class PositionItemRepository {
|
|||
_positionItemDao = appDatabase.positionItemDao();
|
||||
}
|
||||
|
||||
public void insertPositionItem(PositionItem positionItem) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> _positionItemDao.insertPositionItem(positionItem));
|
||||
public void upsertPositionItem(PositionItem positionItem) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> _positionItemDao.upsertPositionItem(positionItem));
|
||||
}
|
||||
|
||||
public void deleteAllPositionItems() {
|
||||
AppDatabase.getDatabaseExecutor().execute(_positionItemDao::deleteAllPositionItems);
|
||||
public LiveData<List<PositionItem>> getPositionItems(String srcCallsign) {
|
||||
return Transformations.distinctUntilChanged(_positionItemDao.getPositionItems(srcCallsign));
|
||||
}
|
||||
|
||||
public void deletePositionItems(String srcCallsign) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> _positionItemDao.deletePositionItems(srcCallsign));
|
||||
public void deletePositionItems(String srcCallsign, int hours) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> {
|
||||
if (srcCallsign == null && hours == -1)
|
||||
_positionItemDao.deleteAllPositionItems();
|
||||
else if (srcCallsign == null)
|
||||
_positionItemDao.deletePositionItemsOlderThanTimestamp(DateTools.currentTimestampMinusHours(hours));
|
||||
else if (hours == -1)
|
||||
_positionItemDao.deletePositionItemsFromCallsign(srcCallsign);
|
||||
else
|
||||
_positionItemDao.deletePositionItems(srcCallsign, DateTools.currentTimestampMinusHours(hours));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import androidx.annotation.NonNull;
|
|||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class PositionItemViewModel extends AndroidViewModel {
|
||||
|
@ -17,9 +19,11 @@ public class PositionItemViewModel extends AndroidViewModel {
|
|||
_positionItemRepository = new PositionItemRepository(application);
|
||||
}
|
||||
|
||||
public void deleteAllPositionItems() { _positionItemRepository.deleteAllPositionItems(); }
|
||||
public LiveData<List<PositionItem>> getPositionItems(String srcCallsign) {
|
||||
return _positionItemRepository.getPositionItems(srcCallsign);
|
||||
}
|
||||
|
||||
public void deletePositionItems(String srcCallsign) {
|
||||
_positionItemRepository.deletePositionItems(srcCallsign);
|
||||
public void deletePositionItems(String srcCallsign, int hours) {
|
||||
_positionItemRepository.deletePositionItems(srcCallsign, hours);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
package com.radio.codec2talkie.storage.station;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
import com.radio.codec2talkie.protocol.aprs.tools.AprsSymbolTable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity(indices = {@Index(value = {"srcCallsign"}, unique = true)})
|
||||
public class StationItem {
|
||||
@NonNull
|
||||
@PrimaryKey
|
||||
private String srcCallsign;
|
||||
private long timestampEpoch;
|
||||
public String dstCallsign;
|
||||
public String digipath;
|
||||
private String maidenHead;
|
||||
public double latitude;
|
||||
public double longitude;
|
||||
public double altitudeMeters;
|
||||
public double bearingDegrees;
|
||||
public double speedMetersPerSecond;
|
||||
public String status;
|
||||
public String comment;
|
||||
public String symbolCode;
|
||||
public String logLine;
|
||||
public int privacyLevel;
|
||||
public double rangeMiles;
|
||||
public int directivityDeg;
|
||||
|
||||
public StationItem(@NonNull String srcCallsign) {
|
||||
this.srcCallsign = srcCallsign;
|
||||
}
|
||||
|
||||
public long getTimestampEpoch() { return timestampEpoch; }
|
||||
|
||||
public String getSrcCallsign() { return srcCallsign; }
|
||||
|
||||
public String getDstCallsign() { return dstCallsign; }
|
||||
|
||||
public String getDigipath() { return digipath; }
|
||||
|
||||
public double getLatitude() { return latitude; }
|
||||
|
||||
public double getLongitude() { return longitude; }
|
||||
|
||||
public String getMaidenHead() { return maidenHead; }
|
||||
|
||||
public double getAltitudeMeters() { return altitudeMeters; }
|
||||
|
||||
public double getBearingDegrees() { return bearingDegrees; }
|
||||
|
||||
public double getSpeedMetersPerSecond() { return speedMetersPerSecond; }
|
||||
|
||||
public String getStatus() { return status; }
|
||||
|
||||
public String getComment() { return comment; }
|
||||
|
||||
public String getSymbolCode() { return symbolCode; }
|
||||
|
||||
public String getLogLine() { return logLine; }
|
||||
|
||||
public int getPrivacyLevel() { return privacyLevel; }
|
||||
|
||||
public double getRangeMiles() { return rangeMiles; }
|
||||
|
||||
public int getDirectivityDeg() { return directivityDeg; }
|
||||
|
||||
public void setTimestampEpoch(long timestampEpoch) { this.timestampEpoch = timestampEpoch; }
|
||||
|
||||
public void setSrcCallsign(@NonNull String srcCallsign) { this.srcCallsign = srcCallsign; }
|
||||
|
||||
public void setDigipath(String digipath) { this.digipath = digipath; }
|
||||
|
||||
public void setMaidenHead(String maidenHead) { this.maidenHead = maidenHead; }
|
||||
|
||||
public void setDstCallsign(String dstCallsign) { this.dstCallsign = dstCallsign; }
|
||||
|
||||
public void setLatitude(double latitude) { this.latitude = latitude; }
|
||||
|
||||
public void setLongitude(double longitude) { this.longitude = longitude; }
|
||||
|
||||
public void setAltitudeMeters(double altitudeMeters) { this.altitudeMeters = altitudeMeters; }
|
||||
|
||||
public void setBearingDegrees(double bearingDegrees) { this.bearingDegrees = bearingDegrees; }
|
||||
|
||||
public void setSpeedMetersPerSecond(double speedMetersPerSecond) { this.speedMetersPerSecond = speedMetersPerSecond; }
|
||||
|
||||
public void setStatus(String status) { this.status = status; }
|
||||
|
||||
public void setComment(String comment) { this.comment = comment; }
|
||||
|
||||
public void setSymbolCode(String symbolCode) { this.symbolCode = symbolCode; }
|
||||
|
||||
public void setPrivacyLevel(int privacyLevel) { this.privacyLevel = privacyLevel; }
|
||||
|
||||
public void setLogLine(String logLine) { this.logLine = logLine; }
|
||||
|
||||
public void setRangeMiles(double rangeMiles) { this.rangeMiles = rangeMiles; }
|
||||
|
||||
public void setDirectivityDeg(int directivityDeg) { this.directivityDeg = directivityDeg; }
|
||||
|
||||
public void updateFrom(StationItem stationItem) {
|
||||
setTimestampEpoch(stationItem.getTimestampEpoch());
|
||||
// update position if known
|
||||
if (stationItem.getMaidenHead() != null) {
|
||||
setMaidenHead(stationItem.getMaidenHead());
|
||||
setLatitude(stationItem.getLatitude());
|
||||
setLongitude(stationItem.getLongitude());
|
||||
setAltitudeMeters(stationItem.getAltitudeMeters());
|
||||
setBearingDegrees(stationItem.getBearingDegrees());
|
||||
setSpeedMetersPerSecond(stationItem.getSpeedMetersPerSecond());
|
||||
setPrivacyLevel(stationItem.getPrivacyLevel());
|
||||
setRangeMiles(stationItem.getRangeMiles());
|
||||
setDirectivityDeg(stationItem.getDirectivityDeg());
|
||||
}
|
||||
if (stationItem.getStatus() != null)
|
||||
setStatus(stationItem.getStatus());
|
||||
if (stationItem.getComment() != null)
|
||||
setComment(stationItem.getComment());
|
||||
if (stationItem.getSymbolCode() != null)
|
||||
setSymbolCode(stationItem.getSymbolCode());
|
||||
if (stationItem.getLogLine() != null)
|
||||
setLogLine(stationItem.getLogLine());
|
||||
if (stationItem.getDigipath() != null)
|
||||
setDigipath(stationItem.getDigipath());
|
||||
if (stationItem.getDstCallsign() != null)
|
||||
setDstCallsign(stationItem.getDstCallsign());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
StationItem stationItem = (StationItem)o;
|
||||
return srcCallsign.equals(stationItem.getSrcCallsign()) &&
|
||||
timestampEpoch == stationItem.getTimestampEpoch() &&
|
||||
Objects.equals(comment, stationItem.getComment()) &&
|
||||
Objects.equals(dstCallsign, stationItem.getDstCallsign()) &&
|
||||
latitude == stationItem.getLatitude() &&
|
||||
longitude == stationItem.getLongitude();
|
||||
}
|
||||
|
||||
public BitmapDrawable drawLabelWithIcon(Context context, float textSize) {
|
||||
String callsign = getSrcCallsign();
|
||||
|
||||
Bitmap bitmapIcon = AprsSymbolTable.getInstance(context).bitmapFromSymbol(getSymbolCode(), false);
|
||||
if (bitmapIcon == null) return null;
|
||||
|
||||
// construct and calculate bounds
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setTextSize(textSize);
|
||||
Rect bounds = new Rect();
|
||||
paint.getTextBounds(callsign, 0, callsign.length(), bounds);
|
||||
int width = Math.max(bitmapIcon.getWidth(), bounds.width());
|
||||
int height = bitmapIcon.getHeight() + bounds.height();
|
||||
|
||||
// create overlay bitmap
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setDensity(DisplayMetrics.DENSITY_DEFAULT);
|
||||
|
||||
// draw APRS icon
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
float bitmapLeft = width > bitmapIcon.getWidth() ? width / 2.0f - bitmapIcon.getWidth() / 2.0f : 0;
|
||||
// do not rotate
|
||||
if (getBearingDegrees() == 0 || !AprsSymbolTable.needsRotation(getSymbolCode())) {
|
||||
canvas.drawBitmap(bitmapIcon, bitmapLeft, 0, null);
|
||||
// rotate
|
||||
} else {
|
||||
float rotationDeg = (float) (getBearingDegrees() - 90.0f);
|
||||
Matrix m = new Matrix();
|
||||
// flip/rotate
|
||||
if (getBearingDegrees() > 180) {
|
||||
m.postScale(-1, 1);
|
||||
m.postTranslate(bitmapIcon.getWidth(), 0);
|
||||
m.postRotate(rotationDeg - 180, bitmapIcon.getWidth() / 2.0f, bitmapIcon.getHeight() / 2.0f);
|
||||
// rotate
|
||||
} else {
|
||||
m.postRotate(rotationDeg, bitmapIcon.getWidth() / 2.0f, bitmapIcon.getHeight() / 2.0f);
|
||||
}
|
||||
m.postTranslate(bitmapLeft, 0);
|
||||
canvas.drawBitmap(bitmapIcon, m, null);
|
||||
}
|
||||
|
||||
// draw background
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setAlpha(120);
|
||||
bounds.set(0, bitmapIcon.getHeight(), width, height);
|
||||
canvas.drawRect(bounds, paint);
|
||||
|
||||
// draw text
|
||||
paint.setColor(Color.BLACK);
|
||||
paint.setAlpha(255);
|
||||
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
|
||||
canvas.drawText(callsign, 0, height, paint);
|
||||
|
||||
// add marker
|
||||
return new BitmapDrawable(context.getResources(), bitmap);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package com.radio.codec2talkie.storage.station;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class StationItemAdapter extends ListAdapter<StationItem, StationItemHolder> {
|
||||
|
||||
private View.OnClickListener _clickListener;
|
||||
|
||||
public StationItemAdapter(@NonNull DiffUtil.ItemCallback<StationItem> diffCallback) {
|
||||
super(diffCallback);
|
||||
}
|
||||
|
||||
public void setClickListener(View.OnClickListener clickListener) {
|
||||
_clickListener = clickListener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public StationItemHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return StationItemHolder.create(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(StationItemHolder holder, int position) {
|
||||
StationItem current = getItem(position);
|
||||
holder.itemView.setOnClickListener(_clickListener);
|
||||
holder.bind(current);
|
||||
}
|
||||
|
||||
public static class StationItemDiff extends DiffUtil.ItemCallback<StationItem> {
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull StationItem oldItem, @NonNull StationItem newItem) {
|
||||
return oldItem.getSrcCallsign().equals(newItem.getSrcCallsign());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull StationItem oldItem, @NonNull StationItem newItem) {
|
||||
return oldItem.getSrcCallsign().equals(newItem.getSrcCallsign()) &&
|
||||
oldItem.getLatitude() == newItem.getLatitude() &&
|
||||
oldItem.getLongitude() == newItem.getLongitude() &&
|
||||
Objects.equals(oldItem.getComment(), newItem.getComment());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
package com.radio.codec2talkie.storage.station;
|
||||
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import androidx.room.Transaction;
|
||||
import androidx.room.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public abstract class StationItemDao {
|
||||
|
||||
@Insert
|
||||
public abstract void insertStationItem(StationItem stationItem);
|
||||
|
||||
@Update
|
||||
public abstract void updateStationItem(StationItem stationItem);
|
||||
|
||||
@Transaction
|
||||
public void upsertStationItem(StationItem stationItem) {
|
||||
StationItem oldStationItem = getStationItem(stationItem.getSrcCallsign());
|
||||
if (oldStationItem == null) {
|
||||
try {
|
||||
insertStationItem(stationItem);
|
||||
} catch (SQLiteConstraintException ex) {
|
||||
oldStationItem = getStationItem(stationItem.getSrcCallsign());
|
||||
}
|
||||
}
|
||||
if (oldStationItem != null) {
|
||||
oldStationItem.updateFrom(stationItem);
|
||||
updateStationItem(oldStationItem);
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT * FROM StationItem WHERE srcCallsign = :srcCallsign")
|
||||
public abstract StationItem getStationItem(String srcCallsign);
|
||||
|
||||
@Query("SELECT * FROM StationItem ORDER BY srcCallsign ASC")
|
||||
public abstract LiveData<List<StationItem>> getAllStationItems();
|
||||
|
||||
@Query("SELECT *, (SELECT count(*) FROM PositionItem pos WHERE st.srcCallsign = pos.srcCallsign) AS positionCount " +
|
||||
"FROM StationItem st " +
|
||||
"WHERE positionCount > :minCount")
|
||||
public abstract LiveData<List<StationItem>> getMovingStationItems(int minCount);
|
||||
|
||||
@Query("DELETE FROM StationItem WHERE srcCallsign = :srcCallsign")
|
||||
public abstract void deleteStationItemsFromCallsign(String srcCallsign);
|
||||
|
||||
@Query("DELETE FROM StationItem WHERE timestampEpoch < :timestamp")
|
||||
public abstract void deleteStationItemsOlderThanTimestamp(long timestamp);
|
||||
|
||||
@Query("DELETE FROM StationItem WHERE timestampEpoch < :timestamp AND srcCallsign = :srcCallsign")
|
||||
public abstract void deleteStationItems(String srcCallsign, long timestamp);
|
||||
|
||||
@Query("DELETE FROM StationItem")
|
||||
public abstract void deleteAllStationItems();
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package com.radio.codec2talkie.storage.log.group;
|
||||
package com.radio.codec2talkie.storage.station;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
@ -21,7 +21,7 @@ import com.radio.codec2talkie.tools.UnitTools;
|
|||
|
||||
import java.util.Locale;
|
||||
|
||||
public class LogItemGroupHolder extends RecyclerView.ViewHolder {
|
||||
public class StationItemHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private static final String TAG = RecyclerView.class.getSimpleName();
|
||||
|
||||
|
@ -32,7 +32,7 @@ public class LogItemGroupHolder extends RecyclerView.ViewHolder {
|
|||
private final LocationManager _locationManager;
|
||||
private final AprsSymbolTable _symbolTable;
|
||||
|
||||
private LogItemGroupHolder(View itemView) {
|
||||
private StationItemHolder(View itemView) {
|
||||
super(itemView);
|
||||
_logItemViewTitle = itemView.findViewById(R.id.log_view_group_item_title);
|
||||
_logItemViewDistance = itemView.findViewById(R.id.log_view_group_item_distance);
|
||||
|
@ -42,7 +42,7 @@ public class LogItemGroupHolder extends RecyclerView.ViewHolder {
|
|||
_locationManager = (LocationManager) itemView.getContext().getSystemService(Context.LOCATION_SERVICE);
|
||||
}
|
||||
|
||||
public void bind(LogItemGroup group) {
|
||||
public void bind(StationItem group) {
|
||||
@SuppressLint("MissingPermission") Location loc = _locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
if (loc == null) {
|
||||
_logItemViewDistance.setText("");
|
||||
|
@ -63,9 +63,7 @@ public class LogItemGroupHolder extends RecyclerView.ViewHolder {
|
|||
(int)group.getAltitudeMeters(),
|
||||
group.getStatus(),
|
||||
group.getComment());
|
||||
if (group.getMaidenHead() != null) {
|
||||
_logItemViewMessage.setText(status);
|
||||
}
|
||||
_logItemViewMessage.setText(group.getMaidenHead() == null ? group.getLogLine() : status);
|
||||
String symbol = group.getSymbolCode();
|
||||
Bitmap iconBitmap = _symbolTable.bitmapFromSymbol(symbol == null ? "/." : symbol, false);
|
||||
if (iconBitmap == null) {
|
||||
|
@ -75,9 +73,9 @@ public class LogItemGroupHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
static LogItemGroupHolder create(ViewGroup parent) {
|
||||
static StationItemHolder create(ViewGroup parent) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(
|
||||
R.layout.activity_log_view_group_item, parent, false);
|
||||
return new LogItemGroupHolder(view);
|
||||
return new StationItemHolder(view);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package com.radio.codec2talkie.storage.station;
|
||||
|
||||
import android.app.Application;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
|
||||
import com.radio.codec2talkie.storage.AppDatabase;
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StationItemRepository {
|
||||
private static final String TAG = StationItemRepository.class.getSimpleName();
|
||||
|
||||
private static final int MIN_POSITION_COUNT = 2;
|
||||
|
||||
private final StationItemDao _stationItemDao;
|
||||
private final LiveData<List<StationItem>> _stationItems;
|
||||
private final LiveData<List<StationItem>> _stationItemsMoving;
|
||||
|
||||
public StationItemRepository(Application application) {
|
||||
AppDatabase appDatabase = AppDatabase.getDatabase(application);
|
||||
_stationItemDao = appDatabase.stationitemDao();
|
||||
_stationItems = Transformations.distinctUntilChanged(_stationItemDao.getAllStationItems());
|
||||
_stationItemsMoving = Transformations.distinctUntilChanged(_stationItemDao.getMovingStationItems(MIN_POSITION_COUNT));
|
||||
}
|
||||
|
||||
public LiveData<List<StationItem>> getAllStationItems(boolean movingOnly) {
|
||||
return movingOnly ? _stationItemsMoving : _stationItems;
|
||||
}
|
||||
|
||||
public void upsertStationItem(StationItem stationItem) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> {
|
||||
_stationItemDao.upsertStationItem(stationItem);
|
||||
});
|
||||
}
|
||||
|
||||
public void deleteStationItems(String srcCallsign, int hours) {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> {
|
||||
AppDatabase.getDatabaseExecutor().execute(() -> {
|
||||
if (srcCallsign == null && hours == -1)
|
||||
_stationItemDao.deleteAllStationItems();
|
||||
else if (srcCallsign == null)
|
||||
_stationItemDao.deleteStationItemsOlderThanTimestamp(DateTools.currentTimestampMinusHours(hours));
|
||||
else if (hours == -1)
|
||||
_stationItemDao.deleteStationItemsFromCallsign(srcCallsign);
|
||||
else
|
||||
_stationItemDao.deleteStationItems(srcCallsign, DateTools.currentTimestampMinusHours(hours));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.radio.codec2talkie.storage.station;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import com.radio.codec2talkie.tools.DateTools;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class StationItemViewModel extends AndroidViewModel {
|
||||
|
||||
private final StationItemRepository _stationItemRepository;
|
||||
|
||||
public StationItemViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
_stationItemRepository = new StationItemRepository(application);
|
||||
}
|
||||
|
||||
public LiveData<List<StationItem>> getAllStationItems(boolean movingOnly) { return _stationItemRepository.getAllStationItems(movingOnly); }
|
||||
|
||||
public void deleteStationItems(String srcCallsign, int hours) {
|
||||
_stationItemRepository.deleteStationItems(srcCallsign, hours);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
package com.radio.codec2talkie.tools;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.radio.codec2talkie.R;
|
||||
import com.radio.codec2talkie.app.AppWorker;
|
||||
import com.radio.codec2talkie.settings.PreferenceKeys;
|
||||
import com.radio.codec2talkie.settings.SettingsWrapper;
|
||||
|
@ -76,14 +78,26 @@ public class AudioTools {
|
|||
return null;
|
||||
}
|
||||
|
||||
public static String getSpeedStatusText(String codec2ModeName, SharedPreferences sharedPreferences) {
|
||||
public static String getModulationAsText(SharedPreferences sharedPreferences) {
|
||||
int modulation = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_MOD, "0"));
|
||||
return modulation == RadioTools.ModulationTypeLora ? "LoRa" : "FSK";
|
||||
}
|
||||
|
||||
public static String getSpeedStatusText(SharedPreferences sharedPreferences, Resources resources) {
|
||||
|
||||
// use freedv mode text instead if it is active
|
||||
String freedvModeLabel = getFreedvModeAsText(sharedPreferences);
|
||||
if (freedvModeLabel != null) return freedvModeLabel;
|
||||
|
||||
// codec2 speed
|
||||
String speedModeInfo = "C2: " + AudioTools.extractCodec2Speed(codec2ModeName);
|
||||
String speedModeInfo;
|
||||
if (SettingsWrapper.isCodec2Enabled(sharedPreferences)) {
|
||||
String codec2ModeName = sharedPreferences.getString(PreferenceKeys.CODEC2_MODE, resources.getStringArray(R.array.codec2_modes)[0]);
|
||||
speedModeInfo = "C2: " + AudioTools.extractCodec2Speed(codec2ModeName);
|
||||
} else {
|
||||
int speed = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.OPUS_BIT_RATE, "3200"));
|
||||
speedModeInfo = "OPUS: " + speed;
|
||||
}
|
||||
|
||||
// radio speed
|
||||
int radioSpeedBps = RadioTools.getRadioSpeed(sharedPreferences);
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package com.radio.codec2talkie.tools;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class BitmapTools {
|
||||
public static BitmapDrawable drawLabel(Context context, String text, float textSize) {
|
||||
Paint paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setTextSize(textSize);
|
||||
|
||||
Rect bounds = new Rect();
|
||||
paint.getTextBounds(text, 0, text.length(), bounds);
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888);
|
||||
bitmap.setDensity(DisplayMetrics.DENSITY_DEFAULT);
|
||||
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
||||
paint.setColor(Color.WHITE);
|
||||
//paint.setAlpha(200);
|
||||
canvas.drawRect(0, 0, bounds.width(), bounds.height(), paint);
|
||||
|
||||
paint.setColor(Color.BLACK);
|
||||
paint.setAlpha(255);
|
||||
paint.setFlags(Paint.ANTI_ALIAS_FLAG);
|
||||
canvas.drawText(text, -bounds.left, bounds.height(), paint);
|
||||
|
||||
return new BitmapDrawable(context.getResources(), bitmap);
|
||||
}
|
||||
}
|
|
@ -12,4 +12,15 @@ public class DateTools {
|
|||
sdf.setTimeZone(TimeZone.getDefault());
|
||||
return sdf.format(new Date(timeMilliseconds));
|
||||
}
|
||||
|
||||
public static String epochToIso8601Time(long timeMilliseconds) {
|
||||
String format = "HH:mm:ss";
|
||||
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault());
|
||||
sdf.setTimeZone(TimeZone.getDefault());
|
||||
return sdf.format(new Date(timeMilliseconds));
|
||||
}
|
||||
|
||||
public static long currentTimestampMinusHours(int hours) {
|
||||
return System.currentTimeMillis() - (hours * 60L * 60L * 1000L);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,25 +7,37 @@ import com.radio.codec2talkie.settings.PreferenceKeys;
|
|||
import com.radio.codec2talkie.settings.SettingsWrapper;
|
||||
|
||||
public class RadioTools {
|
||||
|
||||
public static final int ModulationTypeLora = 0;
|
||||
public static final int ModulationTypeFsk = 1;
|
||||
|
||||
public static int calculateLoraSpeedBps(int bw, int sf, int cr) {
|
||||
return (int)(sf * (4.0 / cr) / (Math.pow(2.0, sf) / bw));
|
||||
}
|
||||
|
||||
public static int getRadioSpeed(SharedPreferences sharedPreferences) {
|
||||
int resultBps = 0;
|
||||
int maxSpeedBps = 128000;
|
||||
try {
|
||||
if (!SettingsWrapper.isSoundModemEnabled(sharedPreferences) && SettingsWrapper.isKissExtensionEnabled(sharedPreferences)) {
|
||||
int bw = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_BANDWIDTH, "125000"));
|
||||
int sf = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_SF, "7"));
|
||||
int cr = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_CR, "5"));
|
||||
resultBps = RadioTools.calculateLoraSpeedBps(bw, sf, cr);
|
||||
}
|
||||
} catch (NumberFormatException|ArithmeticException e) {
|
||||
e.printStackTrace();
|
||||
int modulation = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_MOD, "0"));
|
||||
if (SettingsWrapper.isSoundModemEnabled(sharedPreferences)) {
|
||||
return SettingsWrapper.getFskSpeed(sharedPreferences);
|
||||
}
|
||||
return (resultBps > 0 && resultBps <= maxSpeedBps) ? resultBps : 0;
|
||||
if (modulation == ModulationTypeLora) {
|
||||
int resultBps = 0;
|
||||
int maxSpeedBps = 128000;
|
||||
try {
|
||||
if (!SettingsWrapper.isSoundModemEnabled(sharedPreferences) && SettingsWrapper.isKissExtensionEnabled(sharedPreferences)) {
|
||||
int bw = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_BANDWIDTH, "125000"));
|
||||
int sf = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_SF, "7"));
|
||||
int cr = Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_CR, "5"));
|
||||
resultBps = RadioTools.calculateLoraSpeedBps(bw, sf, cr);
|
||||
}
|
||||
} catch (NumberFormatException | ArithmeticException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return (resultBps > 0 && resultBps <= maxSpeedBps) ? resultBps : 0;
|
||||
} else if (modulation == ModulationTypeFsk){
|
||||
return Integer.parseInt(sharedPreferences.getString(PreferenceKeys.KISS_EXTENSIONS_RADIO_FSK_BIT_RATE, "4.8"));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static double calculateLoraSensitivity(SharedPreferences sharedPreferences) {
|
||||
|
|
|
@ -24,18 +24,18 @@ public class ScramblingTools {
|
|||
private static final String SCRAMBLING_ALGORITHM = "AES/CBC/PKCS5Padding";
|
||||
private static final String PBE_ALGORITHM = "PBEwithSHA256and128BITAES-CBC-BC";
|
||||
|
||||
private static final SecureRandom _randomGenerator = new SecureRandom();
|
||||
|
||||
public static ScrambledData scramble(String masterKey, byte[] rawData, int iterations)
|
||||
throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeySpecException,
|
||||
InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException {
|
||||
|
||||
SecureRandom rnd = new SecureRandom();
|
||||
|
||||
ScrambledData encData = new ScrambledData();
|
||||
encData.salt = new byte[SALT_BYTES];
|
||||
encData.iv = new byte[BLOCK_SIZE];
|
||||
|
||||
rnd.nextBytes(encData.salt);
|
||||
rnd.nextBytes(encData.iv);
|
||||
_randomGenerator.nextBytes(encData.salt);
|
||||
_randomGenerator.nextBytes(encData.iv);
|
||||
|
||||
PBEKeySpec keySpec = new PBEKeySpec(masterKey.toCharArray(), encData.salt, iterations);
|
||||
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(PBE_ALGORITHM);
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.util.Log;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public class TextTools {
|
||||
public static String addZeroWidthSpaces(String text) {
|
||||
|
@ -52,4 +53,14 @@ public class TextTools {
|
|||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static byte[] hexStringToByteArray(String s) {
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for (int i = 0; i < len; i += 2) {
|
||||
data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i+1), 16));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,4 +81,8 @@ public class UnitTools {
|
|||
public static int metersPerSecondToKilometersPerHour(int speedMetersPerSecond) {
|
||||
return (int) (speedMetersPerSecond * 3.6);
|
||||
}
|
||||
|
||||
public static double milesToKilometers(double rangeMiles) {
|
||||
return rangeMiles * 1.609344;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.io.IOException;
|
|||
|
||||
public class UsbSerial implements Transport {
|
||||
|
||||
private static final int RX_TIMEOUT = 100;
|
||||
private static final int RX_TIMEOUT = 5;
|
||||
private static final int TX_TIMEOUT = 2000;
|
||||
|
||||
private final UsbSerialPort _usbPort;
|
||||
|
|
|
@ -61,6 +61,17 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textStatus" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textTelemetry"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/textRssi"
|
||||
app:layout_constraintTop_toBottomOf="@+id/textStatus" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textStatus"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent">
|
||||
<org.osmdroid.views.MapView android:id="@+id/map"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
</LinearLayout>
|
|
@ -23,6 +23,7 @@
|
|||
<androidx.appcompat.widget.AppCompatEditText
|
||||
android:id="@+id/messages_edit"
|
||||
android:layout_width="0dp"
|
||||
android:maxLength="67"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@+id/messages_send"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
@ -5,5 +5,23 @@
|
|||
android:title="@string/log_view_menu_stations" />
|
||||
<item
|
||||
android:id="@+id/log_view_menu_clear"
|
||||
android:title="@string/log_view_menu_clear" />
|
||||
android:title="@string/log_view_menu_clear_title">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/log_view_menu_clear_1h"
|
||||
android:title="@string/log_view_menu_clear_1h" />
|
||||
<item
|
||||
android:id="@+id/log_view_menu_clear_12h"
|
||||
android:title="@string/log_view_menu_clear_12h" />
|
||||
<item
|
||||
android:id="@+id/log_view_menu_clear_1d"
|
||||
android:title="@string/log_view_menu_clear_1d" />
|
||||
<item
|
||||
android:id="@+id/log_view_menu_clear_7d"
|
||||
android:title="@string/log_view_menu_clear_7d" />
|
||||
<item
|
||||
android:id="@+id/log_view_menu_clear_all"
|
||||
android:title="@string/log_view_menu_clear_all" />
|
||||
</menu>
|
||||
</item>
|
||||
</menu>
|
|
@ -13,6 +13,9 @@
|
|||
<item
|
||||
android:id="@+id/aprs_log"
|
||||
android:title="@string/menu_aprs_log" />
|
||||
<item
|
||||
android:id="@+id/aprs_map"
|
||||
android:title="@string/menu_aprs_map" />
|
||||
</group>
|
||||
<group android:id="@+id/group_main">
|
||||
<item
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/map_menu_move_map"
|
||||
android:title="@string/map_menu_move_map"
|
||||
android:checkable="true"
|
||||
android:checked="false"/>
|
||||
<item
|
||||
android:id="@+id/map_menu_rotate_map"
|
||||
android:title="@string/map_menu_rotate_map"
|
||||
android:checkable="true"
|
||||
android:checked="false"/>
|
||||
<item
|
||||
android:id="@+id/map_menu_show_range"
|
||||
android:title="@string/map_menu_show_range"
|
||||
android:checkable="true"
|
||||
android:checked="false"/>
|
||||
<item
|
||||
android:id="@+id/map_menu_show_moving"
|
||||
android:title="@string/map_menu_show_moving"
|
||||
android:checkable="true"
|
||||
android:checked="false"/>
|
||||
<item
|
||||
android:id="@+id/map_menu_clear_cache"
|
||||
android:title="@string/map_menu_clear_cache_title" />
|
||||
</menu>
|
|
@ -13,6 +13,11 @@
|
|||
<item>115200</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="codec_type_modes">
|
||||
<item>Codec2</item>
|
||||
<item>OPUS</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="codec2_modes">
|
||||
<item>MODE_450=10</item>
|
||||
<item>MODE_700C=8</item>
|
||||
|
@ -24,6 +29,32 @@
|
|||
<item>MODE_3200=0</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="opus_frame_size">
|
||||
<item>2.5</item>
|
||||
<item>5</item>
|
||||
<item>10</item>
|
||||
<item>20</item>
|
||||
<item>40</item>
|
||||
<item>60</item>
|
||||
<item>80</item>
|
||||
<item>100</item>
|
||||
<item>120</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="opus_complexity_value">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
<item>5</item>
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
<item>10</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="lora_bandwidths">
|
||||
<item>7800</item>
|
||||
<item>10400</item>
|
||||
|
@ -38,7 +69,6 @@
|
|||
</string-array>
|
||||
|
||||
<string-array name="lora_spreading_factors">
|
||||
<item>6</item>
|
||||
<item>7</item>
|
||||
<item>8</item>
|
||||
<item>9</item>
|
||||
|
@ -275,7 +305,76 @@
|
|||
<item>/y</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="radio_mod_entries">
|
||||
<item>LoRa</item>
|
||||
<item>FSK</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="radio_mod_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fsk_rx_bw_entries">
|
||||
<item>4800 Hz</item>
|
||||
<item>5800 Hz</item>
|
||||
<item>7300 Hz</item>
|
||||
<item>9700 Hz</item>
|
||||
<item>11700 Hz</item>
|
||||
<item>14600 Hz</item>
|
||||
<item>19500 Hz</item>
|
||||
<item>23400 Hz</item>
|
||||
<item>29300 Hz</item>
|
||||
<item>39000 Hz</item>
|
||||
<item>46900 Hz</item>
|
||||
<item>58600 Hz</item>
|
||||
<item>78200 Hz</item>
|
||||
<item>93800 Hz</item>
|
||||
<item>117300 Hz</item>
|
||||
<item>156200 Hz</item>
|
||||
<item>187200 Hz</item>
|
||||
<item>234300 Hz</item>
|
||||
<item>312000 Hz</item>
|
||||
<item>373000 Hz</item>
|
||||
<item>467000 Hz</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="fsk_rx_bw_values">
|
||||
<item>4800</item>
|
||||
<item>5800</item>
|
||||
<item>7300</item>
|
||||
<item>9700</item>
|
||||
<item>11700</item>
|
||||
<item>14600</item>
|
||||
<item>19500</item>
|
||||
<item>23400</item>
|
||||
<item>29300</item>
|
||||
<item>39000</item>
|
||||
<item>46900</item>
|
||||
<item>58600</item>
|
||||
<item>78200</item>
|
||||
<item>93800</item>
|
||||
<item>117300</item>
|
||||
<item>156200</item>
|
||||
<item>187200</item>
|
||||
<item>234300</item>
|
||||
<item>312000</item>
|
||||
<item>373000</item>
|
||||
<item>467000</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="lora_power_entries">
|
||||
<item>-9 dBm</item>
|
||||
<item>-8 dBm</item>
|
||||
<item>-7 dBm</item>
|
||||
<item>-6 dBm</item>
|
||||
<item>-5 dBm</item>
|
||||
<item>-4 dBm</item>
|
||||
<item>-3 dBm</item>
|
||||
<item>-2 dBm</item>
|
||||
<item>-1 dBm</item>
|
||||
<item>0 dBm</item>
|
||||
<item>1 dBm</item>
|
||||
<item>2 dBm</item>
|
||||
<item>3 dBm</item>
|
||||
<item>4 dBm</item>
|
||||
|
@ -300,6 +399,17 @@
|
|||
</string-array>
|
||||
|
||||
<string-array name="lora_power_values">
|
||||
<item>-9</item>
|
||||
<item>-8</item>
|
||||
<item>-7</item>
|
||||
<item>-6</item>
|
||||
<item>-5</item>
|
||||
<item>-4</item>
|
||||
<item>-3</item>
|
||||
<item>-2</item>
|
||||
<item>-1</item>
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
|
|
|
@ -12,13 +12,24 @@
|
|||
<string name="usb_bt_client_name_title">Bluetooth device</string>
|
||||
<string name="usb_bt_client_name_summary">Default Bluetooth device to connect</string>
|
||||
|
||||
<string name="codec2_category_title">Codec2 settings</string>
|
||||
<string name="codec_category_title">Audio Codec Settings</string>
|
||||
<string name="codec_type_title">Select codec type</string>
|
||||
<string name="codec_type_settings_title">Codec settings</string>
|
||||
<string name="codec_type_settings_summary">Change selected codec settings, such as bit rate, frame size</string>
|
||||
|
||||
<string name="codec2_category_title">Codec2 Settings</string>
|
||||
<string name="codec2_mode_title">Mode/Speed</string>
|
||||
<string name="codec2_test_mode_title">Loopback test mode</string>
|
||||
<string name="codec2_test_mode_summary">Records and plays recording without transmission</string>
|
||||
<string name="codec2_recorder_title">Enable recorder</string>
|
||||
<string name="codec2_recorder_summary">Record incoming and outgoing transmissions for future playback</string>
|
||||
|
||||
<string name="opus_category_title">OPUS Settings</string>
|
||||
<string name="opus_frame_size_title">PCM frame duration [ms]</string>
|
||||
<string name="opus_bit_rate_title">Bit rate</string>
|
||||
<string name="opus_bit_rate_summary">Bit rate from 2400 up to 512000bps, set larger frame duration below for smaller bit rates</string>
|
||||
<string name="opus_complexity_title">Complexity</string>
|
||||
|
||||
<string name="main_status_loopback_test">Loopback</string>
|
||||
<string name="main_status_stop">STOPPED</string>
|
||||
<string name="main_status_tx">TRANSMITTING</string>
|
||||
|
@ -54,11 +65,24 @@
|
|||
<string name="kiss_extensions_enable_title">Enable extensions</string>
|
||||
<string name="kiss_extensions_enable_summary">Enable radio control and signal level reports</string>
|
||||
|
||||
<string name="kiss_extensions_radio_mod_title">Select radio modulation type</string>
|
||||
|
||||
<string name="kiss_extensions_radio_control_title">Set radio parameters</string>
|
||||
<string name="kiss_extensions_radio_control_summary">Set frequency, bandwidth and other radio parameters</string>
|
||||
|
||||
<string name="kiss_extensions_radio_category_lora_title">Set LoRa modulation parameters</string>
|
||||
<string name="kiss_extensions_radio_category_fsk_title">Set FSK modulation parameters</string>
|
||||
|
||||
<string name="kiss_extensions_radio_category_other_title">Modem control</string>
|
||||
|
||||
<string name="kiss_extension_radio_split_freq_title">Split frequency operation</string>
|
||||
<string name="kiss_extension_radio_split_freq_summary">Use separate frequencies for RX and TX</string>
|
||||
|
||||
<string name="kiss_extensions_radio_frequency_title">Frequency (Hz)</string>
|
||||
<string name="kiss_extensions_radio_frequency_summary">Set radio frequency</string>
|
||||
<string name="kiss_extensions_radio_frequency_summary">Set radio frequency for RX/TX or RX frequency when split operation is enabled</string>
|
||||
|
||||
<string name="kiss_extensions_radio_frequency_title_tx">Frequency TX (Hz)</string>
|
||||
<string name="kiss_extensions_radio_frequency_summary_tx">Set transmit radio frequency</string>
|
||||
|
||||
<string name="kiss_extensions_radio_bandwidth_title">Bandwidth (Hz)</string>
|
||||
<string name="kiss_extensions_radio_bandwidth_summary">Set radio bandwidth</string>
|
||||
|
@ -78,10 +102,18 @@
|
|||
<string name="kiss_extensions_radio_crc_title">Enable CRC check</string>
|
||||
<string name="kiss_extensions_radio_crc_summary">Enable packet CRC check</string>
|
||||
|
||||
<string name="kiss_extension_radio_fsk_bit_rate_title">Bit rate (600–300000 bps)</string>
|
||||
<string name="kiss_extension_radio_fsk_bit_rate_summary">Set bit rate</string>
|
||||
|
||||
<string name="kiss_extension_radio_fsk_freq_dev_title">Set freq deviation (600–200000 Hz)</string>
|
||||
<string name="kiss_extension_radio_fsk_freq_dev_summary">Set freq deviation</string>
|
||||
|
||||
<string name="kiss_extension_radio_fsk_rx_bw_title">Set receive bandwidth (Hz)</string>
|
||||
|
||||
<string name="app_volume_ptt_title">Use volume keys for PTT</string>
|
||||
<string name="app_volume_ptt_summary">Volume up/down keys will be used for PTT</string>
|
||||
|
||||
<string name="app_category_title">Application settings</string>
|
||||
<string name="app_category_title">Application Settings</string>
|
||||
|
||||
<string name="app_keep_screen_on_title">Keep screen ON</string>
|
||||
<string name="app_keep_screen_on_summary">Prevent screen switching off when app is active</string>
|
||||
|
@ -148,7 +180,7 @@
|
|||
<string name="kiss_toast_modem_reboot">Modem reboot requested</string>
|
||||
|
||||
<string name="codec2_tx_frame_max_size_title">Maximum super frame size (bytes)</string>
|
||||
<string name="codec2_tx_frame_max_size_summary">Multiple Codec2 samples are aggregated into super frame not larger than this value</string>
|
||||
<string name="codec2_tx_frame_max_size_summary">Multiple Codec2 encoded samples are aggregated into super frame not larger than this value</string>
|
||||
|
||||
<string name="usb_settings_title">USB settings</string>
|
||||
<string name="usb_data_bits_title">Serial data bits</string>
|
||||
|
@ -161,6 +193,15 @@
|
|||
<string name="usb_serial_title">USB serial settings</string>
|
||||
<string name="usb_serial_summary">Set USB serial settings, such as speed, bits, parity, etc.</string>
|
||||
|
||||
<string name="tnc_extended_title">Extended TNC settings</string>
|
||||
<string name="tnc_extended_summary">Additional TNC specific settings</string>
|
||||
<string name="tnc_extended_uart_title">UART modem prefix</string>
|
||||
|
||||
<string name="custom_prefix_enabled_title">Enable UART modem TX prefix</string>
|
||||
<string name="custom_prefix_enabled_summary">Prefix data with the HEX string, used in fixed transmission mode by some UART modems to specify transmission target</string>
|
||||
<string name="custom_prefix_title">Packet prefix value as a HEX string</string>
|
||||
<string name="custom_prefix_summary">Prefix content as a HEX string, e.g. C0FFEE</string>
|
||||
|
||||
<string name="app_audio_output_speaker_title">Play audio through the speaker</string>
|
||||
<string name="app_audio_output_speaker_summary">Output incoming audio through the speaker</string>
|
||||
<string name="app_audio_input_voice_communication_title">Microphone enhancements</string>
|
||||
|
@ -232,6 +273,7 @@
|
|||
<string name="menu_send_position">Send position</string>
|
||||
|
||||
<string name="voax25_label">☎</string>
|
||||
<string name="text_packets_label">🔔</string>
|
||||
|
||||
<string name="menu_aprs_log">View log</string>
|
||||
|
||||
|
@ -337,7 +379,9 @@
|
|||
<string name="aprs_is_settings_title">Internet APRS-IS</string>
|
||||
<string name="aprs_is_settings_summary">Configure internet APRS-IS server connectivity</string>
|
||||
<string name="aprs_is_tcpip_server_title">Server</string>
|
||||
<string name="aprs_is_tcpip_server_summary">APRS-IS TCP server (port 14580) to connect</string>
|
||||
<string name="aprs_is_tcpip_server_summary">APRS-IS TCP server to connect</string>
|
||||
<string name="aprs_is_tcpip_server_port_title">Server port</string>
|
||||
<string name="aprs_is_tcpip_server_port_summary">APRS-IS TCP server port to use</string>
|
||||
<string name="aprs_is_enable_rx_gate_title">Enable APRS-IS RX gate</string>
|
||||
<string name="aprs_is_enable_tx_gate_title">Enable APRS-IS TX gate</string>
|
||||
<string name="aprs_is_enable_rx_gate_summary">Radio packets will be forwarded to APRS-IS</string>
|
||||
|
@ -354,4 +398,19 @@
|
|||
<string name="aprsis_wrong_pass">APRS-IS wrong pass</string>
|
||||
<string name="aprs_is_enable_self_title">Enable own APRS to APRS-IS</string>
|
||||
<string name="aprs_is_enable_self_summary">Send own APRS data to APRS-IS in addition to currently selected transport</string>
|
||||
<string name="menu_aprs_map">Map</string>
|
||||
<string name="map_menu_clear_cache_title">Clear map tile cache</string>
|
||||
<string name="log_view_menu_clear_title">Clear log</string>
|
||||
<string name="log_view_menu_clear_1h">Older than 1 hour</string>
|
||||
<string name="log_view_menu_clear_12h">Older than 12 hours</string>
|
||||
<string name="log_view_menu_clear_1d">Older than 1 day</string>
|
||||
<string name="log_view_menu_clear_7d">Older than 7 days</string>
|
||||
<string name="log_view_menu_clear_all">Clear all</string>
|
||||
<string name="log_item_activity_delete_hours_title">This will remove everything older than %d hours. Are you sure?</string>
|
||||
<string name="map_menu_rotate_map">Rotate map with compass</string>
|
||||
<string name="map_menu_show_range">Show range circles</string>
|
||||
<string name="map_menu_show_moving">Show moving stations</string>
|
||||
<string name="aprs_text_packets_enable_title">Enable text packets</string>
|
||||
<string name="aprs_text_packets_enable_summary">Send lora aprs compatible text packets (0x3c,0xff,0x01 prefix)</string>
|
||||
<string name="map_menu_move_map">Move map with own position</string>
|
||||
</resources>
|
|
@ -15,12 +15,11 @@
|
|||
<!-- 0x067B / 0x2303: Prolific PL2303 -->
|
||||
<usb-device vendor-id="1659" product-id="8963" />
|
||||
|
||||
<!-- 0x1a86 / 0x?523: Qinheng CH34x -->
|
||||
<!-- 0x1a86 / 0x?523: Qinheng CH34x (Arduino) -->
|
||||
<usb-device vendor-id="6790" product-id="21795" /> <!-- 0x5523: CH341A -->
|
||||
<usb-device vendor-id="6790" product-id="29987" /> <!-- 0x7523: CH340 -->
|
||||
|
||||
<!-- CDC driver -->
|
||||
<usb-device vendor-id="9025" /> <!-- 0x2341 / ......: Arduino -->
|
||||
<usb-device vendor-id="5824" product-id="1155" /> <!-- 0x16C0 / 0x0483: Teensyduino -->
|
||||
<usb-device vendor-id="1003" product-id="8260" /> <!-- 0x03EB / 0x2044: Atmel Lufa -->
|
||||
<usb-device vendor-id="7855" product-id="4" /> <!-- 0x1eaf / 0x0004: Leaflabs Maple -->
|
||||
|
@ -29,8 +28,13 @@
|
|||
<!-- spark fun -->
|
||||
<usb-device vendor-id="6991" /> <!-- 0x1b4f / ......: Spark Fun -->
|
||||
|
||||
<!-- arduino -->
|
||||
<usb-device vendor-id="9025" /> <!-- 0x2341 / ......: Arduino -->
|
||||
|
||||
<!-- RIG CAT control -->
|
||||
<usb-device vendor-id="1155" product-id="22322" /> <!-- 0x0483 / 0x5732: STM, MCHF -->
|
||||
<usb-device vendor-id="4292" product-id="60000" /> <!-- 0x10c4 / 0xea60: CP2102/2109, iCom -->
|
||||
|
||||
<!-- Raspberry PI -->
|
||||
<usb-device vendor-id="11914" /> <!-- 0x2E8A / ......: Raspberry -->
|
||||
</resources>
|
|
@ -112,28 +112,34 @@
|
|||
app:fragment="com.radio.codec2talkie.settings.SettingsActivity$SettingsSoundModemFragment">
|
||||
</Preference>
|
||||
|
||||
<Preference
|
||||
app:key="ports_tnc_extended"
|
||||
app:title="@string/tnc_extended_title"
|
||||
app:summary="@string/tnc_extended_summary"
|
||||
app:fragment="com.radio.codec2talkie.settings.SettingsActivity$SettingsTncExtendedFragment">
|
||||
</Preference>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="codec2_category"
|
||||
app:title="@string/codec2_category_title">
|
||||
app:key="codec_category"
|
||||
app:title="@string/codec_category_title">
|
||||
|
||||
<ListPreference
|
||||
app:key="codec2_mode"
|
||||
app:title="@string/codec2_mode_title"
|
||||
app:entries="@array/codec2_modes"
|
||||
app:entryValues="@array/codec2_modes"
|
||||
app:defaultValue="MODE_450=10"
|
||||
app:key="codec_type"
|
||||
app:title="@string/codec_type_title"
|
||||
app:entries="@array/codec_type_modes"
|
||||
app:entryValues="@array/codec_type_modes"
|
||||
app:defaultValue="Codec2"
|
||||
app:summary="%s">
|
||||
</ListPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="codec2_tx_frame_max_size"
|
||||
app:title="@string/codec2_tx_frame_max_size_title"
|
||||
app:summary="@string/codec2_tx_frame_max_size_summary"
|
||||
app:defaultValue="48">
|
||||
</EditTextPreference>
|
||||
|
||||
<Preference
|
||||
app:key="codec_type_settings"
|
||||
app:title="@string/codec_type_settings_title"
|
||||
app:summary="@string/codec_type_settings_summary"
|
||||
app:fragment="com.radio.codec2talkie.settings.SettingsActivity$SettingsCodecFragment">
|
||||
</Preference>
|
||||
|
||||
<SwitchPreference
|
||||
app:key="codec2_recording_enabled"
|
||||
app:title="@string/codec2_recorder_title"
|
||||
|
@ -215,6 +221,14 @@
|
|||
app:defaultValue="true">
|
||||
</SwitchPreference>
|
||||
|
||||
<SwitchPreference
|
||||
app:key="aprs_text_packets_enable"
|
||||
app:title="@string/aprs_text_packets_enable_title"
|
||||
app:summary="@string/aprs_text_packets_enable_summary"
|
||||
app:dependency="aprs_enable"
|
||||
app:defaultValue="false">
|
||||
</SwitchPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="aprs_callsign"
|
||||
app:title="@string/aprs_callsign_title"
|
||||
|
|
|
@ -18,6 +18,13 @@
|
|||
app:defaultValue="euro.aprs2.net">
|
||||
</EditTextPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="aprs_is_tcpip_server_port"
|
||||
app:title="@string/aprs_is_tcpip_server_port_title"
|
||||
app:summary="@string/aprs_is_tcpip_server_port_summary"
|
||||
app:defaultValue="14580">
|
||||
</EditTextPreference>
|
||||
|
||||
<SwitchPreference
|
||||
app:key="aprs_is_enable_rx_gate"
|
||||
app:title="@string/aprs_is_enable_rx_gate_title"
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="codec2_category"
|
||||
app:title="@string/codec2_category_title">
|
||||
|
||||
<ListPreference
|
||||
app:key="codec2_mode"
|
||||
app:title="@string/codec2_mode_title"
|
||||
app:entries="@array/codec2_modes"
|
||||
app:entryValues="@array/codec2_modes"
|
||||
app:defaultValue="MODE_450=10"
|
||||
app:summary="%s">
|
||||
</ListPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="codec2_tx_frame_max_size"
|
||||
app:title="@string/codec2_tx_frame_max_size_title"
|
||||
app:summary="@string/codec2_tx_frame_max_size_summary"
|
||||
app:defaultValue="48">
|
||||
</EditTextPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="opus_category"
|
||||
app:title="@string/opus_category_title">
|
||||
|
||||
<EditTextPreference
|
||||
app:key="opus_bit_rate"
|
||||
app:title="@string/opus_bit_rate_title"
|
||||
app:summary="@string/opus_bit_rate_summary"
|
||||
app:defaultValue="3200">
|
||||
</EditTextPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="opus_frame_size"
|
||||
app:title="@string/opus_frame_size_title"
|
||||
app:entries="@array/opus_frame_size"
|
||||
app:entryValues="@array/opus_frame_size"
|
||||
app:defaultValue="40"
|
||||
app:summary="%s">
|
||||
</ListPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="opus_complexity"
|
||||
app:title="@string/opus_complexity_title"
|
||||
app:entries="@array/opus_complexity_value"
|
||||
app:entryValues="@array/opus_complexity_value"
|
||||
app:defaultValue="5"
|
||||
app:summary="%s">
|
||||
</ListPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
|
@ -13,14 +13,21 @@
|
|||
app:defaultValue="433775000">
|
||||
</EditTextPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_bandwidth"
|
||||
app:title="@string/kiss_extensions_radio_bandwidth_title"
|
||||
app:entries="@array/lora_bandwidths"
|
||||
app:entryValues="@array/lora_bandwidths"
|
||||
app:defaultValue="125000"
|
||||
app:summary="%s">
|
||||
</ListPreference>
|
||||
<SwitchPreference
|
||||
app:key="kiss_extension_radio_split_freq"
|
||||
app:title="@string/kiss_extension_radio_split_freq_title"
|
||||
app:summary="@string/kiss_extension_radio_split_freq_summary"
|
||||
app:defaultValue="false">
|
||||
</SwitchPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="kiss_extension_radio_frequency_tx"
|
||||
app:title="@string/kiss_extensions_radio_frequency_title_tx"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:summary="@string/kiss_extensions_radio_frequency_summary_tx"
|
||||
app:dependency="kiss_extension_radio_split_freq"
|
||||
app:defaultValue="433775000">
|
||||
</EditTextPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_power"
|
||||
|
@ -32,43 +39,104 @@
|
|||
</ListPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_sf"
|
||||
app:title="@string/kiss_extensions_radio_sf_title"
|
||||
app:key="kiss_extension_radio_mod"
|
||||
app:title="@string/kiss_extensions_radio_mod_title"
|
||||
app:entries="@array/radio_mod_entries"
|
||||
app:entryValues="@array/radio_mod_values"
|
||||
app:summary="%s"
|
||||
app:entries="@array/lora_spreading_factors"
|
||||
app:entryValues="@array/lora_spreading_factors"
|
||||
app:defaultValue="7">
|
||||
app:defaultValue="0">
|
||||
</ListPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_cr"
|
||||
app:title="@string/kiss_extensions_radio_cr_title"
|
||||
app:summary="%s"
|
||||
app:entries="@array/lora_coding_rates"
|
||||
app:entryValues="@array/lora_coding_rates"
|
||||
app:defaultValue="6">
|
||||
</ListPreference>
|
||||
<PreferenceCategory
|
||||
app:key="kiss_extensions_radio_category_lora"
|
||||
app:title="@string/kiss_extensions_radio_category_lora_title">
|
||||
|
||||
<EditTextPreference
|
||||
app:key="kiss_extension_radio_sync"
|
||||
app:title="@string/kiss_extensions_radio_sync_title"
|
||||
app:summary="@string/kiss_extensions_radio_sync_summary"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:defaultValue="34">
|
||||
</EditTextPreference>
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_bandwidth"
|
||||
app:title="@string/kiss_extensions_radio_bandwidth_title"
|
||||
app:entries="@array/lora_bandwidths"
|
||||
app:entryValues="@array/lora_bandwidths"
|
||||
app:defaultValue="125000"
|
||||
app:summary="%s">
|
||||
</ListPreference>
|
||||
|
||||
<CheckBoxPreference
|
||||
app:key="kiss_extension_radio_crc"
|
||||
app:title="@string/kiss_extensions_radio_crc_title"
|
||||
app:summary="@string/kiss_extensions_radio_crc_summary"
|
||||
app:defaultValue="true">
|
||||
</CheckBoxPreference>
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_sf"
|
||||
app:title="@string/kiss_extensions_radio_sf_title"
|
||||
app:summary="%s"
|
||||
app:entries="@array/lora_spreading_factors"
|
||||
app:entryValues="@array/lora_spreading_factors"
|
||||
app:defaultValue="7">
|
||||
</ListPreference>
|
||||
|
||||
<Preference
|
||||
app:key="kiss_extension_reboot"
|
||||
app:title="@string/kiss_extension_reboot_title"
|
||||
app:summary="@string/kiss_extension_reboot_summary">
|
||||
</Preference>
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_cr"
|
||||
app:title="@string/kiss_extensions_radio_cr_title"
|
||||
app:summary="%s"
|
||||
app:entries="@array/lora_coding_rates"
|
||||
app:entryValues="@array/lora_coding_rates"
|
||||
app:defaultValue="6">
|
||||
</ListPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="kiss_extension_radio_sync"
|
||||
app:title="@string/kiss_extensions_radio_sync_title"
|
||||
app:summary="@string/kiss_extensions_radio_sync_summary"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:defaultValue="34">
|
||||
</EditTextPreference>
|
||||
|
||||
<CheckBoxPreference
|
||||
app:key="kiss_extension_radio_crc"
|
||||
app:title="@string/kiss_extensions_radio_crc_title"
|
||||
app:summary="@string/kiss_extensions_radio_crc_summary"
|
||||
app:defaultValue="true">
|
||||
</CheckBoxPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="kiss_extensions_radio_category_fsk"
|
||||
app:title="@string/kiss_extensions_radio_category_fsk_title">
|
||||
|
||||
<EditTextPreference
|
||||
app:key="kiss_extension_radio_fsk_bit_rate"
|
||||
app:title="@string/kiss_extension_radio_fsk_bit_rate_title"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:summary="@string/kiss_extension_radio_fsk_bit_rate_summary"
|
||||
app:defaultValue="4800">
|
||||
</EditTextPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="kiss_extension_radio_fsk_freq_dev"
|
||||
app:title="@string/kiss_extension_radio_fsk_freq_dev_title"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
app:summary="@string/kiss_extension_radio_fsk_freq_dev_summary"
|
||||
app:defaultValue="1200">
|
||||
</EditTextPreference>
|
||||
|
||||
<ListPreference
|
||||
app:key="kiss_extension_radio_fsk_rx_bw"
|
||||
app:title="@string/kiss_extension_radio_fsk_rx_bw_title"
|
||||
app:entries="@array/fsk_rx_bw_entries"
|
||||
app:entryValues="@array/fsk_rx_bw_values"
|
||||
app:summary="%s"
|
||||
app:defaultValue="9700">
|
||||
</ListPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="kiss_extensions_radio_category_other"
|
||||
app:title="@string/kiss_extensions_radio_category_other_title">
|
||||
|
||||
<Preference
|
||||
app:key="kiss_extension_reboot"
|
||||
app:title="@string/kiss_extension_reboot_title"
|
||||
app:summary="@string/kiss_extension_reboot_summary">
|
||||
</Preference>
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<PreferenceCategory
|
||||
app:key="tnc_extended_uart"
|
||||
app:title="@string/tnc_extended_uart_title">
|
||||
|
||||
<CheckBoxPreference
|
||||
app:key="custom_prefix_enabled"
|
||||
app:title="@string/custom_prefix_enabled_title"
|
||||
app:summary="@string/custom_prefix_enabled_summary"
|
||||
app:defaultValue="false">
|
||||
</CheckBoxPreference>
|
||||
|
||||
<EditTextPreference
|
||||
app:key="custom_prefix"
|
||||
app:title="@string/custom_prefix_title"
|
||||
app:summary = "@string/custom_prefix_summary"
|
||||
app:dependency="custom_prefix_enabled"
|
||||
app:defaultValue="C0FFEE">
|
||||
</EditTextPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
|
@ -54,5 +54,6 @@
|
|||
app:summary="@string/usb_rts_summary"
|
||||
app:defaultValue="false">
|
||||
</CheckBoxPreference>
|
||||
|
||||
</PreferenceCategory>
|
||||
</PreferenceScreen>
|
Plik binarny nie jest wyświetlany.
Po Szerokość: | Wysokość: | Rozmiar: 806 KiB |
|
@ -74,12 +74,12 @@ task compileCodec2 {
|
|||
doLast {
|
||||
exec {
|
||||
workingDir "$projectDir/build/codec2_build_linux"
|
||||
commandLine "/usr/bin/cmake", "$projectDir/src/codec2"
|
||||
commandLine "cmake", "$projectDir/src/codec2"
|
||||
}
|
||||
|
||||
exec {
|
||||
workingDir "$projectDir/build/codec2_build_linux"
|
||||
commandLine "/usr/bin/make"
|
||||
commandLine "make"
|
||||
}
|
||||
|
||||
for(String abi : rootProject.ext.ABI_FILTERS.split(";")) {
|
||||
|
@ -96,7 +96,7 @@ task compileCodec2 {
|
|||
|
||||
exec {
|
||||
workingDir "$projectDir/build/codec2_build_android_" + abi
|
||||
commandLine "/usr/bin/cmake", "--build", "."
|
||||
commandLine "cmake", "--build", "."
|
||||
}
|
||||
|
||||
copy {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
/build
|
||||
**/.cxx
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
apply plugin: 'digital.wup.android-maven-publish'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath "digital.wup:android-maven-publish:3.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
cppFlags ""
|
||||
}
|
||||
}
|
||||
ndk {
|
||||
abiFilters = []
|
||||
abiFilters.addAll(rootProject.ext.ABI_FILTERS.split(';').collect{it as String})
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['build/imported-lib']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// As per: https://github.com/googlesamples/android-ndk/blob/master/hello-libs/app/build.gradle
|
||||
tasks.whenTaskAdded { task ->
|
||||
if (task.name == 'externalNativeBuildRelease') {
|
||||
task.dependsOn compileOpus
|
||||
} else if (task.name == 'externalNativeBuildDebug') {
|
||||
task.dependsOn compileOpus
|
||||
}
|
||||
}
|
||||
|
||||
task compileOpus {
|
||||
doFirst {
|
||||
project.file("build/opus_build_linux").mkdirs()
|
||||
for(String abi : rootProject.ext.ABI_FILTERS.split(";")) {
|
||||
project.file("build/opus_build_android_" + abi).mkdirs()
|
||||
project.file("build/imported-lib/"+abi).mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
doLast {
|
||||
exec {
|
||||
workingDir "$projectDir/build/opus_build_linux"
|
||||
commandLine "cmake", "$projectDir/src/opus"
|
||||
}
|
||||
|
||||
exec {
|
||||
workingDir "$projectDir/build/opus_build_linux"
|
||||
commandLine "make"
|
||||
}
|
||||
|
||||
for(String abi : rootProject.ext.ABI_FILTERS.split(";")) {
|
||||
System.out.println("Handle abi " + abi)
|
||||
exec {
|
||||
workingDir "$projectDir/build/opus_build_android_" + abi
|
||||
commandLine "cmake", "$projectDir/src/opus",
|
||||
"-DOPUS_BUILD_SHARED_LIBRARY=true",
|
||||
"-DCMAKE_TOOLCHAIN_FILE=" + android.ndkDirectory + "/build/cmake/android.toolchain.cmake",
|
||||
"-DANDROID_NATIVE_API_LEVEL=23", "-DANDROID_ABI="+abi,
|
||||
"-DANDROID_STL=c++_shared"
|
||||
}
|
||||
|
||||
exec {
|
||||
workingDir "$projectDir/build/opus_build_android_" + abi
|
||||
commandLine "cmake", "--build", "."
|
||||
}
|
||||
|
||||
copy {
|
||||
from "$projectDir/build/opus_build_android_" + abi +"/libopus.so"
|
||||
into "$projectDir/build/imported-lib/"+abi
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'com.android.support:support-annotations:28.0.0'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.2'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
|
||||
}
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
mavenAar(MavenPublication) {
|
||||
from components.android
|
||||
|
||||
groupId rootProject.group
|
||||
artifactId project.name
|
||||
version "${rootProject.version}"
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
//Set this in projectdir/extrasettings.gradle
|
||||
maven {
|
||||
url rootProject.file(rootProject.ext.buildConfigProperties['repo.dir']).getAbsolutePath()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.radio.opus">
|
||||
<application>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,41 @@
|
|||
# For more information about using CMake with Android Studio, read the
|
||||
# documentation: https://d.android.com/studio/projects/add-native-code.html
|
||||
|
||||
# Sets the minimum version of CMake required to build the native library.
|
||||
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
set(main_DIR ${CMAKE_SOURCE_DIR})
|
||||
add_library(libopus SHARED IMPORTED)
|
||||
set_target_properties(libopus PROPERTIES IMPORTED_LOCATION
|
||||
${main_DIR}/../../../build/imported-lib/${ANDROID_ABI}/libopus.so)
|
||||
|
||||
include_directories(${main_DIR}/opus/)
|
||||
|
||||
|
||||
# Searches for a specified prebuilt library and stores the path as a
|
||||
## variable. Because CMake includes system libraries in the search path by
|
||||
# default, you only need to specify the name of the public NDK library
|
||||
# you want to add. CMake verifies that the library exists before
|
||||
# completing its build.
|
||||
|
||||
|
||||
add_library( # Sets the name of the library.
|
||||
OpusJNI
|
||||
## Sets the library as a shared library.
|
||||
SHARED
|
||||
## Provides a relative path to your source file(s).
|
||||
OpusJNI.cpp)
|
||||
|
||||
find_library( # Sets the name of the path variable.
|
||||
log-lib
|
||||
# Specifies the name of the NDK library that
|
||||
# you want CMake to locate.
|
||||
log)
|
||||
|
||||
# Specifies libraries CMake should link to your target library. You
|
||||
# can link multiple libraries, such as libraries you define in this
|
||||
# build script, prebuilt third-party libraries, or system libraries.
|
||||
target_link_libraries(
|
||||
OpusJNI
|
||||
libopus
|
||||
${log-lib})
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue