|
|
@@ -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; |
|
|
} |
|
|
} |
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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.