---
name: vcu-plugin
description: >
  Write, review, or debug Dynam Labs VCU+ Lua plugins. Use this whenever
  someone is working in the Bolt plugin editor, mentions onTick, a VCU+
  plugin, plugin Lua, firmware-gated plugin API, getSignal, sendCAN from a
  plugin, Bolt MCP, or wants a script that runs on the VCU. Also use it when
  they paste plugin code and ask if it will run, even if they never say
  "skill" or "plugin reference".
---

# VCU+ plugins

A VCU+ plugin is Lua that runs on the controller, in a sandbox, on the plugin loop. The function list changes with firmware. A catalog copied into this skill would go stale, so fetch the live manual before you write or judge code.

## Load the API

Fetch this Markdown before proposing a plugin or reviewing one:

https://dynamlabs.com/documentation/vcu/plugin-reference-manual.md

It is generated from the same source as the HTML manual and the PDF. Read it for signatures, parameters, sample code, firmware gates, and deprecations.

Fetch the JSON only when you need fields a program can parse (autocomplete, a firmware check, a list of signal names):

https://dynamlabs.com/api/v1/plugin-reference

If you cannot fetch either URL, say so and do not invent bindings.

## Bolt MCP

Bolt 2.3 and newer can run a local MCP server for the plugin editor. The user turns it on in Bolt Settings with **Enable MCP server**. Bolt has to stay open. The endpoint is `http://127.0.0.1:8765/mcp` unless the port was changed. A connection without the bearer token from that screen is rejected, so use the copied client config, which includes the token. JSON is for Cursor, Claude, and Gemini CLI. VS Code uses a different key. Codex uses TOML.

This is Bolt on the machine. It is not a Dynam Labs website API.

When those tools are connected, prefer them over the public URLs. They see the connected VCU.

- `get_vcu_context` for firmware and whether a VCU is connected. Write for that firmware.
- `search_plugin_reference` to look up a name. `get_plugin_reference` only when you need the full catalog.
- `validate_lua` before handing the script over. It reports syntax, deprecation, and firmware gates against the connected VCU.
- `get_current_plugin` for the draft already in the editor and its character limit.
- `apply_plugin_draft` to put the script in the editor. It does not run it.
- `save_and_run_plugin` only when the user asks to run it on the VCU. It is blocked when Bolt is disconnected or the script has syntax errors.

`list_output_channels`, `search_calibration`, and `get_vcu_faq` are for the connected car and the product FAQ, not a substitute for the plugin catalog.

If the tools are missing, say so and point at Settings. Then fetch the manual and let the user paste the Lua.

## Runtime

The plugin runtime loads `math` and nothing else. The `string`, `table`, `io`, `os`, `utf8`, `coroutine`, `debug`, and `package` libraries are absent, and so are base functions such as `pairs`, `ipairs`, `type`, `tostring`, `tonumber`, `pcall`, and `error`. A script that calls them fails to load. Table constructors, indexing, `#`, and `..` are language syntax, so `{...}` works. `sendCAN` payloads and `receiveCAN` data are tables. Stay with arithmetic, numeric `for`, `#`, `..`, `math.*`, and table literals. Bitwise operators are part of the language, so they work on integers. `print` logs its first argument only, as one string in the Bolt plugin log. Concatenate before calling it. It is not Lua's `print`.

`onTick` is required. If it is missing, or if it raises, Bolt logs the error and the plugin restarts about once a second. Values stored outside `onTick` survive from tick to tick while `onTick` keeps succeeding. An error inside a `receiveCAN` handler is logged and does not restart the plugin. `onSleep` is optional. It runs once, just before the VCU sleeps, after the controller has decided it is safe to sleep.

`onTick` runs only when the Ignition signal is valid and on. Key-off does not call it, including while the controller stays awake to manage thermals. The plugin loop and `receiveCAN` handlers keep running in that window. A fan or pump written only in `onTick` stops at key-off.

The period is the plugin loop rate, set in calibration in Bolt, from 1 to 1000 ms. The script does not choose that rate. A plugin call that runs longer than 100 ms is stopped, so a busy loop is a fault, not a wait.

`setOut` and `clearOut` log an error and do nothing unless that channel is assigned to the plugin. `getOut` only checks that the name is a real output.

Call `receiveCAN` once from the top of the script, not from `onTick`. Each call adds a handler, and the limit is 32. A second handler for the same bus and id never runs; the first registration wins. The handler runs for a frame that has already arrived, before `onTick` when ignition is on, and at key-off as well. The data table is 1-indexed integers. Read it with a numeric `for` or `data[n]`.

`setLogEntry` takes an integer slot from 1 to 8 and a number. For `setAnalogOut`, follow the manual's note on which inverter and pedal types reject the call. That rejection raises, so the plugin restarts. Voltage outside 0–5 V is clamped and does not abort. `getBmsMinCellV` and `getBmsMaxCellV` return nothing when the BMS has no per-cell voltages. `setPWMFreq` and `setPWMDuty` are ignored when a gauge already uses that channel.

Bolt limits a plugin to 14000 characters. `get_current_plugin` reports that limit.

## Write code the firmware will accept

Use only functions, constants, and signals that appear in the manual or the Bolt MCP catalog you just loaded. A plausible name that is not listed will fail on the device, and that catalog is the contract.

State a pin, bus, or firmware limit only when that catalog says it, or when `validate_lua` reports it for the connected VCU. If both are silent, name what the user still has to assign. Do not invent a reservation: it sends them looking for a conflict that is not there.

Check the firmware line on each function. If the user's firmware is older than `minFirmware`, do not call that function. Prefer the current name when the manual marks one deprecated. `getSignal` replaced `getSensor`.

`getSignal` returns nil when that sensor has not been updated in time. Do arithmetic only after you know the value is present, or a silent BMS turns into a bad comparison.

Plugin `sendCAN` may use CAN2 or CAN3. CAN1 is ignored. Each payload entry has to be an integer, at most 8 bytes, because the firmware rejects floats. `math.floor` is how a temperature becomes a byte. Values below 0 or above 255 wrap in a single byte, so say so if the quantity can leave that range.

The Bolt editor rejects any source byte outside ASCII (`U+0000`–`U+007F`). A degree sign in a comment is enough to fail the script. Write units as `deg C` or `C`.

A single temperature threshold chatters when the reading sits on the line. The VCU's own fan logic uses separate on and off temperatures for that reason. A first example can use one threshold if you say what it will do at the boundary.

## Output

Lead with the Lua the user can paste into Bolt. Note the firmware it needs, and name any pin or bus they still have to assign to the plugin. Do not describe admin or sales APIs. Those are not part of the plugin runtime.
