# Worked example: embed a Pixelblaze controller in an MBXHub Charm

This example uses MBXHub's documented Charm extension points to open the
Pixelblaze's existing web interface inside MBXHub. The controller used for the
example is `http://<ipaddress>/`; replace it with your own controller address.
MBXHub supplies the Charm entry and wrapper;
Pixelblaze supplies its controller UI and communicates with its own hardware.

The companion [generation prompt](pixelblaze-charm-prompt.html)
asks an LLM to produce the two installation files and their documentation from
`llms.txt`. Neither MBXHub nor PixelblazeDotNet needs to be modified or rebuilt
for this approach.

**Evidence, 26 September 2026:** the device page and headers were fetched and
inspected; local MBXHub, PixelblazeDotNet, and theMatrix source was read. The
example below has not been installed, rendered in a browser, or exercised
against the LEDs. It is a documented implementation example, not a report of
successful end-to-end testing.

## 1. The brief and the chosen approach

This walkthrough shows how a small page Charm can bring an existing LAN
controller into MBXHub. The companion prompt asks an LLM to create that Charm
and its documentation from the public `llms.txt` contract.

Reusing that page means the existing pattern picker, brightness controls,
pattern controls, and other device functions remain Pixelblaze's own UI. This
example does not add music-linked lighting or duplicate those controls in the
wrapper. It embeds the full device interface, rather than a restricted subset
of its functions.

The resulting flow is:

```text
MBXHub dashboard Charm
  -> /pages/pixelblaze.html       served by MBXHub
     -> iframe: http://<ipaddress>/
        -> ws://<ipaddress>:81/  opened by Pixelblaze's own page
```

The browser displaying the Charm must be able to reach the controller on its
LAN. Reachability from the MBXHub server alone is insufficient. The iframe is
cross-origin: the wrapper cannot read Pixelblaze's document, connection status,
or controls. Pixelblaze's own status indicator is authoritative.

### Why this is embedding, rather than an HTTP proxy

MBXHub's `POST /api/proxy` takes a JSON request describing an HTTP/HTTPS call.
It is useful for device API requests, but is not a navigable reverse proxy for
an entire site and does not relay WebSocket upgrades.

The inspected Pixelblaze page calls `connectWebsocket(window.location.hostname)`
and constructs `ws://<hostname>:81/`. If its HTML were fetched and served under
the hub's origin, that expression would target the hub instead of Pixelblaze.
Its relative HTTP requests would also resolve against the new origin.

Loading the actual device URL in an iframe preserves both behaviors. A full
reverse proxy would need an asset/request routing design and WebSocket relay,
plus a verified treatment of that hostname-and-port construction. It is outside
this example; no undocumented MBXHub endpoint is assumed.

### What the device inspection established

| Observation | Implication |
| --- | --- |
| GET `/` returned HTTP 200 and `Content-Type: text/html` | The controller page was reachable from the development machine. |
| Response headers contained `Connection: close` and `Content-Type`; neither `X-Frame-Options` nor `Content-Security-Policy` was present | No response-header restriction on framing was observed on this response. This is not proof that every browser/host will embed it. |
| The page builds a WebSocket URL with its own hostname and port 81 | Preserve the device origin by embedding its URL directly. |
| Styles and scripts were present inline; the external script/link scan found the two local icon links | There is no need to copy its large HTML bundle into MBXHub. A full runtime network audit remains unperformed. |

The HTML was inspected as text. No device control command, firmware operation,
pattern change, or brightness change was issued during this inspection.

## 2. Inputs to the LLM

Use the target MBXHub instance's `/llms.txt` as the primary integration contract.
The published reference is [mbxhub.com/llms.txt](../../llms.txt). A live instance's
reference is preferable when its version differs from the published site.

Read these sections before generating the Charm:

- **Charms:** manifest fields, `expand[]`, `webapp`, `display`, visibility, and
  any registration policy applying to the target installation.
