![Pulse-Eight logo](https://pulseeight.files.wordpress.com/2016/02/pulse-eight-logo-white-on-green.png?w=200)

# libCEC — Node.js binding

A native [N-API](https://nodejs.org/api/n-api.html) addon that binds libCEC
through its C API (`include/cecc.h`), the same surface the .NET binding
(`src/dotnetlib`) uses. It lets Node.js applications control CEC-capable HDMI
devices — power a TV on/standby, become the active source, send remote keys,
read device state — and receive bus events (log, key presses, commands,
source changes, alerts).

It works anywhere libCEC and a C++ toolchain do: Linux, macOS, Raspberry Pi and
Windows, over the Pulse-Eight USB-CEC adapter or a SoC-native CEC backend.

## Requirements

* **libCEC** installed with its development files. On Unix these are discovered
  via `pkg-config` (`pkg-config --exists libcec`) — on Debian/Ubuntu that is the
  `libcec8-dev` package; or build and `make install` this repository first.
* **Node.js ≥ 16** and a C++17 toolchain (`node-gyp` prerequisites: a compiler,
  `make`, and Python 3).

## Install

```
npm install libcec
```

The package carries sources, not a prebuilt binary: installing runs `node-gyp
rebuild`, which compiles `src/addon.cc` against the libCEC already on the
machine. The requirements above have to be met first, or the install fails at
that step. Being built on the consumer's machine is also what lets one package
serve every platform and every Node ≥ 16.

## Build from this repository

```
cd src/nodejs
npm install        # runs node-gyp rebuild, compiling src/addon.cc against libcec
node example/simple.js
```

The compiled addon lands at `build/Release/cec_native.node`.

### Windows

Windows has no `pkg-config`, so tell `node-gyp` where libCEC's headers and its
`cec.lib` import library are with two environment variables (defaults point at an
installed *USB-CEC Adapter* SDK). From a repo build:

```
set LIBCEC_INCLUDE_DIR=..\..\include
set LIBCEC_LIB_DIR=..\..\build\Release\x64\lib
npm install
```

At runtime the addon needs `cec.dll` on the DLL search path — keep it next to
`cec_native.node` (a `.node` resolves its dependencies from its own directory).
The x64 Windows installer ships a prebuilt addon set up this way under its
`nodejs` folder, so end users don't need a compiler; see
[docs/README.windows.md](../../docs/README.windows.md). The x86 installer does
not — Node.js has had no 32-bit Windows build since v23, so there is no
`node.lib` to link a 32-bit addon against.

## Test client

`client/cec-client.js` is an interactive REPL modelled on the C++ `cec-client`:
the Node counterpart for exercising the library against real hardware and a
worked example of every API call. Run it with `npm run client` (or
`node client/cec-client.js [port]`, or the installed `cec-client-node` bin):

```
$ npm run client
opening /dev/ttyACM0 ...
connection opened. type 'h' for help, 'q' to quit.
scan
...
tx 1F:82:10:00      # transfer raw bytes
on 0                # power on the TV
pow 0               # query the TV's power status
q
```

`node client/cec-client.js --help` lists the command-line options (`--list-devices`,
`--info`, `--type`, `--monitor`, `--log-level`, …).

## Usage

```js
const cec = require('libcec'); // or require('.') from this folder

const adapter = new cec.CecAdapter({
  deviceName: 'CECNode',
  deviceType: cec.CecDeviceType.RecordingDevice,
});

adapter.on('log', (m) => console.log(m.message));
adapter.on('keyPress', (k) => console.log('key', cec.userControlKeyToString(k.keycode)));
adapter.on('command', (c) => console.log('cmd', cec.opcodeToString(c.opcode)));

adapter.open(); // or adapter.open(port) with a path from detectAdapters()

adapter.powerOnDevices(cec.CecLogicalAddress.TV);
for (const addr of adapter.getActiveDevices())
  console.log(cec.logicalAddressToString(addr), adapter.getDeviceOSDName(addr));

adapter.close();
```

### Events

`CecAdapter` is an `EventEmitter`. libCEC fires its callbacks from its own
worker thread; the addon marshals each one onto the Node event loop via a
`ThreadSafeFunction`, so handlers run on the main thread like any other event.

| event | argument(s) | source callback |
|-------|-------------|-----------------|
| `log` | `{ message, level, time }` | `logMessage` |
| `keyPress` | `{ keycode, duration }` | `keyPress` |
| `command` | `{ initiator, destination, opcode, parameters, ack, eom, opcodeSet, transmitTimeout }` | `commandReceived` |
| `sourceActivated` | `(logicalAddress, activated)` | `sourceActivated` |
| `alert` | `alert` (a `CecAlert`) | `alert` |
| `configurationChanged` | `{ deviceName, deviceTypes, physicalAddress, logicalAddresses, cecVersion, adapterType, firmwareVersion, … }` | `configurationChanged` |
| `menuStateChanged` | `state` (a `CecMenuState`) | `menuStateChanged` |
| `commandHandler` | same shape as `command` | `commandHandler` (opt-in) |

`commandHandler` is off by default — pass `{ enableCommandHandler: true }` to the
constructor to receive it. It routes *every* command through libCEC's blocking
command-handler path and carries the same data as the cheaper `command` event, so
it's only worth enabling if you specifically need that hook.

### Methods

Lifecycle: `open(port, timeout=10000)`, `close()`.

Control: `transmit(command)`, `powerOnDevices(address)`, `standbyDevices(address)`,
`setActiveSource(deviceType)`, `setInactiveView()`, `volumeUp()`, `volumeDown()`,
`muteAudio()`, `sendKeypress(destination, key, wait=true)`,
`sendKeyRelease(destination, wait=true)`, `setOSDString(destination, duration, message)`.

Queries: `getActiveSource()`, `isActiveSource(address)`,
`getDevicePowerStatus(address)`, `getDeviceVendorId(address)`,
`getDevicePhysicalAddress(address)`, `getDeviceCecVersion(address)`,
`getDeviceOSDName(address)`, `getActiveDevices()`, `pollDevice(address)`,
`rescanDevices()`, `pingAdapters()`, `detectAdapters()`, `getLibInfo()`.

Enum helpers (module-level): `cecVersionToString`, `powerStatusToString`,
`logicalAddressToString`, `vendorIdToString`, `opcodeToString`,
`userControlKeyToString`, plus the enum tables (`CecLogicalAddress`,
`CecDeviceType`, `CecPowerStatus`, `CecUserControlCode`, `CecOpcode`, …).

## Notes / limitations

* `commandHandler` and `menuStateChanged` cannot **suppress** libCEC's default
  handling from JavaScript. Both are dispatched on libCEC's callback thread and
  expect a synchronous "handled?" return; the addon always answers "not handled"
  so the library keeps its default behaviour. Honouring a JS return would mean
  blocking that thread on the Node event loop and racing libCEC's 1000ms timeout.
  They are therefore exposed as observe-only events.
* Call `close()` when done. It stops libCEC's worker thread before releasing the
  thread-safe callbacks, so no event can fire against a torn-down adapter.

## Licence

GPL-2.0-or-later, or a commercial licence from
[Pulse-Eight](http://www.pulse-eight.com/) — the same dual licence as libCEC
itself.
