mirror of
https://github.com/arduino/Arduino.git
synced 2025-03-13 10:29:35 +01:00
Merge pull request #8038 from cmaglie/pluggable-discovery
Pluggable discovery: search in platform.txt (WIP)
This commit is contained in:
commit
0e45f4e0d4
@ -267,6 +267,8 @@ public class Base {
|
||||
splash.splashText(tr("Initializing packages..."));
|
||||
BaseNoGui.initPackages();
|
||||
|
||||
parser.getUploadPort().ifPresent(BaseNoGui::selectSerialPort);
|
||||
|
||||
splash.splashText(tr("Preparing boards..."));
|
||||
|
||||
if (!isCommandLine()) {
|
||||
|
@ -52,7 +52,6 @@ import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
@ -96,6 +95,7 @@ import cc.arduino.view.StubMenuListener;
|
||||
import cc.arduino.view.findreplace.FindReplace;
|
||||
import jssc.SerialPortException;
|
||||
import processing.app.debug.RunnerException;
|
||||
import processing.app.debug.TargetBoard;
|
||||
import processing.app.forms.PasswordAuthorizationDialog;
|
||||
import processing.app.helpers.DocumentTextChangeListener;
|
||||
import processing.app.helpers.Keys;
|
||||
@ -149,9 +149,6 @@ public class Editor extends JFrame implements RunnerListener {
|
||||
}
|
||||
}
|
||||
|
||||
private final static List<String> BOARD_PROTOCOLS_ORDER = Arrays.asList("serial", "network");
|
||||
private final static List<String> BOARD_PROTOCOLS_ORDER_TRANSLATIONS = Arrays.asList(tr("Serial ports"), tr("Network ports"));
|
||||
|
||||
final Base base;
|
||||
|
||||
// otherwise, if the window is resized with the message label
|
||||
@ -1043,22 +1040,30 @@ public class Editor extends JFrame implements RunnerListener {
|
||||
private BoardPort port;
|
||||
|
||||
public BoardPortJCheckBoxMenuItem(BoardPort port) {
|
||||
super(port.getLabel());
|
||||
super();
|
||||
this.port = port;
|
||||
setText(toString());
|
||||
addActionListener(e -> {
|
||||
selectSerialPort(port.getAddress());
|
||||
base.onBoardOrPortChange();
|
||||
});
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// This is required for serialPrompt()
|
||||
return port.getLabel();
|
||||
String label = port.getLabel();
|
||||
if (port.getBoardName() != null && !port.getBoardName().isEmpty()) {
|
||||
label += " (" + port.getBoardName() + ")";
|
||||
}
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
private void populatePortMenu() {
|
||||
final List<String> PROTOCOLS_ORDER = Arrays.asList("serial", "network");
|
||||
final List<String> PROTOCOLS_LABELS = Arrays.asList(tr("Serial ports"), tr("Network ports"));
|
||||
|
||||
portMenu.removeAll();
|
||||
|
||||
String selectedPort = PreferencesData.get("serial.port");
|
||||
@ -1067,31 +1072,43 @@ public class Editor extends JFrame implements RunnerListener {
|
||||
|
||||
ports = platform.filterPorts(ports, PreferencesData.getBoolean("serial.ports.showall"));
|
||||
|
||||
Collections.sort(ports, new Comparator<BoardPort>() {
|
||||
@Override
|
||||
public int compare(BoardPort o1, BoardPort o2) {
|
||||
return (BOARD_PROTOCOLS_ORDER.indexOf(o1.getProtocol()) - BOARD_PROTOCOLS_ORDER.indexOf(o2.getProtocol())) * 10 +
|
||||
o1.getAddress().compareTo(o2.getAddress());
|
||||
}
|
||||
ports.stream() //
|
||||
.filter(port -> port.getProtocolLabel() == null || port.getProtocolLabel().isEmpty())
|
||||
.forEach(port -> {
|
||||
int labelIdx = PROTOCOLS_ORDER.indexOf(port.getProtocol());
|
||||
if (labelIdx != -1) {
|
||||
port.setProtocolLabel(PROTOCOLS_LABELS.get(labelIdx));
|
||||
} else {
|
||||
port.setProtocolLabel(port.getProtocol());
|
||||
}
|
||||
});
|
||||
|
||||
Collections.sort(ports, (port1, port2) -> {
|
||||
String pr1 = port1.getProtocol();
|
||||
String pr2 = port2.getProtocol();
|
||||
int prIdx1 = PROTOCOLS_ORDER.contains(pr1) ? PROTOCOLS_ORDER.indexOf(pr1) : 999;
|
||||
int prIdx2 = PROTOCOLS_ORDER.contains(pr2) ? PROTOCOLS_ORDER.indexOf(pr2) : 999;
|
||||
int r = prIdx1 - prIdx2;
|
||||
if (r != 0)
|
||||
return r;
|
||||
r = port1.getProtocolLabel().compareTo(port2.getProtocolLabel());
|
||||
if (r != 0)
|
||||
return r;
|
||||
return port1.getAddress().compareTo(port2.getAddress());
|
||||
});
|
||||
|
||||
String lastProtocol = null;
|
||||
String lastProtocolTranslated;
|
||||
String lastProtocol = "";
|
||||
String lastProtocolLabel = "";
|
||||
for (BoardPort port : ports) {
|
||||
if (lastProtocol == null || !port.getProtocol().equals(lastProtocol)) {
|
||||
if (lastProtocol != null) {
|
||||
if (!port.getProtocol().equals(lastProtocol) || !port.getProtocolLabel().equals(lastProtocolLabel)) {
|
||||
if (!lastProtocol.isEmpty()) {
|
||||
portMenu.addSeparator();
|
||||
}
|
||||
lastProtocol = port.getProtocol();
|
||||
|
||||
if (BOARD_PROTOCOLS_ORDER.indexOf(port.getProtocol()) != -1) {
|
||||
lastProtocolTranslated = BOARD_PROTOCOLS_ORDER_TRANSLATIONS.get(BOARD_PROTOCOLS_ORDER.indexOf(port.getProtocol()));
|
||||
} else {
|
||||
lastProtocolTranslated = port.getProtocol();
|
||||
}
|
||||
JMenuItem lastProtocolMenuItem = new JMenuItem(tr(lastProtocolTranslated));
|
||||
lastProtocolMenuItem.setEnabled(false);
|
||||
portMenu.add(lastProtocolMenuItem);
|
||||
lastProtocolLabel = port.getProtocolLabel();
|
||||
JMenuItem item = new JMenuItem(tr(lastProtocolLabel));
|
||||
item.setEnabled(false);
|
||||
portMenu.add(item);
|
||||
}
|
||||
String address = port.getAddress();
|
||||
|
||||
@ -2403,9 +2420,9 @@ public class Editor extends JFrame implements RunnerListener {
|
||||
for (BoardPort port : ports) {
|
||||
if (port.getAddress().equals(selectedPort)) {
|
||||
label = port.getBoardName();
|
||||
vid = port.getVID();
|
||||
pid = port.getPID();
|
||||
iserial = port.getISerial();
|
||||
vid = port.getPrefs().get("vid");
|
||||
pid = port.getPrefs().get("pid");
|
||||
iserial = port.getPrefs().get("iserial");
|
||||
protocol = port.getProtocol();
|
||||
found = true;
|
||||
break;
|
||||
@ -2575,12 +2592,12 @@ public class Editor extends JFrame implements RunnerListener {
|
||||
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
|
||||
|
||||
protected void onBoardOrPortChange() {
|
||||
Map<String, String> boardPreferences = BaseNoGui.getBoardPreferences();
|
||||
if (boardPreferences != null)
|
||||
lineStatus.setBoardName(boardPreferences.get("name"));
|
||||
TargetBoard board = BaseNoGui.getTargetBoard();
|
||||
if (board != null)
|
||||
lineStatus.setBoardName(board.getName());
|
||||
else
|
||||
lineStatus.setBoardName("-");
|
||||
lineStatus.setSerialPort(PreferencesData.get("serial.port"));
|
||||
lineStatus.setPort(PreferencesData.get("serial.port"));
|
||||
lineStatus.repaint();
|
||||
}
|
||||
|
||||
|
@ -51,8 +51,7 @@ public class EditorLineStatus extends JComponent {
|
||||
|
||||
String text = "";
|
||||
String name = "";
|
||||
String serialport = "";
|
||||
String serialnumber = "";
|
||||
String port = "";
|
||||
|
||||
public EditorLineStatus() {
|
||||
background = Theme.getColor("linestatus.bgcolor");
|
||||
@ -92,13 +91,13 @@ public class EditorLineStatus extends JComponent {
|
||||
|
||||
public void paintComponent(Graphics graphics) {
|
||||
Graphics2D g = Theme.setupGraphics2D(graphics);
|
||||
if (name.isEmpty() && serialport.isEmpty()) {
|
||||
if (name.isEmpty() && port.isEmpty()) {
|
||||
PreferencesMap boardPreferences = BaseNoGui.getBoardPreferences();
|
||||
if (boardPreferences != null)
|
||||
setBoardName(boardPreferences.get("name"));
|
||||
else
|
||||
setBoardName("-");
|
||||
setSerialPort(PreferencesData.get("serial.port"));
|
||||
setPort(PreferencesData.get("serial.port"));
|
||||
}
|
||||
g.setColor(background);
|
||||
Dimension size = getSize();
|
||||
@ -112,8 +111,8 @@ public class EditorLineStatus extends JComponent {
|
||||
g.setColor(messageForeground);
|
||||
|
||||
String statusText;
|
||||
if (serialport != null && !serialport.isEmpty()) {
|
||||
statusText = I18n.format(tr("{0} on {1}"), name, serialport);
|
||||
if (port != null && !port.isEmpty()) {
|
||||
statusText = I18n.format(tr("{0} on {1}"), name, port);
|
||||
} else {
|
||||
statusText = name;
|
||||
}
|
||||
@ -132,12 +131,8 @@ public class EditorLineStatus extends JComponent {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public void setSerialPort(String serialport) {
|
||||
this.serialport = serialport;
|
||||
}
|
||||
|
||||
public void setSerialNumber(String serialnumber) {
|
||||
this.serialnumber = serialnumber;
|
||||
public void setPort(String port) {
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public Dimension getPreferredSize() {
|
||||
|
@ -29,22 +29,36 @@
|
||||
|
||||
package cc.arduino.packages;
|
||||
|
||||
import processing.app.BaseNoGui;
|
||||
import processing.app.debug.TargetBoard;
|
||||
import processing.app.debug.TargetPackage;
|
||||
import processing.app.debug.TargetPlatform;
|
||||
import processing.app.helpers.PreferencesMap;
|
||||
|
||||
public class BoardPort {
|
||||
|
||||
private String address;
|
||||
private String protocol;
|
||||
private String address; // unique name for this port, used by Preferences
|
||||
private String protocol; // how to communicate, used for Ports menu sections
|
||||
private String protocolLabel; // protocol extended name to display on GUI
|
||||
private String boardName;
|
||||
private String vid;
|
||||
private String pid;
|
||||
private String iserial;
|
||||
private String label;
|
||||
private final PreferencesMap prefs;
|
||||
private boolean online;
|
||||
private String label; // friendly name shown in Ports menu
|
||||
private final PreferencesMap identificationPrefs; // data to match with boards.txt
|
||||
private final PreferencesMap prefs; // "vendorId", "productId", "serialNumber"
|
||||
private boolean online; // used by SerialBoardsLister (during upload??)
|
||||
|
||||
public BoardPort() {
|
||||
this.prefs = new PreferencesMap();
|
||||
this.identificationPrefs = new PreferencesMap();
|
||||
}
|
||||
|
||||
public BoardPort(BoardPort bp) {
|
||||
prefs = new PreferencesMap(bp.prefs);
|
||||
identificationPrefs = new PreferencesMap(bp.identificationPrefs);
|
||||
address = bp.address;
|
||||
protocol = bp.protocol;
|
||||
boardName = bp.boardName;
|
||||
label = bp.label;
|
||||
online = bp.online;
|
||||
}
|
||||
|
||||
public String getAddress() {
|
||||
@ -63,6 +77,14 @@ public class BoardPort {
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
public String getProtocolLabel() {
|
||||
return protocolLabel;
|
||||
}
|
||||
|
||||
public void setProtocolLabel(String protocolLabel) {
|
||||
this.protocolLabel = protocolLabel;
|
||||
}
|
||||
|
||||
public String getBoardName() {
|
||||
return boardName;
|
||||
}
|
||||
@ -75,6 +97,10 @@ public class BoardPort {
|
||||
return prefs;
|
||||
}
|
||||
|
||||
public PreferencesMap getIdentificationPrefs() {
|
||||
return identificationPrefs;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
@ -91,28 +117,76 @@ public class BoardPort {
|
||||
return online;
|
||||
}
|
||||
|
||||
public void setVIDPID(String vid, String pid) {
|
||||
this.vid = vid;
|
||||
this.pid = pid;
|
||||
}
|
||||
|
||||
public String getVID() {
|
||||
return vid;
|
||||
}
|
||||
|
||||
public String getPID() {
|
||||
return pid;
|
||||
}
|
||||
|
||||
public void setISerial(String iserial) {
|
||||
this.iserial = iserial;
|
||||
}
|
||||
public String getISerial() {
|
||||
return iserial;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.address+"_"+this.vid+"_"+this.pid;
|
||||
return this.address;
|
||||
}
|
||||
|
||||
// Search for the board which matches identificationPrefs.
|
||||
// If found, boardName is set to the name from boards.txt
|
||||
// and the board is returned. If not found, null is returned.
|
||||
public TargetBoard searchMatchingBoard() {
|
||||
if (identificationPrefs == null || identificationPrefs.isEmpty()) return null;
|
||||
for (TargetPackage targetPackage : BaseNoGui.packages.values()) {
|
||||
for (TargetPlatform targetPlatform : targetPackage.getPlatforms().values()) {
|
||||
for (TargetBoard board : targetPlatform.getBoards().values()) {
|
||||
if (matchesBoard(board)) {
|
||||
setBoardName(board.getName());
|
||||
return board;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean matchesBoard(TargetBoard board) {
|
||||
PreferencesMap identificationProps = getIdentificationPrefs();
|
||||
PreferencesMap boardProps = board.getPreferences();
|
||||
|
||||
String wildMatcher = identificationProps.get(".");
|
||||
if (wildMatcher != null) {
|
||||
if (wildMatcher.equals(board.getId())) {
|
||||
return true;
|
||||
}
|
||||
if (wildMatcher.equals(board.getFQBN())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Identification properties are defined in boards.txt with a ".N" suffix
|
||||
// for example:
|
||||
//
|
||||
// uno.name=Arduino/Genuino Uno
|
||||
// uno.vid.0=0x2341
|
||||
// uno.pid.0=0x0043
|
||||
// uno.vid.1=0x2341
|
||||
// uno.pid.1=0x0001
|
||||
// uno.vid.2=0x2A03
|
||||
// uno.pid.2=0x0043
|
||||
// uno.vid.3=0x2341
|
||||
// uno.pid.3=0x0243
|
||||
//
|
||||
// so we must search starting from suffix ".0" and increasing until we
|
||||
// found a match or the board has no more identification properties defined
|
||||
|
||||
for (int suffix = 0;; suffix++) {
|
||||
boolean found = true;
|
||||
for (String prop : identificationProps.keySet()) {
|
||||
String value = identificationProps.get(prop);
|
||||
prop += "." + suffix;
|
||||
if (!boardProps.containsKey(prop)) {
|
||||
return false;
|
||||
}
|
||||
if (!value.equalsIgnoreCase(boardProps.get(prop))) {
|
||||
found = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,13 +29,21 @@
|
||||
|
||||
package cc.arduino.packages;
|
||||
|
||||
import cc.arduino.packages.discoverers.NetworkDiscovery;
|
||||
import cc.arduino.packages.discoverers.SerialDiscovery;
|
||||
import static processing.app.I18n.format;
|
||||
import static processing.app.I18n.tr;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static processing.app.I18n.tr;
|
||||
import cc.arduino.packages.discoverers.PluggableDiscovery;
|
||||
import cc.arduino.packages.discoverers.serial.SerialDiscovery;
|
||||
import cc.arduino.packages.discoverers.NetworkDiscovery;
|
||||
import processing.app.PreferencesData;
|
||||
import processing.app.debug.TargetPackage;
|
||||
import processing.app.debug.TargetPlatform;
|
||||
import processing.app.helpers.PreferencesMap;
|
||||
import processing.app.helpers.StringReplacer;
|
||||
|
||||
public class DiscoveryManager {
|
||||
|
||||
@ -43,17 +51,47 @@ public class DiscoveryManager {
|
||||
private final SerialDiscovery serialDiscoverer = new SerialDiscovery();
|
||||
private final NetworkDiscovery networkDiscoverer = new NetworkDiscovery();
|
||||
|
||||
public DiscoveryManager() {
|
||||
// private final Map<String, TargetPackage> packages;
|
||||
|
||||
public DiscoveryManager(Map<String, TargetPackage> packages) {
|
||||
// this.packages = packages;
|
||||
|
||||
discoverers = new ArrayList<>();
|
||||
discoverers.add(serialDiscoverer);
|
||||
discoverers.add(networkDiscoverer);
|
||||
|
||||
// Search for discoveries in installed packages
|
||||
for (TargetPackage targetPackage : packages.values()) {
|
||||
for (TargetPlatform platform: targetPackage.getPlatforms().values()) {
|
||||
//System.out.println("installed: "+platform);
|
||||
PreferencesMap prefs = platform.getPreferences().subTree("discovery");
|
||||
for (String discoveryName : prefs.firstLevelMap().keySet()) {
|
||||
PreferencesMap discoveryPrefs = prefs.subTree(discoveryName);
|
||||
|
||||
String pattern = discoveryPrefs.get("pattern");
|
||||
if (pattern == null) {
|
||||
System.out.println(format(tr("No recipes defined for discovery '{0}'"),discoveryName));
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
System.out.println("found discovery: " + discoveryName + " -> " + pattern);
|
||||
System.out.println("with preferencess -> " + discoveryPrefs);
|
||||
pattern = StringReplacer.replaceFromMapping(pattern, PreferencesData.getMap());
|
||||
String[] cmd = StringReplacer.formatAndSplit(pattern, discoveryPrefs);
|
||||
discoverers.add(new PluggableDiscovery(discoveryName, cmd));
|
||||
} catch (Exception e) {
|
||||
System.out.println(format(tr("Could not start discovery '{0}': {1}"), discoveryName, e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start all discoverers
|
||||
for (Discovery d : discoverers) {
|
||||
try {
|
||||
new Thread(d).start();
|
||||
} catch (Exception e) {
|
||||
System.err.println(tr("Error starting discovery method: ") + d.getClass());
|
||||
System.err.println(tr("Error starting discovery method: ") + d.toString());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
@ -113,15 +113,12 @@ public class NetworkDiscovery implements Discovery, ServiceListener, Runnable {
|
||||
String label = name + " at " + address;
|
||||
if (board != null && BaseNoGui.packages != null) {
|
||||
String boardName = BaseNoGui.getPlatform().resolveDeviceByBoardID(BaseNoGui.packages, board);
|
||||
if (boardName != null) {
|
||||
label += " (" + boardName + ")";
|
||||
}
|
||||
port.setBoardName(boardName);
|
||||
} else if (description != null) {
|
||||
label += " (" + description + ")";
|
||||
}
|
||||
|
||||
port.setAddress(address);
|
||||
port.setBoardName(name);
|
||||
port.setProtocol("network");
|
||||
port.setLabel(label);
|
||||
|
||||
@ -165,7 +162,7 @@ public class NetworkDiscovery implements Discovery, ServiceListener, Runnable {
|
||||
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards() {
|
||||
synchronized (reachableBoardPorts) {
|
||||
synchronized (reachableBoardPorts) {
|
||||
return getBoardPortsDiscoveredWithJmDNS();
|
||||
}
|
||||
}
|
||||
@ -179,8 +176,8 @@ public class NetworkDiscovery implements Discovery, ServiceListener, Runnable {
|
||||
|
||||
public void setReachableBoardPorts(List<BoardPort> newReachableBoardPorts) {
|
||||
synchronized (reachableBoardPorts) {
|
||||
this.reachableBoardPorts.clear();
|
||||
this.reachableBoardPorts.addAll(newReachableBoardPorts);
|
||||
reachableBoardPorts.clear();
|
||||
reachableBoardPorts.addAll(newReachableBoardPorts);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* This file is part of Arduino.
|
||||
*
|
||||
* Arduino 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.
|
||||
*
|
||||
* This program 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
* As a special exception, you may use this file as part of a free software
|
||||
* library without restriction. Specifically, if other files instantiate
|
||||
* templates or use macros or inline functions from this file, or you compile
|
||||
* this file and link it with other files to produce an executable, this
|
||||
* file does not by itself cause the resulting executable to be covered by
|
||||
* the GNU General Public License. This exception does not however
|
||||
* invalidate any other reasons why the executable file might be covered by
|
||||
* the GNU General Public License.
|
||||
*
|
||||
* Copyright 2018 Arduino SA (http://www.arduino.cc/)
|
||||
*/
|
||||
|
||||
package cc.arduino.packages.discoverers;
|
||||
|
||||
import static processing.app.I18n.format;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import cc.arduino.packages.BoardPort;
|
||||
import cc.arduino.packages.Discovery;
|
||||
import processing.app.PreferencesData;
|
||||
import processing.app.helpers.StringUtils;
|
||||
|
||||
public class PluggableDiscovery implements Discovery {
|
||||
|
||||
private final String discoveryName;
|
||||
private final String[] cmd;
|
||||
private final List<BoardPort> portList = new ArrayList<>();
|
||||
private Process program=null;
|
||||
private Thread pollingThread;
|
||||
|
||||
private void debug(String x) {
|
||||
if (PreferencesData.getBoolean("discovery.debug"))
|
||||
System.out.println(discoveryName + ": " + x);
|
||||
}
|
||||
|
||||
public PluggableDiscovery(String discoveryName, String[] cmd) {
|
||||
this.cmd = cmd;
|
||||
this.discoveryName = discoveryName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// this method is started as a new thread, it will constantly listen
|
||||
// to the discovery tool and keep track of the discovered ports
|
||||
try {
|
||||
start();
|
||||
InputStream input = program.getInputStream();
|
||||
JsonFactory factory = new JsonFactory();
|
||||
JsonParser parser = factory.createParser(input);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.setVisibility(PropertyAccessor.ALL, Visibility.NONE);
|
||||
mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY);
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
|
||||
while (program != null && program.isAlive()) {
|
||||
JsonNode tree = mapper.readTree(parser);
|
||||
if (tree == null) {
|
||||
if (program != null && program.isAlive()) {
|
||||
System.err.println(format("{0}: Invalid json message", discoveryName));
|
||||
}
|
||||
break;
|
||||
}
|
||||
debug("Received json: " + tree);
|
||||
|
||||
processJsonNode(mapper, tree);
|
||||
}
|
||||
debug("thread exit normally");
|
||||
} catch (InterruptedException e) {
|
||||
debug("thread exit by interrupt");
|
||||
e.printStackTrace();
|
||||
} catch (Exception e) {
|
||||
debug("thread exit other exception");
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
stop();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
private void processJsonNode(ObjectMapper mapper, JsonNode node) {
|
||||
JsonNode eventTypeNode = node.get("eventType");
|
||||
if (eventTypeNode == null) {
|
||||
System.err.println(format("{0}: Invalid message, missing eventType", discoveryName));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventTypeNode.asText()) {
|
||||
case "error":
|
||||
try {
|
||||
PluggableDiscoveryMessage msg = mapper.treeToValue(node, PluggableDiscoveryMessage.class);
|
||||
debug("error: " + msg.getMessage());
|
||||
if (msg.getMessage().contains("START_SYNC")) {
|
||||
startPolling();
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return;
|
||||
|
||||
case "list":
|
||||
JsonNode portsNode = node.get("ports");
|
||||
if (portsNode == null) {
|
||||
System.err.println(format("{0}: Invalid message, missing ports list", discoveryName));
|
||||
return;
|
||||
}
|
||||
if (!portsNode.isArray()) {
|
||||
System.err.println(format("{0}: Invalid message, ports list should be an array", discoveryName));
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (portList) {
|
||||
portList.clear();
|
||||
}
|
||||
portsNode.forEach(portNode -> {
|
||||
BoardPort port = mapJsonNodeToBoardPort(mapper, node);
|
||||
if (port != null) {
|
||||
addOrUpdate(port);
|
||||
}
|
||||
});
|
||||
return;
|
||||
|
||||
// Messages for SYNC updates
|
||||
|
||||
case "add":
|
||||
BoardPort addedPort = mapJsonNodeToBoardPort(mapper, node);
|
||||
if (addedPort != null) {
|
||||
addOrUpdate(addedPort);
|
||||
}
|
||||
return;
|
||||
|
||||
case "remove":
|
||||
BoardPort removedPort = mapJsonNodeToBoardPort(mapper, node);
|
||||
if (removedPort != null) {
|
||||
remove(removedPort);
|
||||
}
|
||||
return;
|
||||
|
||||
default:
|
||||
debug("Invalid event: " + eventTypeNode.asText());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private BoardPort mapJsonNodeToBoardPort(ObjectMapper mapper, JsonNode node) {
|
||||
try {
|
||||
BoardPort port = mapper.treeToValue(node.get("port"), BoardPort.class);
|
||||
// if no label, use address
|
||||
if (port.getLabel() == null || port.getLabel().isEmpty()) {
|
||||
port.setLabel(port.getAddress());
|
||||
}
|
||||
port.searchMatchingBoard();
|
||||
return port;
|
||||
} catch (JsonProcessingException e) {
|
||||
System.err.println(format("{0}: Invalid BoardPort message", discoveryName));
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws Exception {
|
||||
try {
|
||||
debug("Starting: " + StringUtils.join(cmd, " "));
|
||||
program = Runtime.getRuntime().exec(cmd);
|
||||
} catch (Exception e) {
|
||||
program = null;
|
||||
return;
|
||||
}
|
||||
debug("START_SYNC");
|
||||
write("START_SYNC\n");
|
||||
pollingThread = null;
|
||||
}
|
||||
|
||||
private void startPolling() {
|
||||
// Discovery tools not supporting START_SYNC require a periodic
|
||||
// LIST command. A second thread is created to send these
|
||||
// commands, while the run() thread above listens for the
|
||||
// discovery tool output.
|
||||
debug("START");
|
||||
write("START\n");
|
||||
Thread pollingThread = new Thread() {
|
||||
public void run() {
|
||||
try {
|
||||
while (program != null && program.isAlive()) {
|
||||
debug("LIST");
|
||||
write("LIST\n");
|
||||
sleep(2500);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
};
|
||||
pollingThread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws Exception {
|
||||
if (pollingThread != null) {
|
||||
pollingThread.interrupt();
|
||||
pollingThread = null;
|
||||
}
|
||||
write("STOP\n");
|
||||
if (program != null) {
|
||||
program.destroy();
|
||||
program = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void write(String command) {
|
||||
if (program != null && program.isAlive()) {
|
||||
OutputStream out = program.getOutputStream();
|
||||
try {
|
||||
out.write(command.getBytes());
|
||||
out.flush();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addOrUpdate(BoardPort port) {
|
||||
String address = port.getAddress();
|
||||
if (address == null)
|
||||
return; // address required for "add" & "remove"
|
||||
|
||||
synchronized (portList) {
|
||||
// if address already on the list, discard old info
|
||||
portList.removeIf(bp -> address.equals(bp.getAddress()));
|
||||
portList.add(port);
|
||||
}
|
||||
}
|
||||
|
||||
private void remove(BoardPort port) {
|
||||
String address = port.getAddress();
|
||||
if (address == null)
|
||||
return; // address required for "add" & "remove"
|
||||
synchronized (portList) {
|
||||
portList.removeIf(bp -> address.equals(bp.getAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards() {
|
||||
synchronized (portList) {
|
||||
return new ArrayList<>(portList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards(boolean complete) {
|
||||
// XXX: parameter "complete "is really needed?
|
||||
// should be checked on all existing discoveries
|
||||
synchronized (portList) {
|
||||
return new ArrayList<>(portList);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return discoveryName;
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* This file is part of Arduino.
|
||||
*
|
||||
* Arduino 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.
|
||||
*
|
||||
* This program 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
* As a special exception, you may use this file as part of a free software
|
||||
* library without restriction. Specifically, if other files instantiate
|
||||
* templates or use macros or inline functions from this file, or you compile
|
||||
* this file and link it with other files to produce an executable, this
|
||||
* file does not by itself cause the resulting executable to be covered by
|
||||
* the GNU General Public License. This exception does not however
|
||||
* invalidate any other reasons why the executable file might be covered by
|
||||
* the GNU General Public License.
|
||||
*
|
||||
* Copyright 2018 Arduino SA (http://www.arduino.cc/)
|
||||
*/
|
||||
|
||||
package cc.arduino.packages.discoverers;
|
||||
|
||||
public class PluggableDiscoveryMessage {
|
||||
private String eventType; // "add", "remove", "error"
|
||||
private String message; // optional message, e.g. "START_SYNC not supported"
|
||||
|
||||
public String getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
/*
|
||||
* This file is part of Arduino.
|
||||
*
|
||||
* Arduino 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.
|
||||
*
|
||||
* This program 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 this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*
|
||||
* As a special exception, you may use this file as part of a free software
|
||||
* library without restriction. Specifically, if other files instantiate
|
||||
* templates or use macros or inline functions from this file, or you compile
|
||||
* this file and link it with other files to produce an executable, this
|
||||
* file does not by itself cause the resulting executable to be covered by
|
||||
* the GNU General Public License. This exception does not however
|
||||
* invalidate any other reasons why the executable file might be covered by
|
||||
* the GNU General Public License.
|
||||
*
|
||||
* Copyright 2013 Arduino LLC (http://www.arduino.cc/)
|
||||
*/
|
||||
|
||||
package cc.arduino.packages.discoverers;
|
||||
|
||||
import cc.arduino.packages.BoardPort;
|
||||
import cc.arduino.packages.Discovery;
|
||||
import cc.arduino.packages.discoverers.serial.SerialBoardsLister;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
|
||||
public class SerialDiscovery implements Discovery, Runnable {
|
||||
|
||||
private Timer serialBoardsListerTimer;
|
||||
private final List<BoardPort> serialBoardPorts;
|
||||
private SerialBoardsLister serialBoardsLister = new SerialBoardsLister(this);
|
||||
|
||||
public SerialDiscovery() {
|
||||
this.serialBoardPorts = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards() {
|
||||
return getSerialBoardPorts(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards(boolean complete) {
|
||||
return getSerialBoardPorts(complete);
|
||||
}
|
||||
|
||||
private List<BoardPort> getSerialBoardPorts(boolean complete) {
|
||||
if (complete) {
|
||||
return new LinkedList<>(serialBoardPorts);
|
||||
}
|
||||
List<BoardPort> onlineBoardPorts = new LinkedList<>();
|
||||
for (BoardPort port : serialBoardPorts) {
|
||||
if (port.isOnline() == true) {
|
||||
onlineBoardPorts.add(port);
|
||||
}
|
||||
}
|
||||
return onlineBoardPorts;
|
||||
}
|
||||
|
||||
public void setSerialBoardPorts(List<BoardPort> newSerialBoardPorts) {
|
||||
serialBoardPorts.clear();
|
||||
serialBoardPorts.addAll(newSerialBoardPorts);
|
||||
}
|
||||
|
||||
public void forceRefresh() {
|
||||
serialBoardsLister.retriggerDiscovery(false);
|
||||
}
|
||||
|
||||
public void setUploadInProgress(boolean param) {
|
||||
serialBoardsLister.uploadInProgress = param;
|
||||
}
|
||||
|
||||
public void pausePolling(boolean param) { serialBoardsLister.pausePolling = param;}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
this.serialBoardsListerTimer = new Timer(SerialBoardsLister.class.getName());
|
||||
serialBoardsLister.start(serialBoardsListerTimer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
this.serialBoardsListerTimer.purge();
|
||||
}
|
||||
}
|
@ -29,41 +29,90 @@
|
||||
|
||||
package cc.arduino.packages.discoverers.serial;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import cc.arduino.packages.BoardPort;
|
||||
import cc.arduino.packages.discoverers.SerialDiscovery;
|
||||
import cc.arduino.packages.Discovery;
|
||||
import processing.app.BaseNoGui;
|
||||
import processing.app.Platform;
|
||||
import processing.app.debug.TargetBoard;
|
||||
|
||||
import java.util.*;
|
||||
public class SerialDiscovery implements Discovery, Runnable {
|
||||
|
||||
public class SerialBoardsLister extends TimerTask {
|
||||
|
||||
private final SerialDiscovery serialDiscovery;
|
||||
private final List<BoardPort> boardPorts = new LinkedList<>();
|
||||
private List<String> oldPorts = new LinkedList<>();
|
||||
private Timer serialBoardsListerTimer;
|
||||
private final List<BoardPort> serialBoardPorts = new ArrayList<>();
|
||||
private final List<BoardPort> boardPorts = new ArrayList<>();
|
||||
private final List<String> oldPorts = new ArrayList<>();
|
||||
public boolean uploadInProgress = false;
|
||||
public boolean pausePolling = false;
|
||||
private BoardPort oldUploadBoardPort = null;
|
||||
|
||||
public SerialBoardsLister(SerialDiscovery serialDiscovery) {
|
||||
this.serialDiscovery = serialDiscovery;
|
||||
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards() {
|
||||
return listDiscoveredBoards(false);
|
||||
}
|
||||
|
||||
public void start(Timer timer) {
|
||||
timer.schedule(this, 0, 1000);
|
||||
@Override
|
||||
public List<BoardPort> listDiscoveredBoards(boolean complete) {
|
||||
if (complete) {
|
||||
return new ArrayList<>(serialBoardPorts);
|
||||
}
|
||||
List<BoardPort> onlineBoardPorts = new ArrayList<>();
|
||||
for (BoardPort port : serialBoardPorts) {
|
||||
if (port.isOnline() == true) {
|
||||
onlineBoardPorts.add(port);
|
||||
}
|
||||
}
|
||||
return onlineBoardPorts;
|
||||
}
|
||||
|
||||
public synchronized void retriggerDiscovery(boolean polled) {
|
||||
public void setSerialBoardPorts(List<BoardPort> newSerialBoardPorts) {
|
||||
serialBoardPorts.clear();
|
||||
serialBoardPorts.addAll(newSerialBoardPorts);
|
||||
}
|
||||
|
||||
public void setUploadInProgress(boolean param) {
|
||||
uploadInProgress = param;
|
||||
}
|
||||
|
||||
public void pausePolling(boolean param) {
|
||||
pausePolling = param;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
serialBoardsListerTimer = new Timer(SerialDiscovery.class.getName());
|
||||
serialBoardsListerTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (BaseNoGui.packages != null && !pausePolling) {
|
||||
forceRefresh();
|
||||
}
|
||||
}
|
||||
}, 0, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
serialBoardsListerTimer.cancel();
|
||||
}
|
||||
|
||||
public synchronized void forceRefresh() {
|
||||
Platform platform = BaseNoGui.getPlatform();
|
||||
if (platform == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (polled && pausePolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> ports = platform.listSerials();
|
||||
if (ports.equals(oldPorts)) {
|
||||
return;
|
||||
@ -113,19 +162,18 @@ public class SerialBoardsLister extends TimerTask {
|
||||
Map<String, Object> boardData = platform.resolveDeviceByVendorIdProductId(port, BaseNoGui.packages);
|
||||
|
||||
BoardPort boardPort = null;
|
||||
boolean updatingInfos = false;
|
||||
int i = 0;
|
||||
// create new board or update existing
|
||||
for (BoardPort board : boardPorts) {
|
||||
if (board.toString().equals(newPort)) {
|
||||
updatingInfos = true;
|
||||
boardPort = boardPorts.get(i);
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
if (!updatingInfos) {
|
||||
if (boardPort == null) {
|
||||
boardPort = new BoardPort();
|
||||
boardPorts.add(boardPort);
|
||||
}
|
||||
boardPort.setAddress(port);
|
||||
boardPort.setProtocol("serial");
|
||||
@ -136,50 +184,35 @@ public class SerialBoardsLister extends TimerTask {
|
||||
if (boardData != null) {
|
||||
boardPort.getPrefs().put("vid", boardData.get("vid").toString());
|
||||
boardPort.getPrefs().put("pid", boardData.get("pid").toString());
|
||||
boardPort.setVIDPID(parts[1], parts[2]);
|
||||
|
||||
String iserial = boardData.get("iserial").toString();
|
||||
if (iserial.length() >= 10) {
|
||||
boardPort.getPrefs().put("iserial", iserial);
|
||||
boardPort.setISerial(iserial);
|
||||
}
|
||||
if (uploadInProgress && oldUploadBoardPort!=null) {
|
||||
oldUploadBoardPort.getPrefs().put("iserial", iserial);
|
||||
oldUploadBoardPort.setISerial(iserial);
|
||||
}
|
||||
|
||||
TargetBoard board = (TargetBoard) boardData.get("board");
|
||||
if (board != null) {
|
||||
String boardName = board.getName();
|
||||
if (boardName != null) {
|
||||
label += " (" + boardName + ")";
|
||||
}
|
||||
boardPort.setBoardName(boardName);
|
||||
}
|
||||
} else {
|
||||
if (!parts[1].equals("0000")) {
|
||||
boardPort.setVIDPID(parts[1], parts[2]);
|
||||
boardPort.getPrefs().put("vid", parts[1]);
|
||||
boardPort.getPrefs().put("pid", parts[2]);
|
||||
// ask Cloud API to match the board with known VID/PID pair
|
||||
platform.getBoardWithMatchingVidPidFromCloud(parts[1], parts[2]);
|
||||
} else {
|
||||
boardPort.setVIDPID("0000", "0000");
|
||||
boardPort.setISerial("");
|
||||
boardPort.getPrefs().put("vid", "0000");
|
||||
boardPort.getPrefs().put("pid", "0000");
|
||||
boardPort.getPrefs().put("iserial", "");
|
||||
}
|
||||
}
|
||||
|
||||
boardPort.setLabel(label);
|
||||
if (!updatingInfos) {
|
||||
boardPorts.add(boardPort);
|
||||
}
|
||||
}
|
||||
serialDiscovery.setSerialBoardPorts(boardPorts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (BaseNoGui.packages == null) {
|
||||
return;
|
||||
}
|
||||
retriggerDiscovery(true);
|
||||
setSerialBoardPorts(boardPorts);
|
||||
}
|
||||
}
|
@ -223,7 +223,7 @@ public class BaseNoGui {
|
||||
|
||||
public static DiscoveryManager getDiscoveryManager() {
|
||||
if (discoveryManager == null) {
|
||||
discoveryManager = new DiscoveryManager();
|
||||
discoveryManager = new DiscoveryManager(packages);
|
||||
}
|
||||
return discoveryManager;
|
||||
}
|
||||
@ -506,7 +506,7 @@ public class BaseNoGui {
|
||||
}
|
||||
|
||||
if (discoveryManager == null) {
|
||||
discoveryManager = new DiscoveryManager();
|
||||
discoveryManager = new DiscoveryManager(packages);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,4 +100,8 @@ public class LegacyTargetBoard implements TargetBoard {
|
||||
return containerPlatform;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFQBN() {
|
||||
return getContainerPlatform().getContainerPackage().getId() + ":" + getContainerPlatform().getId() + ":" + getId();
|
||||
}
|
||||
}
|
||||
|
@ -92,4 +92,6 @@ public interface TargetBoard {
|
||||
|
||||
public TargetPlatform getContainerPlatform();
|
||||
|
||||
public String getFQBN();
|
||||
|
||||
}
|
||||
|
@ -41,6 +41,7 @@ public class CommandlineParser {
|
||||
private String getPref;
|
||||
private String boardToInstall;
|
||||
private String libraryToInstall;
|
||||
private Optional<String> uploadPort = Optional.empty();
|
||||
private final List<String> filenames = new LinkedList<>();
|
||||
|
||||
public CommandlineParser(String[] args) {
|
||||
@ -141,7 +142,7 @@ public class CommandlineParser {
|
||||
i++;
|
||||
if (i >= args.length)
|
||||
BaseNoGui.showError(null, tr("Argument required for --port"), 3);
|
||||
BaseNoGui.selectSerialPort(args[i]);
|
||||
uploadPort = Optional.of(args[i]);
|
||||
if (action == ACTION.GUI)
|
||||
action = ACTION.NOOP;
|
||||
continue;
|
||||
@ -356,4 +357,8 @@ public class CommandlineParser {
|
||||
public boolean isPreserveTempFiles() {
|
||||
return preserveTempFiles;
|
||||
}
|
||||
|
||||
public Optional<String> getUploadPort() {
|
||||
return uploadPort;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user