From 6afe6b29b973feaa3c4ac4e44ce01f024ba7bf07 Mon Sep 17 00:00:00 2001 From: Lukas Feye Date: Sat, 31 Jan 2026 17:46:29 +0100 Subject: [PATCH 1/2] Enhance Neewer GL1 control script with MAC address support for IP resolution and update README. Adjust command delay and improve error handling for missing parameters. Bump version to 2.0.1. --- README.md | 24 +++++++++++--- index.mjs | 79 ++++++++++++++++++++++++++++++++++++++--------- package-lock.json | 4 +-- src/index.coffee | 51 +++++++++++++++++++++++------- 4 files changed, 126 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 3166e42..add3869 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,25 @@ This script allows you to interact with a Neewer GL1 LED panel over your network. You must configure the panel to connect to your wifi network for this script to work. -The Neewer GL1 is a lower cost alternative to an El Gato Key Light. It supports Wifi (configured via mobile app). I reverse engineered the protocol between my computer and the light and found that the light listens for commands over UDP on port 5052. As far as I can tell, the light does not return any data such as current state. +The Neewer GL1 is a lower cost alternative to an El Gato Key Light. It supports Wifi (configured via mobile app). Out of the box the light broadcasts its own SSID for setup (e.g. **ATK-625BFB**); you connect to that from the Neewer app to tell the light your home network. I reverse engineered the protocol between my computer and the light and found that the light listens for commands over UDP on port 5052. As far as I can tell, the light does not return any data such as current state. By being able to control the light using a script, you can integrate controls with something like a Stream Deck, which is how I use it. As of Version 2.0.0, you no longer need to keep the Neewer Live application running for it to work. In any case, the Neewer Live app on windows is a raging dumpster fire. If for whatever reason the app crashes, or your computer crashes, the app will stop working, because the `DeviceInfo.xml` and/or `UserInfo.xml` file(s) will be corrupted. That's because the executable is constantly updating those files, *and* requires them for operation. The application does not self-heal those files once they're corrupted. You basically need to back up those files and copy them back in when something goes wrong. -In order to use this script, you basically need to know the IP adddress of the light, which you can get after you configure the light using the Neewer mobile app. You will also need to know the IP address of your computer, which is easy enough. +In order to use this script, you need either the light’s IP address or hostname (`-h`) or its MAC address (`-m`). You can get the IP after configuring the light with the Neewer mobile app (though not within the app), or from your router or Wireshark. Using `-m` (MAC) looks up the current IP from your ARP table, so the light’s IP can change (e.g. DHCP) and the script still finds it—as long as something has talked to the light recently (this script or the Neewer app). You will also need to know the IP address of your computer, which is easy enough (`ipconfig getifaddr en0`). ## Parameters ``` --h, --host [required] ip or hostname +-h, --host [char] light IP or hostname (use -h OR -m) +-m, --mac [char] light MAC address (e.g. 08:F9:E0:62:5B:FB ). Resolves IP from your ARP table so the light’s IP can change (DHCP). Use -h OR -m. -I, --client_ip [char] your computer's IP. If you don't provide it, the script will try to guess your IP (first one it finds) -H, --hex -p, --power [on,off] --b, --brightness (requires -t) 1-100 --t, --temperature (requires -b) 29-70 +-b, --brightness 1-100 (optional; default 100 if only -t is set) +-t, --temperature 29-70 (optional; default 50 = 5000K if only -b is set) -d, --delay [int] in milliseconds. Default is 500. YMMV if you shorten the delay. The script may run faster, but may not execute. ``` @@ -55,12 +56,25 @@ node index.mjs -h 192.168.1.236 -p on -b 10 -t 33 -I 192.168.1.100 -d 400 # turn light off, script guesses your IP node index.mjs -h 192.168.1.236 -p off +# using hostname (e.g. neewergl1pro from router/DHCP) +node index.mjs -h neewergl1pro -p on +node index.mjs -h neewergl1pro -p off -b 50 -t 33 + # set brightness to 10%, temperature to 3300k (light must be turned on first) node index.mjs -h 192.168.1.236 -b 10 -t 33 # set brightness to 10%, temperature to 5000k using manual hex node index.mjs -h 192.168.1.236 -H 800503020a32c6 +# use MAC so the light’s IP can change (resolved from ARP) +node index.mjs -m 08:F9:E0:62:5B:FB -p on -I 192.168.178.165 +node index.mjs -m 08:F9:E0:62:5B:FB -p off + +# set only brightness (temperature defaults to 5000K) +node index.mjs -h 192.168.1.236 -b 50 +# set only temperature (brightness defaults to 100%) +node index.mjs -h 192.168.1.236 -t 33 + ``` ## Protocol Details diff --git a/index.mjs b/index.mjs index bac944a..4784cc7 100644 --- a/index.mjs +++ b/index.mjs @@ -1,6 +1,6 @@ // import dgram from 'dgram' // dgram with promises -var PORT, command_delay, command_queue, convertIp, guessIp, hexify, options, parse_and_execute, presets, send; +var PORT, command_delay, command_queue, convertIp, guessIp, hexify, normalizeMac, options, parse_and_execute, presets, resolveMacToIp, send; import { DgramAsPromised @@ -16,6 +16,10 @@ import program from 'commander'; import os from 'os'; +import { + execSync +} from 'child_process'; + PORT = 5052; presets = { @@ -23,7 +27,7 @@ presets = { "off": "800502010088" }; -program.version("Neewer GL1 Key Light Control 2.0").option("-h, --host [char]").option("-H, --hex [char]").option("-I, --client_ip [char]").option("-p, --power [off/on]").option("-b, --brightness [int]").option("-t, --temperature [int]").option("-d, --delay [int]").parse(process.argv); +program.version("Neewer GL1 Key Light Control 2.0").option("-h, --host [char]").option("-m, --mac [char]", "light MAC address (e.g. 08:F9:E0:62:5B:FB ); resolves IP from ARP so IP changes are OK").option("-H, --hex [char]").option("-I, --client_ip [char]").option("-p, --power [off/on]").option("-b, --brightness [int]").option("-t, --temperature [int]").option("-d, --delay [int]").parse(process.argv); // Default command prints out a list of ports program.parse(); @@ -32,7 +36,7 @@ options = program.opts(); command_queue = []; -command_delay = 500; +command_delay = 50; send = async function(host, port) { var bytes, client, closed, hexCommand, message; @@ -75,6 +79,37 @@ guessIp = function() { return addresses.first().address; }; +normalizeMac = function(mac) { + var hex = mac.toLowerCase().replace(/[^a-f0-9]/g, ''); + return hex.padStart(12, '0'); +}; + +resolveMacToIp = function(mac) { + var i, ip, len, line, lineMac, match, normalized, output, ref; + normalized = normalizeMac(mac); + try { + output = execSync('arp -a', { + encoding: 'utf8' + }); + } catch (error) { + return null; + } + ref = output.split('\n'); + for (i = 0, len = ref.length; i < len; i++) { + line = ref[i]; + // macOS: ? (192.168.178.88) at 08:F9:E0:62:5B:FB on en0 + match = line.match(/\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:.-]+)/); + if (match) { + ip = match[1]; + lineMac = normalizeMac(match[2]); + if (lineMac === normalized) { + return ip; + } + } + } + return null; +}; + hexify = function(brightness, temperature) { var hex, setting; setting = [128, 5, 3, 2]; @@ -87,8 +122,20 @@ hexify = function(brightness, temperature) { }; parse_and_execute = async function(options) { - var initCommand, ipAddress; - if (options.host != null) { + var brightness, host, initCommand, ipAddress, ref, ref1, temperature; + host = options.mac ? resolveMacToIp(options.mac) : options.host; + if (options.mac && !host) { + console.log(chalk.red(`MAC ${options.mac} not found in ARP table. Use the Neewer app or ping the light once so it appears.`)); + return; + } + if (!host) { + console.log(chalk.red("Provide -h/--host (light IP) or -m/--mac (light MAC) to run.")); + return; + } + if (host != null) { + if (options.mac != null) { + console.log(chalk.green(`Resolved ${options.mac} -> ${host}`)); + } if (options.client_ip != null) { ipAddress = options.client_ip; console.log(chalk.green(`Using ${ipAddress} as the local IP address`)); @@ -104,7 +151,7 @@ parse_and_execute = async function(options) { initCommand = `80021000000d${convertIp(ipAddress)}2e`; // console.log initCommand command_queue.append([initCommand, initCommand, initCommand]); - console.log(`${chalk.yellow("Light Host:")} ${options.host}:${PORT}`); + console.log(`${chalk.yellow("Light Host:")} ${host}:${PORT}`); if (options.hex != null) { console.log(`${chalk.red("Hex Override:")} ${options.hex}`); // send options.host, PORT, options.hex @@ -124,16 +171,20 @@ parse_and_execute = async function(options) { console.log(chalk.red(`Invalid power state '${options.power.toLowerCase()}'. Valid states are 'on' and 'off' only.`)); } } - if ((options.brightness != null) && (options.temperature != null)) { - console.log(chalk.yellow(`Set brightness to ${options.brightness}% and temperature to ${options.temperature}00K`)); - command_queue.append(hexify(options.brightness, options.temperature)); - } else { - console.log(chalk.red("When setting brightness or temperature, BOTH parameters are required.")); + if ((options.brightness != null) || (options.temperature != null)) { + brightness = (ref = options.brightness) != null ? ref : 100; + temperature = (ref1 = options.temperature) != null ? ref1 : 50; + if (options.brightness == null) { + console.log(chalk.yellow(`Brightness not set, using default ${brightness}%`)); + } + if (options.temperature == null) { + console.log(chalk.yellow(`Temperature not set, using default ${temperature}00K`)); + } + console.log(chalk.yellow(`Set brightness to ${brightness}% and temperature to ${temperature}00K`)); + command_queue.append(hexify(brightness, temperature)); } } - return (await send(options.host, PORT)); - } else { - return console.log(chalk.red("No options provided. Did not run.")); + return (await send(host, PORT)); } }; diff --git a/package-lock.json b/package-lock.json index 265d9f6..5ab605b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "neewer-gl1", - "version": "1.0.0", + "version": "2.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "neewer-gl1", - "version": "1.0.0", + "version": "2.0.1", "license": "MIT", "dependencies": { "chalk": "^5.0.1", diff --git a/src/index.coffee b/src/index.coffee index 275cc63..7f7de46 100644 --- a/src/index.coffee +++ b/src/index.coffee @@ -7,6 +7,7 @@ import chalk from "chalk" import program from 'commander' import os from 'os' +import { execSync } from 'child_process' PORT = 5052 @@ -18,6 +19,7 @@ presets = program .version("Neewer GL1 Key Light Control 2.0") .option("-h, --host [char]") +.option("-m, --mac [char]", "light MAC address (e.g. 08:F9:E0:62:5B:FB ); resolves IP from ARP so IP changes are OK") .option("-H, --hex [char]") .option("-I, --client_ip [char]") .option("-p, --power [off/on]") @@ -70,6 +72,25 @@ guessIp = ()-> addresses = addresses.flatten().filter( {family: "IPv4"} ) return addresses.first().address +normalizeMac = (mac)-> + hex = mac.toLowerCase().replace(/[^a-f0-9]/g, '') + hex.padStart(12, '0') + +resolveMacToIp = (mac)-> + normalized = normalizeMac(mac) + try + output = execSync('arp -a', { encoding: 'utf8' }) + catch + return null + for line in output.split('\n') + # macOS: ? (192.168.178.88) at 08:F9:E0:62:5B:FB on en0 + match = line.match(/\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:.-]+)/) + if match + ip = match[1] + lineMac = normalizeMac(match[2]) + return ip if lineMac is normalized + null + hexify = (brightness, temperature)-> setting = [128,5,3,2] setting.append [parseInt(brightness),parseInt(temperature)] @@ -79,8 +100,16 @@ hexify = (brightness, temperature)-> return hex.join("") parse_and_execute = (options)-> - if options.host? - + host = if options.mac then resolveMacToIp(options.mac) else options.host + if options.mac and not host + console.log chalk.red("MAC #{options.mac} not found in ARP table. Use the Neewer app or ping the light once so it appears.") + return + if not host + console.log chalk.red("Provide -h/--host (light IP) or -m/--mac (light MAC) to run.") + return + if host? + if options.mac? + console.log chalk.green("Resolved #{options.mac} -> #{host}") if options.client_ip? ipAddress = options.client_ip console.log chalk.green("Using #{ipAddress} as the local IP address") @@ -98,7 +127,7 @@ parse_and_execute = (options)-> command_queue.append [ initCommand, initCommand, initCommand ] - console.log "#{chalk.yellow("Light Host:")} #{options.host}:#{PORT}" + console.log "#{chalk.yellow("Light Host:")} #{host}:#{PORT}" if options.hex? console.log "#{chalk.red("Hex Override:")} #{options.hex}" # send options.host, PORT, options.hex @@ -116,13 +145,13 @@ parse_and_execute = (options)-> else console.log chalk.red("Invalid power state '#{options.power.toLowerCase()}'. Valid states are 'on' and 'off' only.") - if options.brightness? && options.temperature? - console.log chalk.yellow("Set brightness to #{options.brightness}% and temperature to #{options.temperature}00K") - command_queue.append hexify(options.brightness, options.temperature) - else - console.log chalk.red("When setting brightness or temperature, BOTH parameters are required.") - await send options.host, PORT - else - console.log chalk.red("No options provided. Did not run.") + if options.brightness? or options.temperature? + brightness = options.brightness ? 100 + temperature = options.temperature ? 50 + console.log chalk.yellow("Brightness not set, using default #{brightness}%") if not options.brightness? + console.log chalk.yellow("Temperature not set, using default #{temperature}00K") if not options.temperature? + console.log chalk.yellow("Set brightness to #{brightness}% and temperature to #{temperature}00K") + command_queue.append hexify(brightness, temperature) + await send host, PORT parse_and_execute(options) From 332e95e76c7c12f865028b2204001132bc0c702c Mon Sep 17 00:00:00 2001 From: Lukas Feye Date: Sun, 1 Feb 2026 23:38:38 +0100 Subject: [PATCH 2/2] Refactor Neewer GL1 control script to implement a handshake mechanism for command execution. Update README to clarify client IP detection and command requirements. Adjust command delay to 50ms for improved performance. --- README.md | 32 +++++++-------- index.mjs | 121 ++++++++++++++++++++++++++++++++++++------------------ 2 files changed, 97 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index add3869..1cbe60a 100644 --- a/README.md +++ b/README.md @@ -9,19 +9,19 @@ By being able to control the light using a script, you can integrate controls wi As of Version 2.0.0, you no longer need to keep the Neewer Live application running for it to work. In any case, the Neewer Live app on windows is a raging dumpster fire. If for whatever reason the app crashes, or your computer crashes, the app will stop working, because the `DeviceInfo.xml` and/or `UserInfo.xml` file(s) will be corrupted. That's because the executable is constantly updating those files, *and* requires them for operation. The application does not self-heal those files once they're corrupted. You basically need to back up those files and copy them back in when something goes wrong. -In order to use this script, you need either the light’s IP address or hostname (`-h`) or its MAC address (`-m`). You can get the IP after configuring the light with the Neewer mobile app (though not within the app), or from your router or Wireshark. Using `-m` (MAC) looks up the current IP from your ARP table, so the light’s IP can change (e.g. DHCP) and the script still finds it—as long as something has talked to the light recently (this script or the Neewer app). You will also need to know the IP address of your computer, which is easy enough (`ipconfig getifaddr en0`). +In order to use this script, you need either the light’s IP address or hostname (`-h`) or its MAC address (`-m`). You can get the IP after configuring the light with the Neewer mobile app (though not within the app), or from your router or Wireshark. Using `-m` (MAC) looks up the current IP from your ARP table, so the light’s IP can change (e.g. DHCP) and the script still finds it—as long as something has talked to the light recently (this script or the Neewer app). You will also need the IP address of your computer for the handshake. If you don't pass `-I`, the script tries to detect it (first non-loopback IPv4). If that fails (e.g. multiple interfaces or VPN), set it explicitly with `-I` (e.g. `ipconfig getifaddr en0` on macOS, or your router's client list). ## Parameters ``` -h, --host [char] light IP or hostname (use -h OR -m) -m, --mac [char] light MAC address (e.g. 08:F9:E0:62:5B:FB ). Resolves IP from your ARP table so the light’s IP can change (DHCP). Use -h OR -m. --I, --client_ip [char] your computer's IP. If you don't provide it, the script will try to guess your IP (first one it finds) +-I, --client_ip [char] your computer's IP. If you don't provide it, the script guesses (first non-loopback IPv4). If detection fails, you must set it with -I (e.g. -I 192.168.178.165). -H, --hex -p, --power [on,off] --b, --brightness 1-100 (optional; default 100 if only -t is set) --t, --temperature 29-70 (optional; default 50 = 5000K if only -b is set) --d, --delay [int] in milliseconds. Default is 500. YMMV if you shorten the delay. The script may run faster, but may not execute. +-b, --brightness 1-100 (requires -t when used) +-t, --temperature 29-70 (requires -b when used; 50 = 5000K) +-d, --delay [int] in milliseconds. Default is 50. YMMV if you shorten the delay. The script may run faster, but may not execute. ``` You can generally mix and match the parameters as required, with exceptions: @@ -70,10 +70,8 @@ node index.mjs -h 192.168.1.236 -H 800503020a32c6 node index.mjs -m 08:F9:E0:62:5B:FB -p on -I 192.168.178.165 node index.mjs -m 08:F9:E0:62:5B:FB -p off -# set only brightness (temperature defaults to 5000K) -node index.mjs -h 192.168.1.236 -b 50 -# set only temperature (brightness defaults to 100%) -node index.mjs -h 192.168.1.236 -t 33 +# set brightness and temperature (both required) +node index.mjs -h 192.168.1.236 -b 50 -t 33 ``` @@ -88,22 +86,22 @@ I'm not sure how the Neewer Live app scans the network for the lights, but to co To reverse engineer the protocol, you can watch traffic between your computer and the light using Wireshark. -You'll notice that there's an initializing handshake between Neewer Live and your light, where it sends a message three times, and then the app sends a constant stream of short heartbeat messages afterwards. +You'll notice that there's an initializing handshake between Neewer Live and your light, where it sends a message three times, the light replies with `80:03` to accept the handshake, and then the app can send commands. This script does the same: it sends three handshake messages, waits for the light's `80:03` reply, then sends power/brightness/temperature commands on the same socket. -The light never sends any information back, so if you change your light settings manually, your light will be out of sync with the app's state for the light. +The light does not report status (e.g. current brightness), so if you change settings manually on the light, the app/script has no way to know. It does send a brief `80:03` reply to accept the handshake. -I eventually realized that the command structure works like this: +The command structure works like this: -1. Your computer sends three UDP hex messages containing your computer's IP in succession. -2. You can send any command or a heartbeat to the light to keep the window for the light to receive commands from your computer's IP open. -3. If no heartbeat command is received within a certain time window, then any further commands are rejected unless you resend the commands in #1. +1. Your computer sends three UDP handshake messages (containing your computer's IP) in succession. The light replies with `80:03` when it accepts the handshake. +2. After that, you can send power, brightness, or temperature commands on the same socket. The script waits for `80:03` before sending these. +3. If no command is received within a certain time window, the light may close the session and you must repeat the handshake (steps 1–2). -There is a finite time window for a command to be received. If it's too fast or too slow, then the commands will be rejected. I could not be bothered to figure out this exact timing (but it is 4-5 times per second), as I only care that the script works for my uses. I have included an overridable delay between issuing UDP requests, defaulted to 500ms. I have found setting it shorter can result in faster response times for the light, but missed commands too. YMMV. +There is a finite time window for a command to be received. If it's too fast or too slow, then the commands will be rejected. I could not be bothered to figure out this exact timing (but it is 4-5 times per second), as I only care that the script works for my uses. I have included an overridable delay between issuing UDP requests, defaulted to 50ms. I have found setting it shorter can result in faster response times for the light, but missed commands too. YMMV. ### Commands -The handshake/wakeup command looks like this: `80021000000d3139322e3136382e312e3130382e` for client IP address 192.168.1.108. The structure of the message is `80 02 10 00 00 0d [ip ascii to hex]`, terminated by `2e` +The handshake/wakeup command has the structure `80 02 12 00 00 0f [your IP as 15 ASCII chars, each as hex] [checksum]`, where the checksum is the low byte of the sum of all preceding bytes (e.g. for IP 192.168.1.108 the payload is the ASCII bytes of that string as hex, plus the checksum byte). To turn on the light, you send the hexadecimal code `800502010189` over UDP. To turn off the light, you send the hexadecimal code `800502010088` over UDP. diff --git a/index.mjs b/index.mjs index 4784cc7..d4645ce 100644 --- a/index.mjs +++ b/index.mjs @@ -1,6 +1,6 @@ // import dgram from 'dgram' // dgram with promises -var PORT, command_delay, command_queue, convertIp, guessIp, hexify, normalizeMac, options, parse_and_execute, presets, resolveMacToIp, send; +var PORT, buildHandshake, command_delay, command_queue, guessIp, hexify, normalizeMac, options, parse_and_execute, presets, resolveMacToIp, send; import { DgramAsPromised @@ -27,7 +27,7 @@ presets = { "off": "800502010088" }; -program.version("Neewer GL1 Key Light Control 2.0").option("-h, --host [char]").option("-m, --mac [char]", "light MAC address (e.g. 08:F9:E0:62:5B:FB ); resolves IP from ARP so IP changes are OK").option("-H, --hex [char]").option("-I, --client_ip [char]").option("-p, --power [off/on]").option("-b, --brightness [int]").option("-t, --temperature [int]").option("-d, --delay [int]").parse(process.argv); +program.version("Neewer GL1 Key Light Control 2.0").option("-h, --host [char]").option("-m, --mac [char]", "light MAC address (e.g. 08:F9:E0:62:5B:FB); resolves IP from ARP so IP changes are OK").option("-H, --hex [char]").option("-I, --client_ip [char]").option("-p, --power [off/on]").option("-b, --brightness [int]").option("-t, --temperature [int]").option("-d, --delay [int]").parse(process.argv); // Default command prints out a list of ports program.parse(); @@ -38,45 +38,89 @@ command_queue = []; command_delay = 50; +const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + send = async function(host, port) { - var bytes, client, closed, hexCommand, message; + var bytes, client, hexCommand, message; + if (command_queue.length === 0) { + console.log("Command queue is empty! Done."); + return; + } client = DgramAsPromised.createSocket('udp4'); - if (command_queue.length > 0) { - hexCommand = command_queue.shift(); - message = new Buffer.from(hexCommand, "hex"); - bytes = (await client.send(message, 0, message.length, port, host)); //, (err, bytes) -> - console.log(chalk.blue(`Sent command [${hexCommand}] (${bytes} bytes) to ${host}:${port}`)); - - closed = (await client.close()); - // console.log "Connection closed. Going to next in queue." - // prevent commands from being issued too quickly - return send.delay(command_delay, host, port); - } else { - return console.log(chalk.green("Command queue is empty! Done.")); + await client.bind(port, '0.0.0.0'); + + const rawSocket = client.socket; + const waitFor8003Reply = () => + new Promise((resolve) => { + const onMessage = (msg) => { + if (msg.length >= 2 && msg[0] === 0x80 && msg[1] === 0x03) { + rawSocket.off("message", onMessage); + resolve(); + } + }; + rawSocket.on("message", onMessage); + }); + + try { + // Send handshake packets (first 3 in queue) with 50ms between each + for (var i = 0; i < 3 && command_queue.length > 0; i++) { + hexCommand = command_queue.shift(); + message = Buffer.from(hexCommand, "hex"); + await client.send(message, 0, message.length, port, host); + console.log(`Sent handshake [${hexCommand}] to ${host}:${port}`); + if (i < 2) await delay(command_delay); + } + + // Wait for light's 80:03 reply before sending power/other commands + await waitFor8003Reply(); + console.log("Received 80:03 reply from light."); + + // Send remaining commands (power, brightness/temp, etc.) on same socket + while (command_queue.length > 0) { + hexCommand = command_queue.shift(); + message = Buffer.from(hexCommand, "hex"); + bytes = await client.send(message, 0, message.length, port, host); + console.log(`Sent message [${hexCommand}] (${bytes} bytes) to ${host}:${port}`); + await delay(command_delay); + } + } finally { + await client.close(); + console.log("Connection closed. Done."); } }; -convertIp = function(ip) { - var ascii, hexified, segments; - segments = ip.split("."); - ascii = segments.map; - hexified = Array.from(ip).map(function(char, index) { - return char.charCodeAt(0).toString(16); +// Same handshake as index.mjs: 80 02 12 00 00 0f [IP 15 chars as hex] [checksum = sum of all bytes & 0xff] +buildHandshake = function(ip) { + var all, checksum, header, headerHex, ipBytes, ipHex, sum; + header = [0x80, 0x02, 0x12, 0x00, 0x00, 0x0f]; + ipBytes = Array.from(ip).map(function(c) { + return c.charCodeAt(0); }); - return hexified.join(""); + all = header.concat(ipBytes); + sum = all.reduce(function(a, b) { + return a + b; + }, 0); + checksum = (sum & 0xff).toString(16).padStart(2, "0"); + headerHex = header.map(function(b) { + return b.toString(16).padStart(2, "0"); + }).join(""); + ipHex = Array.from(ip).map(function(c) { + return c.charCodeAt(0).toString(16); + }).join(""); + return `${headerHex}${ipHex}${checksum}`; }; guessIp = function() { - var addresses, nics; + var addresses, nics, ref; nics = os.networkInterfaces(); addresses = []; Object.keys(nics).forEach(function(key) { return addresses.push(nics[key]); }); - addresses = addresses.flatten().filter({ - family: "IPv4" + addresses = addresses.flatten().filter(function(addr) { + return addr.family === "IPv4" && addr.address !== "127.0.0.1"; }); - return addresses.first().address; + return (ref = addresses.first()) != null ? ref.address : void 0; }; normalizeMac = function(mac) { @@ -122,7 +166,7 @@ hexify = function(brightness, temperature) { }; parse_and_execute = async function(options) { - var brightness, host, initCommand, ipAddress, ref, ref1, temperature; + var host, initCommand, ipAddress; host = options.mac ? resolveMacToIp(options.mac) : options.host; if (options.mac && !host) { console.log(chalk.red(`MAC ${options.mac} not found in ARP table. Use the Neewer app or ping the light once so it appears.`)); @@ -141,15 +185,17 @@ parse_and_execute = async function(options) { console.log(chalk.green(`Using ${ipAddress} as the local IP address`)); } else { ipAddress = guessIp(); + if (!ipAddress) { + console.log(chalk.red("Could not determine your computer's IP (no non-loopback IPv4). Use -I to set it (e.g. -I 192.168.178.165).")); + return; + } console.log(chalk.yellow(`client_ip not provided, using ${ipAddress} as the local IP address`)); } if (options.delay != null) { command_delay = options.delay; } console.log(`Command delay set to ${command_delay}ms`); - //init command - initCommand = `80021000000d${convertIp(ipAddress)}2e`; - // console.log initCommand + initCommand = buildHandshake(ipAddress); command_queue.append([initCommand, initCommand, initCommand]); console.log(`${chalk.yellow("Light Host:")} ${host}:${PORT}`); if (options.hex != null) { @@ -172,16 +218,13 @@ parse_and_execute = async function(options) { } } if ((options.brightness != null) || (options.temperature != null)) { - brightness = (ref = options.brightness) != null ? ref : 100; - temperature = (ref1 = options.temperature) != null ? ref1 : 50; - if (options.brightness == null) { - console.log(chalk.yellow(`Brightness not set, using default ${brightness}%`)); - } - if (options.temperature == null) { - console.log(chalk.yellow(`Temperature not set, using default ${temperature}00K`)); + if ((options.brightness != null) && (options.temperature != null)) { + console.log(chalk.yellow(`Set brightness to ${options.brightness}% and temperature to ${options.temperature}00K`)); + command_queue.append(hexify(options.brightness, options.temperature)); + } else { + console.log(chalk.red("When setting brightness or temperature, BOTH parameters are required.")); + return; } - console.log(chalk.yellow(`Set brightness to ${brightness}% and temperature to ${temperature}00K`)); - command_queue.append(hexify(brightness, temperature)); } } return (await send(host, PORT));