@thalaguer/buzzer 1.0.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thalaguer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/Readme.md ADDED
@@ -0,0 +1,198 @@
1
+ # @thalaguer/buzzer
2
+
3
+ **The easiest way to control your Buzz! wireless controllers in Node.js**
4
+ Light up the LEDs, detect every button press, and bring back the PS2 game show vibes — with zero hassle. 🔔🔥
5
+
6
+ > **Note**: This library supports the **wired** Buzz! controllers (with USB dongle), not the wireless versions.
7
+
8
+ ## Features
9
+
10
+ - ✅ Automatic detection and initialization
11
+ - ✅ Real-time button press/release events
12
+ - ✅ Individual LED control for up to 4 players
13
+ - ✅ Clean event-based API
14
+ - ✅ Cross-platform support (Windows, macOS, Linux)
15
+ - ✅ Built-in startup LED animation
16
+
17
+ ## Installation
18
+
19
+ This package depends on `node-hid` which requires native compilation.
20
+
21
+ **Recommended installation command:**
22
+
23
+ ```bash
24
+ npm install @thalaguer/buzzer --ignore-scripts
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ```javascript
30
+ import Buzzer from '@thalaguer/buzzer';
31
+
32
+ const buzzers = Buzzer();
33
+
34
+ // Called when the system is fully initialized
35
+ buzzers.onReady(() => {
36
+ console.log("Buzzers are ready & functional.");
37
+
38
+ // Turn on player 1's LED
39
+ buzzers.setLeds(true, false, false, false);
40
+
41
+ // Or using array syntax
42
+ buzzers.setLedsarray([true, false, true, false]); // Players 1 & 3 on
43
+ });
44
+
45
+ // Button press events
46
+ buzzers.onPress((data) => {
47
+ console.log(`Buzzer #${data.controller} pressed the ${data.color} button(${data.button}).`);
48
+ console.log(`Full event: ${JSON.stringify(data)}`);
49
+ });
50
+
51
+ // Button release events
52
+ buzzers.onRelease((data) => {
53
+ console.log(`Buzzer #${data.controller} released the ${data.color} button.`);
54
+ });
55
+
56
+ // Any state change (press or release)
57
+ buzzers.onChange((data) => {
58
+ console.log(`Buzzer #${data.controller} ${data.state} the ${data.color} button.`);
59
+ });
60
+
61
+ buzzers.onError((data) => {
62
+ console.log(`An error occurred : ${data.message}`);
63
+ })
64
+
65
+ // Clean up when done (optional)
66
+ // await buzzers.close();
67
+ ```
68
+
69
+ ## API Reference
70
+
71
+ ### `Buzzer()`
72
+ Creates a new buzzer manager instance.
73
+
74
+ ### `onReady(callback)`
75
+ Registers a callback when the system is fully initialized. The startup LED animation will play when ready.
76
+
77
+ ### `onPress(callback)`
78
+ Registers a callback for button press events. The callback receives an event object with:
79
+ - `controller`: Player number (1-4)
80
+ - `color`: Button color ('RED', 'BLUE', 'ORANGE', 'GREEN', 'YELLOW')
81
+ - `button`: Button identifier ( 0=> red, 1=> blue, 2=> orange, 3=> green, 4=> yellow)
82
+ - `state`: Event state ('press', 'released') - mostly useful for `onChange(callback)`.
83
+ ### `onRelease(callback)`
84
+ Registers a callback for button release events (same event object structure as `onPress`).
85
+
86
+ ### `onChange(callback)`
87
+ Registers a callback for any button state change (both press and release).
88
+
89
+ ### `onError(callback)`
90
+ Registers a callback for any error happening.
91
+
92
+ ### `setLeds(player1, player2, player3, player4)`
93
+ Controls the red LEDs for each player:
94
+ - `player1` - Player 1 LED (boolean)
95
+ - `player2` - Player 2 LED (boolean)
96
+ - `player3` - Player 3 LED (boolean)
97
+ - `player4` - Player 4 LED (boolean)
98
+
99
+ ### `setLedsarray([player1, player2, player3, player4])`
100
+ Convenience method that accepts an array of boolean values.
101
+
102
+ ### `close()`
103
+ Closes connections and releases resources. Returns a Promise.
104
+
105
+ ## Windows Driver Setup (ZADIG)
106
+
107
+ On Windows, the Buzz! dongle might not work with the default driver. You need to install the WinUSB driver using ZADIG:
108
+
109
+ 1. **Download ZADIG** from https://zadig.akeo.ie/
110
+
111
+ 2. **Run ZADIG** as Administrator
112
+
113
+ 3. **Configure ZADIG**:
114
+ - Go to `Options` → `List All Devices`
115
+ - Check both `Ignore Hubs or Composite Parents` and `Show only current configuration`
116
+
117
+ 4. **Select the Buzz! Dongle**:
118
+ - From the dropdown, select the Buzz! device (should appear as "Buzz!" or with VID `054C` and PID `1000`)
119
+ - If you don't see it, try:
120
+ - Unplugging and replugging the dongle
121
+ - Checking if it appears under a different name
122
+ - Trying with and without controllers connected
123
+
124
+ 5. **Install/Replace Driver**:
125
+ - Ensure the driver selected is `WinUSB` (not libusb)
126
+ - Click `Replace Driver` or `Install Driver`
127
+ - Wait for the installation to complete
128
+
129
+ 6. **Test**:
130
+ - Unplug and replug the dongle
131
+ - Run your application again
132
+
133
+ **Note**: If you have multiple Buzz! dongles, repeat these steps for each one.
134
+
135
+ ## Linux Permissions
136
+
137
+ On Linux, you may need to add a udev rule or run with `sudo`:
138
+
139
+ ```bash
140
+ # Temporary solution (run with sudo)
141
+ sudo node your-app.js
142
+
143
+ # Permanent solution (create udev rule):
144
+ sudo nano /etc/udev/rules.d/99-buzz.rules
145
+ ```
146
+
147
+ Add this line:
148
+ ```
149
+ SUBSYSTEM=="usb", ATTR{idVendor}=="054c", ATTR{idProduct}=="1000", MODE="0666"
150
+ ```
151
+
152
+ Then reload udev rules:
153
+ ```bash
154
+ sudo udevadm control --reload-rules
155
+ sudo udevadm trigger
156
+ ```
157
+
158
+ ## Troubleshooting
159
+
160
+ ### Dongle not found
161
+ 1. Check the USB connection
162
+ 2. Verify the driver is installed (Windows: ZADIG)
163
+ 3. Check if another application is using the device
164
+ 4. Try a different USB port
165
+
166
+ ### Different VID/PID
167
+ Most Buzz! dongles use `VID: 0x054c` and `PID: 0x1000`. If yours is different:
168
+
169
+ ```javascript
170
+ // In your node_modules/@thalaguer/buzzer/src/config.js
171
+ export const VID = 0x054c; // ← change to your VID
172
+ export const PID = 0x1000; // ← change to your PID
173
+ ```
174
+
175
+ To find your dongle's VID/PID:
176
+ - **Windows**: Device Manager → Properties → Details → Hardware IDs
177
+ - **Linux**: `lsusb` command
178
+ - **macOS**: System Information → USB
179
+
180
+ ### No button events
181
+ 1. Ensure controllers are connected to the dongle (press any button to pair)
182
+ 2. Check that the dongle LED is solid (not blinking)
183
+ 3. Verify your event listeners are set up before initialization completes
184
+
185
+ ## Supported Controllers
186
+
187
+ This library supports the **wireless** Buzz! controllers that come with a USB dongle. Each dongle supports up to 4 controllers.
188
+
189
+ **Known compatible models:**
190
+ - Buzz! Buzzers Wireless (PS2/PS3/PC)
191
+ - Most Buzz! controllers with model number "BUZZ001" or similar
192
+
193
+ **Not tested:**
194
+ - Buzz! Wireless controllers
195
+
196
+ ## License
197
+
198
+ This project is licensed under the **MIT License** — see the [LICENSE](LICENSE) file for details.
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import Buzzer from "./src/Buzzer.js";
2
+
3
+ export default Buzzer;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@thalaguer/buzzer",
3
+ "version": "1.0.0",
4
+ "description": "Node.js driver for Sony Buzz! wireless controllers (PS2/PS3 quiz buzzers) using HID/USB",
5
+ "type": "module",
6
+ "main": "./index.js",
7
+ "exports": {
8
+ ".": "./index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=18.0.0"
12
+ },
13
+ "dependencies": {
14
+ "node-hid": "^3.2.0",
15
+ "usb": "^2.16.0"
16
+ },
17
+ "keywords": [
18
+ "buzz",
19
+ "buzzer",
20
+ "ps2",
21
+ "ps3",
22
+ "playstation",
23
+ "hid",
24
+ "usb",
25
+ "node-hid",
26
+ "game-controller",
27
+ "quiz-controller"
28
+ ],
29
+ "author": "Thalaguer",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/thalaguer/buzzer.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/thalaguer/buzzer/issues"
37
+ },
38
+ "homepage": "https://github.com/thalaguer/buzzer#readme",
39
+ "files": [
40
+ "index.js",
41
+ "src/**/*.js",
42
+ "!**/*.test.js",
43
+ "!**/__tests__/**",
44
+ "README.md",
45
+ "LICENSE"
46
+ ],
47
+ "scripts": {
48
+ "test": "echo \"No tests yet – coming soon!\" && exit 0",
49
+ "prepublishOnly": "npm run lint && npm run test",
50
+ "lint": "echo \"Add eslint/prettier later\" && exit 0"
51
+ }
52
+ }
package/src/Buzzer.js ADDED
@@ -0,0 +1,507 @@
1
+ // ──────────────────────────────────────────────────────────────
2
+ // Error handling
3
+ // ──────────────────────────────────────────────────────────────
4
+ const isDebug = process.env.DEBUG === 'true' || process.argv.includes('--debug');
5
+
6
+ process.on('uncaughtException', (error) => {
7
+ if (isDebug) {
8
+ console.error(colors.red + colors.bright + '┌────────────────────────────────────────────────────────────┐' + colors.reset);
9
+ console.error(colors.red + colors.bright + '│ CRITICAL: Uncaught EXCEPTION (synchronous) │' + colors.reset);
10
+ console.error(colors.red + colors.bright + '└────────────────────────────────────────────────────────────┘' + colors.reset);
11
+ console.error(colors.yellow + 'Message:' + colors.reset, error.message || error);
12
+ console.error(colors.yellow + 'Stack:' + colors.reset, error.stack || error);
13
+ console.error(colors.dim + `Time: ${new Date().toISOString()}` + colors.reset);
14
+ }
15
+ });
16
+
17
+ process.on('unhandledRejection', (reason, promise) => {
18
+ if (isDebug) {
19
+ console.error(colors.magenta + colors.bright + '┌────────────────────────────────────────────────────────────┐' + colors.reset);
20
+ console.error(colors.magenta + colors.bright + '│ UNHANDLED PROMISE REJECTION │' + colors.reset);
21
+ console.error(colors.magenta + colors.bright + '└────────────────────────────────────────────────────────────┘' + colors.reset);
22
+ const error = reason instanceof Error ? reason : new Error(String(reason));
23
+ if (error instanceof CustomError) {
24
+ console.error(colors.yellow + 'Message:' + colors.reset, `[${error.timestamp}] ${error.name}: ${error.message}`);
25
+ console.error(colors.yellow + 'Details:' + colors.reset, error.details || 'none');
26
+ console.error(colors.yellow + 'Stack:' + colors.reset, error.stack);
27
+ } else {
28
+ console.error(colors.yellow + 'Unexpected rejection:' + colors.reset, error.message);
29
+ console.error(error.stack || error);
30
+ }
31
+ console.error(colors.dim + `Time: ${new Date().toISOString()}` + colors.reset);
32
+ }
33
+ });
34
+
35
+ import { findByIds } from "usb";
36
+ import { EventEmitter } from 'node:events';
37
+ import HID from 'node-hid';
38
+
39
+ import { VID, PID, prefix } from './config.js';
40
+ import { BUTTONS_DATA, colors } from "./constants.js";
41
+ import { EVENTS } from "./event.js";
42
+ import { formatEvent } from "./utils.js";
43
+ import { CustomError } from "./error/CustomError.js";
44
+
45
+ /**
46
+ * Creates and manages a Buzz! controller dongle instance.
47
+ *
48
+ * Handles connection to the Buzz! wireless dongle, button event detection,
49
+ * LED control, and provides an event-based API for button interactions.
50
+ *
51
+ * @returns {Object} Buzzer manager object with public methods.
52
+ *
53
+ * @example
54
+ * const buzz = Buzzer();
55
+ *
56
+ * // Wait for initialization
57
+ * buzz.onReady(() => {
58
+ * console.log('Buzzers ready!');
59
+ * buzz.setLeds(true, false, false, false); // Turn on player 1 LED
60
+ * });
61
+ *
62
+ * // Listen for button events
63
+ * buzz.onPress((event) => {
64
+ * console.log(`Button pressed: ${event.button} on controller ${event.controller}`);
65
+ * });
66
+ *
67
+ * // Clean up when done
68
+ * // await buzz.close();
69
+ */
70
+ export default function Buzzer() {
71
+ const ee = new EventEmitter();
72
+
73
+ let device = null;
74
+ let iface = null;
75
+ let endpoint = null;
76
+ let hidDevice = null;
77
+ let currentStates = new Array(BUTTONS_DATA.length).fill(false);
78
+
79
+ // "Ready" state management
80
+ let isReady = false;
81
+ let readyPromise = null;
82
+
83
+ // ──────────────────────────────────────────────────────────────
84
+ // USB device management
85
+ // ──────────────────────────────────────────────────────────────
86
+
87
+ /**
88
+ * Opens the USB device with the specified VID/PID and claims interface 0.
89
+ *
90
+ * This function:
91
+ * - Searches for the Buzz! dongle using VID and PID
92
+ * - Opens the device
93
+ * - Gets interface 0
94
+ * - Detaches kernel driver if active (Linux/macOS only)
95
+ * - Claims the interface for exclusive access
96
+ *
97
+ * @async
98
+ * @returns {Promise<{device: import('usb').Device, iface: import('usb').Interface}>}
99
+ * Object containing the opened device and claimed interface
100
+ * @throws {CustomError} If the device is not found
101
+ * @throws {CustomError} If interface 0 is not available
102
+ * @throws {CustomError} If any other USB operation fails (open, claim, etc.)
103
+ */
104
+ async function openAndClaim() {
105
+ try {
106
+ device = findByIds(VID, PID);
107
+ if (!device) {
108
+ const errMsg = "Dongle not found. Check connection and VID/PID.";
109
+ ee.emit(EVENTS.ERROR, errMsg);
110
+ throw new CustomError(errMsg);
111
+ }
112
+
113
+ device.open();
114
+
115
+ iface = device.interface(0);
116
+ if (!iface) {
117
+ const errMsg = "Interface 0 not found";
118
+ ee.emit(EVENTS.ERROR, errMsg);
119
+ throw new CustomError(errMsg);
120
+ }
121
+
122
+ if (process.platform !== 'win32' && iface.isKernelDriverActive?.()) {
123
+ iface.detachKernelDriver();
124
+ }
125
+
126
+ iface.claim();
127
+
128
+ return { device, iface };
129
+ } catch (err) {
130
+ const errMsg = "Failed to open the connection to the dongle.";
131
+ ee.emit(EVENTS.ERROR, errMsg);
132
+ throw new CustomError(errMsg, { cause: err });
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Finds and returns the interrupt IN endpoint from the given USB interface.
138
+ *
139
+ * This function searches through all endpoints of the interface for one that:
140
+ * - Has direction 'in' (device → host)
141
+ * - Uses interrupt transfer type (transferType === 3)
142
+ *
143
+ * If no suitable endpoint is found, it throws an error with a detailed list
144
+ * of all available endpoints for debugging purposes.
145
+ *
146
+ * @param {import('usb').Interface} iface - The USB interface to search endpoints in
147
+ * @returns {import('usb').Endpoint} The interrupt IN endpoint found
148
+ * @throws {CustomError} If no interrupt IN endpoint is found.
149
+ * The error message includes a formatted list of all available endpoints.
150
+ */
151
+ function findInterruptEndpoint(iface) {
152
+ const endpoint = iface.endpoints.find(ep =>
153
+ ep.direction === 'in' && ep.transferType === 3
154
+ );
155
+
156
+ if (!endpoint) {
157
+ const errMsg = "No INTERRUPT IN endpoint found!\n" +
158
+ "Available endpoints:\n" +
159
+ iface.endpoints.map(e => ` - ${e.direction} ${e.transferType} (addr 0x${e.address.toString(16)})`).join("\n");
160
+ ee.emit(EVENTS.ERROR, errMsg);
161
+ throw new CustomError(errMsg);
162
+ }
163
+
164
+ return endpoint;
165
+ }
166
+
167
+ // ──────────────────────────────────────────────────────────────
168
+ // LEDs
169
+ // ──────────────────────────────────────────────────────────────
170
+
171
+ /**
172
+ * Internal LED control without initialization check.
173
+ * Used during boot sequence to avoid circular dependencies.
174
+ *
175
+ * @private
176
+ * @param {boolean} [player1=false] - Turn on the LED for player 1
177
+ * @param {boolean} [player2=false] - Turn on the LED for player 2
178
+ * @param {boolean} [player3=false] - Turn on the LED for player 3
179
+ * @param {boolean} [player4=false] - Turn on the LED for player 4
180
+ * @returns {void}
181
+ */
182
+ function privateSetLeds(player1 = false, player2 = false, player3 = false, player4 = false) {
183
+ if (!hidDevice) {
184
+ console.warn(`${prefix} HID device not available`);
185
+ return;
186
+ }
187
+
188
+ const report = Buffer.alloc(8);
189
+ report[0] = 0x00;
190
+ report[1] = 0x00;
191
+ report[2] = player1 ? 0xFF : 0x00;
192
+ report[3] = player2 ? 0xFF : 0x00;
193
+ report[4] = player3 ? 0xFF : 0x00;
194
+ report[5] = player4 ? 0xFF : 0x00;
195
+ report[6] = 0x00;
196
+ report[7] = 0x00;
197
+
198
+ try {
199
+ hidDevice.write(report);
200
+ } catch (err) {
201
+ const shortReport = Buffer.from([
202
+ 0x00, 0x00,
203
+ player1 ? 0xFF : 0x00,
204
+ player2 ? 0xFF : 0x00,
205
+ player3 ? 0xFF : 0x00,
206
+ player4 ? 0xFF : 0x00
207
+ ]);
208
+ try {
209
+ hidDevice.write(shortReport);
210
+ } catch (innerErr) {
211
+ ee.emit(EVENTS.ERROR, "Failed to set LEDs");
212
+ if (isDebug) {
213
+ console.error(`${prefix} LED set error:`, innerErr.message);
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Controls the LEDs on the Buzz! controllers.
221
+ *
222
+ * Ensures the system is ready before setting LEDs.
223
+ *
224
+ * @param {boolean} [player1=false] - Turn on LED for player 1
225
+ * @param {boolean} [player2=false] - Turn on LED for player 2
226
+ * @param {boolean} [player3=false] - Turn on LED for player 3
227
+ * @param {boolean} [player4=false] - Turn on LED for player 4
228
+ * @returns {Promise<void>}
229
+ */
230
+ async function setLeds(player1 = false, player2 = false, player3 = false, player4 = false) {
231
+ await ensureReady();
232
+ privateSetLeds(player1, player2, player3, player4);
233
+ }
234
+
235
+ /**
236
+ * Controls the LEDs using an array of boolean values.
237
+ *
238
+ * @param {boolean[]} players - Array of 4 booleans for players 1-4
239
+ * @returns {Promise<void>}
240
+ * @throws {CustomError} If array length is not 4
241
+ */
242
+ async function setLedsarray(players) {
243
+ if (!Array.isArray(players) || players.length !== 4) {
244
+ throw new CustomError("Invalid array: must be boolean[4]");
245
+ }
246
+ await setLeds(...players);
247
+ }
248
+
249
+ // ──────────────────────────────────────────────────────────────
250
+ // Setup
251
+ // ──────────────────────────────────────────────────────────────
252
+
253
+ /**
254
+ * Sets up the listener for Buzz! controller events.
255
+ *
256
+ * Initializes USB connection, HID device, starts polling for button data,
257
+ * and emits events on button press/release.
258
+ *
259
+ * @async
260
+ * @returns {Promise<void>}
261
+ * @throws {CustomError} If setup fails
262
+ * @fires press When a button is pressed (payload: button object)
263
+ * @fires release When a button is released (payload: button object)
264
+ * @fires error When a critical error occurs during setup
265
+ */
266
+ async function setupBuzzListener() {
267
+ try {
268
+ const result = await openAndClaim();
269
+ device = result.device;
270
+ iface = result.iface;
271
+
272
+ hidDevice = new HID.HID(VID, PID);
273
+
274
+ endpoint = findInterruptEndpoint(iface);
275
+
276
+ endpoint.startPoll(3, endpoint.descriptor.wMaxPacketSize);
277
+
278
+ endpoint.on('data', (data) => {
279
+ if (data.length < 5) return;
280
+
281
+ const previousStates = [...currentStates];
282
+ const pressedBits = data.readUInt16LE(2) | (data.readUInt8(4) << 16);
283
+
284
+ BUTTONS_DATA.forEach((button, index) => {
285
+ const isPressedNow = (pressedBits & button.bit) !== 0;
286
+ const wasPressed = previousStates[index];
287
+
288
+ currentStates[index] = isPressedNow;
289
+
290
+ if (isPressedNow !== wasPressed) {
291
+ const eventType = isPressedNow ? EVENTS.PRESS : EVENTS.RELEASE;
292
+ ee.emit(eventType, { ...button, type: eventType });
293
+ }
294
+ });
295
+ });
296
+
297
+ if(isDebug) console.log(colors.magenta + colors.bright + `${prefix} Listening started` + colors.reset);
298
+ } catch (err) {
299
+ throw new CustomError("Buzzers setup failed", { cause: err });
300
+ }
301
+ }
302
+
303
+ // ──────────────────────────────────────────────────────────────
304
+ // Events methods
305
+ // ──────────────────────────────────────────────────────────────
306
+
307
+ /**
308
+ * Internal function to ensure the buzzer system is ready.
309
+ * Initializes the system if not already initialized.
310
+ *
311
+ * @private
312
+ * @async
313
+ * @returns {Promise<void>}
314
+ * @throws {CustomError} If initialization fails
315
+ */
316
+ async function ensureReady() {
317
+ if (isReady) return;
318
+ if (readyPromise) return readyPromise;
319
+
320
+ readyPromise = setupBuzzListener()
321
+ .then(() => {
322
+ const cycles = 3;
323
+ const onTime = 350; // clearly visible
324
+ const offTime = 200;
325
+
326
+ return new Promise((resolve) => {
327
+ let delay = 0;
328
+ for (let i = 0; i < cycles; i++) {
329
+ setTimeout(() => {
330
+ privateSetLeds(true, true, true, true);
331
+ }, delay);
332
+ delay += onTime;
333
+
334
+ setTimeout(() => {
335
+ privateSetLeds(false, false, false, false);
336
+ }, delay);
337
+ delay += offTime;
338
+ }
339
+
340
+ setTimeout(() => {
341
+ isReady = true;
342
+ if(isDebug) console.log(colors.yellow + colors.bright + `${prefix} Buzzers are ready` + colors.reset);
343
+ ee.emit(EVENTS.READY);
344
+ resolve();
345
+ }, delay + 200);
346
+ });
347
+ })
348
+ .catch(err => {
349
+ const customError = new CustomError(err.message || "Setup failed", { cause: err });
350
+ ee.emit(EVENTS.ERROR, customError);
351
+ throw customError;
352
+ });
353
+
354
+ return readyPromise;
355
+ }
356
+
357
+ /**
358
+ * Registers a callback to be called when the buzzer system is fully initialized.
359
+ *
360
+ * @param {function} callback - Function called when system is ready
361
+ * @returns {void}
362
+ */
363
+ function onReady(callback) {
364
+ ee.on(EVENTS.READY, callback);
365
+ }
366
+
367
+ /**
368
+ * Registers a callback to be called when any buzzer button is pressed.
369
+ *
370
+ * The callback receives a simplified event object with the following properties:
371
+ * - `controller`: The controller number (1 to 4)
372
+ * - `button`: The button name (e.g., 'RED', 'BLUE', etc.)
373
+ * - `Name`: The full button identifier (e.g., 'C1_RED', 'C2_BLUE')
374
+ *
375
+ * Returns a cleanup function to remove the listener.
376
+ *
377
+ * @param {function(Object): void} callback - Function called when a button is pressed
378
+ * @returns {function(): void} Cleanup function to remove the listener
379
+ */
380
+ function onPress(callback) {
381
+ ensureReady().catch((err) => {
382
+ ee.emit(EVENTS.ERROR, err);
383
+ });
384
+ const handler = (originalEvent) => {
385
+ callback(formatEvent(originalEvent, EVENTS.PRESS));
386
+ };
387
+
388
+ ee.on(EVENTS.PRESS, handler);
389
+ return () => ee.off(EVENTS.PRESS, handler);
390
+ }
391
+
392
+ /**
393
+ * Registers a callback to be called when any buzzer button is released.
394
+ *
395
+ * The callback receives a simplified event object with the following properties:
396
+ * - `controller`: The controller number (1 to 4)
397
+ * - `button`: The button name (e.g., 'RED', 'BLUE', etc.)
398
+ * - `Name`: The full button identifier (e.g., 'C1_RED', 'C2_BLUE')
399
+ *
400
+ * Returns a cleanup function to remove the listener.
401
+ *
402
+ * @param {function(Object): void} callback - Function called when a button is released
403
+ * @returns {function(): void} Cleanup function to remove the listener
404
+ */
405
+ function onRelease(callback) {
406
+ ensureReady().catch((err) => {
407
+ ee.emit(EVENTS.ERROR, err);
408
+ });
409
+ const handler = (originalEvent) => {
410
+ callback(formatEvent(originalEvent, EVENTS.RELEASE));
411
+ };
412
+
413
+ ee.on(EVENTS.RELEASE, handler);
414
+ return () => ee.off(EVENTS.RELEASE, handler);
415
+ }
416
+
417
+ /**
418
+ * Registers a callback to be called whenever a button state changes
419
+ * (either pressed or released).
420
+ *
421
+ * The callback receives a simplified event object with the following properties:
422
+ * - `controller`: The controller number (1 to 4)
423
+ * - `button`: The button name (e.g., 'RED', 'BLUE', etc.)
424
+ * - `Name`: The full button identifier (e.g., 'C1_RED', 'C2_BLUE')
425
+ *
426
+ * Returns a cleanup function to remove the listener.
427
+ *
428
+ * @param {function(Object): void} callback - Function called on any button state change
429
+ * @returns {function(): void} Cleanup function to remove the listener
430
+ */
431
+ function onChange(callback) {
432
+ ensureReady().catch((err) => {
433
+ ee.emit(EVENTS.ERROR, err);
434
+ });
435
+
436
+ const handler = (originalEvent) => {
437
+ callback(formatEvent(originalEvent, originalEvent.type));
438
+ };
439
+
440
+ ee.on(EVENTS.PRESS, handler);
441
+ ee.on(EVENTS.RELEASE, handler);
442
+
443
+ return () => {
444
+ ee.off(EVENTS.PRESS, handler);
445
+ ee.off(EVENTS.RELEASE, handler);
446
+ };
447
+ }
448
+
449
+ /**
450
+ * Registers a callback to be called when an error occurs.
451
+ *
452
+ * @param {function(CustomError|string): void} callback - Function called on error
453
+ * @returns {function(): void} Cleanup function to remove the listener
454
+ */
455
+ function onError(callback) {
456
+ ensureReady().catch((err) => {
457
+ callback(err);
458
+ });
459
+ ee.on(EVENTS.ERROR, callback);
460
+ return () => ee.off(EVENTS.ERROR, callback);
461
+ }
462
+
463
+ /**
464
+ * Closes all connections and releases resources.
465
+ * Should be called when the application is done using the buzzer system.
466
+ *
467
+ * @async
468
+ * @returns {Promise<void>}
469
+ */
470
+ async function close() {
471
+ return new Promise((resolve) => {
472
+ try {
473
+ if (endpoint) endpoint.stopPoll();
474
+ if (iface) {
475
+ iface.release(true, () => {
476
+ if (device) device.close();
477
+ if (hidDevice) hidDevice.close();
478
+ isReady = false;
479
+ readyPromise = null;
480
+ if(isDebug) console.log(`${prefix} Resources released`);
481
+ resolve();
482
+ });
483
+ } else {
484
+ resolve();
485
+ }
486
+ } catch (err) {
487
+ console.warn(`${prefix} Close error:`, err.message);
488
+ resolve();
489
+ }
490
+ });
491
+ }
492
+
493
+ // ──────────────────────────────────────────────────────────────
494
+ // public API
495
+ // ──────────────────────────────────────────────────────────────
496
+
497
+ return {
498
+ setLeds,
499
+ setLedsarray,
500
+ onReady,
501
+ onPress,
502
+ onRelease,
503
+ onChange,
504
+ onError,
505
+ close
506
+ };
507
+ }
package/src/config.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Configuration constants for the Buzz! controller dongle.
3
+ *
4
+ * @module config
5
+ */
6
+
7
+ /** USB Vendor ID for the Buzz! dongle */
8
+ export const VID = 0x054c;
9
+
10
+ /** USB Product ID for the Buzz! dongle */
11
+ export const PID = 0x1000;
12
+
13
+ /** Prefix used for logging messages */
14
+ export const prefix = '[Buzzer]';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Constants for button data and console colors.
3
+ *
4
+ * @module constants
5
+ */
6
+
7
+ /** Array of button configurations for all controllers */
8
+ export const BUTTONS_DATA = [
9
+ // Controller 1
10
+ { name: 'C1_RED', color: 'red', controller: 1, button: 0, bit: 0x000001 },
11
+ { name: 'C1_BLUE', color: 'blue', controller: 1, button: 1, bit: 0x000010 },
12
+ { name: 'C1_ORANGE', color: 'orange', controller: 1, button: 2, bit: 0x000008 },
13
+ { name: 'C1_GREEN', color: 'green', controller: 1, button: 3, bit: 0x000004 },
14
+ { name: 'C1_YELLOW', color: 'yellow', controller: 1, button: 4, bit: 0x000002 },
15
+
16
+ // Controller 2
17
+ { name: 'C2_RED', color: 'red', controller: 2, button: 0, bit: 0x000020 },
18
+ { name: 'C2_BLUE', color: 'blue', controller: 2, button: 1, bit: 0x000200 },
19
+ { name: 'C2_ORANGE', color: 'orange', controller: 2, button: 2, bit: 0x000100 },
20
+ { name: 'C2_GREEN', color: 'green', controller: 2, button: 3, bit: 0x000080 },
21
+ { name: 'C2_YELLOW', color: 'yellow', controller: 2, button: 4, bit: 0x000040 },
22
+
23
+ // Controller 3
24
+ { name: 'C3_RED', color: 'red', controller: 3, button: 0, bit: 0x000400 },
25
+ { name: 'C3_BLUE', color: 'blue', controller: 3, button: 1, bit: 0x004000 },
26
+ { name: 'C3_ORANGE', color: 'orange', controller: 3, button: 2, bit: 0x002000 },
27
+ { name: 'C3_GREEN', color: 'green', controller: 3, button: 3, bit: 0x001000 },
28
+ { name: 'C3_YELLOW', color: 'yellow', controller: 3, button: 4, bit: 0x000800 },
29
+
30
+ // Controller 4
31
+ { name: 'C4_RED', color: 'red', controller: 4, button: 0, bit: 0x008000 },
32
+ { name: 'C4_BLUE', color: 'blue', controller: 4, button: 1, bit: 0x080000 },
33
+ { name: 'C4_ORANGE', color: 'orange', controller: 4, button: 2, bit: 0x040000 },
34
+ { name: 'C4_GREEN', color: 'green', controller: 4, button: 3, bit: 0x020000 },
35
+ { name: 'C4_YELLOW', color: 'yellow', controller: 4, button: 4, bit: 0x010000 },
36
+ ];
37
+
38
+ /** ANSI color codes for console logging */
39
+ export const colors = {
40
+ reset: '\x1b[0m',
41
+ bright: '\x1b[1m',
42
+ dim: '\x1b[2m',
43
+ red: '\x1b[31m',
44
+ green: '\x1b[32m',
45
+ yellow: '\x1b[33m',
46
+ blue: '\x1b[34m',
47
+ magenta: '\x1b[35m',
48
+ cyan: '\x1b[36m',
49
+ white: '\x1b[37m',
50
+ bgRed: '\x1b[41m',
51
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Custom error class that extends the built-in Error class.
3
+ * Includes additional details and a timestamp.
4
+ *
5
+ * @module CustomError
6
+ */
7
+ export class CustomError extends Error {
8
+ /**
9
+ * Creates a new CustomError instance.
10
+ *
11
+ * @param {string} message - The error message.
12
+ * @param {Object} [details={}] - Additional details about the error.
13
+ */
14
+ constructor(message, details = {}) {
15
+ super(message);
16
+ this.name = 'CustomError';
17
+ this.details = details;
18
+ this.timestamp = new Date().toISOString();
19
+ }
20
+ }
package/src/event.js ADDED
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Event types for buzzer interactions.
3
+ *
4
+ * @module event
5
+ */
6
+ export const EVENTS = {
7
+ /** Event emitted when a button is pressed */
8
+ PRESS: 'press',
9
+ /** Event emitted when a button is released */
10
+ RELEASE: 'release',
11
+ /** Event emitted when the buzzer system is ready */
12
+ READY: 'ready',
13
+ /** Event emitted when an error occurs */
14
+ ERROR: 'error'
15
+ };
package/src/utils.js ADDED
@@ -0,0 +1,20 @@
1
+ import { EVENTS } from "./event.js";
2
+
3
+ /**
4
+ * Formats the original button event object into a simplified structure.
5
+ *
6
+ * @param {Object} original - The original event object from the endpoint data.
7
+ * @param {string} eventType - The type of event (PRESS or RELEASE).
8
+ * @returns {Object} Formatted event object with controller, button, name, color, pressed, and state.
9
+ */
10
+ export function formatEvent(original, eventType) {
11
+ const isPressed = eventType === EVENTS.PRESS;
12
+ return {
13
+ controller: original.controller,
14
+ button: original.button,
15
+ Name: original.name,
16
+ color: original.color,
17
+ pressed: isPressed,
18
+ state: isPressed ? 'pressed' : 'released'
19
+ };
20
+ }