npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

lifxlan

v0.0.62

Published

LIFX LAN protocol in JavaScript

Downloads

103

Readme

No dependencies. Bring your own socket.

Works with Node.js, Bun, and Deno.

Examples

Node.js / Bun

import dgram from 'node:dgram';
import { Client, Router, Devices, GetServiceCommand } from 'lifxlan';

const socket = dgram.createSocket('udp4');

// Router handles outgoing messages and forwards responses to clients
const router = Router({
  onSend(message, port, address) {
    // A message is ready to be sent
    socket.send(message, port, address);
  },
});

// Devices keeps track of devices discovered on the network
const devices = Devices({
  onAdded(device) {
    // A device has been discovered
    console.log(device);
  },
});

socket.on('message', (message, remote) => {
  // Forward received messages to the router
  const { header, serialNumber } = router.receive(message);
  // Forward the message to devices so it can keep track
  devices.register(serialNumber, remote.port, remote.address, header.target);
});

// Client handles communication with devices
const client = Client({ router });

socket.once('listening', () => {
  socket.setBroadcast(true);
  // Discover devices on the network
  client.broadcast(GetServiceCommand());
});

socket.bind();

setTimeout(() => {
  socket.close();
}, 1000);

Deno

import { Client, Router, Devices, GetServiceCommand } from 'lifxlan';

const socket = Deno.listenDatagram({
  hostname: '0.0.0.0',
  port: 0,
  transport: 'udp',
});

const router = Router({
  onSend(message, port, hostname) {
    socket.send(message, { port, hostname });
  }
});

const devices = Devices({
  onAdded(device) {
    console.log(device);
  },
});

const client = Client({ router });

client.broadcast(GetServiceCommand());

setTimeout(() => {
  socket.close();
}, 1000);

for await (const [message, remote] of socket) {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.hostname, header.target);
}

How to turn a light on

import dgram from 'node:dgram';
import { Client, Devices, Router, GetServiceCommand, SetPowerCommand } from 'lifxlan';

const socket = dgram.createSocket('udp4');

const router = Router({
  onSend(message, port, address) {
    socket.send(message, port, address);
  },
});

const devices = Devices();

socket.on('message', (message, remote) => {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.hostname, header.target);
});

await new Promise((resolve, reject) => {
  socket.once('error', reject);
  socket.once('listening', resolve);
  socket.bind();
});

socket.setBroadcast(true);

const client = Client({ router });

// Start scanning for devices
client.broadcast(GetServiceCommand());
const scanInterval = setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 1000);

const device = await devices.get('d07123456789');

// Stop scanning since device was found
clearInterval(scanInterval);

await client.sendOnlyAcknowledge(SetPowerCommand(true), device);

socket.close();

How to retry

for (let i = 0; i < 3; i++) {
  try {
    console.log(await client.send(GetColorCommand(), device));
    break;
  } catch (err) {
    const delay = Math.random() * Math.min(Math.pow(2, i) * 1000, 30 * 1000);
    await new Promise((resolve) => setTimeout(resolve, delay));
  }
}

How to specify a custom timeout

const controller = new AbortController();

const timeout = setTimeout(() => {
  controller.abort();
}, 100);

try {
  console.log(await client.send(GetColorCommand(), device, controller.signal));
} finally {
  clearTimeout(timeout)
}

How to use without device discovery

import dgram from 'node:dgram';
import { Client, Device, Router, GetServiceCommand, SetPowerCommand } from 'lifxlan';

const socket = dgram.createSocket('udp4');

const router = Router({
  onSend(message, port, address) {
    socket.send(message, port, address);
  },
});

socket.on('message', (message, remote) => {
  router.receive(message);
});

await new Promise((resolve, reject) => {
  socket.once('error', reject);
  socket.once('listening', resolve);
  socket.bind();
});

const client = Client({ router });

// Create the device directly
const device = Device({
  serialNumber: 'd07123456789',
  address: '192.168.1.50',
});

await client.sendOnlyAcknowledge(SetPowerCommand(true), device);

socket.close();

How to create a custom command

/**
 * @param {Uint8Array} bytes
 * @param {{ current: number; }} offsetRef
 */
function decodeCustom(bytes, offsetRef) {
  const val1 = bytes[offsetRef.current++];
  const val2 = bytes[offsetRef.current++];
  return {
    val1,
    val2,
  };
}

