net.ab0oo.aprs.avrs.AVRSServer.java Source code

Java tutorial

Introduction

Here is the source code for net.ab0oo.aprs.avrs.AVRSServer.java

Source

/*
 * AVRS - http://avrs.sourceforge.net/
 *
 * Copyright (C) 2011 John Gorkos, AB0OO
 *
 * AVRS is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 2 of the License,
 * or (at your option) any later version.
 *
 * AVRS is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with AVRS; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 */

package net.ab0oo.aprs.avrs;

/**
 * This is the main class for the AVRS (Automatic Voice Relay System) that Bob
 * Bruninga, WB4APR has outlined here:  http://www.aprs.org/avrs.html
 * This class connects to the APRS-IS system and monitors for packets destined
 * for AVRS (in the destination call segment of a Message Packet).  
 * It interfaces with a postgreSQL spatially enabled database to determine relative
 * distances to Internet/radio interface nodes and creates messages appropriate to
 * facilitating connections between distant stations using Internet linking technologies
 *
 */
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.Socket;
import java.text.DecimalFormat;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import net.ab0oo.aprs.avrs.db.AVRSDao;
import net.ab0oo.aprs.avrs.db.jdbc.JdbcAVRSDao;
import net.ab0oo.aprs.avrs.models.AllPositionEntry;
import net.ab0oo.aprs.clients.PacketListener;
import net.ab0oo.aprs.parser.APRSPacket;
import net.ab0oo.aprs.parser.InformationField;
import net.ab0oo.aprs.parser.MessagePacket;
import net.ab0oo.aprs.parser.Parser;
import net.ab0oo.aprs.parser.Utilities;
import net.ab0oo.aprs.avrs.models.ReferencePoint;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.postgresql.ds.PGSimpleDataSource;

class AVRSServer implements PacketListener {

    static DecimalFormat df = new DecimalFormat("###.000000");
    static DecimalFormat distFmt = new DecimalFormat("###.00");
    static DataOutputStream outToServer;
    static Socket clientSocket;
    static BufferedReader inFromServer;
    static ScheduledThreadPoolExecutor timer = new ScheduledThreadPoolExecutor(3);
    static Map<String, ConversationThread> scheduledMessages = new HashMap<String, ConversationThread>();
    static Map<String, ConversationThread> scheduledAcks = new HashMap<String, ConversationThread>();
    static Map<Date, ScheduledFuture<ConversationThread>> futures = new HashMap<Date, ScheduledFuture<ConversationThread>>();
    static String username = null, database = null;
    static String password = null, host = null, aprsIsServer = null;
    static String callsign = null, aprsPass = null;
    private int staleMs = 900000;
    private double maxLinkDistance = 100;
    static int messageNumber = 0, port = 10152;
    private AVRSDao dao;

    public static void main(String argv[]) {
        AVRSServer server = new AVRSServer();
        try {
            XMLConfiguration config = new XMLConfiguration("config/avrs.xml");
            username = config.getString("postgres.username");
            database = config.getString("postgres.dbname");
            password = config.getString("postgres.password");
            host = config.getString("postgres.host");
            aprsIsServer = config.getString("aprsis.host");
            port = config.getInt("aprsis.port");
            callsign = config.getString("aprsis.callsign");
            aprsPass = config.getString("aprsis.password");
            server.setStaleMs(config.getInt("avrs.staleMs"));
            server.setMaxLinkDistance(config.getDouble("avrs.maxLinkDistance"));
        } catch (ConfigurationException cfex) {
            System.err.println("Unable to load avrs.xml configuration.");
            System.exit(1);
        }
        PGSimpleDataSource source = new PGSimpleDataSource();
        source.setServerName(host);
        source.setDatabaseName(database);
        source.setUser(username);
        source.setPassword(password);
        AVRSDao adao = new JdbcAVRSDao();
        adao.setDataSource(source);
        server.setDao(adao);

        server.connectAndListen();
    }

    public AVRSServer() {
        System.out.println("Initializing AVRS Server");
        // timer.scheduleAtFixedRate(new AckReaper(), 5, 2, TimeUnit.SECONDS);
    }

