mirror of https://github.com/rusefi/rusefi.git
421 lines
14 KiB
Java
421 lines
14 KiB
Java
package com.rusefi;
|
|
|
|
import com.devexperts.logging.Logging;
|
|
import com.rusefi.autodetect.SerialAutoChecker;
|
|
import com.rusefi.binaryprotocol.IncomingDataBuffer;
|
|
import com.rusefi.binaryprotocol.IoHelper;
|
|
import com.rusefi.config.generated.Fields;
|
|
import com.rusefi.core.RusEfiSignature;
|
|
import com.rusefi.core.SignatureHelper;
|
|
import com.rusefi.io.IoStream;
|
|
import com.rusefi.io.LinkManager;
|
|
import com.rusefi.io.serial.BufferedSerialIoStream;
|
|
import com.rusefi.io.tcp.TcpConnector;
|
|
import com.rusefi.maintenance.DfuFlasher;
|
|
import com.rusefi.io.UpdateOperationCallbacks;
|
|
import com.rusefi.maintenance.MaintenanceUtil;
|
|
import com.rusefi.maintenance.StLinkFlasher;
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
import java.io.IOException;
|
|
import java.util.*;
|
|
import java.util.concurrent.CopyOnWriteArrayList;
|
|
import java.util.stream.Collectors;
|
|
|
|
/**
|
|
* @author Andrey Belomutskiy
|
|
*/
|
|
public enum SerialPortScanner {
|
|
INSTANCE;
|
|
|
|
private final static Logging log = Logging.getLogging(SerialPortScanner.class);
|
|
|
|
public enum SerialPortType {
|
|
Ecu("ECU", 20),
|
|
EcuWithOpenblt("ECU w/ BL", 20),
|
|
OpenBlt("OpenBLT Bootloader", 10),
|
|
CAN("CAN", 30),
|
|
Unknown("Unknown", 100),
|
|
;
|
|
|
|
public final String friendlyString;
|
|
public final int sortOrder;
|
|
|
|
SerialPortType(String friendlyString, int sortOrder) {
|
|
this.friendlyString = friendlyString;
|
|
this.sortOrder = sortOrder;
|
|
}
|
|
}
|
|
|
|
public static class PortResult {
|
|
public final String port;
|
|
public final SerialPortType type;
|
|
public final RusEfiSignature signature;
|
|
|
|
public PortResult(String port, SerialPortType type, String signature) {
|
|
this.port = port;
|
|
this.type = type;
|
|
this.signature = SignatureHelper.parse(signature);
|
|
}
|
|
|
|
public PortResult(String port, SerialPortType type) {
|
|
this(port, type, null);
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
if (type.friendlyString == null) {
|
|
return this.port;
|
|
} else {
|
|
return this.port + " (" + type.friendlyString + ")";
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (o == this) {
|
|
return true;
|
|
}
|
|
|
|
if (o == null) {
|
|
return false;
|
|
}
|
|
|
|
if (getClass() != o.getClass()) {
|
|
return false;
|
|
}
|
|
|
|
PortResult other = (PortResult) o;
|
|
|
|
return this.port.equals(other.port) && this.type.equals(other.type);
|
|
}
|
|
|
|
public boolean isEcu() {
|
|
return type == SerialPortType.Ecu || type == SerialPortType.EcuWithOpenblt;
|
|
}
|
|
}
|
|
|
|
private volatile boolean isRunning = true;
|
|
|
|
private static final boolean SHOW_SOCKETCAN = FileLog.isLinux();
|
|
|
|
private final Object lock = new Object();
|
|
@NotNull
|
|
private AvailableHardware knownHardware = new AvailableHardware(Collections.emptyList(), false, false, false);
|
|
|
|
private final List<Listener> listeners = new CopyOnWriteArrayList<>();
|
|
|
|
public void addListener(Listener listener) {
|
|
boolean shouldStart = listeners.isEmpty();
|
|
listeners.add(listener);
|
|
if (shouldStart)
|
|
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<PortResult> inspectPorts(final List<String> ports) {
|
|
if (ports.isEmpty()) {
|
|
return new ArrayList<>();
|
|
}
|
|
|
|
final Object resultsLock = new Object();
|
|
final Map<String, PortResult> 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<Thread> 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 {
|
|
// todo: see if everyone has already finished - make this sleep conditional!
|
|
// todo: lowe this timeout?
|
|
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<String, PortResult> portCache = new HashMap<>();
|
|
|
|
/**
|
|
* Find all available serial ports and checks if simulator local TCP port is available
|
|
*/
|
|
private void findAllAvailablePorts(boolean includeSlowLookup) {
|
|
List<PortResult> ports = new ArrayList<>();
|
|
boolean dfuConnected;
|
|
boolean stLinkConnected;
|
|
boolean PCANConnected;
|
|
|
|
String[] serialPorts = LinkManager.getCommPorts();
|
|
|
|
List<String> portsToInspect = new ArrayList<>();
|
|
|
|
for (String serialPort : serialPorts) {
|
|
// 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<String> 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));
|
|
}
|
|
}
|
|
|
|
if (includeSlowLookup) {
|
|
for (String tcpPort : TcpConnector.getAvailablePorts()) {
|
|
ports.add(new PortResult(tcpPort, SerialPortType.Ecu));
|
|
}
|
|
dfuConnected = DfuFlasher.detectSTM32BootloaderDriverState(UpdateOperationCallbacks.DUMMY);
|
|
stLinkConnected = StLinkFlasher.detectStLink(UpdateOperationCallbacks.DUMMY);
|
|
PCANConnected = MaintenanceUtil.detectPcan(UpdateOperationCallbacks.DUMMY);
|
|
} else {
|
|
dfuConnected = false;
|
|
stLinkConnected = false;
|
|
PCANConnected = false;
|
|
}
|
|
if (PCANConnected)
|
|
ports.add(new PortResult(LinkManager.PCAN, SerialPortType.CAN));
|
|
if (SHOW_SOCKETCAN)
|
|
ports.add(new PortResult(LinkManager.SOCKET_CAN, SerialPortType.CAN));
|
|
|
|
boolean isListUpdated;
|
|
AvailableHardware currentHardware = new AvailableHardware(ports, dfuConnected, stLinkConnected, PCANConnected);
|
|
synchronized (lock) {
|
|
isListUpdated = !knownHardware.equals(currentHardware);
|
|
knownHardware = currentHardware;
|
|
}
|
|
if (isListUpdated) {
|
|
for (Listener listener : listeners)
|
|
listener.onChange(currentHardware);
|
|
}
|
|
}
|
|
|
|
private void startTimer() {
|
|
Thread portsScanner = new Thread(() -> {
|
|
boolean isFirstTime = true;
|
|
while (isRunning) {
|
|
findAllAvailablePorts(!isFirstTime);
|
|
isFirstTime = false;
|
|
try {
|
|
Thread.sleep(300);
|
|
} catch (InterruptedException e) {
|
|
log.error("sleep interrupted", e);
|
|
}
|
|
}
|
|
|
|
}, "Ports Scanner");
|
|
portsScanner.setDaemon(true);
|
|
portsScanner.start();
|
|
}
|
|
|
|
public void stopTimer() {
|
|
isRunning = false;
|
|
}
|
|
|
|
interface Listener {
|
|
void onChange(AvailableHardware currentHardware);
|
|
}
|
|
|
|
public static class AvailableHardware {
|
|
|
|
private final List<PortResult> ports;
|
|
private final boolean dfuFound;
|
|
private final boolean stLinkConnected;
|
|
private final boolean PCANConnected;
|
|
|
|
public <T> AvailableHardware(List<PortResult> ports, boolean dfuFound, boolean stLinkConnected, boolean PCANConnected) {
|
|
this.ports = ports;
|
|
this.dfuFound = dfuFound;
|
|
this.stLinkConnected = stLinkConnected;
|
|
this.PCANConnected = PCANConnected;
|
|
}
|
|
|
|
@NotNull
|
|
public List<PortResult> getKnownPorts() {return new ArrayList<>(ports);}
|
|
|
|
public boolean isDfuFound() {
|
|
return dfuFound;
|
|
}
|
|
|
|
public boolean isStLinkConnected() {return stLinkConnected;}
|
|
public boolean isPCANConnected(){return PCANConnected;}
|
|
|
|
@Override
|
|
public boolean equals(Object o) {
|
|
if (this == o) return true;
|
|
if (o == null || getClass() != o.getClass()) return false;
|
|
AvailableHardware that = (AvailableHardware) o;
|
|
return dfuFound == that.dfuFound && stLinkConnected == that.stLinkConnected && PCANConnected == that.PCANConnected && ports.equals(that.ports);
|
|
}
|
|
|
|
public boolean isEmpty() {
|
|
return !dfuFound && !stLinkConnected && !PCANConnected && ports.isEmpty();
|
|
}
|
|
}
|
|
|
|
public static String getEcuSignature(String port) {
|
|
try (IoStream stream = BufferedSerialIoStream.openPort(port)) {
|
|
return SerialAutoChecker.checkResponse(stream, callbackContext -> null);
|
|
} catch (Exception e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public static boolean ecuHasOpenblt(String port) {
|
|
try (IoStream stream = BufferedSerialIoStream.openPort(port)) {
|
|
if (stream == null) {
|
|
return false;
|
|
}
|
|
|
|
stream.sendPacket(new byte[]{(byte) Fields.TS_QUERY_BOOTLOADER});
|
|
|
|
byte[] response = stream.getDataBuffer().getPacket(500, "ecuHasOpenblt");
|
|
if (!IoHelper.checkResponseCode(response, (byte) Fields.TS_RESPONSE_OK)) {
|
|
// ECU didn't understand request, bootloader certainly not supported
|
|
return false;
|
|
}
|
|
|
|
// Data byte indicates bootloader type
|
|
return response[1] == Fields.TS_QUERY_BOOTLOADER_OPENBLT;
|
|
} catch (Exception e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static boolean isPortOpenblt(String port) {
|
|
try (IoStream stream = BufferedSerialIoStream.openPort(port)) {
|
|
if (stream == null) {
|
|
return false;
|
|
}
|
|
|
|
byte[] request = new byte[3];
|
|
request[0] = 2; // packet length
|
|
request[1] = (byte) 0xff; // XCPLOADER_CMD_CONNECT
|
|
request[2] = 0; // connectMode
|
|
|
|
stream.write(request);
|
|
|
|
IncomingDataBuffer idb = stream.getDataBuffer();
|
|
|
|
byte responseLength = idb.readByte(250);
|
|
|
|
// Invalid length, ignore
|
|
if (responseLength != 8) {
|
|
return false;
|
|
}
|
|
|
|
// Read length worth of bytes
|
|
byte[] response = new byte[responseLength];
|
|
idb.waitForBytes(100, "isPortOpenblt", System.currentTimeMillis(), responseLength);
|
|
idb.read(response);
|
|
|
|
// Response packet should start with FF
|
|
// Not much else to check, as the rest of the response is protocol settings from the device.
|
|
return response[0] == (byte) 0xFF;
|
|
} catch (IOException e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|