- **What a host page will do for a framed charm:** available host behavior.
- **Device Proxy:** its HTTP/HTTPS scope, to avoid inventing WebSocket support.
- **Custom page/shared component guidance:** page location, theme tokens, and
  `MBXShared.clientLog`.

For controller context, these references informed the example:

| Reference | What it provides |
| --- | --- |
| [PixelblazeDotNet](https://github.com/halrad-com/PixelblazeDotNet) | The .NET controller library. The inspected local checkout contained `PixelblazeClient.cs`, `PixelblazeNetworkHelper.cs`, `PixelblazePatterns.cs`, and `PixelblazeSettings.cs`. |
| [MBXHub llms.txt](../../llms.txt) | The public API and page-Charm contract; prefer your running instance's copy when versions differ. |
| [Charms SDK](../docs/charms-sdk.html) | Additional Charm integration documentation. |

The initial research also read a local theMatrix Electromage plugin as a usage
example. That checkout is not required to follow this walkthrough. The library
README described its existing status as "Wet Paint" when inspected; the example
does not claim to change the library's status.

These controller references informed the design; they are not new runtime
dependencies. In particular, a browser cannot directly consume this .NET 8
library. No local bridge, .NET process, or protocol port is needed for embedding.

An instructive source discrepancy: theMatrix's pattern-selection comment says
`setActiveProgram`, while the inspected library actually serializes
`activeProgramId`. The library also handles binary pattern-list responses
(message type 7), brightness in the range 0–1, and `save: false` by default for
brightness/control writes. This is why a future custom controller should follow
implementation and observed responses rather than copy a comment. The embedded
page already owns this protocol handling.

## 3. Installation files

The following are the complete contents of the two example files to generate.
Copy each code block into the indicated file. This walkthrough documents the
files; it does not install them into an MBXHub instance.

### `charms/pixelblaze.json`

Use the existing `expand[]` button shape, with a hub-hosted page as the target.
The namespaced example ID avoids claiming another publisher's identity.

```json
{
  "id": "example.pixelblaze",
  "kind": "page",
  "label": "Pixelblaze",
  "expand": [
    {
      "icon": "<svg viewBox='0 0 16 16' width='16' height='16' fill='currentColor'><circle cx='4' cy='4' r='2'/><circle cx='12' cy='4' r='2'/><circle cx='4' cy='12' r='2'/><circle cx='12' cy='12' r='2'/></svg>",
      "label": "Pixelblaze",
      "action": "webapp /pages/pixelblaze.html",
      "display": "both"
    }
  ]
}
```

This is a locally placed page manifest, like the documented drop-in examples.
It does not fabricate a `registration` block or claim a `proxy:lan` grant:
the wrapper does not call the hub's device proxy. Direct device access is a
browser-to-device connection, not an MBXHub-mediated permission grant. Honor
the installed hub's registration policy if it requires additional setup; any
required approval belongs in its existing console workflow.

### `pages/pixelblaze.html`

This wrapper supplies an accessible frame title, a reload button, and a link to
open the device separately, in a header you can collapse to give the device page
the room. The wrapper adopts MBXHub's
theme; the embedded device UI retains Pixelblaze's own theme.

```html
<!doctype html>
<html lang="en" data-theme="__DATA_THEME__">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Pixelblaze</title>
  <link rel="stylesheet" href="/pages/components/shared.css">
  <style id="theme-inline">__THEME_CSS__</style>
  <style>
    * { box-sizing: border-box; }
    html, body { margin: 0; height: 100%; }
    body { display: flex; flex-direction: column; font-family: system-ui, sans-serif; }
    header { padding: .75rem; border-bottom: 1px solid currentColor; }
    /* The controls collapse so the device page can have the room. The title is the
       summary, so the collapsed header is one line and still says what this is. */
    summary { cursor: pointer; }
    h1 { display: inline; margin: 0; font-size: 1.1rem; }
    details[open] > summary { margin-bottom: .5rem; }
    nav { display: flex; flex-wrap: wrap; align-items: center; gap: .75rem; }
    button, a { font: inherit; }
    button { padding: .4rem .65rem; cursor: pointer; }
    a { color: inherit; }
    #status { margin: .5rem 0 0; font-size: .9rem; }
    iframe { width: 100%; flex: 1; min-height: 0; border: 0; }
  </style>
  <script src="/pages/components/shared.js" defer></script>
</head>
<body>
  <header>
    <details id="controls" open>
    <summary><h1>Pixelblaze</h1></summary>
    <nav aria-label="Controller page">
      <button id="reload" type="button">Reload device page</button>
      <a id="open" href="http://<ipaddress>/" target="_blank"
         rel="noopener noreferrer">Open device separately</a>
    </nav>
    <p id="status" role="status">Opening the device page…</p>
    </details>
  </header>
  <iframe id="device" title="Pixelblaze LED controller"
          referrerpolicy="no-referrer"></iframe>
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      const deviceUrl = 'http://<ipaddress>/';
      const frame = document.getElementById('device');
      const status = document.getElementById('status');
      const reload = document.getElementById('reload');
      document.getElementById('open').href = deviceUrl;

      // Open or collapsed, remembered per browser. Storage that throws (private
      // windows, blocked site data) just leaves the controls open.
      const controls = document.getElementById('controls');
      try {
        if (localStorage.getItem('pixelblaze.controls') === 'closed') controls.open = false;
      } catch (e) { }
      controls.addEventListener('toggle', function () {
        try { localStorage.setItem('pixelblaze.controls', controls.open ? 'open' : 'closed'); } catch (e) { }
      });

      function log(level, message) {
        if (window.MBXShared && window.MBXShared.clientLog) {
          window.MBXShared.clientLog(level, '[pixelblaze] ' + message);
        }
      }

      function loadDevice() {
        if (location.protocol !== 'http:') {
          status.textContent = 'This HTTP device needs an HTTP host page. Use “Open device separately”.';
          reload.disabled = true;
          log('warn', 'Inline load skipped: host page is not HTTP.');
          return;
        }
        status.textContent = 'Opening Pixelblaze. If it stays blank, open the device separately.';
        log('info', 'Device frame navigation requested.');
        frame.src = deviceUrl;
      }

      frame.addEventListener('load', function () {
        if (!frame.hasAttribute('src')) return;
        // A cross-origin iframe load event is not a connectivity check.
        status.textContent = 'Use Pixelblaze’s own connection status below. If the page is blank, open the device separately.';
        log('debug', 'Frame load event received; controller connection is not observable here.');
      });
      reload.addEventListener('click', loadDevice);
      loadDevice();
    });
  </script>
</body>
</html>
```

The example uses a fixed device address deliberately. Changing devices means
editing the `deviceUrl` value and the fallback link's `href`. An address picker,
device discovery, and persistence are separate enhancements. The wrapper does
not attempt cross-origin DOM access, inject code into the controller page, or
add an `iframe-cmd` action that Pixelblaze does not implement.

## 4. Install and use the generated files

1. Identify the **active MBXHub instance's data directory**. Use its existing
   `pages/` and `charms/` folders; do not assume a path from a different install.
2. Place the manifest at `charms/pixelblaze.json` and the wrapper at
   `pages/pixelblaze.html`. Preserve any existing files with those names before
   replacing them. These are data-directory files, not embedded DLL resources.
3. Open the hub's `/pages/pixelblaze.html` over HTTP to check the wrapper directly.
   Then reload the dashboard and open its Pixelblaze Charm. Follow the installed
   version's documented Charm refresh/registration workflow if it does not appear.
4. The first example action uses `display: "both"`: normal activation opens inline;
   the documented Shift-click action opens the wrapper separately. The wrapper's
   **Open device separately** link instead opens the actual controller URL.
5. Confirm connection using the status inside Pixelblaze's UI. Select patterns,
   adjust brightness, and use pattern controls there as you would on its own page.

No MBXHub build, process restart, deployment script change, or library installation
is part of these installation steps. When using an AI agent, review the generated
files before authorizing installation or hardware changes.

To remove this example, remove only its two installed files and refresh the
dashboard. Removing the wrapper is not a command to stop the LEDs or restore a
previous pattern. Avoid leaving multiple controller tabs open unnecessarily.

## 5. Verification checklist and limits

Run these checks on the generated files before describing the integration as
working. Record actual results rather than copying this checklist as a pass.

| Check | Expected observation |
| --- | --- |
| Parse the manifest and inspect its action | Valid JSON; action targets `/pages/pixelblaze.html`; fields match the target hub's `llms.txt`. |
| Inspect served wrapper bytes | The running hub serves this wrapper, rather than an older data-directory override. Theme tokens have been resolved. |
| Firefox first: wrapper directly, then dashboard inline | Device page is visible at a usable size; keyboard focus can reach wrapper actions and device controls. |
| Chrome, Edge, and Safari where available | Same core navigation and embedding behavior; record any browser/OS combination not tested. |
| Controller connection inside the frame | Pixelblaze reports its connection; browser network tools show the expected device port 81. A wrapper `load` event is not sufficient. |
| Hardware operation, with authorization | Record the starting state, select a known pattern and modest brightness change, confirm LEDs respond, then restore the starting state. |
| Narrow viewport and scrolling | Wrapper actions remain reachable; the device page remains usable within its own layout. |
| Unreachable test address in a separate test copy | The wrapper does not report "Connected"; the separate-device link remains available. Do not stop the user's controller for this check. |
| HTTPS-hosted hub | Inline loading is skipped with explanatory text; the direct-device link remains usable. No browser security bypass is suggested. |
| LAN without internet access | Wrapper and core controller operations still function. Optional manufacturer links/features are not an offline guarantee. |

The device's current response headers make direct embedding a plausible fit,
but actual browser framing and WebSocket behavior have not yet been checked.
Browser local-network policies, a host content policy, firmware changes, or an
HTTPS host page may affect it. When embedding fails, check the browser's actual
message and the controller response before selecting a different architecture.

The full Pixelblaze UI remains independently owned: MBXHub cannot theme its inner
document, report its live state, or safely automate its controls through the
cross-origin frame. Music-linked lighting would need a separate, deliberate
integration using the device protocol or a local helper; it is not supplied by
putting the controller page inside a Charm.

## 6. How to use and evaluate the generation prompt

Copy the companion prompt into an LLM session with the target hub's `/llms.txt`
available. Replace the hub URL placeholder with the actual host and port. Keep
the device URL as `http://<ipaddress>/` for this example, or replace it explicitly.
Provide local source access only if you have it; the public prompt does not
require theMatrix or MBXHub source access.

The prompt separates three facts: MBXHub defines the extension contract, the
device defines its UI and transport, and the generated wrapper supplies only the
embedding/navigation. It asks for installable files plus usage and implementation
documentation, and requires a verification record that distinguishes static
checks from browser and hardware checks. This guards against a convincing page
being presented as a tested controller integration.

## Verification record for this document

- Read the local `llms.txt` sections and an existing page Charm's manifest/theme wiring.
- Read PixelblazeDotNet and theMatrix connection/control implementation.
- Fetched the specified device root with HTTP GET and inspected response headers
  and the page's WebSocket construction. No other device endpoint was exercised.
- The original example passed manifest JSON and inline JavaScript syntax checks.
  Static syntax checks are not evidence of browser or hardware operation.
- Browser embedding, runtime offline behavior, installation discovery, and LED
  control remain unverified. MBXHub was not built, restarted, or modified.
