0.0.67 • Published 6 months ago

lifxlan v0.0.67

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

No dependencies. Bring your own socket.

Works with Node.js, Bun, and Deno.

Examples

Node.js / Bun (udp broadcast is not supported in Bun yet https://github.com/oven-sh/bun/issues/10381)

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', () => {
  // Broadcast is not supported in Bun yet https://github.com/oven-sh/bun/issues/10381
  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(
  [broadcastSocket, unicastSocket].map((socket) => (
    new Promise((resolve, reject) => {
      socket.once('error', reject);
      socket.once('listening', resolve);
      socket.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;
    });
  }
});

How to run a callback for every message received by a router

import { Router } from 'lifxlan';

const router = Router({
  onMessage(header, payload, serialNumber) {
    // Called for every message received by the router
  },
});

How to run a callback for every message received by a client

import { Client, Router } from 'lifxlan';

const router = Router({
  onSend(message, port, address) {
    // Send the message over the socket
  },
});

const client = Client({
  router,
  onMessage(header, payload, serialNumber) {
    // Called for every message received by the client
  },
});

// ...
0.0.63

6 months ago

0.0.64

6 months ago

0.0.65

6 months ago

0.0.66

6 months ago

0.0.67

6 months ago

0.0.62

10 months ago

0.0.60

11 months ago

0.0.61

10 months ago

0.0.59

11 months ago

0.0.56

11 months ago

0.0.57

11 months ago

0.0.58

11 months ago

0.0.53

11 months ago

0.0.54

11 months ago

0.0.55

11 months ago

0.0.52

1 year ago

0.0.51

1 year ago

0.0.50

1 year ago

0.0.48

1 year ago

0.0.49

1 year ago

0.0.47

1 year ago

0.0.46

1 year ago

0.0.44

1 year ago

0.0.45

1 year ago

0.0.43

1 year ago

0.0.42

1 year ago

0.0.40

1 year ago

0.0.41

1 year ago

0.0.37

2 years ago

0.0.38

2 years ago

0.0.39

1 year ago

0.0.36

2 years ago

0.0.34

2 years ago

0.0.33

2 years ago

0.0.24

2 years ago

0.0.25

2 years ago

0.0.30

2 years ago

0.0.26

2 years ago

0.0.27

2 years ago

0.0.28

2 years ago

0.0.29

2 years ago

0.0.22

2 years ago

0.0.23

2 years ago

0.0.20

2 years ago

0.0.21

2 years ago

0.0.18

2 years ago

0.0.19

2 years ago

0.0.16

2 years ago

0.0.17

2 years ago

0.0.12

2 years ago

0.0.13

2 years ago

0.0.14

2 years ago

0.0.15

2 years ago

0.0.11

2 years ago

0.0.10

2 years ago

0.0.9

2 years ago

0.0.8

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.3

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago

0.0.0

2 years ago