    private void connectAndListen() {
        String sentence;
        String modifiedSentence = "";
        try {
            clientSocket = new Socket(aprsIsServer, port);
            outToServer = new DataOutputStream(clientSocket.getOutputStream());
            inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            // sentence = "user ab0oo-15 pass 19951 vers test 1.0 filter a/35.000/-85.6/30.3555/-80.84";
            sentence = "user " + callsign + " pass " + aprsPass + " vers test 1.0 filter t/mp";
            outToServer.writeBytes(sentence + '\n');
        } catch (Exception ex) {
            System.err.println("Unable to contact/log-in to APRS-IS:  " + ex);
            System.exit(1);
        }
        APRSPacket packet = null;
        while (true) {
            try {
                modifiedSentence = inFromServer.readLine();
                if (modifiedSentence.length() > 1 && modifiedSentence.charAt(0) != '#') {
                    try {
                        packet = Parser.parse(modifiedSentence);
                    } catch (Exception ex) {
                        System.err.println("Unable to parse: " + modifiedSentence);
                        ex.printStackTrace();
                        continue;
                    }
                    if (packet.hasFault()) {
                        System.err.println("Bad Packet");
                        continue;
                    }
                    processPacket(packet);
                } else {
                    System.out.println(modifiedSentence);
                }
                System.out.println(new Date() + ":  " + modifiedSentence);

            } catch (Exception ex) {
                System.err.println("Exception during Network read:  " + ex);
                ex.printStackTrace();
                System.err.println("Culprit was " + modifiedSentence);
            }
        }
        // clientSocket.close();
    }

    @Override
    public void processPacket(APRSPacket packet) {
        try {
            if (packet.isAprs() && packet.getAprsInformation() instanceof MessagePacket) {
                MessagePacket mp = (MessagePacket) packet.getAprsInformation();
                String sourceCall = packet.getSourceCall();
                String targetCall = mp.getTargetCallsign().toUpperCase().trim();
                if (mp.getMessageBody().contains("AA:") || mp.getMessageBody().contains("[AA]")) {
                    System.out.println("Dealing with autoresponder; aborting transaction");
                    System.out.println("Message from " + packet.getSourceCall() + " was: " + mp.getMessageBody());
                    return;
                }
                if (targetCall.equals("AVRS")) {
                    System.out.println(packet.toString());
                    processAvrs(sourceCall, mp);
                } else if (targetCall.equals("LOCATE")) {
                    System.out.println(packet.toString());
                    processLocate(sourceCall, mp);
                } else {
                    // System.out.println(packet.getSourceCall() +
                    // " -> " + mp.getTargetCallsign()
                    // + "["+mp.getMessageNumber()+"]"+":  "
                    // + mp.getMessageBody());
                    // System.out.println("\tExtracted from "+modifiedSentence);
                }
            }
        } catch (Exception ex) {
            System.err.println(new Date() + "!!!!! Exception tossed during packet processing");
            ex.printStackTrace();
        }

    }

    public void processAvrs(String sourceCall, MessagePacket mp) {
        String messageBody = mp.getMessageBody().toUpperCase().trim();
        if (mp.isAck()) {
            System.out.println("ACK " + mp.getMessageNumber() + " rxed from " + sourceCall);
            String key = sourceCall + ":" + mp.getMessageNumber();
            if (scheduledAcks.containsKey(key)) {
                System.out.println("This ACK was expected from " + sourceCall);
                ConversationThread ct = scheduledAcks.remove(key);
                ct.setCount(0);
                System.out.println("The timer for this ACK was removed.");
            }
        } else if (mp.isRej()) {
            System.out.println(
                    sourceCall + " rejects message " + mp.getMessageNumber() + " from " + mp.getTargetCallsign());
        } else if (messageBody.startsWith("?")) {
            if (mp.getMessageBody().trim().length() == 1) {
                sendAck("AVRS", sourceCall, mp.getMessageNumber());
                sendClosestNode(sourceCall, sourceCall, '*');
            } else if (messageBody.length() == 2) {
                // this is a transport-specific query. The user has asked for
                // an echolink (?E), IRLP (?I), or AllStar (?A) node
                sendAck("AVRS", sourceCall, mp.getMessageNumber());
                sendClosestNode(sourceCall, sourceCall, messageBody.charAt(1));
            } else {
                // this is for a query on another station in the format ?N0CAL
                sendAck("AVRS", sourceCall, mp.getMessageNumber());
                sendClosestNode(mp.getMessageBody().substring(1), sourceCall, '*');
            }
        } else {
            sendAck("AVRS", sourceCall, mp.getMessageNumber());
            System.out.println("Message body is \"" + mp.getMessageBody() + "\"");
            setupAvrsLink(sourceCall.toUpperCase(), mp.getMessageBody());

        }
    }

