diff --git a/for-developers/core-development/_category_.json b/for-developers/core-development/_category_.json index 6d01617..42652d2 100644 --- a/for-developers/core-development/_category_.json +++ b/for-developers/core-development/_category_.json @@ -1,6 +1,6 @@ { "label": "Core Companion", - "position": 4, + "position": 5, "link": { "type": "generated-index" } diff --git a/for-developers/maintenance-team/_category_.json b/for-developers/maintenance-team/_category_.json index 859d76d..2619d50 100644 --- a/for-developers/maintenance-team/_category_.json +++ b/for-developers/maintenance-team/_category_.json @@ -1,6 +1,6 @@ { "label": "Maintenance Team Tasks", - "position": 5, + "position": 6, "link": { "type": "generated-index" } diff --git a/for-developers/module-development/_category_.json b/for-developers/module-development/_category_.json index 99d1ae7..4b8687b 100644 --- a/for-developers/module-development/_category_.json +++ b/for-developers/module-development/_category_.json @@ -1,5 +1,5 @@ { - "label": "Module Development", + "label": "Connection Development", "position": 3, "link": { "type": "doc", diff --git a/for-developers/module-development/home.md b/for-developers/module-development/home.md index f269a8d..b3319dc 100644 --- a/for-developers/module-development/home.md +++ b/for-developers/module-development/home.md @@ -1,14 +1,20 @@ --- -title: Getting Started with Modules -sidebar_label: Getting Started with Modules +title: Getting Started with Connections +sidebar_label: Getting Started with Connections sidebar_position: 1 -description: Developer environment setup specific to module development. +description: Developer environment setup specific to connection module development. --- -So, you want to develop a module for Companion? Welcome! +So, you want to develop a connection module for Companion? Welcome! Companion uses plug-ins to expand its capabilities, we call these plug-ins "modules". For every device you can control with Companion there is a "module" that manages the connection. This page describes how to set up your computer for developing Companion modules. Subsequent pages will provide details on the contents of the module and its lifecycle. +:::note +This section covers **connection** modules. If you want to add support for a physical control +surface instead, see the [Surface Development](../surface-development/home.md) section — it shares +much of the same tooling and links back here for the common setup steps. +::: + ## Prerequisites Before you start, make sure you have [Installed the Development Tools](../setting-up-developer-environment.md) and familiarized yourself with [Git Workflows](../git-workflows/git-crashcourse.md). diff --git a/for-developers/module-development/index.md b/for-developers/module-development/index.md index c53357a..0fa92a8 100644 --- a/for-developers/module-development/index.md +++ b/for-developers/module-development/index.md @@ -1,8 +1,12 @@ --- -title: Companion Module Developers' Guide -description: Outline of the Module Development section. +title: Companion Connection Developers' Guide +description: Outline of the Connection Development section. auto_toc: 2 --- -This section describes everything you need to know to develop your own modules for Companion. Below is -an outline of the top-level pages and folders in this section. For pages, the main headings inside each file are listed as bulleted lines. Folders are shown preceded by '> ', and the immediate contents of that folder are shown below it using "└─ " to indicate pages (or subfolders) inside that folder. +This section describes everything you need to know to develop your own **connection** modules for +Companion — the plugins that let Companion control a device or piece of software. If instead you +want to add support for a physical control surface, see the [Surface Development](../surface-development/home.md) +section. + +Below is an outline of the top-level pages and folders in this section. For pages, the main headings inside each file are listed as bulleted lines. Folders are shown preceded by '> ', and the immediate contents of that folder are shown below it using "└─ " to indicate pages (or subfolders) inside that folder. diff --git a/for-developers/surface-development/_category_.json b/for-developers/surface-development/_category_.json new file mode 100644 index 0000000..228be41 --- /dev/null +++ b/for-developers/surface-development/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Surface Development", + "position": 4, + "link": { + "type": "doc", + "id": "index" + }, + "customProps": { + "description": "Everything you need to develop a surface module for Companion." + } +} diff --git a/for-developers/surface-development/home.md b/for-developers/surface-development/home.md new file mode 100644 index 0000000..267534f --- /dev/null +++ b/for-developers/surface-development/home.md @@ -0,0 +1,72 @@ +--- +title: Getting Started with Surfaces +sidebar_label: Getting Started with Surfaces +sidebar_position: 1 +description: Developer environment setup and orientation for surface module development. +--- + +So, you want to develop a surface module for Companion? Welcome! + +A **surface** module teaches Companion how to talk to a physical control surface — reading button +presses and encoder turns, and drawing button graphics, indicators and brightness onto the +hardware. Surface modules are plugins, just like connection modules, and they are built with the +same tooling and released through the same workflows. + +:::tip Most tooling is shared with connection modules +Surface and connection modules share almost all of their development environment, repository +tooling and release process. Rather than duplicate that here, this section links you to the +relevant [Connection Development](../module-development/home.md) pages and concentrates on what is +genuinely different for surfaces. Connection modules are far more common, so if a topic isn't +covered here, the connection docs are almost always the right place to look. +::: + +## Prerequisites + +Before you start, make sure you have [installed the development tools](../setting-up-developer-environment.md) +and are comfortable with [Git workflows](../git-workflows/git-crashcourse.md). These are the same +for all Companion module development. + +## Shared setup — reuse the connection docs + +The following steps are identical for surface and connection modules. Follow the connection pages +and come back here for the surface-specific API: + +- [Installing Companion and setting up the development folder](../module-development/home.md) +- [Setting up a dev folder](../module-development/local-modules.md) so Companion loads your module + while you work +- [Repository file structure](../module-development/module-setup/file-structure.md) +- [The `manifest.json` file](../module-development/module-setup/manifest.json.md) — note that + surface modules declare some surface-specific fields; see [Surface Plugin Overview](./surface-basics/overview.md) +- [Module development 101](../module-development/module-development-101.md) for the general + build → release → maintain lifecycle +- [Packaging](../module-development/module-lifecycle/module-packaging.md) and + [releasing](../module-development/module-lifecycle/releasing-your-module.md) a module + +## What's different for surfaces + +Where a connection module defines actions, feedbacks and variables, a surface module implements +the [`SurfacePlugin`](./surface-basics/overview.md) interface and is responsible for: + +- [Discovering and connecting to surfaces](./surface-basics/discovery.md) +- Representing [each connected surface](./surface-basics/the-surface-instance.md) +- Declaring the surface's [physical layout](./surface-basics/layout.md) +- [Rendering](./surface-basics/rendering.md) the images Companion pushes, plus brightness and LEDs +- Reporting [input events](./surface-basics/input.md) back to Companion +- Handling the [lock screen / pincode entry](./surface-basics/locking-and-pincode.md) + +Read on in [Surfaces: Basics](./surface-basics/index.md). + +## Reference material + +- Generated API reference: [bitfocus.github.io/companion-surface-api](https://bitfocus.github.io/companion-surface-api/) +- [companion-surface-api wiki](https://github.com/bitfocus/companion-surface-api/wiki) + +Existing modules are the best reference for a real, working implementation. Which one to look at +depends on how your surface connects — there's no single "canonical" example because the discovery +flows differ: + +- USB (HID) and remote/network: [`companion-surface-elgato-stream-deck`](https://github.com/bitfocus/companion-surface-elgato-stream-deck) + (comprehensive, but a large codebase) +- Serial port: [`companion-surface-loupedeck`](https://github.com/bitfocus/companion-surface-loupedeck) + +Questions? Reach out on [Slack](https://l.companion.free/q/zYXXxnGyd)! :) diff --git a/for-developers/surface-development/index.md b/for-developers/surface-development/index.md new file mode 100644 index 0000000..5134798 --- /dev/null +++ b/for-developers/surface-development/index.md @@ -0,0 +1,22 @@ +--- +title: Companion Surface Developers' Guide +description: Outline of the Surface Development section. +auto_toc: 2 +--- + +This section describes how to develop your own **surface** modules for Companion — the plugins +that let Companion drive a physical control surface (buttons, encoders, displays and indicators) +and receive input back from it. + +Surface and connection modules share most of their tooling and workflow, so these pages focus on +what is _specific to surfaces_ and link back to the [Connection Development](../module-development/home.md) +section for the common setup, packaging and release steps. If you are building a module that +controls a device or piece of software instead of a control surface, you want the +[Connection Development](../module-development/home.md) section. + +The authoritative, autogenerated API reference lives at +[bitfocus.github.io/companion-surface-api](https://bitfocus.github.io/companion-surface-api/). +Existing modules make good real-world references — which one fits depends on how your surface +connects (see [Getting Started with Surfaces](./home.md)). + +Below is an outline of the top-level pages and folders in this section. diff --git a/for-developers/surface-development/surface-advanced/_category_.json b/for-developers/surface-development/surface-advanced/_category_.json new file mode 100644 index 0000000..2213a89 --- /dev/null +++ b/for-developers/surface-development/surface-advanced/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Surfaces: Advanced", + "position": 17, + "link": { + "type": "doc", + "id": "index" + }, + "customProps": { + "description": "Advanced and special-purpose topics for surface modules." + } +} diff --git a/for-developers/surface-development/surface-advanced/custom-discovery.md b/for-developers/surface-development/surface-advanced/custom-discovery.md new file mode 100644 index 0000000..bcee4eb --- /dev/null +++ b/for-developers/surface-development/surface-advanced/custom-discovery.md @@ -0,0 +1,84 @@ +--- +title: Custom Discovery Flows +sidebar_label: Custom Discovery +sidebar_position: 2 +description: Active scanning and plugin-owned detection for non-HID surfaces. +--- + +The [common discovery flows](../surface-basics/discovery.md) — USB HID and remote/network — cover +most hardware. For surfaces that don't fit either, the API provides two more mechanisms. Reach for +these only when the common flows don't work. + +## Active scan: `scanForSurfaces` + +Some surfaces aren't USB HID devices, so they won't be picked up by Companion's USB hotplug +watcher. When there's no hotplug notification for your device, implement `scanForSurfaces()` on your +plugin to look for them on demand; Companion calls it (for example when the user triggers a scan) +and you return the devices you found: + +```typescript +async scanForSurfaces() { + const ports = await listSerialPorts() + return ports + .filter(isMyDevice) + .map((port) => ({ + surfaceId: port.serialNumber, + deviceHandle: port.path, // stable handle used to re-identify the device between scans + description: 'My Serial Surface', + pluginInfo: { path: port.path }, + })) +} +``` + +Each result is a `DetectionSurfaceInfo` — like the HID `DiscoveredSurfaceInfo`, but with an extra +**`deviceHandle`**: a stable identifier (a serial path, etc.) Companion uses to recognise the same +physical device between scans. Companion then calls +[`openSurface()`](../surface-basics/the-surface-instance.md) for the ones it wants. + +This is the right flow for **serial-port** surfaces. See the +[Loupedeck module](https://github.com/bitfocus/companion-surface-loupedeck) for a real serial-port +implementation. + +## Plugin-owned detection: `SurfacePluginDetection` + +When a device or its vendor library insists on running its _own_ detection (for example a wireless +dongle that surfaces appear and disappear behind), implement the `detection` property — an +`EventEmitter` that: + +- emits **`surfacesAdded`** as devices appear and **`surfacesRemoved`** as they go (emitting + `surfacesRemoved` is important — it releases the unique id reserved for that device), +- implements **`triggerScan()`** to refresh when the user asks for a rescan, and +- implements **`rejectSurface()`** to release resources for a surface Companion chose not to open. + +```typescript +class MyDetection extends EventEmitter> /* … */ { + private onDeviceArrived(dev) { + this.emit('surfacesAdded', [ + { + surfaceId: dev.serial, + deviceHandle: dev.handle, + description: 'My Surface', + pluginInfo: { + /* … */ + }, + }, + ]) + } + async triggerScan() { + /* ask the library to re-enumerate */ + } + rejectSurface(info) { + /* release resources for this surface */ + } +} +``` + +:::note +This mechanism is discouraged where avoidable — the built-in [HID](../surface-basics/discovery.md) +and `scanForSurfaces` abstractions exist to reduce the cost of scanning. Use `detection` only when +the hardware or library gives you no choice. As with all discovery, **don't emit events before +`init()`** has run. +::: + +See the [generated reference](https://bitfocus.github.io/companion-surface-api/) for the exact +`SurfacePluginDetection` and `DetectionSurfaceInfo` definitions. diff --git a/for-developers/surface-development/surface-advanced/firmware-updates.md b/for-developers/surface-development/surface-advanced/firmware-updates.md new file mode 100644 index 0000000..efb8906 --- /dev/null +++ b/for-developers/surface-development/surface-advanced/firmware-updates.md @@ -0,0 +1,42 @@ +--- +title: Firmware Updates +sidebar_label: Firmware Updates +sidebar_position: 3 +description: Reporting available firmware updates for a surface. +--- + +If your hardware can report its firmware version and you know where newer firmware lives, you can +let Companion surface that to the user. This is optional — implement +`checkForFirmwareUpdates()` on your [surface instance](../surface-basics/the-surface-instance.md) +only if it applies to your device. + +Companion calls it with a small cache helper and expects either update info or `null`: + +```typescript +async checkForFirmwareUpdates(versionsCache) { + // fetchJson is shared/cached across all surfaces, so several devices of the same + // type don't each hit the network. + const latest = await versionsCache.fetchJson('https://example.com/my-surface/firmware.json') + + if (compareVersions(this.currentFirmware, latest.version) >= 0) { + return null // already up to date + } + + return { + updateUrl: 'https://example.com/my-surface/update', // where to send the user to update + } +} +``` + +The current API points the user at an `updateUrl` to perform the update themselves; the actual +update process is whatever your hardware and that page require. + +:::warning Use the provided cache for fetches +We strongly encourage using the supplied `SurfaceFirmwareUpdateCache.fetchJson()` (and the other +caching/util helpers) for any version lookups, rather than fetching directly. This check runs at +startup and again whenever surfaces connect and on an interval, so without caching a user with +several of your surfaces can generate a burst of duplicate requests. +::: + +See the [generated reference](https://bitfocus.github.io/companion-surface-api/) for the +`SurfaceFirmwareUpdateInfo` and `SurfaceFirmwareUpdateCache` definitions. diff --git a/for-developers/surface-development/surface-advanced/index.md b/for-developers/surface-development/surface-advanced/index.md new file mode 100644 index 0000000..bb50807 --- /dev/null +++ b/for-developers/surface-development/surface-advanced/index.md @@ -0,0 +1,12 @@ +--- +title: Surface Module Advanced Topics +description: Advanced and special-purpose topics for Companion surface modules. +auto_toc: 2 +--- + +This section collects topics beyond the [basics](../surface-basics/index.md): the special-purpose +[discovery flows](./custom-discovery.md) that exist to solve specific modules' needs, and +[firmware updates](./firmware-updates.md). + +Most surface modules will not need everything here — reach for these pages when the common +patterns in [Surfaces: Basics](../surface-basics/index.md) don't fit your hardware. diff --git a/for-developers/surface-development/surface-basics/_category_.json b/for-developers/surface-development/surface-basics/_category_.json new file mode 100644 index 0000000..fab8cbe --- /dev/null +++ b/for-developers/surface-development/surface-basics/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Surfaces: Basics", + "position": 15, + "link": { + "type": "doc", + "id": "index" + }, + "customProps": { + "description": "The core concepts and API needed to program a surface module." + } +} diff --git a/for-developers/surface-development/surface-basics/discovery.md b/for-developers/surface-development/surface-basics/discovery.md new file mode 100644 index 0000000..8b002f9 --- /dev/null +++ b/for-developers/surface-development/surface-basics/discovery.md @@ -0,0 +1,140 @@ +--- +title: Discovering Surfaces +sidebar_label: Discovery +sidebar_position: 12 +description: Detecting USB (HID) and remote surfaces and reporting them to Companion. +--- + +Before Companion can use a surface, your [plugin](./overview.md) has to tell it the device exists. +The API offers several discovery flows so it can cover everything from a USB Stream Deck to a +network surface behind a dongle. This page covers the two **common** flows — USB (HID) and +remote/network. The specialised flows (active scanning, plugin-owned detection) are covered in +[Custom Discovery](../surface-advanced/custom-discovery.md). + +## USB (HID) surfaces + +This is the right flow for most USB-attached surfaces, and the simplest to implement. + +### 1. Declare your USB IDs in the manifest + +Companion runs a single shared HID hotplug watcher for all surface plugins. It will only offer your +plugin devices whose USB vendor/product IDs you declare in your manifest's `usbIds` array — so you +never see, or have to filter out, unrelated devices: + +```json +{ + "type": "surface", + "id": "my-surface", + "usbIds": [{ "vendorId": 4057, "productIds": [96, 109, 99] }] +} +``` + +> `usbIds` — "List of USB vendor and product IDs that the module supports. Your module will only be +> notified of devices matching these IDs." + +`vendorId` and the `productIds` entries are **numbers** (decimal). A device that advertises itself +in hex as `0x0fd9` / `0x0060` is therefore `4057` / `96` here. + +### 2. Claim matching devices with `checkSupportsHidDevice` + +For each matching device, Companion calls `checkSupportsHidDevice(device)`. Inspect the +[`HIDDevice`](https://bitfocus.github.io/companion-surface-api/) info and either return a +`DiscoveredSurfaceInfo` to claim it, or `null` to decline. **Do not open the device here** — this +is a cheap, synchronous probe based only on the info provided: + +```typescript +checkSupportsHidDevice(device: HIDDevice): DiscoveredSurfaceInfo | null { + // Narrow further than usbIds if one productId covers several models, + // or use device.usagePage / device.interface to pick the right HID interface. + if (device.vendorId !== 4057) return null + + return { + // Typically the serial number. The host resolves collisions by adding a suffix + // unless you set surfaceIdIsNotUnique: true. + surfaceId: device.serialNumber, + // Shown to the user — usually the model name. + description: device.product ?? 'My Surface', + // Your own data, handed back to you in openSurface(). + pluginInfo: { path: device.path }, + } +} +``` + +:::warning +Companion will call this for each attached device upon every scan. It is important that you return a stable surfaceId here, to stop Companion from seeing it as a new surface each time. +::: + +If Companion decides to use the device, it then calls +[`openSurface(surfaceId, pluginInfo, context)`](./the-surface-instance.md) — and `pluginInfo` is +the object you returned above. That is where you actually open the hardware. + +## Remote / network surfaces + +For surfaces reached over the network (IP or cloud), your plugin manages the connections itself. +This flow is driven by the **remote connections the user configures in Companion** — Companion +stores the connection configs and asks your plugin to start and stop them. Implement the `remote` +property on your plugin with a `SurfacePluginRemote`, whose key pieces are: + +- **`configFields`** defines the form the user fills in when adding a remote connection (for + example an address and port — see [input field types](./input.md)). Companion persists these and + passes them back in `startConnections()`. +- **`startConnections` / `stopConnections`** are how Companion tells you to connect to, or + disconnect from, the configured remotes. +- As surfaces come up, you emit **`surfacesConnected`**; for surfaces you _discover_ but the user + hasn't added yet, emit **`connectionsFound`** (and `connectionsForgotten` when they go away) so + Companion can suggest them in the UI. + +After you emit `surfacesConnected`, Companion decides what to do with each surface — exactly as for +HID. It either calls [`openSurface(surfaceId, pluginInfo, context)`](./the-surface-instance.md) to +open it, or calls `rejectSurface(surfaceInfo)` on your remote class if it opts not to; allowing you to release any +resources you were holding for that device. + +`SurfacePluginRemote` is an `EventEmitter`. As with all discovery, **do not emit any events until +after `init()` has returned**, or they will be lost. + +```typescript +import { EventEmitter } from 'node:events' +import type { SurfacePluginRemote, SurfacePluginRemoteEvents } from '@companion-surface/base' + +class MyRemote + extends EventEmitter> + implements SurfacePluginRemote +{ + readonly configFields = [{ id: 'host', type: 'textinput', label: 'Address', default: '' }] + readonly checkConfigMatchesExpression = '$(objA:host) == $(objB:host)' + + async startConnections(connections) { + for (const conn of connections) { + // connect using conn.config, then announce the surface: + this.emit('surfacesConnected', [ + { + surfaceId: 'serial-123', + deviceHandle: conn.connectionId, + description: 'My Network Surface', + pluginInfo: { path: String(conn.config.host) }, + }, + ]) + } + } + + async stopConnections(connectionIds) { + /* close the matching connections */ + } + rejectSurface(_info) { + /* release any resources for a surface the host declined */ + } +} +``` + +## Choosing a flow + +| Your surface… | Use… | +| ---------------------------------------------- | ----------------------------------------------------------- | +| Is a USB HID device | `checkSupportsHidDevice` + `usbIds` (above) | +| Is reached over the network / cloud | `remote` / `SurfacePluginRemote` (above) | +| Uses a serial port, dongle, or a custom scheme | [Custom Discovery](../surface-advanced/custom-discovery.md) | + +See the [generated reference](https://bitfocus.github.io/companion-surface-api/) for the exact +`HIDDevice`, `DiscoveredSurfaceInfo` and `SurfacePluginRemote` definitions. For a real +implementation of both the HID and remote flows, see the +[Elgato Stream Deck module](https://github.com/bitfocus/companion-surface-elgato-stream-deck). diff --git a/for-developers/surface-development/surface-basics/index.md b/for-developers/surface-development/surface-basics/index.md new file mode 100644 index 0000000..fe82fc7 --- /dev/null +++ b/for-developers/surface-development/surface-basics/index.md @@ -0,0 +1,14 @@ +--- +title: Surface Module Basic API +description: The core concepts and API for Companion surface modules. +auto_toc: 2 +--- + +This section describes the core elements of the Companion surface-module API: the +[`SurfacePlugin`](./overview.md) you implement, how surfaces are +[discovered](./discovery.md) and [represented](./the-surface-instance.md), how you declare a +surface's [layout](./layout.md), how [rendering](./rendering.md) and [input](./input.md) work, and +how to handle the [lock screen](./locking-and-pincode.md). + +For the exact type signatures, always cross-reference the generated API documentation at +[bitfocus.github.io/companion-surface-api](https://bitfocus.github.io/companion-surface-api/). diff --git a/for-developers/surface-development/surface-basics/input.md b/for-developers/surface-development/surface-basics/input.md new file mode 100644 index 0000000..8b7ac1b --- /dev/null +++ b/for-developers/surface-development/surface-basics/input.md @@ -0,0 +1,65 @@ +--- +title: Reporting Input and Variables +sidebar_label: Input & Variables +sidebar_position: 16 +description: Sending button, encoder and other input back to Companion via SurfaceContext. +--- + +The other half of a surface is input. When the user interacts with the hardware, you report it back +to Companion through the **`SurfaceContext`** you were given in +[`openSurface()`](./the-surface-instance.md). Hold onto that context in your surface instance. + +## Button and encoder events + +Map each physical control to its [layout](./layout.md) `controlId`, then call the matching context +method: + +```typescript +// In your device's event handlers: +this.context.keyDownById('0/2') // press +this.context.keyUpById('0/2') // release +// or, if your hardware only signals a single "click": +this.context.keyDownUpById('0/2') + +// Encoders / rotary controls: +this.context.rotateLeftById('0/2') +this.context.rotateRightById('0/2') +``` + +Report both down and up where the hardware distinguishes them — many user configurations rely on +press-and-hold behaviour. + +## Page changing + +If your surface drives page changes itself (for example a swipe gesture), enable it by setting +`canChangePage` in [`registerProps`](./the-surface-instance.md) and call `context.changePage(forward)`. +The user must opt in to this in the surface settings before Companion accepts it. + +## Transfer variables + +Buttons aside, surfaces often have non-button values to exchange — a T-bar position, a battery +level, or LEDs next to a control. These are modelled as **transfer variables**, declared in +`registerProps.transferVariables`. Values the surface produces are sent with +`context.sendVariableValue(name, value)`; values the surface consumes arrive via your instance's +`onVariableValue(name, value)` method. See the +[generated reference](https://bitfocus.github.io/companion-surface-api/) for the exact direction +semantics of `input` vs `output` variables. + +How these show up for the user is worth knowing: + +- For values the surface **produces** (e.g. a T-bar), the user can store the value into a Companion + custom variable and then use it however they like. +- For values the surface **consumes** (e.g. an LED), the user provides a Companion expression that + drives the value, and Companion pushes the result to your surface. + +## Configuration fields + +If your surface needs user configuration, define `configFields` in `registerProps` using the +standard Companion input field types (`textinput`, `dropdown`, `number`, `checkbox`, +`static-text`). Companion renders them and calls your instance's `updateConfig(config)` when they +change. These are the same field definitions used by connection modules — see +[Input Field Types](../../module-development/connection-basics/input-field-types.md) for the field +shapes (note surfaces support a subset). + +For a worked example of translating raw HID input into Companion events, see the +[Elgato Stream Deck module](https://github.com/bitfocus/companion-surface-elgato-stream-deck). diff --git a/for-developers/surface-development/surface-basics/layout.md b/for-developers/surface-development/surface-basics/layout.md new file mode 100644 index 0000000..b4f6a98 --- /dev/null +++ b/for-developers/surface-development/surface-basics/layout.md @@ -0,0 +1,61 @@ +--- +title: Declaring the Surface Layout +sidebar_label: Layout +sidebar_position: 14 +description: Describing a surface's controls and what should be drawn to them. +--- + +Each device you [open](./the-surface-instance.md) declares a **layout** (the `surfaceLayout` field +of `registerProps`). The layout tells Companion two things: the **controls** the device has and +where they sit on a grid, and the **style** each control needs drawn — a bitmap of a given size, a +background colour, and/or text. + +A layout has two parts: a set of named **style presets** (one called `default` is required), and a +map of **controls**: + +```typescript +const myLayout = { + stylePresets: { + // Required. Used for any control that doesn't name its own preset. + default: { + bitmap: { w: 72, h: 72, format: 'rgb' }, + }, + // Optional extra presets, referenced by name from a control below. + encoder: { + text: true, + colors: 'hex', + }, + }, + controls: { + // Ids are typically "row/column". Companion maps these onto its button grid. + '0/0': { row: 0, column: 0 }, + '0/1': { row: 0, column: 1 }, + '0/2': { row: 0, column: 2, stylePreset: 'encoder' }, + }, +} +``` + +## Style presets + +A preset describes what Companion should produce for a control: + +- **`bitmap: { w, h, format }`** — request a rendered image of this pixel size. `format` is one of + `rgb`, `rgba`, `bgr`, `bgra` (default `rgb`). This is what most button surfaces use. +- **`colors: 'hex' | 'rgb'`** — request a background colour instead of (or alongside) a bitmap, + for buttons with a plain RGB backlight. +- **`text` / `textStyle`** — request button text (and text styling) for text-only displays. + +Whatever you request here is what arrives in `draw()` — see [Rendering](./rendering.md). Get the +bitmap dimensions right: the resolution you declare is the resolution of the images you'll be +given. + +## Controls + +Each control has a `row` and `column` (zero-based) and an optional `stylePreset` naming one of your +presets. The control id (the map key) should be unique and is typically `row/column`; it is the +`controlId` you'll see in [draw](./rendering.md) and [input](./input.md) calls. + +The full schema (including pixel formats and style options) is in the +[generated reference](https://bitfocus.github.io/companion-surface-api/). For a device that +describes several different models, see the +[Elgato Stream Deck module](https://github.com/bitfocus/companion-surface-elgato-stream-deck). diff --git a/for-developers/surface-development/surface-basics/lifecycle.md b/for-developers/surface-development/surface-basics/lifecycle.md new file mode 100644 index 0000000..539355a --- /dev/null +++ b/for-developers/surface-development/surface-basics/lifecycle.md @@ -0,0 +1,28 @@ +--- +title: Plugin Lifecycle +sidebar_label: Lifecycle +sidebar_position: 11 +description: Initialising and tearing down a surface plugin. +--- + +A `SurfacePlugin` has a simple top-level lifecycle built around two methods: + +- **`init()`** — called once when the plugin is loaded, before any surfaces or events are used. Set + up whatever your plugin needs to begin [discovering](./discovery.md) surfaces. Don't block on + hardware that may not be present — report surfaces as they appear instead. +- **`destroy()`** — called once when the plugin is about to be unloaded. Reset and close any open + surfaces and release everything you created in `init()`. As with connection modules, leaking + timers or handles here causes problems that accumulate over time. + +:::warning Don't emit discovery events before `init()` +If your plugin uses the [detection](../surface-advanced/custom-discovery.md) or +[remote](./discovery.md) mechanisms (both are `EventEmitter`s), it must **not** emit any events +until after `init()` has returned — anything emitted earlier is lost. +::: + +The lifecycle of an individual connected device is separate from the plugin lifecycle and is +described in [The Surface Instance](./the-surface-instance.md). + +The build → release → maintain lifecycle of the _module repository_ (packaging, versioning, +releasing) is shared with connection modules — see +[Module Development 101](../../module-development/module-development-101.md). diff --git a/for-developers/surface-development/surface-basics/locking-and-pincode.md b/for-developers/surface-development/surface-basics/locking-and-pincode.md new file mode 100644 index 0000000..a4bed7e --- /dev/null +++ b/for-developers/surface-development/surface-basics/locking-and-pincode.md @@ -0,0 +1,48 @@ +--- +title: Locking and Pincode Entry +sidebar_label: Locking & Pincode +sidebar_position: 17 +description: Declaring a pincode map and handling the locked state. +--- + +Companion can lock surfaces, requiring a pincode before the surface can be used. **Every surface is +expected to handle this** — it's part of the core surface experience, not an advanced extra. You +declare how pincode entry works through the `pincodeMap` field of +[`registerProps`](./the-surface-instance.md). + +## Pincode maps + +There are three kinds of pincode map: + +- **`single-page`** — the digits `0`–`9` are mapped to control ids, plus an optional `pincode` + control. Companion drives the lock screen for you using these controls. + + ```typescript + pincodeMap: { + type: 'single-page', + pincode: '0/0', + 0: '1/0', 1: '1/1', 2: '1/2', 3: '1/3', 4: '1/4', + 5: '2/0', 6: '2/1', 7: '2/2', 8: '2/3', 9: '2/4', + } + ``` + +- **`multiple-page`** — for surfaces with too few buttons to show all digits at once; adds a + `nextPage` control and an array of `pages`. + +- **`custom`** — for surfaces that present their own pincode UI. Companion won't lay the digits out + for you; instead you implement `showLockedStatus()` on your [surface instance](./the-surface-instance.md). + +Set `pincodeMap` to `null` to disable pincode entry entirely. + +## The locked state + +While a surface is locked, Companion shows the lock screen instead of the normal grid. You can read +the current state from `context.isLocked`. + +If you used the **`custom`** map, implement `showLockedStatus(locked, characterCount)`: show your +lock UI when `locked` is true (using `characterCount` to indicate how many digits have been +entered), and return to normal [rendering](./rendering.md) when it becomes false. The +[input](./input.md) you report while locked is what lets the user type their code. + +See the [generated reference](https://bitfocus.github.io/companion-surface-api/) for the full +`SurfacePincodeMap` definitions. diff --git a/for-developers/surface-development/surface-basics/overview.md b/for-developers/surface-development/surface-basics/overview.md new file mode 100644 index 0000000..8efcfde --- /dev/null +++ b/for-developers/surface-development/surface-basics/overview.md @@ -0,0 +1,81 @@ +--- +title: Surface Plugin Overview +sidebar_label: Overview +sidebar_position: 10 +description: The SurfacePlugin model — one plugin per surface type, many connected devices. +--- + +A surface module exports a single object that implements the **`SurfacePlugin`** interface from +`@companion-surface/base`. Where a connection module exposes a _class_ that Companion instantiates +once per connection, a surface plugin is one long-lived object that manages **every** device of +the type it supports. + +:::tip +**One plugin handles all surfaces of a given type.** The plugin discovers and opens devices; each +opened device is then represented by its own [surface instance](./the-surface-instance.md). +::: + +The interface is generic over `TInfo` — a type you define to carry whatever plugin-specific data +you want to associate with a discovered device (a device path, an IP address, a handle into a +vendor library, …). Companion hands it back to you when it asks you to open that device. + +A minimal plugin looks like this: + +```typescript +import type { SurfacePlugin } from '@companion-surface/base' + +interface MyDeviceInfo { + path: string +} + +export const plugin: SurfacePlugin = { + async init() { + // Set up anything the plugin needs. Don't block on hardware that may not be present yet. + }, + async destroy() { + // Tear down anything created in init(). + }, + + // Tell Companion which connected devices are yours — see Discovery. + checkSupportsHidDevice(device) { + return null + }, + + // Open a device Companion has chosen to use — see The Surface Instance. + async openSurface(surfaceId, pluginInfo, context) { + throw new Error('not implemented') + }, +} +``` + +See the [generated API reference](https://bitfocus.github.io/companion-surface-api/) for the full, +always-current interface and its types. + +## The two halves of a plugin + +1. **Discovery** — telling Companion which devices exist. The flow depends on how your hardware + connects; the common ones (USB HID and remote/network) are covered in + [Discovery](./discovery.md), the rest in [Custom Discovery](../surface-advanced/custom-discovery.md). +2. **The surface instance** — once Companion decides to use a device it calls `openSurface()`, + which returns the live [surface instance](./the-surface-instance.md) plus a description of the + device's [layout](./layout.md), brightness support, [pincode handling](./locking-and-pincode.md) + and [config fields](./input.md). + +## `SurfaceContext` + +`openSurface()` is given a `SurfaceContext`. This is your channel _back_ to Companion: you use it +to report [input events](./input.md) (key presses, encoder rotation, …), to disconnect when the +device goes away, and to read host state such as whether the surface is locked. Hold onto it inside +your surface instance. + +## The manifest + +Surface manifests look like connection manifests but have `"type": "surface"` and some +surface-specific fields — most importantly `usbIds`, which determines the HID devices you are +offered (covered on the [Discovery](./discovery.md) page). For the fields shared with connection +modules, see the [connection manifest docs](../../module-development/module-setup/manifest.json.md). + +## Reference + +- [Generated API reference](https://bitfocus.github.io/companion-surface-api/) +- [companion-surface-api wiki](https://github.com/bitfocus/companion-surface-api/wiki) diff --git a/for-developers/surface-development/surface-basics/rendering.md b/for-developers/surface-development/surface-basics/rendering.md new file mode 100644 index 0000000..2608781 --- /dev/null +++ b/for-developers/surface-development/surface-basics/rendering.md @@ -0,0 +1,60 @@ +--- +title: Rendering, Brightness and LEDs +sidebar_label: Rendering +sidebar_position: 15 +description: Drawing what Companion pushes, plus brightness and clearing the display. +--- + +Companion does the drawing. It renders each control according to the user's configuration and +pushes the result to your [surface instance](./the-surface-instance.md) for the matching control. +Your job is to put that onto the hardware — you don't draw button graphics yourself. + +## `draw` + +`draw(signal, drawProps)` is called whenever a control changes. What `drawProps` contains depends +on what you asked for in that control's [style preset](./layout.md): + +- **`controlId`** — which control this is (the id from your layout). +- **`image`** — a `Uint8Array` of pixels, in the size and format you requested via `bitmap`. +- **`color`** — a hex background colour, if you requested `colors`. +- **`text`** — button text, if you requested `text`. +- **`pageNumber`** — the Companion page the surface is on, where applicable. + +```typescript +async draw(signal: AbortSignal, drawProps) { + if (signal.aborted) return + + const control = this.layoutLookup.get(drawProps.controlId) + if (!control) return + + if (drawProps.image) { + await this.device.fillKeyBuffer(control.keyIndex, drawProps.image) + } else if (drawProps.color) { + await this.device.fillKeyColor(control.keyIndex, drawProps.color) + } +} +``` + +The `signal` is an `AbortSignal` that fires if the draw is superseded before you finish — check it +and bail out early on slow hardware so you don't draw stale frames. + +Because Companion pushes finished output, you generally don't need to track button state yourself — +render what you're given, when you're given it. + +## Brightness and clearing + +Two related methods round out output: + +- **`setBrightness(percent)`** — `0`–`100`. Only called if you set `brightness: true` in + [`registerProps`](./the-surface-instance.md). +- **`blank()`** — set all pixels to black/off, e.g. on shutdown. + +## Indicator LEDs + +LEDs that aren't part of the button grid (status lights, encoder rings, a T-bar's LEDs) are modelled +as **output transfer variables** rather than draws — see [Input & variables](./input.md). + +See the [generated reference](https://bitfocus.github.io/companion-surface-api/) for the exact +`SurfaceDrawProps` and pixel formats, and the +[Elgato Stream Deck module](https://github.com/bitfocus/companion-surface-elgato-stream-deck) for +real image handling. diff --git a/for-developers/surface-development/surface-basics/the-surface-instance.md b/for-developers/surface-development/surface-basics/the-surface-instance.md new file mode 100644 index 0000000..ef6ebc9 --- /dev/null +++ b/for-developers/surface-development/surface-basics/the-surface-instance.md @@ -0,0 +1,72 @@ +--- +title: The Surface Instance +sidebar_label: The Surface Instance +sidebar_position: 13 +description: Opening a device and implementing the live SurfaceInstance. +--- + +While the [plugin](./overview.md) manages a whole _type_ of surface, each individual connected +device is a **surface instance**. Companion creates one by calling your plugin's `openSurface()` +when it decides to use a device that [discovery](./discovery.md) reported. + +## `openSurface` + +`openSurface(surfaceId, pluginInfo, context)` is where you actually open the hardware. `pluginInfo` +is the object you returned from discovery; `context` is the [`SurfaceContext`](./input.md) you use +to talk back to Companion. It returns two things: + +- **`surface`** — your `SurfaceInstance` implementation (below). +- **`registerProps`** — a description of the device's capabilities: its [layout](./layout.md), + whether it supports brightness, its [pincode map](./locking-and-pincode.md), any + [config fields](./input.md) and transfer variables, and an optional user-facing `location`. + +```typescript +async openSurface(surfaceId, pluginInfo, context) { + const surface = new MySurface(surfaceId, pluginInfo, context) + return { + surface, + registerProps: { + brightness: true, + surfaceLayout: myLayout, // see Layout + pincodeMap: { type: 'single-page', /* … */ }, // see Locking & Pincode + location: null, + configFields: null, + }, + } +} +``` + +:::tip +If opening the hardware can throw partway through, make sure you don't leak the underlying device +handle — open inside a `try`/`catch` and close what you've opened before re-throwing, so a failed +open doesn't leave a dangling connection. +::: + +## Implementing `SurfaceInstance` + +The instance carries a `surfaceId` (a stable id, typically the serial number — Companion uses it to +re-associate the device with the user's config across reconnects) and a `productName`. Beyond that +it implements the lifecycle and operation methods Companion calls: + +- **`init()`** — open and prepare the hardware. Called once after creation. +- **`ready()`** — Companion is about to start drawing; transition into normal operation. +- **`draw(signal, drawProps)`** — render a control — see [Rendering](./rendering.md). +- **`setBrightness(percent)`** / **`blank()`** — backlight and clear, also covered in + [Rendering](./rendering.md). +- **`close()`** — clean up and disconnect. Stop any timers and release handles here. +- Optional: **`updateConfig(config)`**, **`onVariableValue(name, value)`** (see + [Input & variables](./input.md)), **`showLockedStatus(...)`** (see + [Locking & pincode](./locking-and-pincode.md)) and **`checkForFirmwareUpdates(...)`** (see + [Firmware updates](../surface-advanced/firmware-updates.md)). + +## Disconnecting + +When the device goes away (unplugged, network dropped), call `context.disconnect(error)` so +Companion knows it is gone. For the [detection](../surface-advanced/custom-discovery.md) and +[remote](./discovery.md) flows you also emit the relevant "removed" event so the reserved id is +released. + +See the [generated reference](https://bitfocus.github.io/companion-surface-api/) for the full +`SurfaceInstance` and `SurfaceRegisterProps` definitions, and the +[Elgato Stream Deck module](https://github.com/bitfocus/companion-surface-elgato-stream-deck) for a +complete implementation. diff --git a/for-developers/surface-development/surface-debugging.md b/for-developers/surface-development/surface-debugging.md new file mode 100644 index 0000000..f14f2e8 --- /dev/null +++ b/for-developers/surface-development/surface-debugging.md @@ -0,0 +1,25 @@ +--- +title: Debugging Surfaces +sidebar_label: Debugging Surfaces +sidebar_position: 7 +description: How to debug a surface module during development. +--- + +Debugging a surface module works the same way as debugging a connection module. Rather than repeat +the details here, see [Debugging Modules](../module-development/module-debugging.md) in the +connection docs, which covers logging through the API, `console.log`, and attaching an interactive +debugger. + +A few things worth keeping in mind that are specific to surfaces: + +- **Hardware state matters.** Many surface bugs only appear with a real device attached. Where + possible, test against the physical surface as well as any simulator. +- **Watch the discovery flow.** A surface that never appears in Companion is usually a discovery or + registration problem rather than a rendering one — start by logging in your + [discovery](./surface-basics/discovery.md) code. +- **Log generously around connect/disconnect.** Hotplug and reconnection edge cases are the most + common source of surface bugs; log when surfaces are added and removed. + +See also the generated API reference at +[bitfocus.github.io/companion-surface-api](https://bitfocus.github.io/companion-surface-api/) for +the exact logging helpers available to your plugin.