Skip to content

Commit

Permalink
Merge pull request #30 from SlimeVR/feat/tracker-emulation/server-tim…
Browse files Browse the repository at this point in the history
…eouts

feat(tracker-emulation)!: server timeouts
  • Loading branch information
TheDevMinerTV authored Jun 6, 2024
2 parents b403e83 + bd5f47f commit eabb26f
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/fresh-crabs-pretend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@slimevr/tracker-emulation': minor
---

implemented server timeouts !BREAKING!
1 change: 1 addition & 0 deletions apps/emulated-tracker-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"main": "dist/index.js",
"license": "(MIT OR Apache-2.0)",
"private": true,
"type": "module",
"author": {
"name": "DevMiner",
"email": "devminer@devminer.xyz"
Expand Down
22 changes: 12 additions & 10 deletions apps/emulated-tracker-demo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@ const tracker = new EmulatedTracker(
const sensors: EmulatedSensor[] = [];

const main = async () => {
tracker.on('ready', () => {
console.log('searching for server...');
});
tracker.on('ready', (ip, port) => console.log(`ready and running on ${ip}:${port}`));
tracker.on('error', (err) => console.error(err));

tracker.on('connected-to-server', async (ip, port) => {
console.log('connected to server', ip, port);
tracker.on('searching-for-server', () => console.log('searching for server...'));
tracker.on('connected-to-server', (ip, port) => console.log('connected to server', ip, port));
tracker.on('disconnected-from-server', (reason) => {
console.log('disconnected from server', reason);
tracker.searchForServer();
});

tracker.on('unknown-incoming-packet', (packet) => {
console.log('unknown packet type', packet.type);
});
tracker.on('server-feature-flags', (flags) => console.log('server feature flags', flags.getAllEnabled()));

tracker.on('error', (err) => console.error(err));
tracker.on('incoming-packet', (packet) => console.log('unknown packet type', packet.type));
tracker.on('unknown-incoming-packet', (buf) => console.log('unknown packet', buf));
tracker.on('outgoing-packet', (packet) => console.log('outgoing packet', packet.type));

await tracker.init();

Expand All @@ -44,7 +46,7 @@ const main = async () => {
{
let i = 0;
setInterval(() => {
tracker.changeBatteryLevel(Math.sin(i) * 0.5 + 3.7, Math.sin(i) * 100);
tracker.changeBatteryLevel(Math.sin(i) * 0.5 + 3.7, Math.sin(i) * 50 + 50);
i += 0.1;
}, 1000).unref();
}
Expand Down
4 changes: 3 additions & 1 deletion apps/emulated-tracker-demo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"],
"compilerOptions": {
"target": "ES2022",
"target": "ESNext",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist"
}
}
109 changes: 78 additions & 31 deletions packages/tracker-emulation/src/EmulatedTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
DeviceBoundHandshakePacket,
DeviceBoundHeartbeatPacket,
DeviceBoundPingPacket,
DeviceBoundSensorInfoPacket,
FirmwareFeatureFlags,
MCUType,
Packet,
Expand All @@ -28,21 +29,42 @@ import {
} from '@slimevr/firmware-protocol';
import { createSocket, RemoteInfo, Socket } from 'dgram';
import EventEmitter from 'events';
import { AddressInfo } from 'net';
import { type StrictEventEmitter } from 'strict-event-emitter-types';
import { EmulatedSensor } from './EmulatedSensor.js';

type State =
| { status: 'initializing' }
| { status: 'disconnected' }
| { status: 'searching-for-server'; discoveryInterval: NodeJS.Timeout }
| { status: 'connected-to-server'; packetNumber: bigint; serverIP: string; serverPort: number };
| {
status: 'connected-to-server';
packetNumber: bigint;
serverIP: string;
serverPort: number;
lastReceivedPacketTimestamp: number;
timeoutCheckInterval: NodeJS.Timeout;
};

export class TimeoutError extends Error {
constructor(desired: string, timeout: number) {
super(`Timed out waiting for ${desired}, waited ${timeout}ms`);
}
}

type DisconnectReason = TimeoutError | Error;

interface EmulatedTrackerEvents {
error: (error: Error) => void;
ready: (address: AddressInfo) => void;
ready: (ip: string, port: number) => void;

'searching-for-server': () => void;
'connected-to-server': (serverIP: string, serverPort: number) => void;
'disconnected-from-server': (reason: DisconnectReason) => void;

'server-feature-flags': (flags: ServerFeatureFlags) => void;
'unknown-incoming-packet': (packet: Packet) => void;

'incoming-packet': (packet: Packet) => void;
'unknown-incoming-packet': (buf: Buffer) => void;
'outgoing-packet': (packet: Packet) => void;
}

Expand All @@ -51,19 +73,11 @@ const SUPPORTED_FIRMWARE_PROTOCOL_VERSION = 13;
export class EmulatedTracker extends (EventEmitter as {
new (): StrictEventEmitter<EventEmitter, EmulatedTrackerEvents>;
}) {
// TODO: Implement timing out the server connection if no packets are received for a while
private lastPacket = Date.now();
private lastPing = {
id: 0,
startTimestamp: 0,
duration: 0
};

private batteryVoltage = 0;
private batteryPercentage = 0;

private readonly socket: Socket;
private state: State;
private state: State = { status: 'initializing' };

private sensors: EmulatedSensor[] = [];

Expand All @@ -74,21 +88,31 @@ export class EmulatedTracker extends (EventEmitter as {
private readonly boardType: BoardType = BoardType.UNKNOWN,
private readonly mcuType: MCUType = MCUType.UNKNOWN,
private readonly serverDiscoveryIP = '255.255.255.255',
private readonly serverDiscoveryPort = 6969
private readonly serverDiscoveryPort = 6969,
private readonly serverTimeout = 5000,
private readonly autoReconnect = true
) {
super();

this.socket = createSocket('udp4');
this.socket.on('message', (msg, addr) => this.handle(msg, addr));
this.socket.on('error', (err) => this.emit('error', err));
this.socket.on('error', (err) => {
if (this.state.status === 'connected-to-server') this.emit('disconnected-from-server', err);
});
}

this.state = { status: 'initializing' };
private disconnectFromServer(reason: DisconnectReason) {
if (this.state.status === 'searching-for-server') {
clearInterval(this.state.discoveryInterval);
}

this.on('connected-to-server', async () => {
await this.sendPacketToServer(new ServerBoundFeatureFlagsPacket(this.featureFlags));
await this.sendBatteryLevel();
await Promise.all(this.sensors.map((sensor) => sensor.sendSensorInfo()));
});
this.state = { status: 'disconnected' };
this.emit('disconnected-from-server', reason);

if (this.autoReconnect) {
this.searchForServer();
}
}

unref() {
Expand Down Expand Up @@ -125,12 +149,16 @@ export class EmulatedTracker extends (EventEmitter as {
await new Promise<void>((resolve) => this.socket.bind(0, () => resolve()));
this.socket.setBroadcast(true);

this.emit('ready', this.socket.address());
const addr = this.socket.address();
this.emit('ready', addr.address, addr.port);
}

searchForServer() {
this.state = {
status: 'searching-for-server',
discoveryInterval: setInterval(() => this.sendDiscovery(), 1000)
};
this.emit('searching-for-server');
}

private log(msg: string) {
Expand Down Expand Up @@ -182,18 +210,18 @@ export class EmulatedTracker extends (EventEmitter as {
}

private async sendPacket(packet: Packet, port: number, ip: string) {
this.emit('outgoing-packet', packet);

const encoded = packet.encode(this.state.status === 'connected-to-server' ? this.state.packetNumber++ : 0n);

await new Promise<void>((res, rej) =>
this.socket.send(encoded, 0, encoded.length, port, ip, (err) => (err ? rej(err) : res()))
);

this.log(`Sent packet to ${ip}:${port} (${encoded.length} bytes): ${encoded.toString('hex')}`);
this.emit('outgoing-packet', packet);
}

private handle(msg: Buffer, addr: RemoteInfo) {
private async handle(msg: Buffer, addr: RemoteInfo) {
if (this.state.status === 'initializing' || this.state.status === 'disconnected') return;

if (this.state.status === 'searching-for-server') {
if (msg.readUint8(0) !== DeviceBoundHandshakePacket.type) return;

Expand All @@ -203,22 +231,37 @@ export class EmulatedTracker extends (EventEmitter as {
status: 'connected-to-server',
packetNumber: 0n,
serverIP: addr.address,
serverPort: addr.port
serverPort: addr.port,
lastReceivedPacketTimestamp: Date.now(),
timeoutCheckInterval: setInterval(() => {
if (this.state.status !== 'connected-to-server') return;
if (Date.now() - this.state.lastReceivedPacketTimestamp < this.serverTimeout) return;

this.disconnectFromServer(new TimeoutError('heartbeat', this.serverTimeout));
}, 1000).unref()
};

await this.sendPacketToServer(new ServerBoundFeatureFlagsPacket(this.featureFlags));
await this.sendBatteryLevel();
await Promise.all(this.sensors.map((sensor) => sensor.sendSensorInfo()));

this.emit('connected-to-server', addr.address, addr.port);

return;
}

if (addr.address !== this.state.serverIP || addr.port !== this.state.serverPort) {
this.emit('error', new Error(`Received packet from unknown server ${addr.address}:${addr.port}`));
return;
}

const [_num, packet] = parse(msg, true);
if (packet === null) {
this.log(`Received unknown packet (${msg.length} bytes): ${msg.toString('hex')}`);

this.emit('unknown-incoming-packet', msg);
return;
}

this.lastPacket = Date.now();
this.state.lastReceivedPacketTimestamp = Date.now();

switch (packet.type) {
case DeviceBoundPingPacket.type: {
Expand All @@ -238,8 +281,12 @@ export class EmulatedTracker extends (EventEmitter as {
break;
}

default:
this.emit('unknown-incoming-packet', packet);
case DeviceBoundSensorInfoPacket.type: {
// Just ignore these packets, they only acknowledge the sensor info we sent
break;
}
}

this.emit('incoming-packet', packet);
}
}

0 comments on commit eabb26f

Please sign in to comment.