    public void processLocate(String sourceCall, MessagePacket mp) {
        if (mp.isAck()) {
            System.out.println("ACK " + mp.getMessageNumber() + " rxed from " + sourceCall);
            String key = sourceCall + ":" + mp.getMessageNumber();
            if (scheduledAcks.containsKey(key)) {
                System.out.println("This ACK was expected from " + sourceCall);
                ConversationThread ct = scheduledAcks.remove(key);
                ct.setCount(0);
                System.out.println("The timer for this ACK was removed.");
            }
        } else if (mp.isRej()) {
            System.out.println(
                    sourceCall + " rejects message " + mp.getMessageNumber() + " from " + mp.getTargetCallsign());
        } else {
            if (mp.getMessageBody().trim().length() == 1) {
                sendAck("LOCATE", sourceCall, mp.getMessageNumber());
                sendLocation(sourceCall, sourceCall, '*', mp.getMessageNumber());
            } else {
                // this is for a query on another station in the format ?N0CAL
                sendAck("LOCATE", sourceCall, mp.getMessageNumber());
                sendLocation(mp.getMessageBody(), sourceCall, '*', mp.getMessageNumber());
            }
        }
    }

    /**
     * 
     * @param callsign
     * @return This method will search the database for the most likely station capable of recieving an AVRS message and
     *         responding to it. It uses symbol, timestamp of last message, movement of target, and any inferred
     *         equipment types to decide which SSID of a callsign is most likely to be monitored.
     */
    private String getBestTarget(String callsign) {
        String bestTarget = callsign;
        System.out.println(
                new Date() + ":  Looking for last position of all SSIDs for " + APRSPacket.getBaseCall(callsign));
        List<AllPositionEntry> allPositions = dao.getPositions(APRSPacket.getBaseCall(callsign));
        if (allPositions == null || allPositions.size() == 0) {
            bestTarget = null;
        }
        System.out.println(new Date() + ":  Found " + allPositions.size() + " valid SSIDs for " + callsign);
        // TODO This needs to actually do more to figure out WHICH station is message capable.
        // right now, it returns the callsign of the most RECENT SSID to transmit
        if (allPositions.size() > 0) {
            Collections.sort(allPositions);
            bestTarget = allPositions.get(0).getCallsign();
            System.out.println("Multiple choices for " + callsign + ", using " + bestTarget);
        }
        return bestTarget;
    }

    private void setupAvrsLink(String sourceCall, String targetCall) {
        System.out.println(new Date() + ":  Looking for best target callsign for " + targetCall);
        String bestTarget = getBestTarget(targetCall.toUpperCase());
        System.out.println("Best target is " + bestTarget);
        if (bestTarget == null) {
            sendAndGetAck("AVRS", sourceCall, "Unable to find a position for " + targetCall);
            return;
        }
        LinkPair nodePair = findBestPath(sourceCall.toUpperCase(), bestTarget.toUpperCase());
        if (nodePair.getTargetNode() == null) {
            sendAndGetAck("AVRS", sourceCall, targetCall + " out of range of internet node");
            return;
        }
        if (nodePair.getSourceNode() == null) {
            sendAndGetAck("AVRS", sourceCall, "You are out of range of internet node");
            return;
        }
        double freq = nodePair.getTargetNode().getFrequency();
        String tone = nodePair.getTargetNode().getTone();
        String targetMessage = String.format("VOICE CALL from %s, PSE QSY %3.3f MHz T%-3s", sourceCall, freq, tone);
        sendAndGetAck("AVRS", bestTarget, targetMessage);
        System.out.println(new Date() + ":  " + bestTarget + ">" + targetMessage);
        freq = nodePair.getSourceNode().getFrequency();
        tone = nodePair.getSourceNode().getTone();
        String type = nodePair.getSourceNode().getNodeType();
        int nodeId = nodePair.getTargetNode().getNode();
        String sourceMessage = String.format("CALL %s on %3.3f MHz T%-3s %s node %d", bestTarget, freq, tone, type,
                nodeId);
        System.out.println(new Date() + ":  " + sourceCall + ">" + sourceMessage);
        sendAndGetAck("AVRS", sourceCall, sourceMessage);
    }

