package com.radio.codec2talkie.protocol; import android.content.Context; import android.content.SharedPreferences; import android.os.Looper; import android.util.Log; import android.widget.Toast; import androidx.preference.PreferenceManager; 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; import com.radio.codec2talkie.transport.Transport; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Locale; import kotlin.text.MatchGroup; import kotlin.text.MatchResult; import kotlin.text.Regex; 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; private ProtocolCallback _parentProtocolCallback; private String _passcode; private String _server; 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 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; _fromAprsIsQueue = ByteBuffer.allocate(4096); _toAprsIsQueue = ByteBuffer.allocate(4096); _rxBuf = new byte[4096]; } @Override public void initialize(Transport transport, Context context, ProtocolCallback protocolCallback) throws IOException { _parentProtocolCallback = protocolCallback; _context = context; _childProtocol.initialize(transport, context, _protocolCallback); SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); _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").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); Log.i(TAG, "AprsIs RX gate: " + _isTxGateEnabled + ", TX gate: " + _isTxGateEnabled + ", server: " + _server); new Thread(this).start(); } @Override public int getPcmAudioRecordBufferSize() { return _childProtocol.getPcmAudioRecordBufferSize(); } @Override public void sendCompressedAudio(String src, String dst, int codec2Mode, byte[] frame) throws IOException { _childProtocol.sendCompressedAudio(src, dst, codec2Mode, frame); } @Override public void sendTextMessage(TextMessage textMessage) throws IOException { _childProtocol.sendTextMessage(textMessage); } @Override public void sendPcmAudio(String src, String dst, int codec2Mode, short[] pcmFrame) throws IOException { _childProtocol.sendPcmAudio(src, dst, codec2Mode, 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 (_toAprsIsQueue) { String rawData = aprsIsData.toString() + "\n"; _toAprsIsQueue.put(rawData.getBytes()); } } _childProtocol.sendData(src, dst, path, data); } private boolean isEligibleForTxGate(AprsIsData aprsIsData) { /* rules: 1. RX gate must be heard on rf within digi hops or range 2. RX gate has not been heard on internet within given period of time or in third party packets 3. ✓ sender must not be heard within given period of time on RF 4. ✓ sender must not have TCPXX, NOGATE, RFONLY */ 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.toString(); return txData.getBytes(); } @Override public boolean receive() throws IOException { String line; 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.rawDigipath, aprsIsData.data.getBytes()); if (isEligibleForTxGate(aprsIsData)) { _childProtocol.sendData(new AX25Callsign(_callsign, _ssid).toString(), Aprs.APRS_ID, _digipath, thirdPartyWrap(aprsIsData)); } } _parentProtocolCallback.onReceiveLog(line); } return _childProtocol.receive(); } ProtocolCallback _protocolCallback = new ProtocolCallback() { @Override protected void onReceivePosition(Position position) { _parentProtocolCallback.onReceivePosition(position); } @Override protected void onReceivePcmAudio(String src, String dst, int codec, short[] pcmFrame) { _parentProtocolCallback.onReceivePcmAudio(src, dst, codec, pcmFrame); } @Override protected void onReceiveCompressedAudio(String src, String dst, int codec2Mode, byte[] audioFrame) { _parentProtocolCallback.onReceiveCompressedAudio(src, dst, codec2Mode, audioFrame); } @Override protected void onReceiveTextMessage(TextMessage textMessage) { _parentProtocolCallback.onReceiveTextMessage(textMessage); } @Override protected void onReceiveData(String src, String dst, String path, byte[] data) { _rfHeardList.add(src); if (_isRxGateEnabled && !_isLoopbackTransport) { // NOTE, https://aprs-is.net/IGateDetails.aspx AprsIsData aprsIsData = new AprsIsData(src, dst, path, new String(data)); if (aprsIsData.isEligibleForRxGate()) { // strip "rf header" for third party packets before gating if (aprsIsData.hasThirdParty()) { aprsIsData = aprsIsData.thirdParty; } String rawData = aprsIsData.toString() + "\n"; synchronized (_toAprsIsQueue) { _toAprsIsQueue.put(rawData.getBytes()); } } } _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, int codec, short[] frame) { _parentProtocolCallback.onTransmitPcmAudio(src, dst, codec, frame); } @Override protected void onTransmitCompressedAudio(String src, String dst, int codec, byte[] frame) { _parentProtocolCallback.onTransmitCompressedAudio(src, dst, codec, 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() { Log.i(TAG, "close()"); _isRunning = false; _childProtocol.close(); } @Override public void run() { Looper.prepare(); TcpIp tcpIp = null; Log.i(TAG, "Started APRS-IS thread"); while (_isRunning) { // connect if (!_isConnected) { tcpIp = runConnect(); } if (tcpIp == null) { _isConnected = false; continue; } runRead(tcpIp); runWrite(tcpIp); } if (tcpIp != null) { try { tcpIp.close(); } catch (IOException e) { e.printStackTrace(); } } Log.i(TAG, "Stopped APRS-IS thread"); } private String getLoginCommand() { String cmd = "user " + new AX25Callsign(_callsign, _ssid).toString() + " pass " + _passcode + " vers " + "C2T " + BuildConfig.VERSION_NAME; if (_filterRadius > 0) { cmd += " filter m/" + _filterRadius; } if (_filter.length() > 0) { if (!cmd.contains("filter")) { cmd += " filter "; } cmd += " " + _filter; } cmd += "\n"; return cmd; } private TcpIp runConnect() { Socket socket = new Socket(); try { socket.connect(new InetSocketAddress(_server, APRSIS_DEFAULT_PORT)); TcpIp tcpIp = new TcpIp(socket, "aprsis"); String loginCmd = getLoginCommand(); Log.i(TAG, "Login command " + loginCmd); tcpIp.write(loginCmd.getBytes()); Log.i(TAG, "Connected to " + _server); Toast.makeText(_context, _context.getString(R.string.aprsis_connected), Toast.LENGTH_LONG).show(); _isConnected = true; return tcpIp; } catch (IOException e) { Log.w(TAG, "Failed to connect"); Toast.makeText(_context, _context.getString(R.string.aprsis_connect_failed), Toast.LENGTH_LONG).show(); e.printStackTrace(); try { Thread.sleep(APRSIS_RETRY_WAIT_MS); } catch (InterruptedException interruptedException) { interruptedException.printStackTrace(); } _isConnected = false; return null; } } private void runWrite(TcpIp tcpIp) { 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"); Toast.makeText(_context, _context.getString(R.string.aprsis_disconnected), Toast.LENGTH_LONG).show(); e.printStackTrace(); _isConnected = false; } } } } private void runRead(TcpIp tcpIp) { // read data int bytesRead; try { // # aprsc 2.1.11-g80df3b4 20 Aug 2022 11:33:40 GMT T2FINLAND 85.188.1.129:14580 // # logresp N0CALL unverified, server T2GYOR<0xd><0xa> bytesRead = tcpIp.read(_rxBuf); } catch (IOException e) { Log.w(TAG, "Lost connection on receive"); Toast.makeText(_context, _context.getString(R.string.aprsis_disconnected), Toast.LENGTH_LONG).show(); e.printStackTrace(); _isConnected = false; return; } if (bytesRead > 0) { // server message if (_rxBuf[0] == '#') { String srvMsg = new String(Arrays.copyOf(_rxBuf, bytesRead)); Log.d(TAG, "APRSIS: " + srvMsg); // wrong password if (srvMsg.matches("# logresp .+ unverified")) { Toast.makeText(_context, _context.getString(R.string.aprsis_wrong_pass), Toast.LENGTH_LONG).show(); try { tcpIp.close(); } catch (IOException e) { e.printStackTrace(); } _isConnected = false; } // update status Regex statusRegex = new Regex(".+ (\\S+ \\d{1,3}[.]\\d{1,3}[.]\\d{1,3}[.]\\d{1,3}:\\d+)"); MatchResult matchResult = statusRegex.find(srvMsg, 0); if (matchResult != null) { MatchGroup matchGroup = matchResult.getGroups().get(1); if (matchGroup != null) { Toast.makeText(_context, matchGroup.getValue(), Toast.LENGTH_SHORT).show(); } } // data } else { synchronized (_fromAprsIsQueue) { try { _fromAprsIsQueue.put(Arrays.copyOf(_rxBuf, bytesRead)); } catch (BufferOverflowException e) { e.printStackTrace(); _fromAprsIsQueue.clear(); } } } } } }