function CustomCommand() {
  return {
    type: 1234,
    decode: decodeCustom,
  };
}

const res = await client.send(CustomCommand(), device);

console.log(res.val1, res.val2);

How to use multiple clients

import dgram from 'node:dgram';
import { Client, Devices, Router, GetServiceCommand, SetPowerCommand } from 'lifxlan';

const socket = dgram.createSocket('udp4');

const router = Router({
  onSend(message, port, address) {
    socket.send(message, port, address);
  },
});

const devices = Devices();

socket.on('message', (message, remote) => {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.hostname, header.target);
});

await new Promise((resolve, reject) => {
  socket.once('error', reject);
  socket.once('listening', resolve);
  socket.bind();
});

socket.setBroadcast(true);

const client1 = Client({ router });

const client2 = Client({ router });

client1.broadcast(GetServiceCommand());
const scanInterval = setInterval(() => {
  client1.broadcast(GetServiceCommand());
}, 1000);

const device = await devices.get('d07123456789');

clearInterval(scanInterval);

await client2.sendOnlyAcknowledge(SetPowerCommand(true), device);

socket.close();

How to use a lot of clients

while (true) {
  const client = Client({ router });

  console.log(await client.send(GetPowerCommand(), device));

  // When creating a lot of clients, call dispose to avoid running out of source values
  client.dispose();
}

How to use one socket for broadcast messages and another socket for unicast messages

import dgram from 'node:dgram';
import { Client, Devices, GetServiceCommand } from 'lifxlan';

const broadcastSocket = dgram.createSocket('udp4');
const unicastSocket = dgram.createSocket('udp4');

const router = Router({
  onSend(message, port, address, serialNumber) {
    if (!serialNumber) {
      broadcastSocket.send(message, port, address);
    } else {
      unicastSocket.send(message, port, address);
    }
  },
});

const devices = Devices();

/**
 * @param {Uint8Array} message
 * @param {{ port: number; address: string; }} remote
 */
function onMessage(message, remote) {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.address, header.target);
}

broadcastSocket.on('message', onMessage);
unicastSocket.on('message', onMessage);

await Promise.all([
  new Promise((resolve, reject) => {
    broadcastSocket.once('error', reject);
    broadcastSocket.once('listening', resolve);
    broadcastSocket.bind();
  }),
  new Promise((resolve, reject) => {
    unicastSocket.once('error', reject);
    unicastSocket.once('listening', resolve);
    unicastSocket.bind();
  }),
]);

broadcastSocket.setBroadcast(true);

const client = Client({ router });

client.broadcast(GetServiceCommand());
const scanInterval = setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 1000);

const device = await devices.get('d07123456789');

clearInterval(scanInterval);

await client.sendOnlyAcknowledge(SetPowerCommand(true), device);

broadcastSocket.close();
unicastSocket.close();

How to use one socket per device

import dgram from 'node:dgram';
import { Client, Device, Router, Devices, GetServiceCommand, SetColorCommand } from 'lifxlan';

/**
 * @param {Uint8Array} message
 * @param {{ port: number; address: string; }} remote 
 */
function onMessage(message, remote) {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.address, header.target);
}

const broadcastSocket = dgram.createSocket('udp4');

broadcastSocket.on('message', onMessage);

await new Promise((resolve, reject) => {
  broadcastSocket.once('error', reject);
  broadcastSocket.once('listening', resolve);
  broadcastSocket.bind();
});

broadcastSocket.setBroadcast(true);

/**
 * @type {Map<string, dgram.Socket>}
 */
const deviceSockets = new Map();

const router = Router({
  onSend(message, port, address, serialNumber) {
    if (!serialNumber) {
      broadcastSocket.send(message, port, address);
    } else {
      const socket = deviceSockets.get(serialNumber);
      if (socket) {
        socket.send(message);
      }
    }
  },
});

/**
 * @param {Device} device 
 */
function setupDeviceSocket(device) {
  const socket = dgram.createSocket('udp4');
  socket.on('message', onMessage);
  socket.bind();
  socket.connect(device.port, device.address);
  deviceSockets.set(device.serialNumber, socket);
}

