48 changes: 43 additions & 5 deletions arduino-core/src/cc/arduino/packages/DiscoveryManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,31 +29,69 @@

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 {

private final List<Discovery> discoverers;
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();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,15 +113,12 @@ public void serviceResolved(ServiceEvent serviceEvent) {
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);

Expand Down Expand Up @@ -165,7 +162,7 @@ public void stop() {

@Override
public List<BoardPort> listDiscoveredBoards() {
synchronized (reachableBoardPorts) {
synchronized (reachableBoardPorts) {
return getBoardPortsDiscoveredWithJmDNS();
}
}
Expand All @@ -179,8 +176,8 @@ public List<BoardPort> listDiscoveredBoards(boolean complete) {

public void setReachableBoardPorts(List<BoardPort> newReachableBoardPorts) {
synchronized (reachableBoardPorts) {
this.reachableBoardPorts.clear();
this.reachableBoardPorts.addAll(newReachableBoardPorts);
reachableBoardPorts.clear();
reachableBoardPorts.addAll(newReachableBoardPorts);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best way: we can call "LIST" just once directly in listDiscoveredBoards() (basically it will poll only when the Tools menu is triggered and not every 2.5 sec). On the other hand this may add some delay on opening the menu unless there is some internal cache in the discovery tool.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already do have the internal cache as the portList of discovered ports in PluggableDiscovery. listDiscoveredBoards() just returns a copy of the list. At least that part is easy.

To make this work, I believe DiscoverManager would need to give all discoverer instances an early notice that listDiscoveredBoards() may soon be called. Maybe this could be done by detecting mouse hover over the Tools menu? Or maybe mouse-over any part of the menus?

Early versions of Teensy's software waited in listDiscoveredBoards(). The result was usually ok, but some Windows & Mac users had a terrible experience with the GUI becoming unresponsive. I believe it's best to always quickly return the list of previously discovered ports.

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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
103 changes: 0 additions & 103 deletions arduino-core/src/cc/arduino/packages/discoverers/SerialDiscovery.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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 SerialBoardsLister extends TimerTask {
public class SerialDiscovery implements Discovery, Runnable {

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);
}

@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 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();
}

public void start(Timer timer) {
timer.schedule(this, 0, 1000);
@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);
}

public synchronized void retriggerDiscovery(boolean polled) {
@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;
Expand Down Expand Up @@ -113,19 +162,18 @@ public synchronized void retriggerDiscovery(boolean polled) {
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");
Expand All @@ -136,50 +184,35 @@ public synchronized void retriggerDiscovery(boolean polled) {
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);
}
}
4 changes: 2 additions & 2 deletions arduino-core/src/processing/app/BaseNoGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ static public File getDefaultSketchbookFolder() {

public static DiscoveryManager getDiscoveryManager() {
if (discoveryManager == null) {
discoveryManager = new DiscoveryManager();
discoveryManager = new DiscoveryManager(packages);
}
return discoveryManager;
}
Expand Down Expand Up @@ -506,7 +506,7 @@ static public void initPackages() throws Exception {
}

if (discoveryManager == null) {
discoveryManager = new DiscoveryManager();
discoveryManager = new DiscoveryManager(packages);
}
}

Expand Down
4 changes: 4 additions & 0 deletions arduino-core/src/processing/app/debug/LegacyTargetBoard.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,8 @@ public TargetPlatform getContainerPlatform() {
return containerPlatform;
}

@Override
public String getFQBN() {
return getContainerPlatform().getContainerPackage().getId() + ":" + getContainerPlatform().getId() + ":" + getId();
}
}
2 changes: 2 additions & 0 deletions arduino-core/src/processing/app/debug/TargetBoard.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,6 @@ public interface TargetBoard {

public TargetPlatform getContainerPlatform();

public String getFQBN();

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ private enum ACTION {
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) {
Expand Down Expand Up @@ -141,7 +142,7 @@ public void parseArgumentsPhase1() {
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;
Expand Down Expand Up @@ -356,4 +357,8 @@ public String getLibraryToInstall() {
public boolean isPreserveTempFiles() {
return preserveTempFiles;
}

public Optional<String> getUploadPort() {
return uploadPort;
}
}