diff --git a/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java b/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java index 00bc9b3f03..3153c832bf 100644 --- a/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java +++ b/java_console/ui/src/main/java/com/rusefi/SerialPortScanner.java @@ -18,6 +18,8 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @author Andrey Belomutskiy @@ -111,30 +113,158 @@ public enum SerialPortScanner { startTimer(); } + private static PortResult inspectPort(String serialPort) { + log.info("Determining type of serial port: " + serialPort); + + boolean isOpenblt = isPortOpenblt(serialPort); + log.info("Port " + serialPort + (isOpenblt ? " looks like" : " does not look like") + " an OpenBLT bootloader"); + if (isOpenblt) { + return new PortResult(serialPort, SerialPortType.OpenBlt); + } else { + // See if this looks like an ECU + String signature = getEcuSignature(serialPort); + boolean isEcu = signature != null; + log.info("Port " + serialPort + (isEcu ? " looks like" : " does not look like") + " an ECU"); + if (isEcu) { + boolean ecuHasOpenblt = ecuHasOpenblt(serialPort); + log.info("ECU at " + serialPort + (ecuHasOpenblt ? " has" : " does not have") + " an OpenBLT bootloader"); + return new PortResult(serialPort, ecuHasOpenblt ? SerialPortType.EcuWithOpenblt : SerialPortType.Ecu, signature); + } else { + // Dunno what this is, leave it in the list anyway + return new PortResult(serialPort, SerialPortType.Unknown); + } + } + } + + private static List inspectPorts(final List ports) { + if (ports.isEmpty()) { + return new ArrayList<>(); + } + + final Object resultsLock = new Object(); + final Map results = new HashMap<>(); + + // When the last port is found, we need to cancel the timeout + final Thread callingThread = Thread.currentThread(); + + // One thread per port to check + final List threads = ports.stream().map(p -> { + Thread t = new Thread(() -> { + PortResult r = inspectPort(p); + + // Record the result under lock + synchronized (resultsLock) { + if (Thread.currentThread().isInterrupted()) { + // If interrupted, don't try to write our result + return; + } + + results.put(p, r); + + if (results.size() == ports.size()) { + // We now have all the results - interrupt the calling thread + callingThread.interrupt(); + } + } + }); + + t.setName("SerialPortScanner inspectPort " + p); + t.setDaemon(true); + t.start(); + + return t; + }).collect(Collectors.toList()); + + // Give everyone a chance to finish + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + // We got interrupted because the last port got found, nothing to do + } + + // Interrupt all threads under lock to ensure no more objects are added to results + synchronized (resultsLock) { + for (Thread t : threads) { + t.interrupt(); + } + } + + // Now check that we got everything - if any timed out, register them as unknown + for (String port : ports) { + if (!results.containsKey(port)) { + log.info("Port " + port + " timed out, adding as Unknown."); + results.put(port, new PortResult(port, SerialPortType.Unknown)); + } + } + + return new ArrayList<>(results.values()); + } + + private final static Map portCache = new HashMap<>(); + /** * Find all available serial ports and checks if simulator local TCP port is available */ private void findAllAvailablePorts(boolean includeSlowLookup) { - List ports = new ArrayList<>(); + List ports = new ArrayList<>(); boolean dfuConnected; boolean stLinkConnected; boolean PCANConnected; String[] serialPorts = LinkManager.getCommPorts(); - if (serialPorts.length > 0) - ports.add(AUTO_SERIAL); + + List portsToInspect = new ArrayList<>(); + for (String serialPort : serialPorts) { - // Filter out some macOS trash - if (serialPort.contains("wlan-debug") || - serialPort.contains("Bluetooth-Incoming-Port") || - serialPort.startsWith("cu.")) { - continue; + // First, check the port cache + if (portCache.containsKey(serialPort)) { + // We've already probed this port - don't re-probe it again + PortResult cached = portCache.get(serialPort); + + ports.add(cached); + } else { + portsToInspect.add(serialPort); + } + } + + for (PortResult p : inspectPorts(portsToInspect)) { + log.info("Port " + p.port + " detected as: " + p.type.friendlyString); + + ports.add(p); + portCache.put(p.port, p); + } + + { + // Clean the port cache of any entries that no longer exist + // If the same port appears later, we want to re-probe it at that time + // In any other scenario, auto could have unexpected behavior for the user + List toRemove = new ArrayList<>(); + for (String x : portCache.keySet()) { + if (Arrays.stream(serialPorts).noneMatch(x::equals)) { + toRemove.add(x); + } + } + + // two steps to avoid ConcurrentModificationException + toRemove.forEach(p -> { + portCache.remove(p); + log.info("Removing port " + p); + }); + } + + // Sort ports by their type to put your ECU at the top + ports.sort(Comparator.comparingInt(a -> a.type.sortOrder)); + + if (includeSlowLookup) { + for (String tcpPort : TcpConnector.getAvailablePorts()) { + ports.add(new PortResult(tcpPort, SerialPortType.Ecu)); } - ports.add(serialPort); } if (includeSlowLookup) { - ports.addAll(TcpConnector.getAvailablePorts()); + for (String tcpPort : TcpConnector.getAvailablePorts()) { + ports.add(new PortResult(tcpPort, SerialPortType.Ecu)); + } dfuConnected = DfuFlasher.detectSTM32BootloaderDriverState(UpdateOperationCallbacks.DUMMY); stLinkConnected = DfuFlasher.detectStLink(UpdateOperationCallbacks.DUMMY); PCANConnected = DfuFlasher.detectPcan(UpdateOperationCallbacks.DUMMY); @@ -144,9 +274,9 @@ public enum SerialPortScanner { PCANConnected = false; } if (PCANConnected) - ports.add(LinkManager.PCAN); + ports.add(new PortResult(LinkManager.PCAN, SerialPortType.CAN)); if (SHOW_SOCKETCAN) - ports.add(LinkManager.SOCKET_CAN); + ports.add(new PortResult(LinkManager.SOCKET_CAN, SerialPortType.CAN)); boolean isListUpdated; AvailableHardware currentHardware = new AvailableHardware(ports, dfuConnected, stLinkConnected, PCANConnected); @@ -188,12 +318,12 @@ public enum SerialPortScanner { public static class AvailableHardware { - private final List ports; + private final List ports; private final boolean dfuFound; private final boolean stLinkConnected; private final boolean PCANConnected; - public AvailableHardware(List ports, boolean dfuFound, boolean stLinkConnected, boolean PCANConnected) { + public AvailableHardware(List ports, boolean dfuFound, boolean stLinkConnected, boolean PCANConnected) { this.ports = ports; this.dfuFound = dfuFound; this.stLinkConnected = stLinkConnected; @@ -201,7 +331,7 @@ public enum SerialPortScanner { } @NotNull - public List getKnownPorts() {return new ArrayList<>(ports);} + public List getKnownPorts() {return new ArrayList<>(ports);} public boolean isDfuFound() { return dfuFound; diff --git a/java_console/ui/src/main/java/com/rusefi/StartupFrame.java b/java_console/ui/src/main/java/com/rusefi/StartupFrame.java index a3ac00d825..d9c59f2525 100644 --- a/java_console/ui/src/main/java/com/rusefi/StartupFrame.java +++ b/java_console/ui/src/main/java/com/rusefi/StartupFrame.java @@ -51,7 +51,7 @@ public class StartupFrame { private final JFrame frame; private final JPanel connectPanel = new JPanel(new FlowLayout()); // todo: move this line to the connectPanel - private final JComboBox comboPorts = new JComboBox<>(); + private final JComboBox comboPorts = new JComboBox<>(); private final JPanel leftPanel = new JPanel(new VerticalFlowLayout()); private final JPanel realHardwarePanel = new JPanel(new MigLayout()); @@ -234,7 +234,7 @@ public class StartupFrame { } private void applyKnownPorts(SerialPortScanner.AvailableHardware currentHardware) { - List ports = currentHardware.getKnownPorts(); + List ports = currentHardware.getKnownPorts(); log.info("Rendering available ports: " + ports); connectPanel.setVisible(!ports.isEmpty()); noPortsMessage.setVisible(ports.isEmpty()); @@ -299,10 +299,11 @@ public class StartupFrame { SerialPortScanner.INSTANCE.stopTimer(); } - private void applyPortSelectionToUIcontrol(List ports) { + private void applyPortSelectionToUIcontrol(List ports) { comboPorts.removeAllItems(); - for (final String port : ports) + for (final SerialPortScanner.PortResult port : ports) { comboPorts.addItem(port); + } String defaultPort = getConfig().getRoot().getProperty(ConsoleUI.PORT_KEY); if (!PersistentConfiguration.getBoolProperty(ALWAYS_AUTO_PORT)) { comboPorts.setSelectedItem(defaultPort); diff --git a/java_console/ui/src/main/java/com/rusefi/maintenance/ProgramSelector.java b/java_console/ui/src/main/java/com/rusefi/maintenance/ProgramSelector.java index 653b6034e9..51ba872429 100644 --- a/java_console/ui/src/main/java/com/rusefi/maintenance/ProgramSelector.java +++ b/java_console/ui/src/main/java/com/rusefi/maintenance/ProgramSelector.java @@ -44,7 +44,7 @@ public class ProgramSelector { private final JPanel controls = new JPanel(new FlowLayout()); private final JComboBox mode = new JComboBox<>(); - public ProgramSelector(JComboBox comboPorts) { + public ProgramSelector(JComboBox comboPorts) { content.add(controls, BorderLayout.NORTH); content.add(noHardware, BorderLayout.SOUTH); controls.setVisible(false); @@ -61,7 +61,7 @@ public class ProgramSelector { @Override public void actionPerformed(ActionEvent e) { final String selectedMode = (String) mode.getSelectedItem(); - final String selectedPort = (String) comboPorts.getSelectedItem(); + final SerialPortScanner.PortResult selectedPort = ((SerialPortScanner.PortResult) comboPorts.getSelectedItem()); getConfig().getRoot().setProperty(getClass().getSimpleName(), selectedMode); @@ -72,7 +72,7 @@ public class ProgramSelector { switch (selectedMode) { case AUTO_DFU: jobName = "DFU update"; - job = (callbacks) -> DfuFlasher.doAutoDfu(comboPorts, selectedPort, callbacks); + job = (callbacks) -> DfuFlasher.doAutoDfu(comboPorts, selectedPort.port, callbacks); break; case MANUAL_DFU: jobName = "DFU update"; @@ -86,11 +86,11 @@ public class ProgramSelector { break; case DFU_SWITCH: jobName = "DFU switch"; - job = (callbacks) -> rebootToDfu(comboPorts, selectedPort, callbacks); + job = (callbacks) -> rebootToDfu(comboPorts, selectedPort.port, callbacks); break; case OPENBLT_SWITCH: jobName = "OpenBLT switch"; - job = (callbacks) -> rebootToOpenblt(comboPorts, selectedPort, callbacks); + job = (callbacks) -> rebootToOpenblt(comboPorts, selectedPort.port, callbacks); break; case OPENBLT_CAN: jobName = "OpenBLT via CAN"; @@ -98,11 +98,11 @@ public class ProgramSelector { break; case OPENBLT_MANUAL: jobName = "OpenBLT via Serial"; - job = (callbacks) -> flashOpenbltSerialJni(selectedPort, callbacks); + job = (callbacks) -> flashOpenbltSerialJni(selectedPort.port, callbacks); break; case OPENBLT_AUTO: jobName = "OpenBLT via Serial"; - job = (callbacks) -> flashOpenbltSerialAutomatic(comboPorts, selectedPort, callbacks); + job = (callbacks) -> flashOpenbltSerialAutomatic(comboPorts, selectedPort.port, callbacks); break; case DFU_ERASE: jobName = "DFU erase";