const devices = Devices({
  onAdded(device) {
    setupDeviceSocket(device);
  },
  onChanged(device) {
    const oldSocket = deviceSockets.get(device.serialNumber);
    if (oldSocket) {
      deviceSockets.delete(device.serialNumber);
      oldSocket.close();
    }
    setupDeviceSocket(device);
  },
});

const client = Client({ router });

client.broadcast(GetServiceCommand());
setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 5000);

const PARTY_COLORS = /** @type {const} */ ([
  [48241, 65535, 65535, 3500],
  [43690, 49151, 65535, 3500],
  [54612, 65535, 65535, 3500],
  [43690, 65535, 65535, 3500],
  [38956, 55704, 65535, 3500],
]);

while (true) {
  const deviceCount = devices.registered.size;

  if (deviceCount > 0) {
    const waitTime = Math.min(2000 / deviceCount, 100);

    for (const device of devices.registered.values()) {
      const [hue, saturation, brightness, kelvin] = PARTY_COLORS[Math.random() * PARTY_COLORS.length | 0];
      client.unicast(SetColorCommand(hue, saturation, brightness, kelvin, 2000), device);
      await new Promise((resolve) => setTimeout(resolve, waitTime));
    }
  } else {
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

Same as the previous example but with only one socket

import dgram from 'node:dgram';
import { Client, Router, Devices, GetServiceCommand, SetColorCommand } from 'lifxlan';

const socket = dgram.createSocket('udp4');

socket.on('message', (message, remote) => {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.address, header.target);
});

await new Promise((resolve, reject) => {
  socket.once('error', reject);
  socket.once('listening', resolve);
  socket.bind();
});

socket.setBroadcast(true);

const router = Router({
  onSend(message, port, address) {
    socket.send(message, port, address);
  },
});

const devices = Devices();

const client = Client({ router });

client.broadcast(GetServiceCommand());
setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 250);

const PARTY_COLORS = /** @type {const} */ ([
  [48241, 65535, 65535, 3500],
  [43690, 49151, 65535, 3500],
  [54612, 65535, 65535, 3500],
  [43690, 65535, 65535, 3500],
  [38956, 55704, 65535, 3500],
]);

while (true) {
  const deviceCount = devices.registered.size;

  if (deviceCount > 0) {
    const waitTime = Math.min(2000 / deviceCount, 100);
    console.log(deviceCount);

    for (const device of devices.registered.values()) {
      const [hue, saturation, brightness, kelvin] = PARTY_COLORS[Math.random() * PARTY_COLORS.length | 0];
      client.unicast(SetColorCommand(hue, saturation, brightness, kelvin, 2000), device);
      await new Promise((resolve) => setTimeout(resolve, waitTime));
    }
  } else {
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

How to discover and organize devices into groups

import dgram from 'node:dgram';
import { Client, Router, Devices, Groups, GetServiceCommand, GetGroupCommand } from 'lifxlan';

const socket = dgram.createSocket('udp4');

await new Promise((resolve, reject) => {
  socket.once('error', reject);
  socket.once('listening', resolve);
  socket.bind();
});

socket.setBroadcast(true);

const router = Router({
  onSend(message, port, address) {
    socket.send(message, port, address);
  },
});

const client = Client({ router });

const groups = Groups({
  onAdded(group) {
    console.log('Group added', group);
  },
  onChanged(group) {
    console.log('Group changed', group);
  },
});

const devices = Devices({
  async onAdded(device) {
    const group = await client.send(GetGroupCommand(), device);
    groups.register(device, group);
  },
});

socket.on('message', (message, remote) => {
  const { header, serialNumber } = router.receive(message);
  devices.register(serialNumber, remote.port, remote.address, header.target);
});

client.broadcast(GetServiceCommand());
const scanInterval = setInterval(() => {
  client.broadcast(GetServiceCommand());
}, 250);

setTimeout(() => {
  clearInterval(scanInterval);
  socket.close();
}, 2000);

How to send a command to all devices discovered in a group

await Promise.all(group.devices.map((device) => client.send(GetLabelCommand(), device, signal)));

How to keep group devices sorted when the devices are discovered or removed

const groups = Groups({
  onChanged(group) {
    group.devices.sort((deviceA, deviceB) => {
      if (deviceA.serialNumber < deviceB.serialNumber) {
        return -1;
      }
      return 1;
    });
  }
});