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.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 (_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.convertToString(false) + "\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(); } } } } } }