-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathSpaceMouse.ts
191 lines (169 loc) · 5.81 KB
/
SpaceMouse.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import { EventEmitter } from 'events'
import { SpaceMouseEvents, Translation, Rotation, SpaceMouseInfo, ButtonStates } from './api'
import { Product, PRODUCTS, VENDOR_IDS } from './products'
import { getBit, literal } from './lib'
import { HIDDevice } from './genericHIDDevice'
export declare interface SpaceMouse {
on<U extends keyof SpaceMouseEvents>(event: U, listener: SpaceMouseEvents[U]): this
emit<U extends keyof SpaceMouseEvents>(event: U, ...args: Parameters<SpaceMouseEvents[U]>): boolean
}
export class SpaceMouse extends EventEmitter {
private product: Product & { productId: number; interface: number }
/** All button states */
private _buttonStates: ButtonStates = new Map()
private _rotateState: Rotation = {
pitch: 0,
roll: 0,
yaw: 0,
}
private _translateState: Translation = {
x: 0,
y: 0,
z: 0,
}
private _initialized = false
private _disconnected = false
/** Vendor ids for the SpaceMouse devices */
static get vendorIds(): number[] {
return VENDOR_IDS
}
constructor(private _device: HIDDevice, private _deviceInfo: DeviceInfo, private _devicePath: string | undefined) {
super()
this.product = this._setupDevice(_deviceInfo)
}
private _setupDevice(deviceInfo: DeviceInfo) {
const findProduct = (): { product: Product; productId: number; interface: number } => {
for (const product of Object.values<Product>(PRODUCTS)) {
if (product.vendorId === deviceInfo.vendorId && product.productId === deviceInfo.productId) {
return {
product,
productId: product.vendorId,
interface: product.productId,
}
}
}
// else:
throw new Error(
`Unknown/Unsupported SpaceMouse: "${deviceInfo.product}" (vendorId: "${deviceInfo.vendorId}", productId: "${deviceInfo.productId}", interface: "${deviceInfo.interface}").\nPlease report this as an issue on our github page!`
)
}
const found = findProduct()
this._device.on('data', (data: Buffer) => {
const messageType = data.readUInt8(0)
if (messageType === 1) {
const x = data.readInt16LE(1) // positive = right
const y = data.readInt16LE(3) // positive = "backwards"
const z = data.readInt16LE(5) // positive = down
if (x !== this._translateState.x || y !== this._translateState.y || z !== this._translateState.z) {
this._translateState = { x, y, z }
this.emit('translate', { x, y, z })
}
} else if (messageType === 2) {
const pitch = data.readInt16LE(1) // positive = right
const roll = data.readInt16LE(3) // positive = "backwards"
const yaw = data.readInt16LE(5) // positive = down
if (
pitch !== this._rotateState.pitch ||
roll !== this._rotateState.roll ||
yaw !== this._rotateState.yaw
) {
this._rotateState = { pitch, roll, yaw }
this.emit('rotate', { pitch, roll, yaw })
}
} else if (messageType === 3) {
// Note: Assuming that each bit represents a pressed key (I don't know if this is true for all devices)
for (let byteIndex = 1; byteIndex < data.length; byteIndex++) {
for (let bitIndex = 0; bitIndex < data.length; bitIndex++) {
const buttonIndex = (byteIndex - 1) * 8 + bitIndex
const isDown = Boolean(getBit(data.readUInt8(byteIndex), bitIndex))
if (isDown) {
if (!this._buttonStates.get(buttonIndex)) {
this._buttonStates.set(buttonIndex, true)
this.emit('down', buttonIndex)
}
} else {
if (this._buttonStates.get(buttonIndex)) {
this._buttonStates.set(buttonIndex, false)
this.emit('up', buttonIndex)
}
}
}
}
} else {
// Unknown message
}
})
this._device.on('error', (err) => {
if ((err + '').match(/could not read from/)) {
// The device has been disconnected
this._triggerHandleDeviceDisconnected()
} else if ((err + '').match(/WebHID disconnected/)) {
this._triggerHandleDeviceDisconnected()
} else {
this.emit('error', err)
}
})
return {
...found.product,
productId: found.productId,
interface: found.interface,
}
}
/** Initialize the device. This ensures that the essential information from the device about its state has been received. */
public async init(): Promise<void> {
// Nothing to do here, but keeping this as a placeholder for future use.
this._initialized = true
}
/** Closes the device. Subsequent commands will raise errors. */
public async close(): Promise<void> {
await this._handleDeviceDisconnected()
}
/** Various information about the device and its capabilities */
public get info(): SpaceMouseInfo {
this.ensureInitialized()
return literal<SpaceMouseInfo>({
name: this.product.name,
vendorId: this.product.vendorId,
productId: this.product.productId,
interface: this.product.interface,
})
}
/**
* Returns an object with current Button states
*/
public getButtons(): ButtonStates {
return new Map(this._buttonStates) // Make a copy
}
private _triggerHandleDeviceDisconnected(): void {
this._handleDeviceDisconnected().catch((error) => {
this.emit('error', error)
})
}
/** (Internal function) Called when there has been detected that the device has been disconnected */
public async _handleDeviceDisconnected(): Promise<void> {
if (!this._disconnected) {
this._disconnected = true
await this._device.close()
this.emit('disconnected')
}
}
public get hidDevice(): HIDDevice {
return this._device
}
public get deviceInfo(): DeviceInfo {
return this._deviceInfo
}
public get devicePath(): string | undefined {
return this._devicePath
}
/** Check that the .init() function has run, throw otherwise */
private ensureInitialized() {
if (!this._initialized) throw new Error('SpaceMouse.init() must be run first!')
}
}
export interface DeviceInfo {
product: string | undefined
vendorId: number
productId: number
interface: number | null // null means "anything goes", used when interface isn't available
}