mirror of
https://github.com/SlimeVR/SlimeVR-Server.git
synced 2026-04-06 02:01:58 +02:00
Move serial communication to desktop subproject (#756)
Co-authored-by: Erimel <marioluigivideo@gmail.com>
This commit is contained in:
2
package-lock.json
generated
2
package-lock.json
generated
@@ -17321,8 +17321,8 @@
|
||||
"react-modal": "3.15.1",
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"semver": "^7.5.3",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"semver": "^7.5.3",
|
||||
"solarxr-protocol": "file:../solarxr-protocol",
|
||||
"tailwind-gradient-mask-image": "^1.0.0",
|
||||
"tailwindcss": "^3.3.2",
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* User Manual available at https://docs.gradle.org/6.3/userguide/java_library_plugin.html
|
||||
*/
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
@@ -64,7 +63,6 @@ dependencies {
|
||||
implementation("org.apache.commons:commons-collections4:4.4")
|
||||
|
||||
implementation("com.illposed.osc:javaosc-core:0.8")
|
||||
implementation("com.fazecast:jSerialComm:2.+")
|
||||
implementation("org.java-websocket:Java-WebSocket:1.+")
|
||||
implementation("com.melloware:jintellitype:1.+")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
|
||||
@@ -79,13 +77,3 @@ dependencies {
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
fun String.runCommand(currentWorkingDir: File = file("./")): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
workingDir = currentWorkingDir
|
||||
commandLine = this@runCommand.split("\\s".toRegex())
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import dev.slimevr.protocol.ProtocolAPI
|
||||
import dev.slimevr.reset.ResetHandler
|
||||
import dev.slimevr.serial.ProvisioningHandler
|
||||
import dev.slimevr.serial.SerialHandler
|
||||
import dev.slimevr.serial.SerialHandlerStub
|
||||
import dev.slimevr.setup.TapSetupHandler
|
||||
import dev.slimevr.status.StatusSystem
|
||||
import dev.slimevr.tracking.processor.HumanPoseManager
|
||||
@@ -44,8 +45,9 @@ typealias SteamBridgeProvider = (
|
||||
const val SLIMEVR_IDENTIFIER = "dev.slimevr.SlimeVR"
|
||||
|
||||
class VRServer @JvmOverloads constructor(
|
||||
driverBridgeProvider: SteamBridgeProvider = { _: VRServer, _: Tracker, _: List<Tracker> -> null },
|
||||
feederBridgeProvider: (VRServer) -> ISteamVRBridge? = { _: VRServer -> null },
|
||||
driverBridgeProvider: SteamBridgeProvider = { _, _, _ -> null },
|
||||
feederBridgeProvider: (VRServer) -> ISteamVRBridge? = { _ -> null },
|
||||
serialHandlerProvider: (VRServer) -> SerialHandler = { _ -> SerialHandlerStub() },
|
||||
// configPath is used by VRWorkout, do not remove!
|
||||
configPath: String,
|
||||
) : Thread("VRServer") {
|
||||
@@ -101,7 +103,7 @@ class VRServer @JvmOverloads constructor(
|
||||
configManager = ConfigManager(configPath)
|
||||
configManager.loadConfig()
|
||||
deviceManager = DeviceManager(this)
|
||||
serialHandler = SerialHandler()
|
||||
serialHandler = serialHandlerProvider(this)
|
||||
provisioningHandler = ProvisioningHandler(this)
|
||||
resetHandler = ResetHandler()
|
||||
tapSetupHandler = TapSetupHandler()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package dev.slimevr.protocol.rpc.serial;
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort;
|
||||
import com.google.flatbuffers.FlatBufferBuilder;
|
||||
import dev.slimevr.protocol.GenericConnection;
|
||||
import dev.slimevr.protocol.ProtocolAPI;
|
||||
import dev.slimevr.protocol.rpc.RPCHandler;
|
||||
import dev.slimevr.serial.SerialListener;
|
||||
import dev.slimevr.serial.SerialPort;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import solarxr_protocol.rpc.*;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package dev.slimevr.serial;
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort;
|
||||
import dev.slimevr.VRServer;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
@@ -107,7 +107,7 @@ public class ProvisioningHandler implements SerialListener {
|
||||
|
||||
|
||||
@Override
|
||||
public void onSerialConnected(SerialPort port) {
|
||||
public void onSerialConnected(@NotNull SerialPort port) {
|
||||
if (!isRunning)
|
||||
return;
|
||||
this.tryProvisioning();
|
||||
@@ -121,7 +121,7 @@ public class ProvisioningHandler implements SerialListener {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSerialLog(String str) {
|
||||
public void onSerialLog(@NotNull String str) {
|
||||
if (!isRunning)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,299 +0,0 @@
|
||||
package dev.slimevr.serial;
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort;
|
||||
import com.fazecast.jSerialComm.SerialPortEvent;
|
||||
import com.fazecast.jSerialComm.SerialPortMessageListener;
|
||||
import io.eiren.util.logging.LogManager;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.collections4.Equator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
||||
public class SerialHandler implements SerialPortMessageListener {
|
||||
|
||||
private final List<SerialListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final Timer getDevicesTimer = new Timer("GetDevicesTimer");
|
||||
|
||||
private SerialPort currentPort = null;
|
||||
|
||||
private boolean watchingNewDevices = false;
|
||||
private SerialPort[] lastKnownPorts = new SerialPort[] {};
|
||||
|
||||
public SerialHandler() {
|
||||
startWatchingNewDevices();
|
||||
}
|
||||
|
||||
public void startWatchingNewDevices() {
|
||||
if (this.watchingNewDevices)
|
||||
return;
|
||||
this.watchingNewDevices = true;
|
||||
this.getDevicesTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
public void run() {
|
||||
try {
|
||||
detectNewPorts();
|
||||
} catch (Throwable t) {
|
||||
LogManager
|
||||
.severe(
|
||||
"[SerialHandler] Error while watching for new devices, cancelling the \"getDevicesTimer\".",
|
||||
t
|
||||
);
|
||||
getDevicesTimer.cancel();
|
||||
}
|
||||
}
|
||||
}, 0, 3000);
|
||||
}
|
||||
|
||||
public void stopWatchingNewDevices() {
|
||||
if (!this.watchingNewDevices)
|
||||
return;
|
||||
this.watchingNewDevices = false;
|
||||
this.getDevicesTimer.cancel();
|
||||
this.getDevicesTimer.purge();
|
||||
}
|
||||
|
||||
public void onNewDevice(SerialPort port) {
|
||||
this.listeners.forEach((listener) -> listener.onNewSerialDevice(port));
|
||||
}
|
||||
|
||||
|
||||
public void addListener(SerialListener channel) {
|
||||
this.listeners.add(channel);
|
||||
}
|
||||
|
||||
public void removeListener(SerialListener l) {
|
||||
listeners.removeIf(listener -> l == listener);
|
||||
}
|
||||
|
||||
public synchronized boolean openSerial(String portLocation, boolean auto) {
|
||||
LogManager.info("[SerialHandler] Trying to open: " + portLocation + ", auto: " + auto);
|
||||
|
||||
SerialPort[] ports = SerialPort.getCommPorts();
|
||||
lastKnownPorts = ports;
|
||||
SerialPort newPort = null;
|
||||
for (SerialPort port : ports) {
|
||||
if (!auto && port.getPortLocation().equals(portLocation)) {
|
||||
newPort = port;
|
||||
break;
|
||||
}
|
||||
|
||||
if (auto && isKnownBoard(port.getDescriptivePortName())) {
|
||||
newPort = port;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newPort == null) {
|
||||
LogManager
|
||||
.info(
|
||||
"[SerialHandler] No serial ports found to connect to ("
|
||||
+ ports.length
|
||||
+ ") total ports"
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this.isConnected()) {
|
||||
if (
|
||||
!newPort.getPortLocation().equals(currentPort.getPortLocation())
|
||||
|| !newPort
|
||||
.getDescriptivePortName()
|
||||
.equals(currentPort.getDescriptivePortName())
|
||||
) {
|
||||
LogManager
|
||||
.info(
|
||||
"[SerialHandler] Closing current serial port "
|
||||
+ currentPort.getDescriptivePortName()
|
||||
);
|
||||
currentPort.removeDataListener();
|
||||
currentPort.closePort();
|
||||
} else {
|
||||
LogManager.info("[SerialHandler] Reusing already open port");
|
||||
this.listeners.forEach((listener) -> listener.onSerialConnected(currentPort));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
currentPort = newPort;
|
||||
LogManager
|
||||
.info(
|
||||
"[SerialHandler] Trying to connect to new serial port "
|
||||
+ currentPort.getDescriptivePortName()
|
||||
);
|
||||
|
||||
currentPort.setBaudRate(115200);
|
||||
currentPort.clearRTS();
|
||||
currentPort.clearDTR();
|
||||
if (!currentPort.openPort(1000)) {
|
||||
LogManager
|
||||
.warning(
|
||||
"[SerialHandler] Can't open serial port "
|
||||
+ currentPort.getDescriptivePortName()
|
||||
+ ", last error: "
|
||||
+ currentPort.getLastErrorCode()
|
||||
);
|
||||
currentPort = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
currentPort.addDataListener(this);
|
||||
this.listeners.forEach((listener) -> listener.onSerialConnected(currentPort));
|
||||
LogManager
|
||||
.info("[SerialHandler] Serial port " + newPort.getDescriptivePortName() + " is open");
|
||||
return true;
|
||||
}
|
||||
|
||||
public void rebootRequest() {
|
||||
this.writeSerial("REBOOT");
|
||||
}
|
||||
|
||||
public void factoryResetRequest() {
|
||||
this.writeSerial("FRST");
|
||||
}
|
||||
|
||||
public void infoRequest() {
|
||||
this.writeSerial("GET INFO");
|
||||
}
|
||||
|
||||
public synchronized void closeSerial() {
|
||||
try {
|
||||
if (currentPort != null)
|
||||
currentPort.closePort();
|
||||
this.listeners.forEach(SerialListener::onSerialDisconnected);
|
||||
LogManager
|
||||
.info(
|
||||
"[SerialHandler] Port "
|
||||
+ (currentPort != null ? currentPort.getDescriptivePortName() : "null")
|
||||
+ " closed okay"
|
||||
);
|
||||
currentPort = null;
|
||||
} catch (Exception e) {
|
||||
LogManager
|
||||
.warning(
|
||||
"[SerialHandler] Error closing port "
|
||||
+ (currentPort != null ? currentPort.getDescriptivePortName() : "null"),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void writeSerial(String serialText) {
|
||||
if (currentPort == null)
|
||||
return;
|
||||
OutputStream os = currentPort.getOutputStream();
|
||||
OutputStreamWriter writer = new OutputStreamWriter(os);
|
||||
try {
|
||||
writer.append(serialText).append("\n");
|
||||
writer.flush();
|
||||
this.addLog("-> " + serialText + "\n");
|
||||
} catch (IOException e) {
|
||||
addLog("[!] Serial error: " + e.getMessage() + "\n");
|
||||
LogManager.warning("[SerialHandler] Serial port write error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void setWifi(String ssid, String passwd) {
|
||||
if (currentPort == null)
|
||||
return;
|
||||
OutputStream os = currentPort.getOutputStream();
|
||||
OutputStreamWriter writer = new OutputStreamWriter(os);
|
||||
try {
|
||||
writer.append("SET WIFI \"").append(ssid).append("\" \"").append(passwd).append("\"\n");
|
||||
writer.flush();
|
||||
this.addLog("-> SET WIFI \"" + ssid + "\" \"" + passwd.replaceAll(".", "*") + "\"\n");
|
||||
} catch (IOException e) {
|
||||
addLog(e + "\n");
|
||||
LogManager.warning("[SerialHandler] Serial port write error", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void addLog(String str) {
|
||||
LogManager.info("[Serial] " + str);
|
||||
this.listeners.forEach(listener -> listener.onSerialLog(str));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getListeningEvents() {
|
||||
return SerialPort.LISTENING_EVENT_PORT_DISCONNECTED
|
||||
| SerialPort.LISTENING_EVENT_DATA_RECEIVED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialEvent(SerialPortEvent event) {
|
||||
if (event.getEventType() == SerialPort.LISTENING_EVENT_DATA_RECEIVED) {
|
||||
byte[] newData = event.getReceivedData();
|
||||
String s = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(newData)).toString();
|
||||
this.addLog(s);
|
||||
} else if (event.getEventType() == SerialPort.LISTENING_EVENT_PORT_DISCONNECTED) {
|
||||
this.closeSerial();
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isConnected() {
|
||||
return this.currentPort != null && this.currentPort.isOpen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getMessageDelimiter() {
|
||||
return new byte[] { (byte) 0x0A };
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean delimiterIndicatesEndOfMessage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public Stream<SerialPort> getKnownPorts() {
|
||||
return Arrays
|
||||
.stream(SerialPort.getCommPorts())
|
||||
.filter((port) -> isKnownBoard(port.getDescriptivePortName()));
|
||||
|
||||
}
|
||||
|
||||
private boolean isKnownBoard(String com) {
|
||||
String lowerCom = com.toLowerCase();
|
||||
|
||||
return lowerCom.contains("ch340")
|
||||
|| lowerCom.contains("cp21")
|
||||
|| lowerCom.contains("ch910")
|
||||
|| (lowerCom.contains("usb")
|
||||
&& lowerCom.contains("seri"));
|
||||
}
|
||||
|
||||
private void detectNewPorts() {
|
||||
try {
|
||||
List<SerialPort> differences = new ArrayList<>(
|
||||
CollectionUtils
|
||||
.removeAll(
|
||||
this.getKnownPorts().collect(Collectors.toList()),
|
||||
Arrays.asList(lastKnownPorts),
|
||||
new Equator<>() {
|
||||
@Override
|
||||
public boolean equate(SerialPort o1, SerialPort o2) {
|
||||
return o1.getPortLocation().equals(o2.getPortLocation())
|
||||
&& o1
|
||||
.getDescriptivePortName()
|
||||
.equals(o1.getDescriptivePortName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hash(SerialPort o) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
lastKnownPorts = SerialPort.getCommPorts();
|
||||
differences.forEach(this::onNewDevice);
|
||||
} catch (Throwable e) {
|
||||
LogManager
|
||||
.severe("[SerialHandler] Using serial ports is not supported on this platform", e);
|
||||
throw new RuntimeException("Serial unsupported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dev.slimevr.serial
|
||||
|
||||
import java.util.stream.Stream
|
||||
|
||||
abstract class SerialHandler {
|
||||
abstract val isConnected: Boolean
|
||||
abstract val knownPorts: Stream<out SerialPort>
|
||||
|
||||
abstract fun addListener(channel: SerialListener)
|
||||
abstract fun removeListener(channel: SerialListener)
|
||||
|
||||
abstract fun openSerial(portLocation: String?, auto: Boolean): Boolean
|
||||
abstract fun rebootRequest()
|
||||
abstract fun factoryResetRequest()
|
||||
abstract fun infoRequest()
|
||||
abstract fun closeSerial()
|
||||
abstract fun setWifi(ssid: String, passwd: String)
|
||||
|
||||
companion object {
|
||||
val supportedSerial: Set<Pair<Int, Int>> = setOf(
|
||||
// / QinHeng
|
||||
// CH340
|
||||
Pair(0x1A86, 0x7522),
|
||||
Pair(0x1A86, 0x7523),
|
||||
// CH341
|
||||
Pair(0x1A86, 0x5523),
|
||||
// CH9102x
|
||||
Pair(0x1A86, 0x55D4),
|
||||
// / Silabs
|
||||
// CP210x
|
||||
Pair(0x10C4, 0xEA60),
|
||||
// / Espressif
|
||||
// ESP32-C3
|
||||
Pair(0x303A, 0x1001)
|
||||
)
|
||||
fun isKnownBoard(port: SerialPort): Boolean =
|
||||
supportedSerial.contains(Pair(port.vendorId, port.productId))
|
||||
}
|
||||
}
|
||||
|
||||
class SerialHandlerStub() : SerialHandler() {
|
||||
override val isConnected: Boolean = false
|
||||
override val knownPorts: Stream<out SerialPort> = Stream.empty()
|
||||
|
||||
override fun addListener(channel: SerialListener) {}
|
||||
|
||||
override fun removeListener(channel: SerialListener) {}
|
||||
|
||||
override fun openSerial(portLocation: String?, auto: Boolean): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun rebootRequest() {}
|
||||
|
||||
override fun factoryResetRequest() {}
|
||||
|
||||
override fun infoRequest() {}
|
||||
|
||||
override fun closeSerial() {}
|
||||
|
||||
override fun setWifi(ssid: String, passwd: String) {}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package dev.slimevr.serial;
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort;
|
||||
|
||||
|
||||
public interface SerialListener {
|
||||
|
||||
void onSerialConnected(SerialPort port);
|
||||
|
||||
void onSerialDisconnected();
|
||||
|
||||
void onSerialLog(String str);
|
||||
|
||||
void onNewSerialDevice(SerialPort port);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package dev.slimevr.serial
|
||||
|
||||
import java.util.*
|
||||
|
||||
abstract class SerialPort {
|
||||
abstract val portLocation: String
|
||||
abstract val descriptivePortName: String
|
||||
abstract val vendorId: Int?
|
||||
abstract val productId: Int?
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
val other: SerialPort = other as? SerialPort ?: return super.equals(other)
|
||||
|
||||
return this.portLocation == other.portLocation &&
|
||||
this.descriptivePortName == other.descriptivePortName &&
|
||||
this.vendorId == other.vendorId &&
|
||||
this.productId == other.productId
|
||||
}
|
||||
|
||||
override fun hashCode(): Int = Objects.hash(portLocation, descriptivePortName, vendorId, productId)
|
||||
}
|
||||
|
||||
interface SerialListener {
|
||||
fun onSerialConnected(port: SerialPort)
|
||||
fun onSerialDisconnected()
|
||||
fun onSerialLog(str: String)
|
||||
fun onNewSerialDevice(port: SerialPort)
|
||||
}
|
||||
@@ -6,7 +6,6 @@
|
||||
* User Manual available at https://docs.gradle.org/6.3/userguide/java_library_plugin.html
|
||||
*/
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
@@ -58,6 +57,7 @@ dependencies {
|
||||
implementation("com.google.protobuf:protobuf-java:3.21.12")
|
||||
implementation("net.java.dev.jna:jna:5.+")
|
||||
implementation("net.java.dev.jna:jna-platform:5.+")
|
||||
implementation("com.fazecast:jSerialComm:2.10.2")
|
||||
}
|
||||
|
||||
tasks.shadowJar {
|
||||
@@ -76,16 +76,6 @@ application {
|
||||
mainClass.set("dev.slimevr.desktop.Main")
|
||||
}
|
||||
|
||||
fun String.runCommand(currentWorkingDir: File = file("./")): String {
|
||||
val byteOut = ByteArrayOutputStream()
|
||||
project.exec {
|
||||
workingDir = currentWorkingDir
|
||||
commandLine = this@runCommand.split("\\s".toRegex())
|
||||
standardOutput = byteOut
|
||||
}
|
||||
return String(byteOut.toByteArray()).trim()
|
||||
}
|
||||
|
||||
buildConfig {
|
||||
useKotlinOutput { topLevelConstants = true }
|
||||
packageName("dev.slimevr.desktop")
|
||||
|
||||
@@ -9,6 +9,7 @@ import dev.slimevr.bridge.ISteamVRBridge
|
||||
import dev.slimevr.desktop.platform.SteamVRBridge
|
||||
import dev.slimevr.desktop.platform.linux.UnixSocketBridge
|
||||
import dev.slimevr.desktop.platform.windows.WindowsNamedPipeBridge
|
||||
import dev.slimevr.desktop.serial.DesktopSerialHandler
|
||||
import dev.slimevr.tracking.trackers.Tracker
|
||||
import io.eiren.util.OperatingSystem
|
||||
import io.eiren.util.collections.FastList
|
||||
@@ -116,7 +117,12 @@ fun main(args: Array<String>) {
|
||||
try {
|
||||
val configDir = resolveConfig()
|
||||
LogManager.info("Using config dir: $configDir")
|
||||
val vrServer = VRServer(::provideSteamVRBridge, ::provideFeederBridge, configDir)
|
||||
val vrServer = VRServer(
|
||||
::provideSteamVRBridge,
|
||||
::provideFeederBridge,
|
||||
{ _ -> DesktopSerialHandler() },
|
||||
configDir
|
||||
)
|
||||
vrServer.start()
|
||||
Keybinding(vrServer)
|
||||
val scanner = thread {
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
package dev.slimevr.desktop.serial
|
||||
|
||||
import com.fazecast.jSerialComm.SerialPort
|
||||
import com.fazecast.jSerialComm.SerialPortEvent
|
||||
import com.fazecast.jSerialComm.SerialPortMessageListener
|
||||
import dev.slimevr.serial.SerialHandler
|
||||
import dev.slimevr.serial.SerialListener
|
||||
import io.eiren.util.logging.LogManager
|
||||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.*
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.stream.Stream
|
||||
import kotlin.concurrent.timerTask
|
||||
import kotlin.streams.asSequence
|
||||
import kotlin.streams.asStream
|
||||
import dev.slimevr.serial.SerialPort as SlimeSerialPort
|
||||
|
||||
class SerialPortWrapper(val port: SerialPort) : SlimeSerialPort() {
|
||||
override val portLocation: String
|
||||
get() = port.portLocation
|
||||
override val descriptivePortName: String
|
||||
get() = port.descriptivePortName
|
||||
|
||||
override val vendorId: Int
|
||||
get() = port.vendorID
|
||||
|
||||
override val productId: Int
|
||||
get() = port.productID
|
||||
}
|
||||
|
||||
class DesktopSerialHandler : SerialHandler(), SerialPortMessageListener {
|
||||
private val listeners: MutableList<SerialListener> = CopyOnWriteArrayList()
|
||||
private val getDevicesTimer = Timer("GetDevicesTimer")
|
||||
private var currentPort: SerialPort? = null
|
||||
private var watchingNewDevices = false
|
||||
private var lastKnownPorts = setOf<SerialPortWrapper>()
|
||||
|
||||
init {
|
||||
startWatchingNewDevices()
|
||||
}
|
||||
|
||||
fun startWatchingNewDevices() {
|
||||
if (watchingNewDevices) return
|
||||
watchingNewDevices = true
|
||||
getDevicesTimer.scheduleAtFixedRate(
|
||||
timerTask {
|
||||
try {
|
||||
detectNewPorts()
|
||||
} catch (t: Throwable) {
|
||||
LogManager.severe(
|
||||
"[SerialHandler] Error while watching for new devices, cancelling the \"getDevicesTimer\".",
|
||||
t
|
||||
)
|
||||
getDevicesTimer.cancel()
|
||||
}
|
||||
},
|
||||
0,
|
||||
3000
|
||||
)
|
||||
}
|
||||
|
||||
fun stopWatchingNewDevices() {
|
||||
if (!watchingNewDevices) return
|
||||
watchingNewDevices = false
|
||||
getDevicesTimer.cancel()
|
||||
getDevicesTimer.purge()
|
||||
}
|
||||
|
||||
fun onNewDevice(port: SerialPort) {
|
||||
listeners.forEach { it.onNewSerialDevice(SerialPortWrapper(port)) }
|
||||
}
|
||||
|
||||
override fun addListener(channel: SerialListener) {
|
||||
listeners.add(channel)
|
||||
}
|
||||
|
||||
override fun removeListener(channel: SerialListener) {
|
||||
listeners.removeIf { channel === it }
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun openSerial(portLocation: String?, auto: Boolean): Boolean {
|
||||
LogManager.info("[SerialHandler] Trying to open: $portLocation, auto: $auto")
|
||||
val ports = SerialPort.getCommPorts()
|
||||
lastKnownPorts = ports.map { SerialPortWrapper(it) }.toSet()
|
||||
val newPort: SerialPort? = ports.find {
|
||||
(!auto && it.portLocation == portLocation) ||
|
||||
(auto && isKnownBoard(SerialPortWrapper(it)))
|
||||
}
|
||||
if (newPort == null) {
|
||||
LogManager.info(
|
||||
"[SerialHandler] No serial ports found to connect to (${ports.size}) total ports"
|
||||
)
|
||||
return false
|
||||
}
|
||||
if (isConnected) {
|
||||
if (SerialPortWrapper(newPort) != currentPort?.let { SerialPortWrapper(it) }) {
|
||||
LogManager.info(
|
||||
"[SerialHandler] Closing current serial port " +
|
||||
currentPort!!.descriptivePortName
|
||||
)
|
||||
currentPort!!.removeDataListener()
|
||||
currentPort!!.closePort()
|
||||
} else {
|
||||
LogManager.info("[SerialHandler] Reusing already open port")
|
||||
listeners.forEach { it.onSerialConnected(SerialPortWrapper(currentPort!!)) }
|
||||
return true
|
||||
}
|
||||
}
|
||||
currentPort = newPort
|
||||
LogManager.info(
|
||||
"[SerialHandler] Trying to connect to new serial port " +
|
||||
currentPort!!.descriptivePortName
|
||||
)
|
||||
currentPort?.setBaudRate(115200)
|
||||
currentPort?.clearRTS()
|
||||
currentPort?.clearDTR()
|
||||
if (currentPort?.openPort(1000) == false) {
|
||||
LogManager.warning(
|
||||
"[SerialHandler] Can't open serial port ${currentPort?.descriptivePortName}, last error: ${currentPort?.lastErrorCode}"
|
||||
|
||||
)
|
||||
currentPort = null
|
||||
return false
|
||||
}
|
||||
currentPort?.addDataListener(this)
|
||||
listeners.forEach { it.onSerialConnected(SerialPortWrapper(currentPort!!)) }
|
||||
LogManager.info("[SerialHandler] Serial port ${newPort.descriptivePortName} is open")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun rebootRequest() {
|
||||
writeSerial("REBOOT")
|
||||
}
|
||||
|
||||
override fun factoryResetRequest() {
|
||||
writeSerial("FRST")
|
||||
}
|
||||
|
||||
override fun infoRequest() {
|
||||
writeSerial("GET INFO")
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun closeSerial() {
|
||||
try {
|
||||
currentPort?.closePort()
|
||||
listeners.forEach { it.onSerialDisconnected() }
|
||||
LogManager.info(
|
||||
"[SerialHandler] Port ${currentPort?.descriptivePortName} closed okay"
|
||||
)
|
||||
currentPort = null
|
||||
} catch (e: Exception) {
|
||||
LogManager.warning(
|
||||
"[SerialHandler] Error closing port ${currentPort?.descriptivePortName}",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun writeSerial(serialText: String) {
|
||||
val os = currentPort?.outputStream ?: return
|
||||
val writer = OutputStreamWriter(os)
|
||||
try {
|
||||
writer.append(serialText).append("\n")
|
||||
writer.flush()
|
||||
addLog("-> $serialText\n")
|
||||
} catch (e: IOException) {
|
||||
addLog("[!] Serial error: ${e.message}\n")
|
||||
LogManager.warning("[SerialHandler] Serial port write error", e)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun setWifi(ssid: String, passwd: String) {
|
||||
val os = currentPort?.outputStream ?: return
|
||||
val writer = OutputStreamWriter(os)
|
||||
try {
|
||||
writer.append("SET WIFI \"").append(ssid).append("\" \"").append(passwd).append("\"\n")
|
||||
writer.flush()
|
||||
addLog("-> SET WIFI \"$ssid\" \"${passwd.replace(".".toRegex(), "*")}\"\n")
|
||||
} catch (e: IOException) {
|
||||
addLog("$e\n")
|
||||
LogManager.warning("[SerialHandler] Serial port write error", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun addLog(str: String) {
|
||||
LogManager.info("[Serial] $str")
|
||||
listeners.forEach { it.onSerialLog(str) }
|
||||
}
|
||||
|
||||
override fun getListeningEvents(): Int {
|
||||
return (
|
||||
SerialPort.LISTENING_EVENT_PORT_DISCONNECTED
|
||||
or SerialPort.LISTENING_EVENT_DATA_RECEIVED
|
||||
)
|
||||
}
|
||||
|
||||
override fun serialEvent(event: SerialPortEvent) {
|
||||
when (event.eventType) {
|
||||
SerialPort.LISTENING_EVENT_DATA_RECEIVED -> {
|
||||
val newData = event.receivedData
|
||||
val s = StandardCharsets.UTF_8.decode(ByteBuffer.wrap(newData)).toString()
|
||||
addLog(s)
|
||||
}
|
||||
SerialPort.LISTENING_EVENT_PORT_DISCONNECTED -> {
|
||||
closeSerial()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
override val isConnected: Boolean
|
||||
get() = currentPort?.isOpen ?: false
|
||||
|
||||
override fun getMessageDelimiter(): ByteArray {
|
||||
return byteArrayOf(0x0A.toByte())
|
||||
}
|
||||
|
||||
override fun delimiterIndicatesEndOfMessage(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override val knownPorts: Stream<SerialPortWrapper>
|
||||
get() = SerialPort.getCommPorts()
|
||||
.asSequence()
|
||||
.map { SerialPortWrapper(it) }
|
||||
.filter { isKnownBoard(it) }
|
||||
.asStream()
|
||||
|
||||
private fun detectNewPorts() {
|
||||
try {
|
||||
val differences = knownPorts.asSequence() - lastKnownPorts
|
||||
lastKnownPorts = SerialPort.getCommPorts().map { SerialPortWrapper(it) }.toSet()
|
||||
differences.forEach { onNewDevice(it.port) }
|
||||
} catch (e: Throwable) {
|
||||
LogManager
|
||||
.severe("[SerialHandler] Using serial ports is not supported on this platform", e)
|
||||
throw RuntimeException("Serial unsupported")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user