From 47c44b811d796b369b7f062a3d87390eb2d9b5a4 Mon Sep 17 00:00:00 2001 From: hbeni Date: Thu, 15 Oct 2020 19:34:00 +0200 Subject: [PATCH] Add FGCom-mumble support (implements #5) --- .../openradar/gui/setup/AirportData.java | 2 +- .../openradar/gui/setup/SetupDialog.java | 15 +- .../gui/status/radio/FgComController.java | 34 +-- .../status/radio/FgComMumbleController.java | 212 ++++++++++++++++++ .../gui/status/radio/RadioController.java | 6 +- 5 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 src/de/knewcleus/openradar/gui/status/radio/FgComMumbleController.java diff --git a/src/de/knewcleus/openradar/gui/setup/AirportData.java b/src/de/knewcleus/openradar/gui/setup/AirportData.java index 15986a91..637c2473 100644 --- a/src/de/knewcleus/openradar/gui/setup/AirportData.java +++ b/src/de/knewcleus/openradar/gui/setup/AirportData.java @@ -109,7 +109,7 @@ public class AirportData implements INavPointListener { private HashSet activeStartingRouteRunways = new HashSet<>(); public enum FgComMode { - Auto, Internal, External, Off + Auto, Internal, External, Mumble, Off }; private FgComMode fgComMode = FgComMode.Internal; diff --git a/src/de/knewcleus/openradar/gui/setup/SetupDialog.java b/src/de/knewcleus/openradar/gui/setup/SetupDialog.java index feebc7f5..75e2e8fc 100644 --- a/src/de/knewcleus/openradar/gui/setup/SetupDialog.java +++ b/src/de/knewcleus/openradar/gui/setup/SetupDialog.java @@ -163,7 +163,7 @@ public class SetupDialog extends JFrame { private FgComMode fgComMode = FgComMode.Internal; private String[] modeModel = new String[] { "Auto: Use the internal fgcom", "Internal: OR starts and controls a fgcom client", - "External: Control external fgcom client instance", "OFF: No FgCom support" }; + "External: Control external fgcom client instance", "Mumble: Connect to FGCom-mumble plugin", "OFF: No FgCom support" }; private List icons = new ArrayList(); @@ -1672,8 +1672,10 @@ private void loadProperties() { cbFgComMode.setSelectedIndex(1); if (fgComMode == FgComMode.External) cbFgComMode.setSelectedIndex(2); - if (fgComMode == FgComMode.Off) + if (fgComMode == FgComMode.Mumble) cbFgComMode.setSelectedIndex(3); + if (fgComMode == FgComMode.Off) + cbFgComMode.setSelectedIndex(4); tfFgComPath.setText(p.getProperty("fgCom.path", "")); tfFgComPath.setText(p.getProperty("fgCom.path", "")); @@ -1893,6 +1895,15 @@ public void actionPerformed(ActionEvent e) { tfFgComHost.setEnabled(true); tfFgComServer.setEnabled(false); } else if (cbFgComMode.getSelectedIndex() == 3) { + fgComMode = FgComMode.Mumble; + cbFgComMode.setToolTipText("You will start Mumble with FGCom-mumble plugin yourself and OpenRadar will control it!"); + // FGCom-mumble + tfFgComPath.setEnabled(false); + tfFgComExec.setEnabled(false); + tfFgComPorts.setEnabled(true); + tfFgComHost.setEnabled(true); + tfFgComServer.setEnabled(false); + } else if (cbFgComMode.getSelectedIndex() == 4) { fgComMode = FgComMode.Off; cbFgComMode.setToolTipText("FGCom will not be controlled by OpenRadar"); // off diff --git a/src/de/knewcleus/openradar/gui/status/radio/FgComController.java b/src/de/knewcleus/openradar/gui/status/radio/FgComController.java index 9e2e2adf..0c8a61c5 100644 --- a/src/de/knewcleus/openradar/gui/status/radio/FgComController.java +++ b/src/de/knewcleus/openradar/gui/status/radio/FgComController.java @@ -74,22 +74,22 @@ */ public class FgComController implements Runnable, IRadioBackend { - private Thread thread = new Thread(this, "OpenRadar - FGComController"); - private GuiMasterController master = null; - private volatile double lon; - private volatile double lat; - private volatile double alt; - private Map fgComProcesses = Collections.synchronizedMap(new TreeMap()); - private List logWriters = Collections.synchronizedList(new ArrayList()); + protected Thread thread = new Thread(this, "OpenRadar - FGComController"); + protected GuiMasterController master = null; + protected volatile double lon; + protected volatile double lat; + protected volatile double alt; + protected Map fgComProcesses = Collections.synchronizedMap(new TreeMap()); + protected List logWriters = Collections.synchronizedList(new ArrayList()); - private final Map radios = Collections.synchronizedMap(new TreeMap()); + protected final Map radios = Collections.synchronizedMap(new TreeMap()); - private DatagramSocket datagramSocket; + protected DatagramSocket datagramSocket; - private volatile boolean isRunning = true; - private int sleeptime = 500; + protected volatile boolean isRunning = true; + protected int sleeptime = 500; - private final static Logger log = LogManager.getLogger(FgComController.class); + protected final static Logger log = LogManager.getLogger(FgComController.class); public FgComController() { } @@ -119,7 +119,7 @@ public void run() { Runtime.getRuntime().addShutdownHook(closeChildThread); } - private void endFgComProcesses() { + protected void endFgComProcesses() { for(LogWriterThread lw : logWriters) { lw.stop(); // marks them to exit @@ -187,7 +187,7 @@ public void run() { } } - private void sendSettings(Radio r) { + protected void sendSettings(Radio r) { if(master.getCurrentATCCallSign()!=null && !master.getCurrentATCCallSign().isEmpty()) { // after initialization try { String message = composeMessage(r); @@ -202,7 +202,7 @@ private void sendSettings(Radio r) { } } - private String composeMessage(Radio r) { + protected String composeMessage(Radio r) { // COM1_FRQ=120.500,COM1_SRV=1,COM2_FRQ=118.300,COM2_SRV=1,NAV1_FRQ=115.800,NAV1_SRV=1,NAV2_FRQ=116.800,NAV2_SRV=1,PTT=0,TRANSPONDER=0,IAS=09.8,GS=00.0,LON=-122.357193,LAT=37.613548,ALT=00004,HEAD=269.9,CALLSIGN=D-W794,MODEL=Aircraft/c172p/Models/c172p.xml StringBuilder sb = new StringBuilder(); sb.append("PTT="); @@ -371,7 +371,7 @@ public void initFgCom(Radio r, String pathToFgComExec, String fgComExec, String } } - private String buildString(List list) { + protected String buildString(List list) { StringBuilder sb = new StringBuilder(); for(String s : list) { if(sb.length()>0) { @@ -432,7 +432,7 @@ public static String getFgComSpecialsPath(AirportData data, String pathToFgComEx return specialsPath; } - private static String getFgComBasePath(String pathToFgComExec) { + protected static String getFgComBasePath(String pathToFgComExec) { String path = pathToFgComExec.trim(); if(path.contains(File.separator)) { File parentDir = pathToFgComExec.isEmpty() ? new File(System.getProperty("user.dir")) : new File(pathToFgComExec).getParentFile(); diff --git a/src/de/knewcleus/openradar/gui/status/radio/FgComMumbleController.java b/src/de/knewcleus/openradar/gui/status/radio/FgComMumbleController.java new file mode 100644 index 00000000..4b0ff169 --- /dev/null +++ b/src/de/knewcleus/openradar/gui/status/radio/FgComMumbleController.java @@ -0,0 +1,212 @@ +/** + * Copyright (C) 2020 Benedikt Hallinger + * + * This file is part of OpenRadar. + * + * OpenRadar 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 3 of the License, or (at your option) any later + * version. + * + * OpenRadar 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 + * OpenRadar. If not, see . + * + * Diese Datei ist Teil von OpenRadar. + * + * OpenRadar ist Freie Software: Sie können es unter den Bedingungen der GNU + * General Public License, wie von der Free Software Foundation, Version 3 der + * Lizenz oder (nach Ihrer Option) jeder späteren veröffentlichten Version, + * weiterverbreiten und/oder modifizieren. + * + * OpenRadar wird in der Hoffnung, dass es nützlich sein wird, aber OHNE JEDE + * GEWÄHRLEISTUNG, bereitgestellt; sogar ohne die implizite Gewährleistung der + * MARKTFÄHIGKEIT oder EIGNUNG FÜR EINEN BESTIMMTEN ZWECK. Siehe die GNU General + * Public License für weitere Details. + * + * Sie sollten eine Kopie der GNU General Public License zusammen mit diesem + * Programm erhalten haben. Wenn nicht, siehe . + */ +package de.knewcleus.openradar.gui.status.radio; + +import java.io.File; +import java.io.IOException; +import java.net.DatagramPacket; +import java.net.DatagramSocket; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import de.knewcleus.openradar.gui.GuiMasterController; +import de.knewcleus.openradar.gui.setup.AirportData; +import de.knewcleus.openradar.gui.setup.AirportData.FgComMode; +import java.security.InvalidParameterException; +import java.text.ParseException; +import java.util.HashMap; +import java.util.List; +import java.util.Vector; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * This class controls the FgCom-mumble plugin. + * + * Message format is described in detail at https://github.com/hbeni/fgcom-mumble/blob/master/client/plugin.spec.md + * + * @author Benedikt Hallinger + * + */ +public class FgComMumbleController extends FgComController implements Runnable, IRadioBackend { + + protected final static Logger log = LogManager.getLogger(FgComMumbleController.class); + + private boolean comZeroDetected = false; + + public FgComMumbleController() { + } + + public FgComMumbleController(GuiMasterController master, String aircraftModel, double lon, double lat, double alt) { + super(master, aircraftModel, lon, lat, alt); + } + + + @Override + public void run() { + while (isRunning) { + synchronized (this) { + sendSettings(); + } + try { + Thread.sleep(sleeptime); + } catch (InterruptedException e) { + } + } + } + + @Override + protected synchronized void sendSettings(Radio r) { + this.sendSettings(); + } + + /* + * Compose and send FGCom-mumble protocol messages + */ + protected synchronized void sendSettings() { + if (master.getCurrentATCCallSign()!=null && !master.getCurrentATCCallSign().isEmpty()) { // after initialization + + String message = ""; + + message += "CALLSIGN="; + message += master.getCurrentATCCallSign(); + message += String.format(",LAT=%.6f",lat); + message += String.format(",LON=%.6f",lon); //.replaceAll(",", ".") + message += String.format(",ALT=%1.0f",alt); + + // Compose individual radio state + for (Radio r : radios.values()) { + if (r.getCallSign() != null && r.getFgComPort() > 0) { + message += composeMessage(r); + } + } + + // Terminate the message + message += System.lineSeparator(); + + // Finally try to send + try { + // System.out.println("Sending fgcom message: "+message); + byte[] msgBytes = message.getBytes(Charset.forName("ISO-8859-1")); + DatagramPacket packet = new DatagramPacket(msgBytes, msgBytes.length); + + // send all info to all configured radio ports, but only once + Vector seenPorts = new Vector(); + for (Radio r : radios.values()) { + int port = r.getFgComPort(); + + if (!seenPorts.contains(port)) { + packet.setSocketAddress(new InetSocketAddress(r.getFgComHost(), port)); + datagramSocket.send(packet); + seenPorts.add(port); + log.debug("UDP-MSG sent: "+message); + } + } + } catch (IOException e) { + log.error("Error while tuning FGCOM!",e); + } + } + } + + /* + * Compose UDP message string for the given radio + */ + protected String composeMessage(Radio r) { + StringBuilder sb = new StringBuilder(); + Touple rName = this.splitComName(r.getKey()); + if (rName.left != null && rName.right != null) { + String rId = rName.left; + Integer rNr = rName.right; + if (rNr == 0 ) comZeroDetected = true; // COM0 detected + if (comZeroDetected) rNr++; // If COM0 was detected: add 1, because COMs start with COM1! + String rKey = rId + rNr.toString(); + + sb.append(","+rKey+"_PTT="); + sb.append(r.isPttActive() ? "1" : "0"); + sb.append(","+rKey+"_FRQ="); + sb.append(r.getFrequency()); + sb.append(","+rKey+"_VOL="); + sb.append(String.format("%.1f",r.getVolumeF()));//.replaceAll(",", ".") + + //System.out.println(sb.toString()); + return sb.toString(); + } else { + throw new InvalidParameterException("Could not parse Touple from com="+r.getKey()); + } + } + + /* + * Split COMn name into Radio type and Number + */ + protected Touple splitComName(String com) { + Pattern p = Pattern.compile("(\\w+)(\\d+)"); + Matcher m = p.matcher(com); + if (m.matches()) { + Touple t = new Touple<>(m.group(1), Integer.valueOf(m.group(2))); + return t; + } else { + throw new InvalidParameterException("Radio key '"+com+"' not in valid syntax COMn!"); + } + + } + + @Override + public synchronized void addRadio(String pathToFgComExec, String fgComExec, String key, String fgComServer, String fgComHost, int localFgComPort, + String callSign, RadioFrequency frequency) { + String key_derived = "COM"+radios.size(); + Radio r = new Radio(key_derived, fgComHost, localFgComPort, callSign, frequency); + //initFgCom(r, pathToFgComExec, fgComExec, fgComServer, localFgComPort); + radios.put(key_derived, r); + } + + + /* + * Simple Touple class + */ + protected class Touple { + Ta left; + Tb right; + Touple(Ta a, Tb b) { + left = a; + right = b; + } + } +} diff --git a/src/de/knewcleus/openradar/gui/status/radio/RadioController.java b/src/de/knewcleus/openradar/gui/status/radio/RadioController.java index 6e431612..e3e76481 100644 --- a/src/de/knewcleus/openradar/gui/status/radio/RadioController.java +++ b/src/de/knewcleus/openradar/gui/status/radio/RadioController.java @@ -86,7 +86,11 @@ public RadioController(GuiMasterController guiInteractionManager) { public void init() { AirportData data = master.getAirportData(); if(data.getFgComMode()!=FgComMode.Off) { - fgComController = new FgComController(master, data.getModel(), data.getLon(), data.getLat(), data.getElevationFt()); + if (data.getFgComMode() == FgComMode.Mumble) { + fgComController = new FgComMumbleController(master, data.getModel(), data.getLon(), data.getLat(), data.getElevationFt()); + } else { + fgComController = new FgComController(master, data.getModel(), data.getLon(), data.getLat(), data.getElevationFt()); + } int i = 0; for (Radio r : data.getRadios().values()) {