898 lines
36 KiB
Plaintext
898 lines
36 KiB
Plaintext
package org.ka2ddo.yaac.io;
|
|
/*
|
|
* Copyright (C) 2011-2021 Andrew Pavlin, KA2DDO
|
|
* This file is part of YAAC (Yet Another APRS Client).
|
|
*
|
|
* YAAC is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* YAAC 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
|
|
* and GNU Lesser General Public License along with YAAC. If not,
|
|
* see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import org.ka2ddo.ax25.AX25Message;
|
|
import org.ka2ddo.ax25.Connector;
|
|
import org.ka2ddo.yaac.util.Localizer;
|
|
|
|
import java.io.Serializable;
|
|
import java.util.Arrays;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
import java.util.prefs.BackingStoreException;
|
|
import java.util.prefs.Preferences;
|
|
|
|
/**
|
|
* This class contains all the parameters for configuring a PortConnector in
|
|
* this application, and the encoding and decoding code for saving its value
|
|
* in the persisted configuration data.
|
|
* @author Andrew Pavlin, KA2DDO
|
|
*/
|
|
public class PortConfig implements Serializable, Comparable<PortConfig> {
|
|
private static final long serialVersionUID = -162577201270002899L;
|
|
/**
|
|
* Empty array for ports that have no supported aliases.
|
|
* @see Cfg#digiAliases
|
|
*/
|
|
public static final String[] NO_ALIASES = new String[0];
|
|
/**
|
|
* Flag bit indicating this port allows transmitting APRS packets.
|
|
* @see Cfg#acceptableProtocolsMask
|
|
*/
|
|
public static final int PROTOCOL_APRS = 1;
|
|
/**
|
|
* Flag bit indicating this port allows transmitting OpenTRAC packets.
|
|
* @see Cfg#acceptableProtocolsMask
|
|
*/
|
|
public static final int PROTOCOL_OPENTRAC = 2;
|
|
/**
|
|
* Flag bit indicating this port allows transmitting AX.25 frames containing traffic of types
|
|
* other than APRS or OpenTRAC UI frames.
|
|
* @see #PROTOCOL_APRS
|
|
* @see #PROTOCOL_OPENTRAC
|
|
* @see Cfg#acceptableProtocolsMask
|
|
*/
|
|
public static final int PROTOCOL_AX25 = 4;
|
|
/**
|
|
* Highest bit number supported in protocol bit mask.
|
|
* @see Cfg#acceptableProtocolsMask
|
|
*/
|
|
public static final int MAX_PROTOCOL_BIT = 2;
|
|
/**
|
|
* No waypoint sentences sent to GPS on this port.
|
|
*/
|
|
public static final int SENTENCE_NONE = 0;
|
|
/**
|
|
* Send NMEA standard waypoint message back to GPS.
|
|
*/
|
|
public static final int SENTENCE_GPWPL = 1;
|
|
/**
|
|
* Send Kenwood-specific waypoint message back to GPS.
|
|
*/
|
|
public static final int SENTENCE_PKWDWPL = 2;
|
|
/**
|
|
* Indicates this is a TNC port over HF (low bandwidth, wide geographic coverage).
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_HF = 1;
|
|
/**
|
|
* Indicates this is a UDP socket (not TCP).
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_UDP = 2;
|
|
/**
|
|
* Indicates that this data should be consumed locally as well as transmitted.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_LOCAL = 4;
|
|
/**
|
|
* Indicates that non-flow-control serial ports should not raise the DTR and RTS signals.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_NODTR = 8;
|
|
/**
|
|
* Indicates that port opening should always retry if it fails, even on initial opening.
|
|
* Meant for APRS-IS port driver when used with flaky and/or intermittent Internet connection.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_ALWAYS_RETRY = 16;
|
|
/**
|
|
* Indicates that work-around for strange device features should be used on this port.
|
|
* For example, the KISS protocol ports use this to control the work-around for Kenwood
|
|
* APRS/TNC radios that could interpret certain characters in a KISS packet as commands
|
|
* from the memory programming application and thereby screw up the radio's settings. However,
|
|
* other TNCs do not accept the work-around.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_USE_WORKAROUND = 32;
|
|
/**
|
|
* Indicates that this port does not require the port driver to wait for the external
|
|
* device to finish processing the last outbound traffic before sending more traffic.
|
|
* This is for ports such as Serial_TNC, which could be connected to quarter-duplex devices
|
|
* like old TNCs that can't handle receiving another outbound packet from the computer
|
|
* while still transmitting the last packet to RF.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_PIPELINED = 64;
|
|
/**
|
|
* When using timeslotting on a TNC port, don't coalesce duplicate packets (such as beacons or
|
|
* status frames). This is useful for cases such as meteor-scatter operation. Not all TNC port types
|
|
* may support this capability. Note this is coded as a negative value to preserve backwards compatibility
|
|
* with YAAC installations configured at an earlier build.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_DONT_COALESCE = 128;
|
|
/**
|
|
* These four bits contain the KISS device ID to be used in KISS frames sent through this port.
|
|
* This supports the {@link KissOverTcpConnector} port type when talking to the DireWolf
|
|
* software TNC which can support up to 6 audio devices (and therefore up to 6 device IDs in
|
|
* KISS frames). Conveniently, since these bits weren't used before, the backwards-compatible
|
|
* default KISS device ID is zero.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_MASK_KISSPORT = 0xF00;
|
|
/**
|
|
* This constant gets the number of bits to shift the above {@link #FLAGS_MASK_KISSPORT} bits right
|
|
* to put them in the least significant bits of an integer value.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_SHIFT_KISSPORT = 8;
|
|
/**
|
|
* When looking up a TCP/IP network object, the {@link Cfg#deviceName deviceName} is not a fully-qualified domain name (FQDN)
|
|
* or numeric IP address of the target, but rather a service instance name for some service supported
|
|
* by the port per Internet RFC 6763 (DNS-Based Service Discovery), such that the code needs to do a
|
|
* service discovery to get the correct host name prior to using DNS to translate the host name.
|
|
* @see Cfg#flags
|
|
*/
|
|
public static final int FLAGS_IS_SRV_INST_NAME = 0x1000;
|
|
/**
|
|
* Indicate that {@link Cfg#deviceName} is of a hardware device rather than
|
|
* some software concept (like a network port).
|
|
*/
|
|
public static final int FLAGS_HARDWARE = 0x2000;
|
|
|
|
/**
|
|
* Enumeration identifying all the fields in a Cfg sub-record. Used by port driver classes to support
|
|
* configuration transfer by XML to indicate which fields are required and also which are to be suppressed (to
|
|
* block duplicate station callsigns, etc.).
|
|
* @see PortConnector
|
|
* @see ConfigImporter
|
|
* @author Andrew Pavlin, KA2DDO
|
|
*/
|
|
public enum Fields {
|
|
/** @see Cfg#enabled */
|
|
enabled,
|
|
/** @see Cfg#deviceName */
|
|
deviceName,
|
|
/** @see Cfg#baudRate */
|
|
baudRate,
|
|
/** @see Cfg#callsign */
|
|
callsign,
|
|
/** @see Cfg#passcode */
|
|
passcode,
|
|
/** @see Cfg#filter */
|
|
filter,
|
|
/** @see Cfg#transmitAllowed */
|
|
transmitAllowed,
|
|
/** @see Cfg#flowControlled */
|
|
flowControlled,
|
|
/** @see Cfg#flags */
|
|
flags,
|
|
/** @see Cfg#digiAliases */
|
|
digiAliases,
|
|
/** @see Cfg#acceptableProtocolsMask */
|
|
acceptableProtocolsMask,
|
|
/** @see Cfg#timeslotCycleLength */
|
|
timeslotCycleLength,
|
|
/** @see Cfg#timeslotOffset */
|
|
timeslotOffset,
|
|
/** @see Cfg#beaconNames */
|
|
beaconNames,
|
|
/** @see Cfg#timeslotLength */
|
|
timeslotLength }
|
|
|
|
/**
|
|
* For the {@link ConfigImporter}, provide hints of how required but non-transferable config fields
|
|
* should be asked for.
|
|
* @author Andrew Pavlin, KA2DDO
|
|
* @see RequireHints
|
|
*/
|
|
public enum HintType {
|
|
/**
|
|
* Just prompt with a plain text edit field.
|
|
*/
|
|
TEXT,
|
|
/**
|
|
* Just prompt with a plain text edit field, but only accept uppercase.
|
|
*/
|
|
TEXT_UC,
|
|
/**
|
|
* Prompt with a combo box pre-loaded with available serial ports.
|
|
*/
|
|
SERIAL_PORTS,
|
|
/**
|
|
* Prompt with a combo box pre-loaded with standard APRS-IS Tier 2 rotator hostnames.
|
|
*/
|
|
APRSIS_HOSTS,
|
|
/**
|
|
* Prompt with a combo box pre-loaded with the localhost name.
|
|
*/
|
|
LOCALHOST,
|
|
/**
|
|
* Prompt with a file chooser. {@link RequireHints} for this type must be a {@link RequireFile}.
|
|
*/
|
|
FILE_CHOOSER,
|
|
/**
|
|
* Prompt with a combo box loaded with a String array obtained from a static
|
|
* getDeviceNames() method provided by the {@link PortConnector} subclass.
|
|
*/
|
|
GENERIC_CALL_FOR_NAMELIST,
|
|
/**
|
|
* Just prompt with a plain text edit field, but only accept characters legal in a callsign-SSID.
|
|
*/
|
|
CALLSIGN_SSID
|
|
}
|
|
|
|
/**
|
|
* Data structure describing how the ConfigImporter should ask for missing (or
|
|
* blanked-out) required port configuration parameters.
|
|
* @author Andrew Pavlin, KA2DDO
|
|
* @see ConfigImporter
|
|
*/
|
|
public static class RequireHints {
|
|
/**
|
|
* The ResourceBundle key for a localized prompt string to display in a dialog box.
|
|
*/
|
|
public final String promptTag;
|
|
/**
|
|
* The data entry field type to be used in the prompting dialog box.
|
|
*/
|
|
public final HintType typeOfDialogPrompt;
|
|
|
|
/**
|
|
* Create a RequireHints object for the specified prompt string and a default text field.
|
|
* @param promptTag String key to a ResourceBundle localized prompt text
|
|
*/
|
|
public RequireHints(String promptTag) {
|
|
this.promptTag = promptTag;
|
|
this.typeOfDialogPrompt = HintType.TEXT;
|
|
}
|
|
|
|
/**
|
|
* Create a RequireHints object for the specified prompt string and the specified type
|
|
* of data entry field.
|
|
* @param promptTag String key to a ResourceBundle localized prompt text
|
|
* @param typeOfDialogPrompt HintType enum for the type of data entry to use
|
|
*/
|
|
public RequireHints(String promptTag, HintType typeOfDialogPrompt) {
|
|
this.promptTag = promptTag;
|
|
this.typeOfDialogPrompt = typeOfDialogPrompt;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Data structure describing how the ConfigImporter should ask for missing (or
|
|
* blanked-out) required port configuration parameters when they are a file in the
|
|
* filesystem.
|
|
* @author Andrew Pavlin, KA2DDO
|
|
* @see ConfigImporter
|
|
* @see HintType#FILE_CHOOSER
|
|
*/
|
|
public static class RequireFile extends RequireHints {
|
|
/**
|
|
* ResourceBundle key to localized text to display on file chooser's select button.
|
|
*/
|
|
public final String fileSelectTag;
|
|
/**
|
|
* Boolean true indicates chooser should select a directory, false for files.
|
|
*/
|
|
public final boolean isDirectory;
|
|
|
|
/**
|
|
* Create a RequireFile object for the specified prompt string and a file chooser.
|
|
* @param promptTag String key to a ResourceBundle localized prompt text
|
|
* @param fileSelectTag String text to a ResourceBundle localized text to display on the file chooser's select button
|
|
* @param isDirectory boolean true if user must select directory, false for file
|
|
*/
|
|
public RequireFile(String promptTag, String fileSelectTag, boolean isDirectory) {
|
|
super(promptTag, HintType.FILE_CHOOSER);
|
|
this.fileSelectTag = fileSelectTag;
|
|
this.isDirectory = isDirectory;
|
|
}
|
|
}
|
|
|
|
private String displayName = "";
|
|
private int portNumber = -1;
|
|
/**
|
|
* Type of PortConnector used to implement this port, generally defined as a String constant
|
|
* named PORT_TYPE on the PortConnector subclass.
|
|
*/
|
|
public String portType = "";
|
|
|
|
/**
|
|
* Whether port should be opened automatically upon startup.
|
|
*/
|
|
public boolean enabled = true;
|
|
|
|
/**
|
|
* Port-type-specific configuration parameters for a PortConnector.
|
|
* @author Andrew Pavlin, KA2DDO
|
|
*/
|
|
public static class Cfg implements Serializable, Comparable<Cfg> {
|
|
private static final long serialVersionUID = 885437494458611609L;
|
|
|
|
/**
|
|
* The device name or network host name/address associated with this port.
|
|
*/
|
|
public String deviceName = "";
|
|
/**
|
|
* The baud rate used for the port, if needed, or port number for TCP and UDP socket connections.
|
|
*/
|
|
public int baudRate = 0;
|
|
/**
|
|
* The amateur radio station callsign associated with this port, if needed.
|
|
* Also used for weather station model name.
|
|
*/
|
|
public String callsign = "";
|
|
/**
|
|
* The authentication passcode associated with this port, if needed.
|
|
*/
|
|
public String passcode = "";
|
|
/**
|
|
* Any filter expression associated with this port, if needed.
|
|
*/
|
|
public String filter = "";
|
|
/**
|
|
* Indicates whether messages can be transmitted from YAAC via this port.
|
|
*/
|
|
public boolean transmitAllowed;
|
|
/**
|
|
* Indicates whether flow control is enabled on this port.
|
|
*/
|
|
public boolean flowControlled;
|
|
/**
|
|
* A collection of flag bits indicating other attributes of the port configuration.
|
|
* @see #FLAGS_HF
|
|
* @see #FLAGS_UDP
|
|
* @see #FLAGS_LOCAL
|
|
* @see #FLAGS_NODTR
|
|
* @see #FLAGS_ALWAYS_RETRY
|
|
* @see #FLAGS_USE_WORKAROUND
|
|
* @see #FLAGS_PIPELINED
|
|
* @see #FLAGS_DONT_COALESCE
|
|
* @see #FLAGS_MASK_KISSPORT
|
|
* @see #FLAGS_IS_SRV_INST_NAME
|
|
* @see #FLAGS_HARDWARE
|
|
*/
|
|
public int flags;
|
|
/**
|
|
* Array of digipeat alias Strings for which this port will digipeat (if transmitAllowed is true).
|
|
*
|
|
* @see #transmitAllowed
|
|
*/
|
|
public String[] digiAliases = NO_ALIASES;
|
|
/**
|
|
* Bit mask or enum number of protocols that can be transmitted through this port. Only meaningful for
|
|
* ports with CAP_XMT_PACKET_DATA set (for protocol bitmask) or CAP_WAYPOINT_SENDER (for enum).
|
|
* @see Connector#CAP_XMT_PACKET_DATA
|
|
* @see Connector#CAP_WAYPOINT_SENDER
|
|
* @see #PROTOCOL_APRS
|
|
* @see #PROTOCOL_OPENTRAC
|
|
* @see #PROTOCOL_AX25
|
|
* @see #SENTENCE_NONE
|
|
* @see #SENTENCE_GPWPL
|
|
* @see #SENTENCE_PKWDWPL
|
|
*/
|
|
public int acceptableProtocolsMask = PROTOCOL_APRS;
|
|
/**
|
|
* Number of seconds in a timeslot cycle, which will be aligned to UTC and the Unix epoch time
|
|
* (if some weird prime number is used). Negative values means timeslotting is disabled, but is
|
|
* preserving the last-used cycle length in case it gets re-enabled.
|
|
*/
|
|
public int timeslotCycleLength = -120;
|
|
/**
|
|
* Number of seconds since the start of a cycle when this port is allowed to transmit.
|
|
* This should be an integer multiple of {@link #timeslotLength} greater than or equal to
|
|
* zero, but less than the {@link #timeslotCycleLength}.
|
|
*/
|
|
public int timeslotOffset = 0;
|
|
/**
|
|
* Number of seconds in a timeslot. This should be a positive integer fraction of the
|
|
* {@link #timeslotCycleLength}. Zero or negative means that once a station enters its
|
|
* timeslot, it can transmit until its queue for the port is empty, rather than having
|
|
* to stop because another station's timeslot has started.
|
|
*/
|
|
public int timeslotLength = 10;
|
|
/**
|
|
* Names of beacon instances to send through this port (zero-length array means only default beacon).
|
|
* @see BeaconData
|
|
* @see BeaconData#MYCALL
|
|
* @see BeaconData#beaconName
|
|
*/
|
|
public String[] beaconNames = new String[0];
|
|
|
|
/**
|
|
* Make a deep copy of this Cfg object.
|
|
*
|
|
* @return duplicate Cfg
|
|
*/
|
|
public Cfg dup() {
|
|
Cfg cfg = new Cfg();
|
|
cfg.deviceName = deviceName;
|
|
cfg.baudRate = baudRate;
|
|
cfg.callsign = callsign;
|
|
cfg.passcode = passcode;
|
|
cfg.transmitAllowed = transmitAllowed;
|
|
cfg.digiAliases = new String[digiAliases.length];
|
|
System.arraycopy(digiAliases, 0, cfg.digiAliases, 0, digiAliases.length);
|
|
cfg.filter = filter;
|
|
cfg.flowControlled = flowControlled;
|
|
cfg.acceptableProtocolsMask = acceptableProtocolsMask;
|
|
cfg.timeslotCycleLength = timeslotCycleLength;
|
|
cfg.timeslotOffset = timeslotOffset;
|
|
cfg.timeslotLength = timeslotLength;
|
|
cfg.flags = flags;
|
|
cfg.beaconNames = Arrays.copyOf(beaconNames, beaconNames.length);
|
|
return cfg;
|
|
}
|
|
|
|
/**
|
|
* Compares this object with the specified object for order. Returns a
|
|
* negative integer, zero, or a positive integer as this object is less
|
|
* than, equal to, or greater than the specified object.
|
|
*
|
|
* @param o the object to be compared.
|
|
* @return a negative integer, zero, or a positive integer as this object
|
|
* is less than, equal to, or greater than the specified object.
|
|
*
|
|
* @throws ClassCastException if the specified object's type prevents it
|
|
* from being compared to this object.
|
|
*/
|
|
public int compareTo(Cfg o) {
|
|
return deviceName.compareTo(o.deviceName);
|
|
}
|
|
|
|
/**
|
|
* Indicates whether some other object is "equal to" this one.
|
|
*
|
|
* @param obj the reference object with which to compare.
|
|
* @return {@code true} if this object is the same as the obj
|
|
* argument; {@code false} otherwise.
|
|
*/
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (obj instanceof Cfg) {
|
|
Cfg other = (Cfg)obj;
|
|
return other.deviceName.equals(deviceName) &&
|
|
other.baudRate == baudRate &&
|
|
other.callsign.equals(callsign) &&
|
|
other.passcode.equals(passcode) &&
|
|
other.transmitAllowed == transmitAllowed &&
|
|
Arrays.equals(digiAliases, other.digiAliases) &&
|
|
other.filter.equals(filter) &&
|
|
other.flowControlled == flowControlled &&
|
|
other.acceptableProtocolsMask == acceptableProtocolsMask &&
|
|
other.timeslotCycleLength == timeslotCycleLength &&
|
|
other.timeslotOffset == timeslotOffset &&
|
|
other.timeslotLength == timeslotLength &&
|
|
other.flags == flags &&
|
|
Arrays.equals(other.beaconNames, beaconNames);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns a hash code value for the object.
|
|
*
|
|
* @return a hash code value for this object.
|
|
* @see #equals(Object)
|
|
*/
|
|
@Override
|
|
public int hashCode() {
|
|
return deviceName.hashCode();
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of the object.
|
|
* @return a string representation of the object.
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
return "PortConfig.Cfg[" + deviceName + ',' + baudRate + ',' + callsign + ',' + (transmitAllowed ? "xmt" : "") +
|
|
",digi=" + Arrays.toString(digiAliases) + ',' + (null != filter && filter.length() > 0 ? "filt=\"" + filter + '"' : "") +
|
|
(flags != 0 ? ",flags=0x" + Integer.toHexString(flags) : "") +
|
|
(timeslotCycleLength > 0 ? ",ts=" + timeslotOffset + '(' + timeslotLength + ")/" + timeslotCycleLength : "") + ']';
|
|
}
|
|
}
|
|
|
|
private final HashMap<String, Cfg> variations = new HashMap<String, Cfg>(2);
|
|
|
|
/**
|
|
* Get the port-type-specific configuration parameters for the currently specified port type.
|
|
* @return type-specific Cfg record
|
|
*/
|
|
public final Cfg current() {
|
|
Cfg cfg;
|
|
if ((cfg = variations.get(portType)) == null) {
|
|
variations.put(portType, cfg = new Cfg());
|
|
}
|
|
return cfg;
|
|
}
|
|
|
|
/**
|
|
* Get the port-type-specific configuration parameters for a specific port type.
|
|
* @param portType the name of the port type whose configuration parameters should be obtained
|
|
* @return type-specific Cfg record (created if it did not previously exist)
|
|
*/
|
|
public final Cfg specific(String portType) {
|
|
Cfg cfg;
|
|
if ((cfg = variations.get(portType)) == null) {
|
|
variations.put(portType, cfg = new Cfg());
|
|
}
|
|
return cfg;
|
|
}
|
|
|
|
/**
|
|
* Create a new PortConfig record with a new port instance identifier.
|
|
*/
|
|
public PortConfig() {}
|
|
|
|
/**
|
|
* Allocate a new port number for this PortConfig object.
|
|
*/
|
|
public void assignPortNumber() {
|
|
portNumber = PortManager.getNextAvailablePortNumber();
|
|
displayName = "Port" + portNumber;
|
|
}
|
|
|
|
/**
|
|
* Craete a PortConfig record with the specified port instance identifier.
|
|
* @param displayName port identifier name to use
|
|
*/
|
|
public PortConfig(String displayName) {
|
|
this.displayName = displayName;
|
|
portNumber = Integer.parseInt(displayName.substring(4).trim());
|
|
}
|
|
|
|
/**
|
|
* Indicates whether some other object is "equal to" this one.
|
|
*
|
|
* @param obj the reference object with which to compare.
|
|
* @return <code>true</code> if this object is the same as the obj
|
|
* argument; <code>false</code> otherwise.
|
|
* @see #hashCode()
|
|
*/
|
|
@Override
|
|
public boolean equals(Object obj) {
|
|
if (obj instanceof PortConfig) {
|
|
PortConfig o = (PortConfig)obj;
|
|
return displayName.equals(o.displayName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns a hash code value for the object. This method is
|
|
* supported for the benefit of hashtables such as those provided by
|
|
* <code>java.util.Hashtable</code>.
|
|
*
|
|
* @return a hash code value for this object.
|
|
* @see #equals(Object)
|
|
*/
|
|
@Override
|
|
public int hashCode() {
|
|
return displayName.hashCode();
|
|
}
|
|
|
|
/**
|
|
* Get the name by which this port will be identified.
|
|
* @return String port name
|
|
*/
|
|
public String getDisplayName() {
|
|
return displayName;
|
|
}
|
|
|
|
/**
|
|
* Get the arbitrary sequence port number assigned to this configuration record.
|
|
* @return port number
|
|
*/
|
|
public int getPortNumber() {
|
|
return portNumber;
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of the object.
|
|
*
|
|
* @return a string representation of the object.
|
|
*/
|
|
@Override
|
|
public String toString() {
|
|
Cfg cfg = current();
|
|
return "PortConfig[" + displayName + '>' + portType + ',' + cfg.deviceName + ',' + cfg.baudRate + ',' + cfg.callsign + ']';
|
|
}
|
|
|
|
/**
|
|
* Make a deep copy of this PortConfig object.
|
|
* @return duplicate PortConfig
|
|
*/
|
|
public PortConfig dup() {
|
|
PortConfig cfg = new PortConfig(displayName);
|
|
cfg.portType = portType;
|
|
cfg.enabled = enabled;
|
|
for (Map.Entry<String, Cfg> entry : variations.entrySet()) {
|
|
cfg.variations.put(entry.getKey(), entry.getValue().dup());
|
|
}
|
|
return cfg;
|
|
}
|
|
|
|
/**
|
|
* Do a deep copy of this PortConfig object into another object..
|
|
* @param dest destination PortConfig
|
|
*/
|
|
public void copyInto(PortConfig dest) {
|
|
dest.portType = portType;
|
|
dest.enabled = enabled;
|
|
dest.variations.clear();
|
|
dest.variations.put(portType, variations.get(portType));
|
|
}
|
|
|
|
/**
|
|
* Compares this object with the specified object for order. Returns a
|
|
* negative integer, zero, or a positive integer as this object is less
|
|
* than, equal to, or greater than the specified object.
|
|
*
|
|
* @param other the object to be compared.
|
|
* @return a negative integer, zero, or a positive integer as this object
|
|
* is less than, equal to, or greater than the specified object.
|
|
*
|
|
* @throws ClassCastException if the specified object's type prevents it
|
|
* from being compared to this object.
|
|
*/
|
|
public int compareTo(PortConfig other) {
|
|
int diff = portType.compareTo(other.portType);
|
|
if (0 == diff) {
|
|
diff = current().compareTo(other.current());
|
|
}
|
|
return diff;
|
|
}
|
|
|
|
/**
|
|
* Write this PortConfig object to Java Preferences on the Ports sub-node of the
|
|
* specified Preferences node.
|
|
* @param root Preferences node to use to store the Ports sub-node
|
|
*/
|
|
public void writeToPreferences(Preferences root) {
|
|
// write or overwrite preferences
|
|
Preferences portNode = root.node("Ports");
|
|
portNode.put(displayName, generatePrefsString());
|
|
try {
|
|
portNode.flush();
|
|
} catch (BackingStoreException e) {
|
|
e.printStackTrace(System.out);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* <p>Generate the encoded String for storing the PortConfig into Java Preferences.
|
|
* The preferences String consists of multiple expressions separated by semicolon ";"
|
|
* characters. The first expression (required) will have the following format:</p>
|
|
* <code><i>portType</i>,<i>deviceName</i>,<i>baudRateOrPortNumber</i>,<i>callsign</i>,<i>passcode</i>,<i>transmitEnabled</i>[,<i>digipeatAlias[,...]]</i></code>
|
|
* <p>where <i>deviceName</i> and <i>passcode</i> are escaped to be legitimate text in an XML tag body,
|
|
* <i>transmitEnabled</i> is either "true" or "false", and there can be any number of digipeat aliases
|
|
* in the syntax of display-format AX.25 callsign-SSID values.</p>
|
|
* <p>The second expression is a APRS-IS filter expression.</p>
|
|
* <p>The third expression is "true" or "false" for whether this port is enabled for operation.</p>
|
|
* <p>The fourth expression is "true" or "false" for whether flow control (or something overloading
|
|
* the meaning of this field) should be enabled or not.</p>
|
|
* <p>The fifth expression is a decimal number of the value of the {@link Cfg#acceptableProtocolsMask} bitmask.</p>
|
|
* <p>The sixth expression is two decimal numbers separated by a slash "/" character representing
|
|
* the timeslot offset (relative to the top of the cycle) in seconds and the length of the timeslot cycle in seconds.
|
|
* Negative cycle length indicates that timeslotting is not used, but the values are preserved
|
|
* in case the user wants to turn timeslotting back on.</p>
|
|
* <p>The seventh expression is a decimal number of the value of the {@link Cfg#flags} bitmask.
|
|
* Bit meanings are portType-specific.</p>
|
|
* <p>The eighth expression is a pipe "|" separated list of names of {@link BeaconData}
|
|
* definitions that should be transmitted through this port. Only meaningful for ports
|
|
* capable of transmitting APRS or OpenTRAC packets.</p>
|
|
* @return encoded String
|
|
* @see #decodePreferenceValue(String)
|
|
*/
|
|
public String generatePrefsString() {
|
|
Cfg cfg = current();
|
|
StringBuilder b = new StringBuilder(portType).append(',').append(passcodeEncode(cfg.deviceName)).append(',').
|
|
append(cfg.baudRate).append(',').append(cfg.callsign).append(',').append(passcodeEncode(cfg.passcode)).append(',').append(cfg.transmitAllowed);
|
|
for (String r : cfg.digiAliases) {
|
|
b.append(',').append(r);
|
|
}
|
|
b.append(';').append(cfg.filter).append(';').append(enabled).append(';').append(cfg.flowControlled).append(';').append(cfg.acceptableProtocolsMask);
|
|
b.append(';').append(cfg.timeslotOffset).append('/').append(cfg.timeslotCycleLength).append('/').append(cfg.timeslotLength);
|
|
b.append(';').append(cfg.flags).append(';');
|
|
for (int i = 0; i < cfg.beaconNames.length; i++) {
|
|
if (0 != i) {
|
|
b.append('|');
|
|
}
|
|
b.append(cfg.beaconNames[i]);
|
|
}
|
|
return b.toString();
|
|
}
|
|
|
|
/**
|
|
* Encode the passcode so characters that would break XML or our field concatenation are escaped.
|
|
* @param passcode String containing passcode
|
|
* @return escaped passcode
|
|
*/
|
|
private static String passcodeEncode(String passcode) {
|
|
boolean needsEscape = false;
|
|
int len = passcode.length();
|
|
for (int i = 0; i < len; i++) {
|
|
char ch = passcode.charAt(i);
|
|
if (ch < ' ' || ch >= '\u007F' || '&' == ch || '"' == ch || '\\' == ch || ',' == ch || ';' == ch) {
|
|
needsEscape = true;
|
|
break;
|
|
}
|
|
}
|
|
if (needsEscape) {
|
|
StringBuilder b = new StringBuilder(len + 20);
|
|
for (int i = 0; i < len; i++) {
|
|
char ch = passcode.charAt(i);
|
|
if (ch < ' ' || ch >= '\u007F' || '&' == ch || '"' == ch || '\\' == ch || ',' == ch || ';' == ch) {
|
|
b.append("\\u").append(Integer.toHexString(0x10000 + (int)ch).substring(1));
|
|
} else {
|
|
b.append(ch);
|
|
}
|
|
}
|
|
passcode = b.toString();
|
|
}
|
|
return passcode;
|
|
}
|
|
|
|
/**
|
|
* Delete this PortConfig from the Java Preferences backing store.
|
|
* @param root Preferences node used to store the Ports sub-node
|
|
*/
|
|
public void removeFromPreferences(Preferences root) {
|
|
// remove this PortConfig from the Preferences
|
|
Preferences portNode = root.node("Ports");
|
|
portNode.remove(displayName);
|
|
}
|
|
|
|
/**
|
|
* Read a PortConfig object from Java Preferences.
|
|
* @param root Preferences node used to store the Ports sub-node
|
|
* @param displayName the String port identifier name to read
|
|
* @return a populated PortConfig object for the specified name, or null if no such record
|
|
* exists in the specified node of Preferences
|
|
*/
|
|
public static PortConfig readFromPreferences(Preferences root, String displayName) {
|
|
PortConfig config = null;
|
|
Preferences portNode = root.node("Ports");
|
|
String values = portNode.get(displayName, null);
|
|
if (values != null) {
|
|
config = new PortConfig(displayName);
|
|
config.decodePreferenceValue(values);
|
|
}
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Decode the storage format used to store a PortConfig in Java Preferences into
|
|
* this PortConfig object.
|
|
* @param values String of encoded values
|
|
* @see #generatePrefsString()
|
|
*/
|
|
public void decodePreferenceValue(String values) {
|
|
String[] prePostFilter = AX25Message.split(values, ';');
|
|
String[] tk = AX25Message.split(prePostFilter[0], ',');
|
|
portType = tk[0];
|
|
if (portType.equals("I-Gate")) {
|
|
portType = "APRS-IS";
|
|
}
|
|
Cfg cfg = current();
|
|
cfg.deviceName = passcodeDecode(tk[1]);
|
|
if (tk.length >= 3) {
|
|
cfg.baudRate = Integer.parseInt(tk[2]);
|
|
if (tk.length >= 4) {
|
|
cfg.callsign = tk[3];
|
|
if (tk.length >= 5) {
|
|
cfg.passcode = passcodeDecode(tk[4]);
|
|
if (tk.length >= 6) {
|
|
cfg.transmitAllowed = Boolean.parseBoolean(tk[5]);
|
|
if (tk.length >= 7) {
|
|
cfg.digiAliases = new String[tk.length - 6];
|
|
System.arraycopy(tk, 6, cfg.digiAliases, 0, cfg.digiAliases.length);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (prePostFilter.length > 1) {
|
|
cfg.filter = prePostFilter[1];
|
|
if (prePostFilter.length > 2) {
|
|
if (prePostFilter[2].length() > 0) {
|
|
enabled = Boolean.parseBoolean(prePostFilter[2]);
|
|
}
|
|
if (prePostFilter.length > 3) {
|
|
if (prePostFilter[3].length() > 0) {
|
|
cfg.flowControlled = Boolean.parseBoolean(prePostFilter[3]);
|
|
}
|
|
if (prePostFilter.length > 4) {
|
|
if (prePostFilter[4].length() > 0) {
|
|
cfg.acceptableProtocolsMask = Integer.parseInt(prePostFilter[4]);
|
|
}
|
|
if (prePostFilter.length > 5) {
|
|
if (prePostFilter[5].length() > 0) {
|
|
String[] timeslotParams = AX25Message.split(prePostFilter[5], '/');
|
|
cfg.timeslotOffset = Integer.parseInt(timeslotParams[0]);
|
|
cfg.timeslotCycleLength = Integer.parseInt(timeslotParams[1]);
|
|
if (timeslotParams.length >= 3) {
|
|
cfg.timeslotLength = Integer.parseInt(timeslotParams[2]);
|
|
} else {
|
|
cfg.timeslotLength = 0; // for backwards compatibility
|
|
}
|
|
}
|
|
if (prePostFilter.length > 6) {
|
|
if (prePostFilter[6].length() > 0) {
|
|
cfg.flags = Integer.parseInt(prePostFilter[6]);
|
|
}
|
|
if (prePostFilter.length > 7) {
|
|
if (prePostFilter[7].length() > 0) {
|
|
cfg.beaconNames = AX25Message.split(prePostFilter[7], '\u001E');
|
|
//TODO: temporary repair until damage from build#107 is removed
|
|
String defaultBeaconDisplayName = Localizer.getMsg("configure.tab.Beacon.DefaultBeaconName");
|
|
for (int i = 0; i < cfg.beaconNames.length; i++) {
|
|
if (cfg.beaconNames[i].equals(defaultBeaconDisplayName)) {
|
|
cfg.beaconNames[i] = BeaconData.MYCALL;
|
|
}
|
|
}
|
|
} else {
|
|
cfg.beaconNames = new String[0];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decode the passcode so characters that would break XML or our field concatenation are un-escaped.
|
|
* @param passcode String containing escaped passcode
|
|
* @return unescaped passcode
|
|
*/
|
|
private static String passcodeDecode(String passcode) {
|
|
boolean hasEscape = false;
|
|
int len = passcode.length();
|
|
for (int i = 0; i < len; i++) {
|
|
char ch = passcode.charAt(i);
|
|
if ('\\' == ch) {
|
|
hasEscape = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hasEscape) {
|
|
StringBuilder b = new StringBuilder(len);
|
|
for (int i = 0; i < len; i++) {
|
|
char ch = passcode.charAt(i);
|
|
if ('\\' == ch) {
|
|
if (passcode.charAt(i+1) != 'u' || i + 6 > len) {
|
|
throw new IllegalArgumentException("invalid encoded string \"" + passcode + '"');
|
|
}
|
|
ch = (char)Integer.parseInt(passcode.substring(i+2,i+6), 16);
|
|
i += 5;
|
|
}
|
|
b.append(ch);
|
|
}
|
|
passcode = b.toString();
|
|
}
|
|
return passcode;
|
|
}
|
|
} |