    private void sendLocation(String callsign, String requestor, char nodeType, String msgNumber) {
        System.out.println(new Date() + ":  " + requestor + " wants to know the location of '" + callsign + "'");
        List<AllPositionEntry> lastPositions = dao.getPositions(callsign);
        if (lastPositions.size() < 1) {
            System.out.println("Unkown last position for " + callsign);
            sendAndGetAck("LOCATE", requestor, "UNKNOWN POSITION FOR " + callsign);
            return;
        }
        for (AllPositionEntry lastPosition : lastPositions) {
            // if the requestor is looking for a SPECIFIC SID, then skip entries unless
            // their callsign matches the requested callsign exactly.  Otherwise, we'll return
            // all known station positions for the requested callsign
            if (callsign.indexOf('-') > 0 && !lastPosition.getCallsign().equals(callsign)) {
                continue;
            }
            System.out.println(new Date() + ":  Last position of " + lastPosition.getCallsign() + " at "
                    + lastPosition.getPosition().getTimestamp() + " at " + lastPosition.getPosition());
            if (System.currentTimeMillis() - lastPosition.getToi().getTime() > staleMs) {
                List<ReferencePoint> closestCities = dao.listClosestCities(lastPosition.getPosition());
                ReferencePoint closestCity = closestCities.get(0);
                String alertString = lastPosition.getCallsign() + " heard ";
                Date lastHeard = lastPosition.getToi();
                System.out.println("lastHeard is " + lastPosition.getToi());
                if (lastHeard != null) {
                    alertString += (toDms(System.currentTimeMillis() - lastPosition.getToi().getTime())) + " ago ";
                }
                double distance = closestCity.getMetersDistance();
                String units = "km ";
                if (requestor.startsWith("A") || requestor.startsWith("G") || requestor.startsWith("K")
                        || requestor.startsWith("N") || requestor.startsWith("W")) {
                    distance = Utilities.metersToMiles(distance);
                    units = "mi ";
                } else {
                    distance = Utilities.metersToKilometers(distance);
                }
                alertString += distFmt.format(distance) + units;
                alertString += Utilities.degressToCardinal(closestCity.getBearingTo()) + " of ";
                alertString += closestCity.getCity() + ", " + closestCity.getRegion();
                sendAndGetAck("LOCATE", requestor, alertString);
            } else {
                System.out.println("Message from " + lastPosition.getToi() + " too old to be valid.");
            }
        }
    }

    public String toDms(long interval) {
        long seconds = interval / 1000;
        long days = seconds / 86400;
        seconds = seconds % 86400;
        long hours = seconds / 3600;
        seconds = seconds % 3600;
        long minutes = seconds / 60;
        seconds = seconds % 60;
        return days + "d" + hours + "h" + minutes + "m" + seconds + "s";

    }

    private void sendClosestNode(String callsign, String requestor, char nodeType) {
        System.out.println(
                new Date() + ":  " + requestor + " wants to know the closest VOIP node to '" + callsign + "'");
        if (nodeType != '*') {
            System.out.println(new Date() + ": Interested only in " + nodeType + " nodes");
        }
        // first we set up the acknowledgment
        List<AllPositionEntry> lastPositions = dao.getPositions(callsign);
        if (lastPositions.size() < 1) {
            System.out.println("Unkown last position for " + callsign);
            sendAndGetAck("AVRS", requestor, "UNKNOWN POSITION FOR " + callsign);
            return;
        }

        if (lastPositions.size() == 0) {
            System.err.println("Unkown last position for " + callsign);
            sendAndGetAck("AVRS", requestor, "UNKNOWN POSITION FOR " + callsign);
            return;
        }
        AllPositionEntry lastPosition = lastPositions.get(0);
        System.out.println(new Date() + ":  Timestamp of last position is " + lastPosition.getToi());
        if (System.currentTimeMillis() - lastPosition.getToi().getTime() > staleMs) {
            long elapsedTime = (System.currentTimeMillis() - lastPosition.getToi().getTime()) / 1000;
            System.out.println(
                    new Date() + ":  Last position from " + callsign + " is " + elapsedTime + " seconds old.");
        }
        String targetCallsign = lastPosition.getCallsign();
        TreeSet<LinkNode> nodes = dao.getNodes(targetCallsign);
        int gwCount = 0;
        for (LinkNode node : nodes) {
            if (nodeType != '*' && node.getNodeType().charAt(0) != nodeType)
                continue;
            gwCount++;
            if (gwCount < 3) {
                String comment = String.format("%3.3f MHz T%-3s %s#%d %2.2f miles", node.getFrequency(),
                        node.getTone(), node.getNodeType(), node.getNode(), node.getDistance());
                // Position p = new Position(node.getLatitude(), node.getLongitude(), 0, '/', 'n');
                // ObjectPacket op = new ObjectPacket(node.getNodeType() + "#" + node.getNode(), true, p, comment);
                if (gwCount == 1) {
                    if (node.getDistance() > maxLinkDistance) {
                        System.out.println(
                                new Date() + ":  " + targetCallsign + " too far away from a working VOIP node");
                        comment = "SRY, " + targetCallsign + " >50 miles from closest VOIP node";
                    }
                    sendAndGetAck("AVRS", requestor, comment);
                }
                // sendNoAck(op);
            }
        }
    }

