Moving wifi provisioning and new device detection to the server (#542)

This commit is contained in:
lucas lelievre
2023-02-12 18:54:40 +01:00
committed by GitHub
parent fcb38292fa
commit 58b1fbba1f
18 changed files with 485 additions and 202 deletions

View File

@@ -451,11 +451,14 @@ onboarding-connect_tracker-description-p0 = Now onto the fun part, connecting al
onboarding-connect_tracker-description-p1 = Simply connect all that aren't connected yet, through a USB port.
onboarding-connect_tracker-issue-serial = I'm having trouble connecting!
onboarding-connect_tracker-usb = USB Tracker
onboarding-connect_tracker-connection_status-connecting = Sending Wi-Fi credentials
onboarding-connect_tracker-connection_status-connected = Connected to Wi-Fi
onboarding-connect_tracker-connection_status-error = Unable to connect to Wi-Fi
onboarding-connect_tracker-connection_status-start_connecting = Looking for trackers
onboarding-connect_tracker-connection_status-handshake = Connected to the Server
onboarding-connect_tracker-connection_status-none = Looking for trackers
onboarding-connect_tracker-connection_status-serial_init = Connecting to serial device
onboarding-connect_tracker-connection_status-provisioning = Sending Wi-Fi credentials
onboarding-connect_tracker-connection_status-connecting = Trying to connect to Wi-Fi
onboarding-connect_tracker-connection_status-looking_for_server = Looking for server
onboarding-connect_tracker-connection_status-connection_error = Unable to connect to Wi-Fi
onboarding-connect_tracker-connection_status-could_not_find_server = Could not find the server
onboarding-connect_tracker-connection_status-done = Connected to the Server
# $amount (Number) - Amount of trackers connected (this is a number, but you can use CLDR plural rules for your language)
# More info on https://www.unicode.org/cldr/cldr-aux/charts/22/supplemental/language_plural_rules.html
# English in this case only has 2 plural rules, which are "one" and "other",

View File

@@ -1,14 +1,12 @@
import { Localized, useLocalization } from '@fluent/react';
import { useEffect, useState } from 'react';
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import {
NewSerialDeviceResponseT,
RpcMessage,
SerialDevicesRequestT,
SerialDevicesResponseT,
SerialDeviceT,
} from 'solarxr-protocol';
import { useConfig } from '../hooks/config';
import { usePrevious } from '../hooks/previous';
import { useWebsocketAPI } from '../hooks/websocket-api';
import { useWifiForm, WifiFormData } from '../hooks/wifi-form';
import { BaseModal } from './commons/BaseModal';
@@ -18,96 +16,18 @@ import { USBIcon } from './commons/icon/UsbIcon';
import { Input } from './commons/Input';
import { Typography } from './commons/Typography';
const mapItems = <T,>(items: T[], getKey: (item: T) => string) => {
const map: any = {};
for (const item of items) {
const key = getKey(item);
map[key] = item;
}
return map;
};
const detectChanges = <T,>(
prevItems: T[],
nextItems: T[],
getKey: (item: T) => string,
compareItems: (a: T, b: T) => boolean
) => {
const mappedItems = mapItems(prevItems, getKey);
const addedItems = [];
const updatedItems = [];
const removedItems = [];
const unchangedItems = [];
for (const nextItem of nextItems) {
const itemKey = getKey(nextItem);
if (itemKey in mappedItems) {
const prevItem = mappedItems[itemKey];
if (delete mappedItems[itemKey] && compareItems(prevItem, nextItem)) {
unchangedItems.push(nextItem);
} else {
updatedItems.push(nextItem);
}
} else {
addedItems.push(nextItem);
}
}
for (const itemKey in mappedItems) {
if (itemKey in mappedItems) {
removedItems.push(mappedItems[itemKey]);
}
}
return { addedItems, updatedItems, removedItems, unchangedItems };
};
export function SerialDetectionModal() {
const { l10n } = useLocalization();
const { config } = useConfig();
const nav = useNavigate();
const { pathname } = useLocation();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const [currentDevices, setCurrentDevices] = useState<SerialDeviceT[] | null>(
null
);
const prevDevices = usePrevious<SerialDeviceT[] | null>(currentDevices);
const { useRPCPacket } = useWebsocketAPI();
const [isOpen, setOpen] = useState<SerialDeviceT | null>(null);
const [showWifiForm, setShowWifiForm] = useState(false);
const { handleSubmit, submitWifiCreds, formState, hasWifiCreds, control } =
useWifiForm();
useEffect(() => {
if (prevDevices == null) return;
const changes = detectChanges(
prevDevices || [],
currentDevices || [],
(item) => item.port?.toString() || 'error',
(a, b) => a.port == b.port && a.name == b.name
);
if (changes.addedItems.length === 1) {
setOpen(changes.addedItems[0]);
}
}, [prevDevices, currentDevices]);
useEffect(() => {
let timerId: NodeJS.Timer;
if (
config?.watchNewDevices &&
!['/settings/serial', '/onboarding/connect-trackers'].includes(pathname)
) {
timerId = setInterval(() => {
sendRPCPacket(
RpcMessage.SerialDevicesRequest,
new SerialDevicesRequestT()
);
}, 3000);
}
return () => {
clearInterval(timerId);
};
}, [config, sendRPCPacket, pathname]);
const closeModal = () => {
setOpen(null);
setShowWifiForm(false);
@@ -134,9 +54,14 @@ export function SerialDetectionModal() {
};
useRPCPacket(
RpcMessage.SerialDevicesResponse,
(val: SerialDevicesResponseT) => {
setCurrentDevices(val.devices);
RpcMessage.NewSerialDeviceResponse,
({ device }: NewSerialDeviceResponseT) => {
if (
config?.watchNewDevices &&
!['/settings/serial', '/onboarding/connect-trackers'].includes(pathname)
) {
setOpen(device);
}
}
);

View File

@@ -85,7 +85,7 @@ export const Input = ({
variant = 'primary',
rules,
}: {
rules: UseControllerProps<any>['rules'];
rules?: UseControllerProps<any>['rules'];
control: Control<any>;
} & InputProps &
Partial<HTMLInputElement>) => {

View File

@@ -3,11 +3,11 @@ import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import {
CloseSerialRequestT,
OpenSerialRequestT,
RpcMessage,
SerialUpdateResponseT,
SetWifiRequestT,
StartWifiProvisioningRequestT,
StopWifiProvisioningRquestT,
WifiProvisioningStatus,
WifiProvisioningStatusResponseT,
} from 'solarxr-protocol';
import { useLayout } from '../../../hooks/layout';
import { useOnboarding } from '../../../hooks/onboarding';
@@ -21,20 +21,23 @@ import { Typography } from '../../commons/Typography';
import { TrackerCard } from '../../tracker/TrackerCard';
const BOTTOM_HEIGHT = 80;
type ConnectionStatus =
| 'CONNECTING'
| 'CONNECTED'
| 'HANDSHAKE'
| 'ERROR'
| 'START-CONNECTING';
const statusLabelMap = {
['CONNECTING']: 'onboarding-connect_tracker-connection_status-connecting',
['CONNECTED']: 'onboarding-connect_tracker-connection_status-connected',
['ERROR']: 'onboarding-connect_tracker-connection_status-error',
['START-CONNECTING']:
'onboarding-connect_tracker-connection_status-start_connecting',
['HANDSHAKE']: 'onboarding-connect_tracker-connection_status-handshake',
[WifiProvisioningStatus.NONE]: 'onboarding-connect_tracker-connection_status-none',
[WifiProvisioningStatus.SERIAL_INIT]:
'onboarding-connect_tracker-connection_status-serial_init',
[WifiProvisioningStatus.PROVISIONING]:
'onboarding-connect_tracker-connection_status-provisioning',
[WifiProvisioningStatus.CONNECTING]:
'onboarding-connect_tracker-connection_status-connecting',
[WifiProvisioningStatus.LOOKING_FOR_SERVER]:
'onboarding-connect_tracker-connection_status-looking_for_server',
[WifiProvisioningStatus.DONE]:
'onboarding-connect_tracker-connection_status-done',
[WifiProvisioningStatus.CONNECTION_ERROR]:
'onboarding-connect_tracker-connection_status-connection_error',
[WifiProvisioningStatus.COULD_NOT_FIND_SERVER]:
'onboarding-connect_tracker-connection_status-could_not_find_server',
};
export function ConnectTrackersPage() {
@@ -44,90 +47,41 @@ export function ConnectTrackersPage() {
const { applyProgress, state, skipSetup } = useOnboarding();
const navigate = useNavigate();
const { sendRPCPacket, useRPCPacket } = useWebsocketAPI();
const [isSerialOpen, setSerialOpen] = useState(false);
const [connectionStatus, setConnectionStatus] =
useState<ConnectionStatus>('START-CONNECTING');
const [provisioningStatus, setProvisioningStatus] =
useState<WifiProvisioningStatus>(WifiProvisioningStatus.NONE);
applyProgress(0.4);
const connectedTrackers = useConnectedTrackers();
const openSerial = () => {
const req = new OpenSerialRequestT();
req.auto = true;
sendRPCPacket(RpcMessage.OpenSerialRequest, req);
};
useEffect(() => {
if (!state.wifi) {
navigate('/onboarding/wifi-creds');
}
openSerial();
const req = new StartWifiProvisioningRequestT();
req.ssid = state.wifi?.ssid as string;
req.password = state.wifi?.password as string;
sendRPCPacket(RpcMessage.StartWifiProvisioningRequest, req);
return () => {
sendRPCPacket(RpcMessage.CloseSerialRequest, new CloseSerialRequestT());
sendRPCPacket(
RpcMessage.StopWifiProvisioningRquest,
new StopWifiProvisioningRquestT()
);
};
}, []);
useRPCPacket(
RpcMessage.SerialUpdateResponse,
(data: SerialUpdateResponseT) => {
if (data.closed) {
setSerialOpen(false);
setConnectionStatus('START-CONNECTING');
setTimeout(() => {
openSerial();
}, 1000);
}
if (!data.closed && !isSerialOpen) {
setSerialOpen(true);
setConnectionStatus('START-CONNECTING');
}
if (data.log) {
const log = data.log as string;
if (connectionStatus === 'START-CONNECTING' && state.wifi) {
setConnectionStatus('CONNECTING');
if (!state.wifi) return;
const wifi = new SetWifiRequestT();
wifi.ssid = state.wifi.ssid;
wifi.password = state.wifi.password;
sendRPCPacket(RpcMessage.SetWifiRequest, wifi);
}
if (log.includes('Connected successfully to SSID')) {
setConnectionStatus('CONNECTED');
}
if (log.includes('Handshake successful')) {
setConnectionStatus('HANDSHAKE');
setTimeout(() => {
setConnectionStatus('START-CONNECTING');
}, 3000);
}
if (
// eslint-disable-next-line quotes
log.includes("Can't connect from any credentials")
) {
setConnectionStatus('ERROR');
}
}
RpcMessage.WifiProvisioningStatusResponse,
({ status }: WifiProvisioningStatusResponseT) => {
setProvisioningStatus(status);
}
);
useEffect(() => {
const id = setInterval(() => {
if (!isSerialOpen) openSerial();
else clearInterval(id);
}, 1000);
return () => {
clearInterval(id);
};
}, [isSerialOpen, sendRPCPacket]);
const isError =
provisioningStatus === WifiProvisioningStatus.CONNECTION_ERROR ||
provisioningStatus === WifiProvisioningStatus.COULD_NOT_FIND_SERVER;
return (
<div className="flex flex-col items-center">
@@ -170,12 +124,12 @@ export function ConnectTrackersPage() {
className={classNames(
'rounded-xl h-16 flex gap-2 p-3 lg:w-full mt-4',
state.alonePage ? 'bg-background-60' : 'bg-background-70',
connectionStatus === 'ERROR' && 'border-2 border-status-critical'
isError && 'border-2 border-status-critical'
)}
>
<div className="flex flex-col justify-center fill-background-10">
<LoaderIcon
youSpinMeRightRoundBabyRightRound={connectionStatus !== 'ERROR'}
youSpinMeRightRoundBabyRightRound={!isError}
></LoaderIcon>
</div>
<div className="flex flex-col">
@@ -183,7 +137,7 @@ export function ConnectTrackersPage() {
{l10n.getString('onboarding-connect_tracker-usb')}
</Typography>
<Typography color="secondary">
{l10n.getString(statusLabelMap[connectionStatus])}
{l10n.getString(statusLabelMap[provisioningStatus])}
</Typography>
</div>
</div>

View File

@@ -17,7 +17,7 @@ export function AutomaticMountingPage() {
return (
<>
<div className="flex flex-col gap-5 h-full items-center w-full justify-center">
<div className="flex flex-col gap-2 h-full items-center w-full justify-center">
<div className="flex flex-col w-full h-full justify-center max-w-3xl gap-5">
<div className="flex flex-col max-w-lg gap-3">
{!state.alonePage && (

View File

@@ -62,6 +62,8 @@ dependencies {
implementation("com.github.jonpeterson:jackson-module-model-versioning:1.2.2")
implementation("org.apache.commons:commons-math3:3.6.1")
implementation("org.apache.commons:commons-lang3:3.12.0")
implementation("org.apache.commons:commons-collections4:4.1")
implementation("net.java.dev.jna:jna:5.+")
implementation("net.java.dev.jna:jna-platform:5.+")
implementation("com.illposed.osc:javaosc-core:0.8")

View File

@@ -13,6 +13,7 @@ import dev.slimevr.platform.linux.UnixSocketBridge;
import dev.slimevr.platform.windows.WindowsNamedPipeBridge;
import dev.slimevr.poserecorder.BVHRecorder;
import dev.slimevr.protocol.ProtocolAPI;
import dev.slimevr.serial.ProvisioningHandler;
import dev.slimevr.serial.SerialHandler;
import dev.slimevr.tracking.DeviceManager;
import dev.slimevr.tracking.processor.HumanPoseManager;
@@ -63,6 +64,7 @@ public class VRServer extends Thread {
private final ConfigManager configManager;
private final Timer timer = new Timer();
private final NanoTimer fpsTimer = new NanoTimer();
private final ProvisioningHandler provisioningHandler;
/**
* This function is used by VRWorkout, do not remove!
@@ -80,6 +82,9 @@ public class VRServer extends Thread {
deviceManager = new DeviceManager(this);
serialHandler = new SerialHandler();
provisioningHandler = new ProvisioningHandler(this);
autoBoneHandler = new AutoBoneHandler(this);
protocolAPI = new ProtocolAPI(this);
@@ -432,6 +437,10 @@ public class VRServer extends Thread {
return fpsTimer;
}
public ProvisioningHandler getProvisioningHandler() {
return provisioningHandler;
}
public void clearTrackersDriftCompensation() {
for (Tracker t : getAllTrackers()) {
Tracker tracker = t.get();

View File

@@ -19,6 +19,8 @@ public class ConnectionContext {
private final List<Integer> subscribedTopics = new ArrayList<>();
private boolean useSerial = false;
private boolean useProvisioning = false;
private boolean useAutoBone = false;
public List<DataFeedConfigT> getDataFeedConfigList() {
@@ -54,4 +56,11 @@ public class ConnectionContext {
this.useAutoBone = useAutoBone;
}
public boolean useProvisioning() {
return useProvisioning;
}
public void setUseProvisioning(boolean useProvisioning) {
this.useProvisioning = useProvisioning;
}
}

View File

@@ -10,6 +10,7 @@ import dev.slimevr.poserecorder.PoseFrames;
import dev.slimevr.protocol.GenericConnection;
import dev.slimevr.protocol.ProtocolAPI;
import dev.slimevr.protocol.ProtocolHandler;
import dev.slimevr.protocol.rpc.serial.RPCProvisioningHandler;
import dev.slimevr.protocol.rpc.serial.RPCSerialHandler;
import dev.slimevr.protocol.rpc.settings.RPCSettingsHandler;
import dev.slimevr.tracking.processor.config.SkeletonConfigOffsets;
@@ -40,6 +41,7 @@ public class RPCHandler extends ProtocolHandler<RpcMessageHeader>
this.api = api;
new RPCSerialHandler(this, api);
new RPCProvisioningHandler(this, api);
new RPCSettingsHandler(this, api);
registerPacketListener(RpcMessage.ResetRequest, this::onResetRequest);

View File

@@ -0,0 +1,84 @@
package dev.slimevr.protocol.rpc.serial;
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.ProvisioningListener;
import dev.slimevr.serial.ProvisioningStatus;
import solarxr_protocol.rpc.*;
import java.util.function.Consumer;
public record RPCProvisioningHandler(RPCHandler rpcHandler, ProtocolAPI api)
implements ProvisioningListener {
public RPCProvisioningHandler(RPCHandler rpcHandler, ProtocolAPI api) {
this.rpcHandler = rpcHandler;
this.api = api;
rpcHandler
.registerPacketListener(
RpcMessage.StartWifiProvisioningRequest,
this::onStartWifiProvisioningRequest
);
rpcHandler
.registerPacketListener(
RpcMessage.StopWifiProvisioningRequest,
this::onStopWifiProvisioningRequest
);
this.api.server.getProvisioningHandler().addListener(this);
}
public void onStartWifiProvisioningRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
StartWifiProvisioningRequest req = (StartWifiProvisioningRequest) messageHeader
.message(new StartWifiProvisioningRequest());
if (req == null)
return;
this.api.server.getProvisioningHandler().start(req.ssid(), req.password(), req.port());
conn.getContext().setUseProvisioning(true);
}
public void onStopWifiProvisioningRequest(
GenericConnection conn,
RpcMessageHeader messageHeader
) {
StopWifiProvisioningRequest req = (StopWifiProvisioningRequest) messageHeader
.message(new StopWifiProvisioningRequest());
if (req == null)
return;
conn.getContext().setUseProvisioning(false);
this.api.server.getProvisioningHandler().stop();
}
@Override
public void onProvisioningStatusChange(ProvisioningStatus status) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
WifiProvisioningStatusResponse.startWifiProvisioningStatusResponse(fbb);
WifiProvisioningStatusResponse.addStatus(fbb, status.getId());
int update = WifiProvisioningStatusResponse.endWifiProvisioningStatusResponse(fbb);
int outbound = rpcHandler
.createRPCMessage(fbb, RpcMessage.WifiProvisioningStatusResponse, update);
fbb.finish(outbound);
this.forAllListeners((conn) -> conn.send(fbb.dataBuffer()));
}
private void forAllListeners(Consumer<? super GenericConnection> action) {
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.filter(conn -> conn.getContext().useProvisioning())
.forEach(action)
);
}
}

View File

@@ -78,6 +78,31 @@ public record RPCSerialHandler(RPCHandler rpcHandler, ProtocolAPI api) implement
});
}
@Override
public void onNewSerialDevice(SerialPort port) {
FlatBufferBuilder fbb = new FlatBufferBuilder(32);
int portOffset = fbb.createString(port.getPortLocation());
int nameOffset = fbb.createString(port.getDescriptivePortName());
int deviceOffset = SerialDevice.createSerialDevice(fbb, portOffset, nameOffset);
int newSerialOffset = NewSerialDeviceResponse
.createNewSerialDeviceResponse(fbb, deviceOffset);
int outbound = rpcHandler
.createRPCMessage(fbb, RpcMessage.NewSerialDeviceResponse, newSerialOffset);
fbb.finish(outbound);
this.api
.getAPIServers()
.forEach(
(server) -> server
.getAPIConnections()
.forEach((conn) -> {
conn.send(fbb.dataBuffer());
})
);
}
@Override
public void onSerialConnected(SerialPort port) {

View File

@@ -114,7 +114,8 @@ public class RPCSettingsBuilder {
tapDetectionConfig.getQuickResetTaps(),
tapDetectionConfig.getMountingResetDelay(),
tapDetectionConfig.getMountingResetEnabled(),
tapDetectionConfig.getMountingResetTaps()
tapDetectionConfig.getMountingResetTaps(),
false
);
}

View File

@@ -0,0 +1,172 @@
package dev.slimevr.serial;
import com.fazecast.jSerialComm.SerialPort;
import dev.slimevr.VRServer;
import io.eiren.util.logging.LogManager;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArrayList;
public class ProvisioningHandler implements SerialListener {
private ProvisioningStatus provisioningStatus = ProvisioningStatus.NONE;
private boolean isRunning = false;
private final List<ProvisioningListener> listeners = new CopyOnWriteArrayList<>();
private String ssid;
private String password;
private String preferredPort;
private final Timer provisioningTickTimer = new Timer("ProvisioningTickTimer");
private long lastStatusChange = -1;
private final VRServer vrServer;
public ProvisioningHandler(VRServer vrServer) {
this.vrServer = vrServer;
vrServer.getSerialHandler().addListener(this);
this.provisioningTickTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
if (!isRunning)
return;
provisioningTick();
}
}, 0, 1000);
}
public void start(String ssid, String password, String port) {
this.isRunning = true;
this.ssid = ssid;
this.password = password;
this.preferredPort = port;
this.provisioningStatus = ProvisioningStatus.NONE;
}
public void stop() {
this.isRunning = false;
this.ssid = null;
this.password = null;
this.changeStatus(ProvisioningStatus.NONE);
this.vrServer.getSerialHandler().closeSerial();
}
public void initSerial(String port) {
this.provisioningStatus = ProvisioningStatus.SERIAL_INIT;
try {
if (port != null)
vrServer.getSerialHandler().openSerial(port, false);
else
vrServer.getSerialHandler().openSerial(null, true);
} catch (Exception e) {
LogManager.severe("Unable to open serial port", e);
} catch (Throwable e) {
LogManager.severe("Using serial ports is not supported on this platform", e);
}
}
public void tryProvisioning() {
this.changeStatus(ProvisioningStatus.PROVISIONING);
vrServer.getSerialHandler().setWifi(this.ssid, this.password);
}
public void provisioningTick() {
if (
this.provisioningStatus == ProvisioningStatus.CONNECTION_ERROR
|| this.provisioningStatus == ProvisioningStatus.DONE
)
return;
if (System.currentTimeMillis() - this.lastStatusChange > 10000) {
if (this.provisioningStatus == ProvisioningStatus.NONE)
this.initSerial(this.preferredPort);
else if (this.provisioningStatus == ProvisioningStatus.PROVISIONING)
this.tryProvisioning();
else if (this.provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER)
this.changeStatus(ProvisioningStatus.COULD_NOT_FIND_SERVER);
}
}
@Override
public void onSerialConnected(SerialPort port) {
if (!isRunning)
return;
this.tryProvisioning();
}
@Override
public void onSerialDisconnected() {
if (!isRunning)
return;
this.changeStatus(ProvisioningStatus.NONE);
}
@Override
public void onSerialLog(String str) {
if (!isRunning)
return;
if (
provisioningStatus == ProvisioningStatus.PROVISIONING
&& str.contains("New wifi credentials set")
) {
this.changeStatus(ProvisioningStatus.CONNECTING);
}
if (
provisioningStatus == ProvisioningStatus.CONNECTING
&& str.contains("Looking for the server")
) {
this.changeStatus(ProvisioningStatus.LOOKING_FOR_SERVER);
}
if (
provisioningStatus == ProvisioningStatus.LOOKING_FOR_SERVER
&& str.contains("Handshake successful")
) {
this.changeStatus(ProvisioningStatus.DONE);
}
if (
provisioningStatus == ProvisioningStatus.CONNECTING
&& str.contains("Can't connect from any credentials")
) {
this.changeStatus(ProvisioningStatus.CONNECTION_ERROR);
}
}
public void changeStatus(ProvisioningStatus status) {
this.lastStatusChange = System.currentTimeMillis();
if (this.provisioningStatus != status) {
this.listeners.forEach((l) -> l.onProvisioningStatusChange(status));
this.provisioningStatus = status;
}
}
@Override
public void onNewSerialDevice(SerialPort port) {
this.initSerial(this.preferredPort);
}
public void addListener(ProvisioningListener channel) {
this.listeners.add(channel);
}
public void removeListener(ProvisioningListener l) {
listeners.removeIf(listener -> l == listener);
}
}

View File

@@ -0,0 +1,6 @@
package dev.slimevr.serial;
public interface ProvisioningListener {
void onProvisioningStatusChange(ProvisioningStatus status);
}

View File

@@ -0,0 +1,23 @@
package dev.slimevr.serial;
public enum ProvisioningStatus {
NONE(0),
SERIAL_INIT(1),
PROVISIONING(2),
CONNECTING(3),
CONNECTION_ERROR(4),
LOOKING_FOR_SERVER(5),
COULD_NOT_FIND_SERVER(6),
DONE(7);
public final int id;
ProvisioningStatus(int id) {
this.id = id;
}
public int getId() {
return id;
}
}

View File

@@ -3,22 +3,57 @@ 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.Arrays;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.List;
import java.util.stream.Stream;
public class SerialHandler implements SerialPortMessageListener {
private final List<SerialListener> listeners = new CopyOnWriteArrayList<>();
private SerialPort trackerPort = null;
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() {
detectNewPorts();
}
}, 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);
@@ -30,37 +65,38 @@ public class SerialHandler implements SerialPortMessageListener {
public boolean openSerial(String portLocation, boolean auto) {
if (this.isConnected()) {
if (trackerPort != null)
trackerPort.closePort();
if (currentPort != null)
currentPort.closePort();
}
System.out.println("Trying to open:" + portLocation + " auto: " + auto);
SerialPort[] ports = SerialPort.getCommPorts();
lastKnownPorts = ports;
for (SerialPort port : ports) {
if (!auto && port.getPortLocation().equals(portLocation)) {
trackerPort = port;
currentPort = port;
break;
}
if (auto && isKnownBoard(port.getDescriptivePortName())) {
trackerPort = port;
currentPort = port;
break;
}
}
if (trackerPort == null) {
if (currentPort == null) {
return false;
}
trackerPort.setBaudRate(115200);
trackerPort.clearRTS();
trackerPort.clearDTR();
if (!trackerPort.openPort()) {
currentPort.setBaudRate(115200);
currentPort.clearRTS();
currentPort.clearDTR();
if (!currentPort.openPort()) {
return false;
}
trackerPort.addDataListener(this);
this.listeners.forEach((listener) -> listener.onSerialConnected(trackerPort));
currentPort.addDataListener(this);
this.listeners.forEach((listener) -> listener.onSerialConnected(currentPort));
return true;
}
@@ -78,20 +114,20 @@ public class SerialHandler implements SerialPortMessageListener {
public void closeSerial() {
try {
if (trackerPort != null)
trackerPort.closePort();
if (currentPort != null)
currentPort.closePort();
this.listeners.forEach(SerialListener::onSerialDisconnected);
System.out.println("Port closed okay");
trackerPort = null;
currentPort = null;
} catch (Exception e) {
System.out.println("Error closing port: " + e.getMessage());
}
}
private void writeSerial(String serialText) {
if (trackerPort == null)
if (currentPort == null)
return;
OutputStream os = trackerPort.getOutputStream();
OutputStream os = currentPort.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(os);
try {
writer.append(serialText).append("\n");
@@ -104,9 +140,9 @@ public class SerialHandler implements SerialPortMessageListener {
}
public void setWifi(String ssid, String passwd) {
if (trackerPort == null)
if (currentPort == null)
return;
OutputStream os = trackerPort.getOutputStream();
OutputStream os = currentPort.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(os);
try {
writer.append("SET WIFI \"").append(ssid).append("\" \"").append(passwd).append("\"\n");
@@ -140,7 +176,7 @@ public class SerialHandler implements SerialPortMessageListener {
}
public boolean isConnected() {
return this.trackerPort != null && this.trackerPort.isOpen();
return this.currentPort != null && this.currentPort.isOpen();
}
@Override
@@ -169,4 +205,34 @@ public class SerialHandler implements SerialPortMessageListener {
|| (lowerCom.contains("usb")
&& lowerCom.contains("seri"));
}
private void detectNewPorts() {
try {
List<SerialPort> differences = new ArrayList<>(
CollectionUtils
.removeAll(
this.getKnownPorts().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("Using serial ports is not supported on this platform", e);
}
}
}

View File

@@ -10,4 +10,6 @@ public interface SerialListener {
void onSerialDisconnected();
void onSerialLog(String str);
void onNewSerialDevice(SerialPort port);
}