macromate-hid 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * MacroMate HID Constants
3
+ * Protocol constants for MacroMate device
4
+ */
5
+
6
+ // Device config
7
+ const VID = 0xCAFE;
8
+ const PID = 0x4010;
9
+ const VENDOR_USAGE_PAGE = 0xFF00;
10
+ const REPORT_ID_VENDOR = 3;
11
+ const DEFAULT_TIMEOUT = 2000; // ms
12
+ const TYPE_STRING_TIMEOUT = 10000; // ms
13
+
14
+ // Commands (from protocol.h)
15
+ const CMD = {
16
+ PING: 0x00,
17
+ GET_VERSION: 0x01,
18
+ BOOTLOADER: 0x02,
19
+ KEY_PRESS: 0x10,
20
+ KEY_RELEASE: 0x11,
21
+ KEY_TAP: 0x12,
22
+ KEY_MODIFIER: 0x13,
23
+ KEY_RELEASE_ALL: 0x14,
24
+ KEY_TYPE_STRING: 0x15,
25
+ MOUSE_MOVE: 0x30,
26
+ MOUSE_CLICK: 0x32,
27
+ MOUSE_PRESS: 0x33,
28
+ MOUSE_RELEASE: 0x34,
29
+ MOUSE_SCROLL: 0x35,
30
+ DELAY: 0x50,
31
+ };
32
+
33
+ // Mouse buttons
34
+ const MOUSE_BTN = {
35
+ LEFT: 0x01,
36
+ RIGHT: 0x02,
37
+ MIDDLE: 0x04
38
+ };
39
+
40
+ // Keyboard modifiers
41
+ const MOD = {
42
+ CTRL: 0x01,
43
+ SHIFT: 0x02,
44
+ ALT: 0x04,
45
+ WIN: 0x08
46
+ };
47
+
48
+ // HID Keycodes
49
+ const KEY = {
50
+ // Letters
51
+ A: 0x04, B: 0x05, C: 0x06, D: 0x07, E: 0x08, F: 0x09, G: 0x0A, H: 0x0B,
52
+ I: 0x0C, J: 0x0D, K: 0x0E, L: 0x0F, M: 0x10, N: 0x11, O: 0x12, P: 0x13,
53
+ Q: 0x14, R: 0x15, S: 0x16, T: 0x17, U: 0x18, V: 0x19, W: 0x1A, X: 0x1B,
54
+ Y: 0x1C, Z: 0x1D,
55
+
56
+ // Numbers
57
+ N1: 0x1E, N2: 0x1F, N3: 0x20, N4: 0x21, N5: 0x22,
58
+ N6: 0x23, N7: 0x24, N8: 0x25, N9: 0x26, N0: 0x27,
59
+
60
+ // Special keys
61
+ ENTER: 0x28, ESC: 0x29, BACKSPACE: 0x2A, TAB: 0x2B, SPACE: 0x2C,
62
+ MINUS: 0x2D, EQUAL: 0x2E, BRACKET_LEFT: 0x2F, BRACKET_RIGHT: 0x30,
63
+ BACKSLASH: 0x31, SEMICOLON: 0x33, QUOTE: 0x34, GRAVE: 0x35,
64
+ COMMA: 0x36, PERIOD: 0x37, SLASH: 0x38, CAPS_LOCK: 0x39,
65
+
66
+ // Function keys
67
+ F1: 0x3A, F2: 0x3B, F3: 0x3C, F4: 0x3D, F5: 0x3E, F6: 0x3F,
68
+ F7: 0x40, F8: 0x41, F9: 0x42, F10: 0x43, F11: 0x44, F12: 0x45,
69
+
70
+ // Navigation
71
+ PRINT_SCREEN: 0x46, SCROLL_LOCK: 0x47, PAUSE: 0x48,
72
+ INSERT: 0x49, HOME: 0x4A, PAGE_UP: 0x4B,
73
+ DELETE: 0x4C, END: 0x4D, PAGE_DOWN: 0x4E,
74
+ RIGHT: 0x4F, LEFT: 0x50, DOWN: 0x51, UP: 0x52,
75
+
76
+ // Numpad
77
+ NUM_LOCK: 0x53, NUMPAD_SLASH: 0x54, NUMPAD_ASTERISK: 0x55,
78
+ NUMPAD_MINUS: 0x56, NUMPAD_PLUS: 0x57, NUMPAD_ENTER: 0x58,
79
+ NUMPAD_1: 0x59, NUMPAD_2: 0x5A, NUMPAD_3: 0x5B,
80
+ NUMPAD_4: 0x5C, NUMPAD_5: 0x5D, NUMPAD_6: 0x5E,
81
+ NUMPAD_7: 0x5F, NUMPAD_8: 0x60, NUMPAD_9: 0x61,
82
+ NUMPAD_0: 0x62, NUMPAD_PERIOD: 0x63,
83
+ };
84
+
85
+ // Status codes
86
+ const STATUS = {
87
+ OK: 0x00,
88
+ ERROR_UNKNOWN: 0x01,
89
+ ERROR_INVALID: 0x02,
90
+ ERROR_OVERFLOW: 0x03,
91
+ };
92
+
93
+ module.exports = {
94
+ VID,
95
+ PID,
96
+ VENDOR_USAGE_PAGE,
97
+ REPORT_ID_VENDOR,
98
+ DEFAULT_TIMEOUT,
99
+ TYPE_STRING_TIMEOUT,
100
+ CMD,
101
+ MOUSE_BTN,
102
+ MOD,
103
+ KEY,
104
+ STATUS,
105
+ };
106
+
@@ -0,0 +1,387 @@
1
+ /**
2
+ * MacroController - Main class for communicating with MacroMate device
3
+ * Promise-based API with async/await support
4
+ */
5
+
6
+ const HID = require('node-hid');
7
+ const {
8
+ VID, PID, VENDOR_USAGE_PAGE, REPORT_ID_VENDOR,
9
+ DEFAULT_TIMEOUT, TYPE_STRING_TIMEOUT,
10
+ CMD, MOUSE_BTN, MOD, KEY, STATUS
11
+ } = require('./constants');
12
+
13
+ class MacroController {
14
+ /**
15
+ * Create a MacroController instance
16
+ * @param {Object} options - Configuration options
17
+ * @param {number} [options.vid=0xCAFE] - Vendor ID
18
+ * @param {number} [options.pid=0x4010] - Product ID
19
+ * @param {number} [options.timeout=2000] - Default command timeout (ms)
20
+ * @param {boolean} [options.debug=false] - Enable debug logging
21
+ */
22
+ constructor(options = {}) {
23
+ this.vid = options.vid || VID;
24
+ this.pid = options.pid || PID;
25
+ this.timeout = options.timeout || DEFAULT_TIMEOUT;
26
+ this.debug = options.debug || false;
27
+ this.device = null;
28
+ this.deviceInfo = null;
29
+ }
30
+
31
+ /**
32
+ * Log debug messages
33
+ * @private
34
+ */
35
+ _log(...args) {
36
+ if (this.debug) {
37
+ console.log('[MacroController]', ...args);
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Find all matching devices
43
+ * @returns {Array} Array of device info objects
44
+ */
45
+ static findDevices(vid = VID, pid = PID) {
46
+ const devices = HID.devices();
47
+ return devices.filter(d =>
48
+ d.vendorId === vid &&
49
+ d.productId === pid &&
50
+ d.usagePage === VENDOR_USAGE_PAGE
51
+ );
52
+ }
53
+
54
+ /**
55
+ * List all HID devices with the specified VID
56
+ * @param {number} [vid=0xCAFE] - Vendor ID to filter by
57
+ * @returns {Array} Array of device info objects
58
+ */
59
+ static listDevices(vid = VID) {
60
+ return HID.devices().filter(d => d.vendorId === vid);
61
+ }
62
+
63
+ /**
64
+ * Check if device is connected
65
+ * @returns {boolean}
66
+ */
67
+ get isConnected() {
68
+ return this.device !== null;
69
+ }
70
+
71
+ /**
72
+ * Connect to the device
73
+ * @returns {boolean} True if connected successfully
74
+ * @throws {Error} If device not found or connection failed
75
+ */
76
+ connect() {
77
+ const devices = HID.devices();
78
+
79
+ this.deviceInfo = devices.find(d =>
80
+ d.vendorId === this.vid &&
81
+ d.productId === this.pid &&
82
+ d.usagePage === VENDOR_USAGE_PAGE
83
+ );
84
+
85
+ if (!this.deviceInfo) {
86
+ throw new Error(`Device not found (VID=0x${this.vid.toString(16)} PID=0x${this.pid.toString(16)})`);
87
+ }
88
+
89
+ this._log('Found device:', this.deviceInfo.product, this.deviceInfo.path);
90
+
91
+ try {
92
+ this.device = new HID.HID(this.deviceInfo.path);
93
+ this._log('Connected successfully');
94
+
95
+ this.device.on('error', (err) => {
96
+ this._log('HID Error:', err.message);
97
+ });
98
+
99
+ return true;
100
+ } catch (e) {
101
+ throw new Error(`Connection failed: ${e.message}`);
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Disconnect from the device
107
+ */
108
+ disconnect() {
109
+ if (this.device) {
110
+ this.device.close();
111
+ this.device = null;
112
+ this.deviceInfo = null;
113
+ this._log('Disconnected');
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Send a raw command to the device
119
+ * @param {number} cmd - Command byte
120
+ * @param {number[]} args - Command arguments
121
+ * @param {Object} options - Options
122
+ * @param {number} [options.timeout] - Command timeout (ms)
123
+ * @returns {Promise<{status: number, data: Buffer}>}
124
+ */
125
+ sendCommand(cmd, args = [], options = {}) {
126
+ const timeout = options.timeout || this.timeout;
127
+
128
+ return new Promise((resolve, reject) => {
129
+ if (!this.device) {
130
+ reject(new Error('Device not connected'));
131
+ return;
132
+ }
133
+
134
+ // Build packet
135
+ const packet = Buffer.alloc(64);
136
+ packet[0] = cmd;
137
+ args.forEach((arg, i) => {
138
+ packet[1 + i] = arg & 0xFF;
139
+ });
140
+
141
+ // Set up timeout
142
+ const timeoutId = setTimeout(() => {
143
+ reject(new Error('Command timeout'));
144
+ }, timeout);
145
+
146
+ // Set up read callback BEFORE writing
147
+ this.device.read((err, data) => {
148
+ clearTimeout(timeoutId);
149
+
150
+ if (err) {
151
+ reject(err);
152
+ return;
153
+ }
154
+
155
+ if (data && data.length > 0) {
156
+ // First byte is Report ID (0x03), skip it
157
+ const payload = data.slice(1);
158
+ const status = payload[0];
159
+
160
+ this._log(`Response: status=${status === 0 ? 'OK' : 'ERROR ' + status}`);
161
+
162
+ resolve({ status, data: Buffer.from(payload) });
163
+ } else {
164
+ reject(new Error('Empty response'));
165
+ }
166
+ });
167
+
168
+ // Send the command
169
+ try {
170
+ this.device.write([REPORT_ID_VENDOR, ...packet]);
171
+ this._log(`Sent cmd=0x${cmd.toString(16).padStart(2, '0')} args=[${args.map(a => '0x' + a.toString(16)).join(', ')}]`);
172
+ } catch (e) {
173
+ clearTimeout(timeoutId);
174
+ reject(e);
175
+ }
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Send command without waiting for response (for bootloader)
181
+ * @param {number} cmd - Command byte
182
+ * @param {number[]} args - Command arguments
183
+ */
184
+ sendCommandNoWait(cmd, args = []) {
185
+ if (!this.device) {
186
+ throw new Error('Device not connected');
187
+ }
188
+
189
+ const packet = Buffer.alloc(64);
190
+ packet[0] = cmd;
191
+ args.forEach((arg, i) => {
192
+ packet[1 + i] = arg & 0xFF;
193
+ });
194
+
195
+ this.device.write([REPORT_ID_VENDOR, ...packet]);
196
+ this._log(`Sent (no wait) cmd=0x${cmd.toString(16).padStart(2, '0')}`);
197
+ }
198
+
199
+ // ============================================================
200
+ // HIGH-LEVEL API
201
+ // ============================================================
202
+
203
+ /**
204
+ * Ping the device
205
+ * @returns {Promise<{version: string}>} Firmware version
206
+ */
207
+ async ping() {
208
+ const result = await this.sendCommand(CMD.PING);
209
+
210
+ // Check for PONG response
211
+ if (result.data[1] === 0x50 && result.data[2] === 0x4F &&
212
+ result.data[3] === 0x4E && result.data[4] === 0x47) {
213
+ return {
214
+ version: `${result.data[5]}.${result.data[6]}.${result.data[7]}`
215
+ };
216
+ }
217
+
218
+ return { version: 'unknown' };
219
+ }
220
+
221
+ /**
222
+ * Get firmware version
223
+ * @returns {Promise<string>} Version string (e.g., "1.0.0")
224
+ */
225
+ async getVersion() {
226
+ const result = await this.sendCommand(CMD.GET_VERSION);
227
+ return `${result.data[1]}.${result.data[2]}.${result.data[3]}`;
228
+ }
229
+
230
+ /**
231
+ * Enter bootloader mode (device will reboot)
232
+ * Connection will be lost after this command
233
+ */
234
+ enterBootloader() {
235
+ this.sendCommandNoWait(CMD.BOOTLOADER);
236
+ }
237
+
238
+ // ============================================================
239
+ // KEYBOARD API
240
+ // ============================================================
241
+
242
+ /**
243
+ * Press a key (and hold)
244
+ * @param {number} keycode - HID keycode (use KEY constants)
245
+ */
246
+ async keyPress(keycode) {
247
+ await this.sendCommand(CMD.KEY_PRESS, [keycode]);
248
+ }
249
+
250
+ /**
251
+ * Release a key
252
+ * @param {number} keycode - HID keycode
253
+ */
254
+ async keyRelease(keycode) {
255
+ await this.sendCommand(CMD.KEY_RELEASE, [keycode]);
256
+ }
257
+
258
+ /**
259
+ * Tap a key (press and release)
260
+ * @param {number} keycode - HID keycode
261
+ */
262
+ async keyTap(keycode) {
263
+ await this.sendCommand(CMD.KEY_TAP, [keycode]);
264
+ }
265
+
266
+ /**
267
+ * Set modifier keys (Ctrl, Shift, Alt, Win)
268
+ * @param {number} modifiers - Modifier flags (use MOD constants, can be combined with |)
269
+ */
270
+ async setModifiers(modifiers) {
271
+ await this.sendCommand(CMD.KEY_MODIFIER, [modifiers]);
272
+ }
273
+
274
+ /**
275
+ * Release all keys and modifiers
276
+ */
277
+ async releaseAll() {
278
+ await this.sendCommand(CMD.KEY_RELEASE_ALL);
279
+ }
280
+
281
+ /**
282
+ * Type a string (ASCII only)
283
+ * @param {string} text - Text to type (max 62 characters per call)
284
+ */
285
+ async typeString(text) {
286
+ const bytes = Buffer.from(text, 'ascii');
287
+ const len = Math.min(bytes.length, 62);
288
+ await this.sendCommand(
289
+ CMD.KEY_TYPE_STRING,
290
+ [len, ...bytes.slice(0, len)],
291
+ { timeout: TYPE_STRING_TIMEOUT }
292
+ );
293
+ }
294
+
295
+ /**
296
+ * Execute a keyboard shortcut (e.g., Ctrl+C)
297
+ * @param {number} modifiers - Modifier flags
298
+ * @param {number} keycode - HID keycode
299
+ */
300
+ async shortcut(modifiers, keycode) {
301
+ await this.setModifiers(modifiers);
302
+ await this.keyTap(keycode);
303
+ await this.releaseAll();
304
+ }
305
+
306
+ // ============================================================
307
+ // MOUSE API
308
+ // ============================================================
309
+
310
+ /**
311
+ * Move mouse relative to current position
312
+ * @param {number} x - X movement (-127 to 127)
313
+ * @param {number} y - Y movement (-127 to 127)
314
+ */
315
+ async mouseMove(x, y) {
316
+ // Convert signed to unsigned byte
317
+ await this.sendCommand(CMD.MOUSE_MOVE, [x & 0xFF, y & 0xFF]);
318
+ }
319
+
320
+ /**
321
+ * Click a mouse button
322
+ * @param {number} [button=MOUSE_BTN.LEFT] - Button to click
323
+ */
324
+ async mouseClick(button = MOUSE_BTN.LEFT) {
325
+ await this.sendCommand(CMD.MOUSE_CLICK, [button]);
326
+ }
327
+
328
+ /**
329
+ * Press a mouse button (and hold)
330
+ * @param {number} [button=MOUSE_BTN.LEFT] - Button to press
331
+ */
332
+ async mousePress(button = MOUSE_BTN.LEFT) {
333
+ await this.sendCommand(CMD.MOUSE_PRESS, [button]);
334
+ }
335
+
336
+ /**
337
+ * Release a mouse button
338
+ * @param {number} [button=MOUSE_BTN.LEFT] - Button to release
339
+ */
340
+ async mouseRelease(button = MOUSE_BTN.LEFT) {
341
+ await this.sendCommand(CMD.MOUSE_RELEASE, [button]);
342
+ }
343
+
344
+ /**
345
+ * Scroll the mouse wheel
346
+ * @param {number} amount - Scroll amount (positive = up, negative = down)
347
+ */
348
+ async mouseScroll(amount) {
349
+ await this.sendCommand(CMD.MOUSE_SCROLL, [amount & 0xFF]);
350
+ }
351
+
352
+ /**
353
+ * Double-click a mouse button
354
+ * @param {number} [button=MOUSE_BTN.LEFT] - Button to click
355
+ * @param {number} [delay=50] - Delay between clicks (ms)
356
+ */
357
+ async mouseDoubleClick(button = MOUSE_BTN.LEFT, delay = 50) {
358
+ await this.mouseClick(button);
359
+ await this._sleep(delay);
360
+ await this.mouseClick(button);
361
+ }
362
+
363
+ // ============================================================
364
+ // UTILITY
365
+ // ============================================================
366
+
367
+ /**
368
+ * Execute device-side delay
369
+ * @param {number} ms - Delay in milliseconds
370
+ */
371
+ async delay(ms) {
372
+ const high = (ms >> 8) & 0xFF;
373
+ const low = ms & 0xFF;
374
+ await this.sendCommand(CMD.DELAY, [high, low]);
375
+ }
376
+
377
+ /**
378
+ * Internal sleep helper
379
+ * @private
380
+ */
381
+ _sleep(ms) {
382
+ return new Promise(resolve => setTimeout(resolve, ms));
383
+ }
384
+ }
385
+
386
+ module.exports = MacroController;
387
+
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * MacroMate HID - Library for communicating with MacroMate device
3
+ */
4
+
5
+ const MacroController = require('./controller');
6
+ const {
7
+ VID, PID, VENDOR_USAGE_PAGE,
8
+ CMD, MOUSE_BTN, MOD, KEY, STATUS,
9
+ DEFAULT_TIMEOUT, TYPE_STRING_TIMEOUT
10
+ } = require('./constants');
11
+
12
+ module.exports = {
13
+ // Main class
14
+ MacroController,
15
+
16
+ // Constants
17
+ KEY,
18
+ MOD,
19
+ MOUSE_BTN,
20
+ CMD,
21
+ STATUS,
22
+
23
+ // Device config (for advanced usage)
24
+ VID,
25
+ PID,
26
+ VENDOR_USAGE_PAGE,
27
+ DEFAULT_TIMEOUT,
28
+ TYPE_STRING_TIMEOUT,
29
+ };
30
+