    /**
     * @param callsign
     *            - Destination callsign of the station this message is destined for
     * @param message
     *            - Body of the message
     * @param txCount
     *            - Number of times to transmit this message (unless an ack is received)
     * @param txInterval
     *            - Frequency in seconds to send this message.
     * @return - the APRS Message Number assigned to this message (for ack purposes)
     */
    private int sendAndGetAck(String fromcall, String callsign, String message, int txCount, int txInterval) {
        MessagePacket mp = new MessagePacket(callsign, message, Integer.toString(messageNumber));
        APRSPacket outgoingPacket = new APRSPacket(fromcall, "APZ013", null, mp);
        System.out.println(outgoingPacket.toString());
        messageNumber++;
        ResponseThread ackThread = new ResponseThread(outgoingPacket, txCount, txInterval, outToServer, timer);
        timer.schedule(ackThread, 0L, TimeUnit.SECONDS);
        String id = mp.getTargetCallsign() + ":" + mp.getMessageNumber();
        scheduledAcks.put(id, ackThread);
        return messageNumber;
    }

    private int sendAndGetAck(String fromcall, String callsign, String message) {
        return sendAndGetAck(fromcall, callsign, message, 3, 10);
    }

    @SuppressWarnings("unused")
    private void sendNoAck(String source, InformationField infoField) {
        APRSPacket outgoingPacket = new APRSPacket(source, "APZ013", null, infoField);
        System.out.println(new Date() + ":  Sending: " + outgoingPacket.toString());
        Runnable msgThread = new ResponseThread(outgoingPacket, 3, 10, outToServer, timer);
        timer.schedule(msgThread, 0L, TimeUnit.SECONDS);
    }

    private void sendAck(String source, String callsign, String messageNumber) {
        if (messageNumber.charAt(2) == '}') {
            System.out.println("Requestor is Reply-ack capable, not sending standalone ack.");
            //return;
        }
        System.out.println("Sending ACK to " + callsign + " for MSG NUM " + messageNumber);
        ConversationThread ackThread = new AckThread(true, source, callsign, messageNumber, 3, outToServer, timer);
        timer.schedule(ackThread, 0L, TimeUnit.SECONDS);
    }

    private LinkPair findBestPath(String source, String target) {
        // start out pulling nodes within 50 miles of both the caller and the callee
        TreeSet<LinkNode> sourceNodes = dao.getNodes(source);
        TreeSet<LinkNode> targetNodes = dao.getNodes(target);
        // step through the source node list, looking in the target node list for matches
        LinkNode bestSourceNode = null;
        LinkNode bestTargetNode = null;
        double bestDistance = maxLinkDistance * 2;
        for (LinkNode sourceNode : sourceNodes) {
            String sourceNodeType = sourceNode.getNodeType();
            for (LinkNode targetNode : targetNodes) {
                String targetNodeType = targetNode.getNodeType();
                if (sourceNodeType.equals(targetNodeType)) {
                    double distance = sourceNode.getDistance() + targetNode.getDistance();
                    if (distance < bestDistance) {
                        bestDistance = distance;
                        bestSourceNode = sourceNode;
                        bestTargetNode = targetNode;
                    }
                }
            }
        }
        return new LinkPair(bestSourceNode, bestTargetNode);
    }

    /**
     * @param dao
     *            the dao to set
     */
    public final void setDao(AVRSDao dao) {
        this.dao = dao;
    }

    /**
     * @return the maxLinkDistance
     */
    public final double getMaxLinkDistance() {
        return maxLinkDistance;
    }

    /**
     * @param maxLinkDistance
     *            the maxLinkDistance to set
     */
    public final void setMaxLinkDistance(double maxLinkDistance) {
        this.maxLinkDistance = maxLinkDistance;
    }

    /*
     * (non-Javadoc)
     * 
     * @see net.ab0oo.aprs.clients.PacketListener#setOutputStream(java.io.DataOutputStream)
     */
    @Override
    public void setOutputStream(DataOutputStream outStream) {
        outToServer = outStream;

    }

    /**
     * @param staleMs
     *            the staleMs to set
     */
    public final void setStaleMs(int staleMs) {
        this.staleMs = staleMs;
    }
}