Merge pull request #42 from sh123/aprs

Implement APRS/AX.25 data packet digirepeating
pull/44/head
sh123 2022-07-09 16:48:21 +03:00 zatwierdzone przez GitHub
commit 8e716d658b
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
15 zmienionych plików z 156 dodań i 67 usunięć

Wyświetl plik

@ -443,6 +443,12 @@ public class MainActivity extends AppCompatActivity {
if (voax25Enabled) {
status += getString(R.string.voax25_label);
}
// Digirepeater
boolean isDigirepeaterEnabled = _sharedPreferences.getBoolean(PreferenceKeys.APRS_DIGIREPEATER_ENABLED, false);
if (isDigirepeaterEnabled) {
status += getString(R.string.digirepeater_label);
}
}
if (_appService != null) {

Wyświetl plik

@ -16,11 +16,12 @@ public enum AppMessage {
EV_STARTED_TRACKING(11),
EV_STOPPED_TRACKING(12),
// commands
CMD_SEND_LOCATION(13),
CMD_SEND_LOCATION_TO_TNC(13),
CMD_PROCESS(14),
CMD_QUIT(15),
CMD_START_TRACKING(16),
CMD_STOP_TRACKING(17);
CMD_STOP_TRACKING(17),
CMD_SEND_SINGLE_TRACKING(18);
private final int _value;

Wyświetl plik

@ -80,7 +80,7 @@ public class AppService extends Service {
public void sendPosition() {
Message msg = Message.obtain();
msg.what = AppMessage.CMD_SEND_LOCATION.toInt();
msg.what = AppMessage.CMD_SEND_SINGLE_TRACKING.toInt();
_onProcess.sendMessage(msg);
}
@ -146,7 +146,7 @@ public class AppService extends Service {
String trackerName = _sharedPreferences.getString(PreferenceKeys.APRS_LOCATION_SOURCE, "manual");
_tracker = TrackerFactory.create(trackerName);
_tracker.initialize(getApplicationContext(), position -> { if (_appWorker != null) _appWorker.sendPosition(position); });
_tracker.initialize(getApplicationContext(), position -> { if (_appWorker != null) _appWorker.sendPositionToTnc(position); });
transportType = (TransportFactory.TransportType) extras.get("transportType");
startAppWorker(transportType);
@ -206,7 +206,7 @@ public class AppService extends Service {
case EV_LISTENING:
hideVoiceNotification();
break;
case CMD_SEND_LOCATION:
case CMD_SEND_SINGLE_TRACKING:
_tracker.sendPosition();
break;
case CMD_START_TRACKING:

Wyświetl plik

@ -21,16 +21,13 @@ import java.util.TimerTask;
import com.radio.codec2talkie.R;
import com.radio.codec2talkie.log.LogItem;
import com.radio.codec2talkie.log.LogItemDatabase;
import com.radio.codec2talkie.log.LogItemRepository;
import com.radio.codec2talkie.protocol.ProtocolCallback;
import com.radio.codec2talkie.protocol.Protocol;
import com.radio.codec2talkie.protocol.ProtocolFactory;
import com.radio.codec2talkie.protocol.aprs.AprsCallsign;
import com.radio.codec2talkie.protocol.position.Position;
import com.radio.codec2talkie.settings.PreferenceKeys;
import com.radio.codec2talkie.tools.AudioTools;
import com.radio.codec2talkie.tools.DebugTools;
import com.radio.codec2talkie.transport.Transport;
import com.radio.codec2talkie.transport.TransportFactory;
@ -40,12 +37,12 @@ public class AppWorker extends Thread {
private static final int AUDIO_MIN_LEVEL = -70;
private static final int AUDIO_MAX_LEVEL = 0;
private final int AUDIO_SAMPLE_SIZE = 8000;
private static final int AUDIO_SAMPLE_SIZE = 8000;
private final int PROCESS_INTERVAL_MS = 20;
private final int LISTEN_AFTER_MS = 1500;
private static final int PROCESS_INTERVAL_MS = 20;
private static final int LISTEN_AFTER_MS = 1500;
private boolean _needsRecording = false;
private boolean _needTransmission = false;
private AppMessage _currentStatus = AppMessage.EV_DISCONNECTED;
private final Protocol _protocol;
@ -150,11 +147,11 @@ public class AppWorker extends Thread {
}
public void startReceive() {
_needsRecording = false;
_needTransmission = false;
}
public void startTransmit() {
_needsRecording = true;
_needTransmission = true;
}
public void stopRunning() {
@ -165,10 +162,10 @@ public class AppWorker extends Thread {
_onMessageReceived.sendMessage(msg);
}
public void sendPosition(Position position) {
public void sendPositionToTnc(Position position) {
if (_currentStatus == AppMessage.EV_DISCONNECTED) return;
Message msg = new Message();
msg.what = AppMessage.CMD_SEND_LOCATION.toInt();
msg.what = AppMessage.CMD_SEND_LOCATION_TO_TNC.toInt();
msg.obj = position;
_onMessageReceived.sendMessage(msg);
}
@ -341,13 +338,13 @@ public class AppWorker extends Thread {
private void processRecordPlaybackToggle() throws IOException {
// playback -> recording
if (_needsRecording && _systemAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
if (_needTransmission && _systemAudioRecorder.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) {
_systemAudioPlayer.stop();
_systemAudioRecorder.startRecording();
sendRxAudioLevelUpdate(null);
}
// recording -> playback
if (!_needsRecording && _systemAudioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
if (!_needTransmission && _systemAudioRecorder.getRecordingState() == AudioRecord.RECORDSTATE_RECORDING) {
_protocol.flush();
_systemAudioRecorder.stop();
_systemAudioPlayer.play();
@ -398,7 +395,7 @@ public class AppWorker extends Thread {
Looper.myLooper().quitSafely();
}
private void onProcessorIncomingMessage(Message msg) {
private void onWorkerIncomingMessage(Message msg) {
switch (AppMessage.values()[msg.what]) {
case CMD_PROCESS:
try {
@ -411,7 +408,7 @@ public class AppWorker extends Thread {
case CMD_QUIT:
quitProcessing();
break;
case CMD_SEND_LOCATION:
case CMD_SEND_LOCATION_TO_TNC:
try {
_protocol.sendPosition((Position)msg.obj);
} catch (IOException e) {
@ -424,11 +421,11 @@ public class AppWorker extends Thread {
}
}
private void startProcessorMessageHandler() {
private void startWorkerMessageHandler() {
_onMessageReceived = new Handler(Looper.myLooper()) {
@Override
public void handleMessage(Message msg) {
onProcessorIncomingMessage(msg);
onWorkerIncomingMessage(msg);
}
};
_processPeriodicTimer.schedule(new TimerTask() {
@ -452,7 +449,7 @@ public class AppWorker extends Thread {
try {
_protocol.initialize(_transport, _context, _protocolCallback);
startProcessorMessageHandler();
startWorkerMessageHandler();
Looper.loop();
} catch (IOException e) {
e.printStackTrace();

Wyświetl plik

@ -9,7 +9,6 @@ import androidx.preference.PreferenceManager;
import com.radio.codec2talkie.protocol.ax25.AX25Packet;
import com.radio.codec2talkie.protocol.position.Position;
import com.radio.codec2talkie.settings.PreferenceKeys;
import com.radio.codec2talkie.tools.DebugTools;
import com.radio.codec2talkie.transport.Transport;
import java.io.IOException;
@ -21,6 +20,7 @@ public class Ax25 implements Protocol {
final Protocol _childProtocol;
private String _digipath;
private boolean _isVoax25Enabled;
private boolean _isDigiRepeaterEnabled;
private ProtocolCallback _parentProtocolCallback;
@ -36,6 +36,7 @@ public class Ax25 implements Protocol {
// NOTE, may need to pass through sendData/sendAudio
_digipath = sharedPreferences.getString(PreferenceKeys.APRS_DIGIPATH, "");
_isVoax25Enabled = sharedPreferences.getBoolean(PreferenceKeys.APRS_VOAX25_ENABLE, false);
_isDigiRepeaterEnabled = sharedPreferences.getBoolean(PreferenceKeys.APRS_DIGIREPEATER_ENABLED, false);
}
@Override
@ -55,7 +56,7 @@ public class Ax25 implements Protocol {
ax25Packet.rawData = frame;
byte[] ax25Frame = ax25Packet.toBinary();
if (ax25Frame == null) {
Log.e(TAG, "Invalid source data for AX.25");
Log.e(TAG, "Cannot convert AX.25 voice packet to binary");
_parentProtocolCallback.onProtocolTxError();
} else {
_childProtocol.sendCompressedAudio(src, dst, codec2Mode, ax25Frame);
@ -81,11 +82,11 @@ public class Ax25 implements Protocol {
ax25Packet.rawData = dataPacket;
byte[] ax25Frame = ax25Packet.toBinary();
if (ax25Frame == null) {
Log.e(TAG, "Invalid source data for AX.25");
Log.e(TAG, "Cannot convert AX.25 data packet to binary");
_parentProtocolCallback.onProtocolTxError();
} else {
_childProtocol.sendData(src, dst, ax25Frame);
_parentProtocolCallback.onTransmitLog(toDebugLogLine(ax25Packet));
_parentProtocolCallback.onTransmitLog(ax25Packet.toString());
}
}
@ -106,7 +107,7 @@ public class Ax25 implements Protocol {
}
@Override
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrames) {
protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrames) {
AX25Packet ax25Data = new AX25Packet();
ax25Data.fromBinary(audioFrames);
if (ax25Data.isValid) {
@ -114,7 +115,8 @@ public class Ax25 implements Protocol {
_parentProtocolCallback.onReceiveCompressedAudio(ax25Data.src, ax25Data.dst, ax25Data.codec2Mode, ax25Data.rawData);
} else {
_parentProtocolCallback.onReceiveData(ax25Data.src, ax25Data.dst, ax25Data.rawData);
_parentProtocolCallback.onReceiveLog(toDebugLogLine(ax25Data));
_parentProtocolCallback.onReceiveLog(ax25Data.toString());
if (_isDigiRepeaterEnabled) digiRepeat(ax25Data);
}
} else {
// fallback to raw audio if ax25 frame is invalid
@ -183,11 +185,22 @@ public class Ax25 implements Protocol {
_childProtocol.close();
}
private String toDebugLogLine(AX25Packet ax25Packet) {
String path = ax25Packet.digipath == null ? "" : ax25Packet.digipath;
if (!path.isEmpty())
path = "," + path;
return String.format("%s>%s%s:%s", ax25Packet.src, ax25Packet.dst,
path, DebugTools.bytesToDebugString(ax25Packet.rawData));
private void digiRepeat(AX25Packet ax25Packet) {
if (!ax25Packet.digiRepeat()) return;
byte[] ax25Frame = ax25Packet.toBinary();
if (ax25Frame == null) {
Log.e(TAG, "Cannot convert AX.25 digi repeated packet to binary");
_parentProtocolCallback.onProtocolTxError();
} else {
try {
_childProtocol.sendData(ax25Packet.src, ax25Packet.dst, ax25Frame);
} catch (IOException e) {
Log.e(TAG, "Cannot send AX.25 digi repeated packet");
e.printStackTrace();
_parentProtocolCallback.onProtocolTxError();
return;
}
_parentProtocolCallback.onTransmitLog(ax25Packet.toString());
}
}
}

Wyświetl plik

@ -24,35 +24,35 @@ public class Kiss implements Protocol {
private static final String TAG = Kiss.class.getSimpleName();
private final int TRANSPORT_OUTPUT_BUFFER_SIZE = 1024;
private final int TRANSPORT_INPUT_BUFFER_SIZE = 1024;
private final int FRAME_OUTPUT_BUFFER_SIZE = 1024;
private final int KISS_CMD_BUFFER_SIZE = 128;
private static final int TRANSPORT_OUTPUT_BUFFER_SIZE = 1024;
private static final int TRANSPORT_INPUT_BUFFER_SIZE = 1024;
private static final int FRAME_OUTPUT_BUFFER_SIZE = 1024;
private static final int KISS_CMD_BUFFER_SIZE = 128;
private final int KISS_RADIO_CONTROL_COMMAND_SIZE = 17;
private static final int KISS_RADIO_CONTROL_COMMAND_SIZE = 17;
private final byte KISS_FEND = (byte)0xc0;
private final byte KISS_FESC = (byte)0xdb;
private final byte KISS_TFEND = (byte)0xdc;
private final byte KISS_TFESC = (byte)0xdd;
private static final byte KISS_FEND = (byte)0xc0;
private static final byte KISS_FESC = (byte)0xdb;
private static final byte KISS_TFEND = (byte)0xdc;
private static final byte KISS_TFESC = (byte)0xdd;
// only port 0 is supported
private final byte KISS_CMD_DATA = (byte)0x00;
private final byte KISS_CMD_TX_DELAY = (byte)0x01;
private final byte KISS_CMD_P = (byte)0x02;
private final byte KISS_CMD_SLOT_TIME = (byte)0x03;
private final byte KISS_CMD_TX_TAIL = (byte)0x04;
private final byte KISS_CMD_SET_HARDWARE = (byte)0x06;
private final byte KISS_CMD_SIGNAL_REPORT = (byte)0x07;
private final byte KISS_CMD_REBOOT = (byte)0x08;
private final byte KISS_CMD_NOCMD = (byte)0x80;
private static final byte KISS_CMD_DATA = (byte)0x00;
private static final byte KISS_CMD_TX_DELAY = (byte)0x01;
private static final byte KISS_CMD_P = (byte)0x02;
private static final byte KISS_CMD_SLOT_TIME = (byte)0x03;
private static final byte KISS_CMD_TX_TAIL = (byte)0x04;
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_NOCMD = (byte)0x80;
private final byte CSMA_PERSISTENCE = (byte)0xff;
private final byte CSMA_SLOT_TIME = (byte)0x00;
private final byte TX_DELAY_10MS_UNITS = (byte)(250 / 10);
private final byte TX_TAIL_10MS_UNITS = (byte)(500 / 10);
private static final byte CSMA_PERSISTENCE = (byte)0xff;
private static final byte CSMA_SLOT_TIME = (byte)0x00;
private static final byte TX_DELAY_10MS_UNITS = (byte)(250 / 10);
private static final byte TX_TAIL_10MS_UNITS = (byte)(500 / 10);
private final int SIGNAL_LEVEL_EVENT_SIZE = 4;
private static final int SIGNAL_LEVEL_EVENT_SIZE = 4;
private enum State {
GET_START,

Wyświetl plik

@ -8,8 +8,8 @@ import java.util.TimerTask;
public class KissBuffered extends Kiss {
private final int BUFFER_SIZE = 3200 * 60 * 10; // 10 min at 3200 bps
private final int GAP_TO_PLAY_MS = 1000;
private static final int BUFFER_SIZE = 3200 * 60 * 10; // 10 min at 3200 bps
private static final int GAP_TO_PLAY_MS = 1000;
private final ByteBuffer _buffer;

Wyświetl plik

@ -9,9 +9,8 @@ import java.util.TimerTask;
public class KissParrot extends Kiss {
private final int BUFFER_SIZE = 3200 * 60 * 5; // 5 min at 3200 bps
private final int PLAYBACK_DELAY_MS = 1000;
private static final int BUFFER_SIZE = 3200 * 60 * 5; // 5 min at 3200 bps
private static final int PLAYBACK_DELAY_MS = 1000;
private final ByteBuffer _buffer;

Wyświetl plik

@ -10,7 +10,7 @@ import java.util.Arrays;
public class Raw implements Protocol {
private final int RX_BUFFER_SIZE = 8192;
private static final int RX_BUFFER_SIZE = 8192;
protected Transport _transport;
protected final byte[] _rxDataBuffer;

Wyświetl plik

@ -23,7 +23,7 @@ public class Recorder implements Protocol {
private static final String TAG = MainActivity.class.getSimpleName();
private final int ROTATION_DELAY_MS = 5000;
private static final int ROTATION_DELAY_MS = 5000;
private File _storage;
private FileOutputStream _activeStream;

Wyświetl plik

@ -95,4 +95,33 @@ public class AX25Callsign {
buffer.get(b);
return b;
}
public boolean isWide() {
return callsign.toUpperCase().startsWith("WIDE");
}
public boolean isTrace() {
return callsign.toUpperCase().startsWith("TRACE");
}
public boolean isSoftware() {
return callsign.toUpperCase().matches("^(AP)[A-Z]{1,4}$");
}
public boolean isPath() {
return isWide();
}
public boolean digiRepeat() {
if (isPath()) {
if (ssid > 0) {
ssid -= 1;
if (ssid == 0) {
callsign += "*";
}
return true;
}
}
return false;
}
}

Wyświetl plik

@ -1,6 +1,8 @@
package com.radio.codec2talkie.protocol.ax25;
import android.util.Log;
import androidx.annotation.NonNull;
import com.radio.codec2talkie.tools.DebugTools;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
@ -63,7 +65,7 @@ public class AX25Packet {
byte ax25Pid = buffer.get();
if (ax25Pid == AX25PID_AUDIO) {
isAudio = true;
codec2Mode = (int)buffer.get();
codec2Mode = buffer.get();
} else if (ax25Pid == AX25PID_NO_LAYER3) {
isAudio = false;
} else {
@ -119,4 +121,34 @@ public class AX25Packet {
buffer.get(b);
return b;
}
public boolean digiRepeat() {
boolean isDigiRepeated = false;
String[] digiPaths = digipath.split(",");
StringBuilder buf = new StringBuilder();
for (String digiPath : digiPaths) {
AX25Callsign rptCallsign = new AX25Callsign();
rptCallsign.fromString(digiPath);
if (rptCallsign.isValid) {
if (rptCallsign.digiRepeat()) {
isDigiRepeated = true;
buf.append(rptCallsign.toString());
} else {
buf.append(digiPath);
}
} else {
buf.append(digiPath);
}
}
digipath = buf.toString();
return isDigiRepeated;
}
@NonNull
public String toString() {
String path = digipath == null ? "" : digipath;
if (!path.isEmpty())
path = "," + path;
return String.format("%s>%s%s:%s", src, dst, path, DebugTools.bytesToDebugString(rawData));
}
}

Wyświetl plik

@ -71,6 +71,7 @@ public final class PreferenceKeys {
public static String APRS_PRIVACY_POSITION_AMBIGUITY = "aprs_privacy_position_ambiguity";
public static String APRS_PRIVACY_SPEED_ENABLED = "aprs_privacy_speed_enable";
public static String APRS_PRIVACY_ALTITUDE_ENABLED = "aprs_privacy_altitude_enable";
public static String APRS_DIGIREPEATER_ENABLED = "aprs_digirepeater_enable";
public static String APRS_LOCATION_SOURCE_SMART_FAST_SPEED = "aprs_location_source_smart_fast_speed";
public static String APRS_LOCATION_SOURCE_SMART_FAST_RATE = "aprs_location_source_smart_fast_rate";

Wyświetl plik

@ -260,4 +260,7 @@
<string name="app_no_lock_summary">Run above the lock screen</string>
<string name="app_turn_screen_on_title">Turn screen ON automatically</string>
<string name="app_turn_screen_on_summary">Turn screen ON automatically on incoming transmissions</string>
<string name="aprs_digirepeater_enable_title">Enable digirepeater</string>
<string name="aprs_digirepeater_enable_summary">Incoming APRS packets will received and digirepeated</string>
<string name="digirepeater_label">&#128257;</string>
</resources>

Wyświetl plik

@ -269,6 +269,14 @@
app:dependency="aprs_enable">
</Preference>
<SwitchPreference
app:key="aprs_digirepeater_enable"
app:title="@string/aprs_digirepeater_enable_title"
app:summary="@string/aprs_digirepeater_enable_summary"
app:dependency="aprs_enable"
app:defaultValue="false">
</SwitchPreference>
</PreferenceCategory>
</PreferenceScreen>