|
1 | | -const net = require("net"); |
2 | 1 | const express = require("express"); |
3 | 2 | const morgan = require("morgan"); |
| 3 | +require("dotenv").config(); |
4 | 4 | require("dotenv").config({ path: __dirname + "/.env" }); |
5 | 5 |
|
| 6 | +const { Client } = require("openrgb-sdk"); |
| 7 | + |
6 | 8 | const app = express(); |
7 | 9 | app.use(express.json()); |
8 | | -app.use(morgan("combined")); |
9 | | - |
10 | | -const OPENRGB_HOST = process.env.OPENRGB_HOST || "doganm95-openrgb-tcp-server"; |
11 | | -const OPENRGB_PORT = Number(process.env.OPENRGB_PORT) || 6742; |
12 | | - |
13 | | -const PacketType = { HELLO: 0, REQUEST: 1, RESPONSE: 2, NOTIFY: 3, ERROR: 4 }; |
14 | | - |
15 | | -const Command = { |
16 | | - GET_CONTROLLER_COUNT: 1, |
17 | | - GET_CONTROLLER_DATA: 2, |
18 | | - SET_LED_COLOR: 4, |
19 | | - SET_LED_COLOR_ALL: 5, |
20 | | - SET_MODE: 7, |
21 | | - SET_BRIGHTNESS: 8, |
22 | | - SET_LED_BRIGHTNESS: 9, |
23 | | - SET_SPEED: 10, |
24 | | - SET_LED_SPEED: 11, |
25 | | - GET_PROFILE: 12, |
26 | | - SET_PROFILE: 13, |
27 | | - SAVE_PROFILE: 14, |
28 | | - GET_DEVICE_COUNT: 15, |
29 | | - GET_DEVICE_DATA: 16, |
30 | | -}; |
31 | | - |
32 | | -function writeInt32LE(value) { |
33 | | - const buf = Buffer.alloc(4); |
34 | | - buf.writeInt32LE(value, 0); |
35 | | - return buf; |
36 | | -} |
37 | | - |
38 | | -function writeUInt16LE(value) { |
39 | | - const buf = Buffer.alloc(2); |
40 | | - buf.writeUInt16LE(value, 0); |
41 | | - return buf; |
42 | | -} |
43 | | - |
44 | | -function writeString(str) { |
45 | | - const strBuf = Buffer.from(str, "utf8"); |
46 | | - const lenBuf = writeInt32LE(strBuf.length); |
47 | | - return Buffer.concat([lenBuf, strBuf]); |
48 | | -} |
49 | | - |
50 | | -class OpenRGBClient { |
51 | | - constructor(host, port) { |
52 | | - this.host = host; |
53 | | - this.port = port; |
54 | | - this.socket = null; |
55 | | - this.nextRequestId = 1; |
56 | | - this.callbacks = new Map(); |
57 | | - this.buffer = Buffer.alloc(0); |
58 | | - } |
59 | | - |
60 | | - connect() { |
61 | | - return new Promise((resolve, reject) => { |
62 | | - this.socket = net.createConnection(this.port, this.host, () => { |
63 | | - this.sendHello().then(resolve).catch(reject); |
64 | | - }); |
65 | | - |
66 | | - this.socket.on("data", (data) => this.onData(data)); |
67 | | - this.socket.on("error", (err) => { |
68 | | - console.error("Socket error:", err); |
69 | | - this.rejectAll(err); |
70 | | - }); |
71 | | - this.socket.on("close", () => { |
72 | | - this.rejectAll(new Error("Connection closed")); |
73 | | - }); |
74 | | - }); |
75 | | - } |
76 | | - |
77 | | - rejectAll(err) { |
78 | | - for (const cb of this.callbacks.values()) { |
79 | | - cb.reject(err); |
80 | | - } |
81 | | - this.callbacks.clear(); |
82 | | - } |
83 | | - |
84 | | - onData(data) { |
85 | | - this.buffer = Buffer.concat([this.buffer, data]); |
86 | | - while (this.buffer.length >= 8) { |
87 | | - const packetLen = this.buffer.readInt32LE(0); |
88 | | - if (this.buffer.length < packetLen + 4) break; |
89 | | - |
90 | | - const packetBuf = this.buffer.slice(4, 4 + packetLen); |
91 | | - this.buffer = this.buffer.slice(4 + packetLen); |
92 | | - |
93 | | - this.handlePacket(packetBuf); |
94 | | - } |
95 | | - } |
96 | | - |
97 | | - handlePacket(buffer) { |
98 | | - const packetType = buffer.readInt32LE(0); |
99 | | - const requestId = buffer.readInt32LE(4); |
100 | | - |
101 | | - if (packetType === PacketType.RESPONSE) { |
102 | | - const cb = this.callbacks.get(requestId); |
103 | | - if (cb) { |
104 | | - this.callbacks.delete(requestId); |
105 | | - cb.resolve(buffer.slice(8)); |
106 | | - } |
107 | | - } |
108 | | - } |
109 | | - |
110 | | - sendPacket(packetType, requestId, payload) { |
111 | | - const length = 8 + (payload ? payload.length : 0); |
112 | | - const header = Buffer.alloc(8); |
113 | | - header.writeInt32LE(length - 4, 0); |
114 | | - header.writeInt32LE(packetType, 4); |
115 | | - |
116 | | - const requestBuf = Buffer.alloc(4); |
117 | | - requestBuf.writeInt32LE(requestId, 0); |
118 | | - |
119 | | - const packet = payload ? Buffer.concat([header, requestBuf, payload]) : Buffer.concat([header, requestBuf]); |
120 | | - this.socket.write(packet); |
121 | | - } |
| 10 | +app.use(morgan("dev")); |
122 | 11 |
|
123 | | - sendRequest(commandId, payload) { |
124 | | - const requestId = this.nextRequestId++; |
125 | | - return new Promise((resolve, reject) => { |
126 | | - this.callbacks.set(requestId, { resolve, reject }); |
127 | | - |
128 | | - const commandBuf = writeInt32LE(commandId); |
129 | | - const fullPayload = payload ? Buffer.concat([commandBuf, payload]) : commandBuf; |
130 | | - |
131 | | - this.sendPacket(PacketType.REQUEST, requestId, fullPayload); |
132 | | - |
133 | | - setTimeout(() => { |
134 | | - if (this.callbacks.has(requestId)) { |
135 | | - this.callbacks.delete(requestId); |
136 | | - reject(new Error("OpenRGB request timeout")); |
137 | | - } |
138 | | - }, 5000); |
139 | | - }); |
140 | | - } |
141 | | - |
142 | | - sendHello() { |
143 | | - const clientName = "OpenRGB-REST-API"; |
144 | | - const clientNameBuf = writeString(clientName); |
145 | | - const versionBuf = writeInt32LE(1); |
146 | | - const payload = Buffer.concat([clientNameBuf, versionBuf]); |
147 | | - return new Promise((resolve, reject) => { |
148 | | - this.socket.once("data", (data) => { |
149 | | - resolve(); |
150 | | - }); |
151 | | - this.sendPacket(PacketType.HELLO, 0, payload); |
152 | | - }); |
153 | | - } |
154 | | - |
155 | | - // --- SDK commands --- |
156 | | - |
157 | | - async getControllerCount() { |
158 | | - const resp = await this.sendRequest(Command.GET_CONTROLLER_COUNT); |
159 | | - return resp.readInt32LE(0); |
160 | | - } |
161 | | - |
162 | | - async getControllerData(controllerIndex) { |
163 | | - const payload = writeInt32LE(controllerIndex); |
164 | | - const resp = await this.sendRequest(Command.GET_CONTROLLER_DATA, payload); |
165 | | - return this.parseControllerData(resp); |
166 | | - } |
167 | | - |
168 | | - parseControllerData(buffer) { |
169 | | - // Parse according to OpenRGB SDK documentation: |
170 | | - // https://gitlab.com/CalcProgrammer1/OpenRGB/-/wikis/OpenRGB-SDK-Protocol |
171 | | - // Example parsing for device name, LEDs count, modes, zones etc. |
172 | | - |
173 | | - let offset = 0; |
174 | | - |
175 | | - function readInt32() { |
176 | | - const val = buffer.readInt32LE(offset); |
177 | | - offset += 4; |
178 | | - return val; |
179 | | - } |
180 | | - |
181 | | - function readString() { |
182 | | - const len = readInt32(); |
183 | | - const str = buffer.toString("utf8", offset, offset + len); |
184 | | - offset += len; |
185 | | - return str; |
186 | | - } |
187 | | - |
188 | | - const name = readString(); |
189 | | - const type = readInt32(); |
190 | | - const deviceType = this.deviceTypeToString(type); |
191 | | - |
192 | | - const activeMode = readInt32(); |
193 | | - const ledCount = readInt32(); |
194 | | - |
195 | | - const leds = []; |
196 | | - for (let i = 0; i < ledCount; i++) { |
197 | | - const ledName = readString(); |
198 | | - leds.push(ledName); |
199 | | - } |
200 | | - |
201 | | - const modeCount = readInt32(); |
202 | | - const modes = []; |
203 | | - for (let i = 0; i < modeCount; i++) { |
204 | | - const modeName = readString(); |
205 | | - modes.push(modeName); |
206 | | - } |
207 | | - |
208 | | - const zoneCount = readInt32(); |
209 | | - const zones = []; |
210 | | - for (let i = 0; i < zoneCount; i++) { |
211 | | - const zoneName = readString(); |
212 | | - zones.push(zoneName); |
213 | | - } |
214 | | - |
215 | | - return { name, deviceType, activeMode, leds, modes, zones }; |
216 | | - } |
217 | | - |
218 | | - deviceTypeToString(type) { |
219 | | - const types = ["Unknown", "Motherboard", "DRAM", "GPU", "Peripheral"]; |
220 | | - return types[type] || "Unknown"; |
221 | | - } |
222 | | - |
223 | | - async setLedColor(controllerIndex, ledIndex, color) { |
224 | | - const payload = Buffer.alloc(4 + 4 + 4); |
225 | | - payload.writeInt32LE(controllerIndex, 0); |
226 | | - payload.writeInt32LE(ledIndex, 4); |
227 | | - payload.writeUInt8(color.red, 8); |
228 | | - payload.writeUInt8(color.green, 9); |
229 | | - payload.writeUInt8(color.blue, 10); |
230 | | - // white channel ignored here |
231 | | - |
232 | | - await this.sendRequest(Command.SET_LED_COLOR, payload); |
233 | | - } |
234 | | - |
235 | | - async setLedColorAll(controllerIndex, color) { |
236 | | - const payload = Buffer.alloc(4 + 4 + 4); |
237 | | - payload.writeInt32LE(controllerIndex, 0); |
238 | | - payload.writeInt32LE(0, 4); // ignored |
239 | | - payload.writeUInt8(color.red, 8); |
240 | | - payload.writeUInt8(color.green, 9); |
241 | | - payload.writeUInt8(color.blue, 10); |
242 | | - |
243 | | - await this.sendRequest(Command.SET_LED_COLOR_ALL, payload); |
244 | | - } |
245 | | - |
246 | | - async setMode(controllerIndex, modeIndex) { |
247 | | - const payload = Buffer.alloc(8); |
248 | | - payload.writeInt32LE(controllerIndex, 0); |
249 | | - payload.writeInt32LE(modeIndex, 4); |
250 | | - |
251 | | - await this.sendRequest(Command.SET_MODE, payload); |
252 | | - } |
253 | | - |
254 | | - async setBrightness(controllerIndex, brightness) { |
255 | | - const payload = Buffer.alloc(8); |
256 | | - payload.writeInt32LE(controllerIndex, 0); |
257 | | - payload.writeInt32LE(brightness, 4); |
258 | | - |
259 | | - await this.sendRequest(Command.SET_BRIGHTNESS, payload); |
260 | | - } |
261 | | - |
262 | | - async setSpeed(controllerIndex, speed) { |
263 | | - const payload = Buffer.alloc(8); |
264 | | - payload.writeInt32LE(controllerIndex, 0); |
265 | | - payload.writeInt32LE(speed, 4); |
266 | | - |
267 | | - await this.sendRequest(Command.SET_SPEED, payload); |
268 | | - } |
269 | | - |
270 | | - // Add more methods like profile save/load, etc., as needed |
271 | | -} |
272 | | - |
273 | | -// REST API endpoints |
274 | | - |
275 | | -const client = new OpenRGBClient(OPENRGB_HOST, OPENRGB_PORT); |
| 12 | +const client = new Client("OpenRGB-REST-API", Number(process.env.OPENRGB_PORT), process.env.OPENRGB_HOST); |
276 | 13 |
|
277 | 14 | app.get("/devices", async (req, res) => { |
278 | | - try { |
279 | | - const count = await client.getControllerCount(); |
280 | | - const devices = []; |
281 | | - for (let i = 0; i < count; i++) { |
282 | | - const data = await client.getControllerData(i); |
283 | | - devices.push({ id: i, ...data }); |
284 | | - } |
285 | | - res.json(devices); |
286 | | - } catch (e) { |
287 | | - res.status(500).json({ error: e.message }); |
288 | | - } |
289 | | -}); |
290 | | - |
291 | | -app.get("/device/:id", async (req, res) => { |
292 | | - const id = Number(req.params.id); |
293 | | - try { |
294 | | - const device = await client.getControllerData(id); |
295 | | - res.json(device); |
296 | | - } catch (e) { |
297 | | - res.status(500).json({ error: e.message }); |
298 | | - } |
299 | | -}); |
300 | | - |
301 | | -app.post("/device/:id/color", async (req, res) => { |
302 | | - const id = Number(req.params.id); |
303 | | - const { red, green, blue } = req.body; |
304 | | - if ([red, green, blue].some((v) => v === undefined || v < 0 || v > 255)) { |
305 | | - return res.status(400).json({ error: "Invalid color values 0-255 required" }); |
306 | | - } |
307 | | - try { |
308 | | - await client.setLedColorAll(id, { red, green, blue }); |
309 | | - res.json({ status: "success", device: id, color: { red, green, blue } }); |
310 | | - } catch (e) { |
311 | | - res.status(500).json({ error: e.message }); |
312 | | - } |
313 | | -}); |
314 | | - |
315 | | -app.post("/device/:id/mode", async (req, res) => { |
316 | | - const id = Number(req.params.id); |
317 | | - const { modeIndex } = req.body; |
318 | | - if (typeof modeIndex !== "number" || modeIndex < 0) { |
319 | | - return res.status(400).json({ error: "modeIndex (number) required" }); |
320 | | - } |
321 | | - try { |
322 | | - await client.setMode(id, modeIndex); |
323 | | - res.json({ status: "success", device: id, modeIndex }); |
324 | | - } catch (e) { |
325 | | - res.status(500).json({ error: e.message }); |
326 | | - } |
327 | | -}); |
328 | | - |
329 | | -app.post("/device/:id/brightness", async (req, res) => { |
330 | | - const id = Number(req.params.id); |
331 | | - const { brightness } = req.body; |
332 | | - if (typeof brightness !== "number" || brightness < 0 || brightness > 100) { |
333 | | - return res.status(400).json({ error: "brightness must be 0-100" }); |
334 | | - } |
335 | | - try { |
336 | | - await client.setBrightness(id, brightness); |
337 | | - res.json({ status: "success", device: id, brightness }); |
338 | | - } catch (e) { |
339 | | - res.status(500).json({ error: e.message }); |
340 | | - } |
341 | | -}); |
342 | | - |
343 | | -app.post("/device/:id/speed", async (req, res) => { |
344 | | - const id = Number(req.params.id); |
345 | | - const { speed } = req.body; |
346 | | - if (typeof speed !== "number" || speed < 0 || speed > 100) { |
347 | | - return res.status(400).json({ error: "speed must be 0-100" }); |
348 | | - } |
349 | | - try { |
350 | | - await client.setSpeed(id, speed); |
351 | | - res.json({ status: "success", device: id, speed }); |
352 | | - } catch (e) { |
353 | | - res.status(500).json({ error: e.message }); |
354 | | - } |
355 | | -}); |
356 | | - |
357 | | -// TODO: Add endpoints for profiles, zones, I2C tools, save/load profiles, etc. |
358 | | - |
359 | | -app.listen(3000, async () => { |
360 | 15 | try { |
361 | 16 | await client.connect(); |
362 | | - console.log("OpenRGB REST API server running at http://localhost:3000"); |
363 | | - } catch (e) { |
364 | | - console.error("Failed to connect to OpenRGB server:", e); |
365 | | - process.exit(1); |
| 17 | + const devices = await client.getAllControllerData(); |
| 18 | + res.status(200).json(devices); |
| 19 | + } catch (err) { |
| 20 | + res.status(500).json({ error: err.message || "Failed to fetch devices" }); |
366 | 21 | } |
367 | 22 | }); |
| 23 | + |
| 24 | +app.listen(3000, () => console.log("OpenRGB REST API running at http://localhost:3000")); |
0 commit comments