Documentation version
LCcommand API v1.0 – ( and fw 1.6.0 beta ) – August 2026
The LCcommand API lets third-party software talk to compatible KipperTie LC electronic filter devices.
It can be used to read device status, set ND values, change fine/step mode, select local, Bluetooth, or USB control, enable deliberate combined-control override, lock or unlock physical controls, read battery state, and access read-only calibration diagnostics where supported.
Apps can connect over Bluetooth LE, or over USB CDC on products where USB command mode is available. Commands are sent as small JSON messages, and the device replies with JSON.
This page is intended for developers building camera-control tools, browser apps, desktop software, or hardware controllers. It is not needed for normal use of the product.
Compatibility
Bluetooth LE connectivity is available on all LCminiND, LCminiFX, and LCcineND devices.
USB CDC is available on LCcineND devices as standard, and can be enabled by request on LCminiND and LCminiFX.
Firmware version 1.5.3 or later is required for the base public v1 API. Firmware 1.6.0 beta is available by request and adds the source and policy features described on this page without changing the API version from 1. Integrations must check for input_api:1 in the api.info response before showing or using combined-control features. Legacy Bluetooth commands work with prior versions but are not officially supported for third-party use.
Fast integration flow
Start here: For a new integration, do not begin by sending control commands. First identify the device, read its limits, then read its current state. This prevents your app from assuming the wrong ND range, showing unsupported controls, or sending values that only apply to another LC product.
- Connect over Bluetooth LE or USB CDC.
- Send
{"cmd":"api.info"}to confirm the firmware responds and to readapi,input_apiwhere present,fw_ver, anddev_type. - Send
{"cmd":"api.limits"}and use the returned limits for the current session. - Send
{"cmd":"dev.status"}to read the current ND, mode, source, policy, lock state, battery, charging state, and LCminiFX percentage where supported. - If
input_api:1is present, usectrl.source.getandctrl.source.setto read or change the selected input and admission policy. - Use
ctrl.set,ctrl.adjust,ctrl.mode.set, andlock.setfor user actions. - Use
cal.readfor read-only calibration diagnostics where required.
Do not hard-code product limits in your app. Call api.limits after every connection and show percentage controls only when the returned pct field is true.
Supported products
| Product | dev_type | Control value range | Percentage control |
|---|---|---|---|
| LCminiND | LCminiND | 0 to 240 | No |
| LCcineND | LCcineND | -24 to 144 | No |
| LCminiFX | LCminiFX | 0 to 240 | Yes, pct 0 to 99 |
Product capabilities may differ between firmware versions. Always use api.limits after connecting rather than assuming these limits in your app.
Control source and input policy
Firmware 1.6.0 introduces two separate control concepts. Do not confuse either one with fine/step mode:
modecontrols local adjustment granularity and isfineorstep.sourceis the selected or preferred input and islocal,ble, orusb.policycontrols which inputs may change the device and isselectedoroverride.
source:"local" and policy:"selected" are the factory and upgrade-migration defaults. Selected policy preserves exclusive behaviour: physical adjustment works only with source:"local", Bluetooth mutation works only with source:"ble", and USB mutation works only with source:"usb".
override deliberately accepts physical, USB, and Bluetooth adjustments together. The last accepted adjustment wins; there is no transport priority or takeover timeout. source remains the displayed preferred source and becomes exclusive again when policy returns to selected.
Bluetooth remains available while a powered-on device is in override, which increases power use. USB availability still follows the stored USB command-port setting.
Lock blocks the physical wheel and physical short-press mode toggle. It does not block a remote ND or percentage command that is authorised by the current source and policy.
The physical source icon identifies source. A solid 3-by-3-pixel marker at the upper-right of the bottom source area identifies policy:"override".
Source, policy, ND, and fine/step mode persist across reboot. When upgrading from older stored data, valid ND and mode are preserved while source and policy default safely to local and selected. Successful explicit ND, mode, source, and policy API changes are stored before the success response. Physical changes retain delayed, wear-aware storage, and sys.reboot flushes the current control state before scheduling the reboot.
This release does not send unsolicited control events. Apps that need to observe physical changes must poll dev.status. event.subscribe and event.unsubscribe remain reserved.
How ND and diffusion values map to internal values
All products expose mode as fine or step. The nd value used by this API is the public control value shown to software, not a DAC value, calibration value, or raw motor position.
Internally, the firmware uses an offset of 96. To convert between public API values and internal calibration-table positions:
internal_value = nd + 96nd = internal_value - 96
The current calibration table has 481 entries, indexed from 0 to 480. The product range is a supported window inside that table. For example, LCminiND uses public nd values from 0 to 240, which correspond to internal calibration positions 96 to 336. LCcineND uses public nd values from -24 to 144, which correspond to internal positions 72 to 240.
The public nd value is also the value used by the KipperTie app control scale. It is not the same number as the displayed optical density. One public nd code step represents 0.00625 optical-density units. The displayed optical density can be calculated as:
displayed_ND = (nd + 96) / 160
In stop terms, because 0.3 ND is one stop:
displayed_stops = (nd + 96) / 48
| Product | Public API value | Internal value | Displayed ND | Displayed stops |
|---|---|---|---|---|
| LCminiND / LCminiFX | nd:0 | 96 | ND 0.6 | 2 stops |
| LCminiND / LCminiFX | nd:48 | 144 | ND 0.9 | 3 stops |
| LCminiND / LCminiFX | nd:240 | 336 | ND 2.1 | 7 stops |
| LCcineND | nd:-24 | 72 | ND 0.45 | 1.5 stops |
| LCcineND | nd:0 | 96 | ND 0.6 | 2 stops |
| LCcineND | nd:144 | 240 | ND 1.5 | 5 stops |
In fine mode, the device can move by one public nd code step at a time. In step mode, the device snaps to the product’s supported fractional-stop positions. For third-party software, the safest approach is still to use api.limits, show the public nd range returned by the connected device, and optionally display a derived ND or stops label using the formula above.
LCminiFX also exposes diffusion/FX control as pct. The public API range is 0 to 99. Internally, this percentage is mapped across the same 240-code control span used by LCminiFX:
internal_value = 96 + floor(pct * 240 / 99)
nd = internal_value - 96
If your user interface shows this as a 1 to 100 percent display, convert between the UI and API values explicitly:
api_pct = ui_percent - 1
ui_percent = api_pct + 1
For example, a UI value of 100% should be sent to the API as pct:99. Do not send pct:100; it is outside the public API range.
Calibration values returned by cal.read are diagnostic table values. They are not public ND values and should not be used directly as user-facing controls.
Transport options
Bluetooth LE
Use the custom GATT service. Write one JSON request to RX and receive one JSON response from a TX notification. A BLE response contains exactly one JSON object without a line ending.
USB CDC
Send one JSON object over the serial command path. Every JSON response is exactly one JSON object followed by one LF (\n). USB service and manufacturing text responses also end in exactly one LF and do not rely on a leading newline.
Request size
Keep JSON requests below 240 bytes. Responses are designed for the same 240-byte firmware response buffer. Larger logical data sets such as calibration tables are read using application-level pagination: call cal.read repeatedly with the returned next offset. Each request still receives one complete JSON response.
USB identity
The expected firmware 1.6.0 beta USB identity is VID:PID 2fe3:0001. PID 0001 and the product strings are configured by the firmware. VID 2fe3 is inherited from the Zephyr 3.3.99 test default and is not explicitly pinned by this beta.
| Product | USB product string |
|---|---|
| LCminiND | KipperTie LCminiND CDC |
| LCcineND | KipperTie LCcineND CDC |
| LCminiFX | KipperTie LCminiFX CDC |
Bluetooth LE UUIDs
| Item | UUID | Direction |
|---|---|---|
| Service | 4ed4587e-5172-429c-8505-8cbe9c4f231f | Primary service |
| RX characteristic | 523d6915-0ac2-40f6-a423-7d5b0a413d9b | App writes request |
| TX characteristic | ac4493cf-becf-42de-9e3b-3ea076249af7 | Device notifies response |
The RX and TX characteristics use authenticated access. Pairing may request the passkey engraved on the device or the default 123456.
USB CDC framing
For JSON commands, send the JSON object and read until LF. Optional surrounding whitespace and a semicolon terminator are accepted. For browser apps, use Web Serial. For desktop tools, use a serial library such as pyserial. A baud value of 115200 is a safe default for tools that require one.
Request and response format
A request is a JSON object with a cmd string.
{"cmd":"api.info"}
Standard success
{"status":"ok","cmd":"api.info","api":1,"input_api":1,"fw_ver":"1.6.0","dev_type":"LCminiND"}
The command API remains version 1. input_api:1 advertises ctrl.source.get, ctrl.source.set, the source and policy status fields, and the arbitration behaviour documented here.
Standard error
{"status":"err","cmd":"ctrl.set","reason":"range"}
| Reason | Meaning |
|---|---|
bad_json | Request did not parse as a supported JSON object. Because there is no trustworthy command name, this error omits cmd. |
unknown_cmd | cmd was parsed but is not recognised. |
bad_value | A required field is missing or a value is invalid. |
range | A numeric value is outside product limits. |
not_supported | The command exists but is unsupported on this product. |
not_implemented | Reserved command name recognised but not implemented yet. |
mode_required | The requested operation requires a different device mode. |
storage | A persistent storage operation failed. |
busy | Another USB or Bluetooth JSON command is currently being processed. Wait for its response before retrying. |
usb_mode_required | USB mutation rejected by the current source and policy. |
btle_mode_required | Bluetooth mutation rejected by the current source and policy. |
Every successfully parsed v1 request includes cmd in an error response. A policy rejection also reports the current source and policy:
{"status":"err","cmd":"ctrl.set","reason":"usb_mode_required","source":"local","policy":"selected"}
Malformed JSON has no trustworthy command name, so its response deliberately omits cmd:
{"status":"err","reason":"bad_json"}
Discovery and status responses
Use api.info to discover the API and firmware feature level, then api.limits for product-specific limits. Firmware 1.6.0 adds source and policy to dev.status.
{"status":"ok","cmd":"dev.status","nd":48,"mode":"fine","source":"usb","policy":"override","lock":false,"battery":87,"charging":true}
LCminiFX additionally includes pct:
{"status":"ok","cmd":"dev.status","nd":120,"mode":"fine","source":"local","policy":"override","lock":false,"battery":92,"charging":true,"pct":49}
Source and policy commands
Read the complete source and policy state:
{"cmd":"ctrl.source.get"}
{"status":"ok","cmd":"ctrl.source.get","source":"usb","policy":"override"}
Set both values together, or update either value without changing the other:
{"cmd":"ctrl.source.set","source":"usb","policy":"override"}
{"cmd":"ctrl.source.set","source":"usb"}
{"cmd":"ctrl.source.set","policy":"selected"}
Success returns the complete resulting state. At least one recognised field is required. Values are case-sensitive; invalid or missing values return bad_value. This command is the recovery and bootstrap path, so it is accepted from authenticated Bluetooth or initialised USB even when that transport is not selected. If a Bluetooth request selects a non-Bluetooth source under selected policy, the success notification is sent before Bluetooth advertising or connection shutdown is applied.
Command admission
The following discovery, read, and recovery commands are accepted over initialised USB and authenticated Bluetooth regardless of the selected source or policy:
api.info,api.limits, anddev.statusctrl.get,ctrl.mode.get,ctrl.source.get, andctrl.source.setlock.get,cfg.name.get, andcal.read- Legacy reads
get_nd,get_bt,get_fw_ver, andget_bt_name
Under selected, other mutating commands require the matching selected transport. Under override, mutating commands are accepted from USB and Bluetooth. BLE authentication and GATT permissions are unchanged.
Battery percentage and device display
The battery field returned by dev.status is a calculated battery percentage. It is not the raw battery voltage. The firmware measures battery voltage, applies the board’s voltage-divider correction, then converts the result into a percentage for the API.
The API percentage is calculated from voltage thresholds, with integer interpolation between the points. At or above 4170 mV, the API reports 100. At or below the bottom of the mapped range, the API reports 1.
| Battery voltage | API battery value |
|---|---|
4170 mV or higher | 100 |
4136 mV | 95 |
4062 mV | 90 |
3980 mV | 85 |
3891 mV | 70 |
3829 mV | 60 |
3749 mV | 40 |
3691 mV | 25 |
3665 mV | 10 |
3633 mV | 7 |
3601 mV | 3 |
3550 mV | 1 |
The device display uses a separate coarse battery indicator derived from the same voltage thresholds. This means the display icon and the API percentage are related, but they are not the same value. The display indicator is deliberately coarser and includes smoothing behaviour while discharging, so it may not change at exactly the same moment as the API percentage.
The charging field is reported separately as true or false. Do not infer charging state from the battery percentage alone.
Public v1 command catalogue
Commands fall into three groups:
- Discovery and status commands read information from the device and do not change its state.
- Control commands change source, policy, ND, percentage, mode, lock state, or device name.
- Service or reserved commands are not intended for normal third-party control apps unless KipperTie has specifically asked you to use them.
Discovery and status
| Command | Example request | Use |
|---|---|---|
api.info | {"cmd":"api.info"} | Read API version, input feature level, firmware version, and device type. |
api.limits | {"cmd":"api.limits"} | Read product ND range, step support, and percentage capability. |
dev.status | {"cmd":"dev.status"} | Read ND, mode, source, policy, lock, battery, charging, and LCminiFX percentage. |
ctrl.get | {"cmd":"ctrl.get"} | Read ND, mode, and lock without battery fields; LCminiFX also includes percentage. |
ctrl.source.get | {"cmd":"ctrl.source.get"} | Read the selected source and input policy. |
Control
| Command | Example request | Use |
|---|---|---|
ctrl.set ND | {"cmd":"ctrl.set","nd":48} | Set ND directly. Returns range if outside limits. |
ctrl.set with clamp | {"cmd":"ctrl.set","nd":999,"clamp":true} | Clamp ND to product min/max and return limited:true when clamped. |
ctrl.set percent | {"cmd":"ctrl.set","pct":50} | Set LCminiFX percentage. Other products return not_supported. |
ctrl.adjust ND | {"cmd":"ctrl.adjust","unit":"nd","delta":1} | Adjust ND by a positive or negative delta. |
ctrl.adjust percent | {"cmd":"ctrl.adjust","unit":"pct","delta":5} | Adjust LCminiFX percentage. Other products return not_supported. |
Source, policy, mode, lock, configuration, and system
| Command | Example request | Use |
|---|---|---|
ctrl.source.get | {"cmd":"ctrl.source.get"} | Read source and policy. |
ctrl.source.set | {"cmd":"ctrl.source.set","source":"usb","policy":"override"} | Set either or both values and return the complete resulting state. |
ctrl.mode.get | {"cmd":"ctrl.mode.get"} | Read fine or step. |
ctrl.mode.set | {"cmd":"ctrl.mode.set","mode":"fine"} | Set mode to fine or step. |
lock.get | {"cmd":"lock.get"} | Read lock state. |
lock.set | {"cmd":"lock.set","state":"toggle"} | Set lock on, off, or toggle. Lock affects physical controls, not authorised remote mutation. |
cfg.name.get | {"cmd":"cfg.name.get"} | Read stored Bluetooth name. |
cfg.name.set | {"cmd":"cfg.name.set","bt_name":"LCminiND","reboot":true} | Store Bluetooth name. Use 1 to 28 characters, with no quotes, backslashes, or control characters. |
sys.reboot | {"cmd":"sys.reboot"} | Flush current control state, send the response, then reboot. |
Calibration read
The cal.read command is a read-only diagnostic command for inspecting calibration data over Bluetooth LE or USB CDC. It does not write, restore, or modify calibration data.
| Command | Example request | Use |
|---|---|---|
cal.read | {"cmd":"cal.read","table":"current","offset":0,"count":12} | Read a chunk from the current calibration table. |
cal.read legacy | {"cmd":"cal.read","table":"legacy","offset":0,"count":12} | Read a chunk from the legacy calibration table when available. |
The table field is optional and defaults to current. Accepted values are current and legacy. The offset field is optional and defaults to 0. The count field is optional and defaults to 12.
Responses are capped at 12 values per request so they remain within the firmware response buffer. To read a full calibration table, keep calling cal.read with offset set to the returned next value until more is false.
Calibration read response
{
"status":"ok",
"cmd":"cal.read",
"table":"current",
"available":true,
"entry_count":481,
"offset":0,
"count":12,
"more":true,
"next":12,
"migrated":true,
"values":[7913,7913,7913,7913,7913,7913,7913,7913,7913,7913,7913,7913]
}
The current calibration table contains 481 entries. The legacy table contains 241 entries when available. If no readable legacy table remains after an upgrade or migration, the command still returns status:"ok", with available:false, count:0, and an empty values array. Treat this as a normal condition rather than a transport or command failure.
Third-party tools should use api.info first to confirm API version and device type, use cal.read only for diagnostics or verification, avoid requesting more than 12 values at once, handle bad_value for unsupported table names, and handle range for invalid offsets.
Reserved v1 placeholders
These command names are recognized and return {"status":"err","reason":"not_implemented"}: preset.list, preset.save, preset.load, preset.delete, ramp.start, ramp.stop, ramp.status, cal.status, cal.write, cal.restore, event.subscribe, event.unsubscribe, cfg.passkey.set, cfg.usb.set, sys.factory_reset, and ota.status.
Legacy JSON compatibility
Legacy commands remain supported but use older response shapes. New integrations should prefer v1 commands. Legacy mutations use the same source and policy admission rules; a parsed policy error includes cmd.
| Legacy command | Example request | Response shape |
|---|---|---|
get_nd | {"cmd":"get_nd"} | {"nd":N} |
set_nd | {"cmd":"set_nd","nd":48} | {"status":"ok"} or {"status":"err"} |
get_bt | {"cmd":"get_bt"} | Battery percentage and charging boolean. |
get_fw_ver | {"cmd":"get_fw_ver"} | Firmware version and device type. |
get_bt_name | {"cmd":"get_bt_name"} | Stored Bluetooth name. |
set_bt_name | {"cmd":"set_bt_name","bt_name":"LCminiND"} | Status response, with Bluetooth name on success. |
USB-only service commands
These commands are service and manufacturing commands, not normal public API commands. Do not expose them in third-party user interfaces unless KipperTie has specifically approved that use case. Incorrect use may change device configuration or calibration data.
| Command | Format | Purpose |
|---|---|---|
| Port enable | -cmd port enable; | Store and report the enabled USB command-port state; this may be a no-op when already enabled. |
| Port disable | -cmd port disable; | Store and report the disabled command-port state; disable takes effect after reboot. |
| Set passkey | -cmd passkey 123456; | Store a fixed six-digit pairing passkey. |
| Set Bluetooth name | -cmd set bt_name "LCminiND"; | Store Bluetooth name. |
| Write reference calibration | -w,v0,v1,...,v160; | Write a 161-value source calibration table. |
Minimal examples
Browser Bluetooth
const SERVICE = '4ed4587e-5172-429c-8505-8cbe9c4f231f';
const RX = '523d6915-0ac2-40f6-a423-7d5b0a413d9b';
const TX = 'ac4493cf-becf-42de-9e3b-3ea076249af7';
const enc = new TextEncoder();
const dec = new TextDecoder();
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE] }],
optionalServices: [SERVICE]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SERVICE);
const rx = await service.getCharacteristic(RX);
const tx = await service.getCharacteristic(TX);
await tx.startNotifications();
tx.addEventListener('characteristicvaluechanged', event => {
console.log(JSON.parse(dec.decode(event.target.value)));
});
await rx.writeValueWithResponse(enc.encode(JSON.stringify({ cmd: 'api.info' })));
Browser USB CDC
const enc = new TextEncoder();
const dec = new TextDecoder();
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });
const writer = port.writable.getWriter();
await writer.write(enc.encode(JSON.stringify({ cmd: 'api.info' }) + '\n'));
const reader = port.readable.getReader();
let buffer = '';
while (!buffer.includes('\n')) {
const { value, done } = await reader.read();
if (done) break;
buffer += dec.decode(value, { stream: true });
}
console.log(JSON.parse(buffer.trim()));
Python USB CDC
import json
import serial
with serial.Serial('/dev/ttyACM0', 115200, timeout=2) as ser:
ser.write(b'{"cmd":"api.info"}\n')
response = json.loads(ser.readline().decode('utf-8'))
print(response)
Reading a full calibration table
async function readCalibration(sendCommand) {
const values = [];
let offset = 0;
while (true) {
const response = await sendCommand({
cmd: 'cal.read',
table: 'current',
offset,
count: 12
});
if (response.status !== 'ok') {
throw new Error(response.reason || 'cal.read failed');
}
if (!response.available) {
return [];
}
values.push(...response.values);
if (!response.more) {
return values;
}
offset = response.next;
}
}
Common mistakes to avoid
- Do not hard-code ND ranges. Use
api.limitsafter every connection. - Do not show percentage controls unless
api.limitsreturnspct:true. - Do not send another command until the previous response has been received.
- Do not confuse fine/step
mode, selectedsource, and admissionpolicy. - Do not assume a connected transport is authorised to mutate state. Handle
usb_mode_requiredandbtle_mode_required, then usectrl.source.setas the recovery path when appropriate. - Do not assume lock blocks remote control. It blocks the physical wheel and physical mode toggle only.
- Do not wait for unsolicited local-control events in firmware 1.6.0. Poll
dev.statuswhen your app needs to observe physical changes. - Do not enable
policy:"override"without making the combined-control behaviour and increased Bluetooth power use clear to the user. - Do not treat
not_supportedas a broken connection. It may simply mean the connected product does not support that feature. - Do not use calibration values as control values.
cal.readis for diagnostics only. - Do not rely on legacy commands for new integrations.
Implementation checklist
- Call
api.info, thenapi.limits, thendev.statusafter connecting. - Check for
input_api:1before showing source or policy controls. Keep the base API compatible with firmware that reports onlyapi:1. - Store
dev_type,nd_min,nd_max, percentage capability, and—when supported—the currentsourceandpolicyfor the session. - Use
ctrl.source.getto read arbitration state andctrl.source.setto change either or both values. Treat all values as case-sensitive. - Clamp UI controls locally, but still handle firmware
rangeerrors. - Hide percentage controls unless
api.limitsreturnspct:true. - Treat
not_supportedas product capability feedback, not as a fatal connection error. - Send one request at a time and wait for one response before sending the next request.
- Handle
busyby waiting for the in-flight command to finish before retrying. - For Bluetooth, subscribe to TX notifications before writing to RX.
- For USB CDC JSON, read until newline.
- For Bluetooth, treat each TX notification as one complete JSON object without a line ending.
- Poll
dev.statusif the UI must track physical changes; firmware 1.6.0 does not emit unsolicited control events. - For
cal.read, iterate using the returnednextvalue untilmore:false. - Gate persistent writes, reboot actions, and service commands behind clear user confirmation.
Disclaimer
This public API is provided for third-party integration, testing, and development use. It is supplied as-is and may be revised as KipperTie products and firmware develop.
Third-party software is responsible for validating user input, checking api.info and api.limits for each connected device, handling errors safely, and testing behaviour on the specific product and firmware version before use in production.
KipperTie is not responsible for loss of footage, production delays, equipment damage, configuration changes, calibration issues, connection failures, or other losses caused by third-party software, unsupported firmware, incorrect commands, misuse of service commands, or failure to follow this guide.
Do not use this API for safety-critical functions. Service, calibration-write, factory-reset, passkey, and reserved commands should not be exposed to end users unless KipperTie has specifically approved that integration.
For integration questions, firmware compatibility, or access to USB CDC on LCminiND or LCminiFX, contact KipperTie support.
LCcommand API v1 — firmware v1.7.0
The LCcommand API lets third-party software read status, set filter values, select fine/step adjustment, lock local controls, inspect battery state, configure output damping and read calibration diagnostics on KipperTie LCminiND, LCcineND and LCminiFX products.
Commands are small UTF-8 JSON objects. Bluetooth, USB serial, USB network and USB MIDI carry the same supported JSON commands; only their delivery and framing differ. MIDI is a transport, not a second command language.
Version and compatibility
The device continues to report the integer api:1. Firmware 1.7.0, JSON API generation 1, and MIDI envelope version 01 are separate identifiers. This reference does not introduce an api:1.1 value or require existing clients to change their JSON schema.
Start each connection with api.info and api.limits. Do not infer capabilities solely from a higher firmware number: v1.7.0 is not a superset of every experimental branch. The separate v1.6.0 beta’s input_api, source, policy and ctrl.source.* features are not implemented in this v1.7.0 firmware.
Damping and the USB transports below describe v1.7.0. An older device reporting api:1 may not support them. There is no new damping-capability flag in api.info; use documented firmware compatibility and handle unknown_cmd when probing a read-only feature. Do not require a C/D prefix in fw_ver.
Firmware installation compatibility is separate from API compatibility. Use the matching LCcineND, LCminiND or LCminiFX firmware page for update eligibility. Do not use API numbers to choose an OTA package or assume an old ZIP is a supported downgrade.
Quick start
- Choose the intended control mode on the LC. For USB, save CDC, NCM or MIDI in startup setup, then select USB mode.
- Open that transport. Subscribe/connect the receive side before sending.
- Send
{"cmd":"api.info"}; check status, api, fw_ver and dev_type. - Send
{"cmd":"api.limits"}, then{"cmd":"dev.status"}. - Where damping is needed, read
ctrl.softening.getbefore showing its setting. - Send one request at a time and await its response before the next request to that LC.
Example identity response from an LCminiND:
{"status":"ok","cmd":"api.info","api":1,"fw_ver":"1.7.0","dev_type":"LCminiND"}
Physical modes and USB setup
| LC selection | Available control |
|---|---|
| Local | Physical wheel and button. No USB control connection. |
| Bluetooth | Authenticated Bluetooth JSON. No USB control connection. |
| USB, with CDC saved | USB serial JSON and the separate CDC service interface. |
| USB, with NCM saved | Direct USB Ethernet, built-in browser page and HTTP JSON. |
| USB, with MIDI saved | USB MIDI 2.0 UMP SysEx8 JSON. |
Each power-on starts in Local. The USB type persists and defaults to CDC if no valid choice exists. Saving a USB type does not itself select USB control mode. USB charging is independent of control selection.
- Turn the LC on with the usual button hold, then release.
- During the three-second splash, make a second, separate short button press.
- Turn the wheel to choose CDC, NCM or MIDI. The outline is the cursor; the dotted border marks the stored choice.
- Press once to save. Two filled flashes acknowledge selection. A changed type restarts the LC; saving the existing type closes setup without a restart.
- Use the normal hold-and-release control to select Local, Bluetooth or USB. The main screen says USB for all USB types.
Normal Local/Bluetooth/USB transitions do not reboot, but USB endpoints disappear/reappear as USB mode is left/entered. Re-enumerate and reconnect instead of retaining a stale port. cfg.usb.set remains reserved: there is no public command to read/set the saved USB type or remotely switch the active control source.
Fine/step mode is adjustment granularity, not connection selection. Lock suppresses local adjustment and the local short-press granularity toggle, not authorised remote commands. In Local, the first complete wheel detent acts immediately after OLED sleep; the sleeping display’s first button click remains wake-only.
Products, stops and percentages
| Product / dev_type | Public nd range | Percentage capability |
|---|---|---|
| LCminiND | 0 to 240 | No |
| LCcineND | -24 to 144 | No |
| LCminiFX | 0 to 240 | Yes: pct 0 to 99 |
All three report step:true. Read api.limits rather than hard-coding ranges. Example ND and FX replies:
{"status":"ok","cmd":"api.limits","nd_min":0,"nd_max":240,"pct":false,"step":true}
{"status":"ok","cmd":"api.limits","nd_min":0,"nd_max":240,"pct":true,"step":true,"pct_min":0,"pct_max":99}
nd is an integer public control code, not a DAC voltage or displayed optical-density number. For ND products:
internal_index = nd + 96
displayed_ND = (nd + 96) / 160
displayed_stops = (nd + 96) / 48
| Product | Public nd | Optical ND label | Stops |
|---|---|---|---|
| LCminiND | 0 | 0.6 | 2 |
| LCminiND | 48 | 0.9 | 3 |
| LCminiND | 240 | 2.1 | 7 |
| LCcineND | -24 | 0.45 | 1.5 |
| LCcineND | 144 | 1.5 | 5 |
One code is 1/48 stop (0.00625 ND). Split the derived stops value into whole/fractional parts for a display; do not round to whole stops before sending. Direct API ND commands use integer codes; local step mode does not turn every remote adjustment into a stop-sized move.
For LCminiFX, present effect percentage rather than describing diffusion as measured optical ND. The API range is 0–99. For a UI using 1–100, explicitly convert api_pct = ui_percent - 1 and ui_percent = api_pct + 1; never send pct:100.
LCminiFX internal_index = 96 + floor(pct * 240 / 99)
LCminiFX reported_pct = floor(nd * 99 / 240)
Integer mapping can make the returned percentage one code lower than requested. A request of pct:50 produces nd:121 and can report pct:49. Show returned state rather than assuming exact percentage round-tripping. Send either nd or pct, not both.
JSON format, size and errors
Each request is one JSON object with a case-sensitive string cmd. Use documented JSON number/boolean/string types. Keep the complete UTF-8 request to 239 bytes or fewer: count bytes, not characters. For MIDI this includes trailing padding spaces but excludes the seven-byte LC header. Responses use a 240-byte firmware buffer including its internal string terminator; that terminator is not sent.
Ordinary v1 successes include status and cmd. Most decoded command errors include status, cmd and reason:
{"status":"ok","cmd":"ctrl.set","nd":48}
{"status":"err","cmd":"ctrl.set","reason":"range"}
Do not require cmd/reason on every failure. Mode checks precede decoding; malformed requests can have an empty command string; empty/oversized requests may return only a status:
{"status":"err","reason":"usb_mode_required"}
{"status":"err","cmd":"","reason":"bad_json"}
{"status":"err"}
| Reason | Meaning / handling |
|---|---|
| bad_json | Malformed JSON or no usable cmd. |
| unknown_cmd | Unrecognised by this firmware. |
| bad_value | Missing/invalid field, unit, string or damping precision. |
| range | Number outside its accepted range. |
| not_supported | Unsupported on this product, e.g. pct on an ND product. |
| not_implemented | Recognised reserved name; no implemented operation. |
| mode_required | Required mode value absent, e.g. ctrl.mode.set without mode. |
| storage | Persistent configuration write failed. |
| busy | Shared command/output state unavailable; wait instead of overlapping. |
| usb_mode_required | USB control not active for this USB JSON transport. |
| btle_mode_required | Bluetooth mode not selected. |
| ncm_mode_required | USB mode with NCM saved not active. |
Detached transports or invalid MIDI frames may produce no reply. Distinguish API errors, HTTP errors and timeouts. A transaction ID correlates replies; it is not duplicate-command suppression.
Command reference
Discovery and status
| Command | Request | Response / purpose |
|---|---|---|
| api.info | {"cmd":"api.info"} | API generation, visible firmware and product. |
| api.limits | {"cmd":"api.limits"} | Product limits, pct and step capability; FX also pct_min/pct_max. |
| dev.status | {"cmd":"dev.status"} | nd, fine/step mode, lock, battery and charging; FX also pct. |
| ctrl.get | {"cmd":"ctrl.get"} | nd, fine/step mode and lock; FX also pct. |
| ctrl.softening.get | {"cmd":"ctrl.softening.get"} | Damping and applied-output snapshot. |
Illustrative status replies (live values vary by device):
{"status":"ok","cmd":"dev.status","nd":48,"mode":"fine","lock":false,"battery":87,"charging":true}
{"status":"ok","cmd":"dev.status","nd":120,"mode":"fine","lock":false,"battery":92,"charging":false,"pct":49}
v1.7.0 does not include the v1.6 beta’s source/policy fields.
Set and adjust filter values
| Command / variant | Request | Behaviour |
|---|---|---|
| ctrl.set ND | {"cmd":"ctrl.set","nd":48} | Set target; out-of-range returns range if clamp is omitted. |
| ctrl.set with clamping | {"cmd":"ctrl.set","nd":999,"clamp":true} | Clamp to product range; limited:true when limited. |
| ctrl.set percentage | {"cmd":"ctrl.set","pct":50} | LCminiFX only; percentage must be 0–99. |
| ctrl.adjust ND | {"cmd":"ctrl.adjust","unit":"nd","delta":1} | Relative integer-code adjustment clamped to product limits. |
| ctrl.adjust percentage | {"cmd":"ctrl.adjust","unit":"pct","delta":5} | LCminiFX only; relative adjustment clamped to 0–99. |
| ctrl.softening.set | {"cmd":"ctrl.softening.set","damping":2.3} | Set transition time; return full softening state. |
Set/adjust replies contain the resulting nd, and percentage operations also return pct. limited:true appears when limiting occurs. Omit clamp for strict ND range checking, or send clamp:true; do not rely on clamp:false as a strict-mode switch in this implementation. Percentage sets are range-checked, not clamped by the ND clamp option.
Mode, lock, configuration and reboot
| Command | Request | Behaviour |
|---|---|---|
| ctrl.mode.get | {"cmd":"ctrl.mode.get"} | Return mode: fine or step. |
| ctrl.mode.set | {"cmd":"ctrl.mode.set","mode":"fine"} | Set fine or step, not connection source. |
| lock.get | {"cmd":"lock.get"} | Return lock boolean. |
| lock.set | {"cmd":"lock.set","state":"toggle"} | Accept on, off or toggle; return lock boolean. |
| cfg.name.get | {"cmd":"cfg.name.get"} | Return stored bt_name. |
| cfg.name.set | {"cmd":"cfg.name.set","bt_name":"My LC"} | Store Bluetooth name; return bt_name. |
| cfg.name.set with reboot | {"cmd":"cfg.name.set","bt_name":"My LC","reboot":true} | Store name, reply, then schedule reboot. |
| sys.reboot | {"cmd":"sys.reboot"} | Reply, then schedule a cold reboot approximately 500 ms later. |
Names must be 1–28 UTF-8 bytes with no quotes, backslashes or control characters; simple ASCII is recommended. Reboot defaults to false. Rename/reboot can terminate the connection. Do not assume a successful control reply guarantees every operational value was synchronously stored in flash. USB type and Bluetooth name are stored configuration; damping is deliberately volatile.
Battery feedback
battery is a calculated percentage, not millivolts. Charging is a separate boolean. The OLED indicator is coarser and includes discharge smoothing, so it need not change at exactly the same instant.
Battery voltage mapping used by this firmware
| Voltage (mV) | API percentage |
|---|---|
| 4170 or higher | 100 |
| 4136 | 95 |
| 4062 | 90 |
| 3980 | 85 |
| 3891 | 70 |
| 3829 | 60 |
| 3749 | 40 |
| 3691 | 25 |
| 3665 | 10 |
| 3633 | 7 |
| 3601 | 3 |
| 3550 or lower | 1 |
Intermediate values are interpolated. There is no public raw-voltage command.
Damping and feedback
ctrl.softening.set accepts 0.0–5.0 seconds in exact 0.1-second increments. Zero bypasses damping. It resets to 0.0 after reboot. Unsupported precision returns bad_value; out-of-range values return range.
{"cmd":"ctrl.softening.set","damping":2.3}
{"status":"ok","cmd":"ctrl.softening.get","damping":2.3,"target_nd":48,"applied_nd":41.625,"moving":true,"remaining_ms":1375,"powered":true,"dac_code":1842,"error":0}
| Field | Meaning |
|---|---|
| damping | Full transition duration in seconds. |
| target_nd | Latest requested public ND target. |
| applied_nd | Current fractional public-code output position derived from calibration, not measured optical density. |
| moving | Whether recent target changes remain in the damping window. |
| remaining_ms | Time until the newest change traverses that window. |
| powered | Whether the output chain is enabled. |
| dac_code | Latest 12-bit DAC output code; -1 while off. |
| error | Latest output-operation error; 0 means success. |
Both softening commands return this state with their own cmd echoed. Ordinary status/get/set replies still report target nd, not interpolated output. The independent output task runs approximately every 5 ms; a set reply does not mean the physical transition has completed.
For an isolated change, calibrated output moves symmetrically, reaches halfway at half the duration, and completes at the end. Continuously changing targets remain damped. Fractional positions interpolate neighbouring calibration entries before DAC output. This is not a measurement of actual transmission.
Poll softening state for applied-output feedback. On LCminiFX, applied_nd remains a code coordinate, not a new measured-percentage field.
Calibration diagnostics
cal.read is read-only: it does not write, restore or recalibrate. Never use returned table values as public filter-control values.
{"cmd":"cal.read","table":"current","offset":0,"count":12}
Table defaults to current; accepted strings are current and legacy. Offset defaults to 0. Count defaults to 12, is capped at 12, and must be positive. Offsets are zero-based. Current and legacy tables have 481 and 241 entries respectively when available.
Illustrative current-table reply:
{"status":"ok","cmd":"cal.read","table":"current","available":true,"entry_count":481,"offset":0,"count":12,"more":true,"next":12,"migrated":false,"values":[3000,2998,2996,2994,2992,2990,2988,2986,2984,2982,2980,2978]}
Continue with offset equal to returned next until more:false. Final replies use next:-1. The migrated flag appears for the current table, not legacy. Unavailable legacy data still returns status ok with available false, count 0, more false, next -1 and an empty values array.
cal.status, cal.write and cal.restore remain reserved. There is no public calibration-restore API.
Bluetooth LE
| Item | UUID | Direction |
|---|---|---|
| Service | 4ed4587e-5172-429c-8505-8cbe9c4f231f | Primary service |
| RX | 523d6915-0ac2-40f6-a423-7d5b0a413d9b | App writes request |
| TX | ac4493cf-becf-42de-9e3b-3ea076249af7 | LC notifies response |
Select Bluetooth mode. Pair/authenticate with the configured passkey and enable TX notifications before writing RX. Use a write with response. Each request is one complete JSON object; each notification is one complete response without a newline. A successful GATT write is not the JSON command reply.
Ensure the negotiated ATT payload accommodates the complete request and reply. The handler does not reassemble JSON split across independent RX writes. Do not invent a chunking protocol; validate notification sizes on the intended platform, especially for calibration replies.
Native iOS Bluetooth clients use Core Bluetooth. The browser example applies only where Web Bluetooth is supported, not as a promise of iOS browser support.
USB CDC
Save CDC, then select USB mode and open the serial port. Use 115200 if a library requires a baud value. Send one JSON object followed by LF; read until LF. Replies end in \n. Surrounding whitespace and a trailing semicolon are accepted; JSON plus LF is simplest.
The port disappears when USB control is left. Physical cable attachment does not authorise control in Local/Bluetooth. Separate service text strings are not JSON commands.
USB NCM / HTTP
Save NCM and select USB mode on a host supporting USB NCM networking. Open the compact control page at http://192.0.2.1/. The host is offered 192.0.2.2/24, with no default gateway or DNS. A hardware-derived .local name is advertised where host discovery supports it.
POST /api/v1/command HTTP/1.1
Host: 192.0.2.1
Content-Type: application/json
Content-Length: 18
{"cmd":"api.info"}
Send exactly Content-Type: application/json, without a charset parameter, and a bounded body with content length. Native clients can call this local endpoint. Browsers must use the same origin: an Origin header, when present, must match the request’s HTTP host. There are no permissive CORS headers for unrelated hosted websites.
Check HTTP status and JSON status. Command errors normally return JSON; malformed HTTP can return plain text. Typical results: 200 success, 400 invalid command/value, 403 mode/origin rejection, 413 oversized body, 415 content type, 501 reserved operation, 503 busy. Do not assume every failure body is JSON.
NCM is unavailable in Local/Bluetooth on v1.7.0. Multiple NCM links reuse point-to-point addresses; do not assume this URL automatically routes to the correct unit on a multi-LC host. Use separate MIDI endpoints for the hub-based multi-device control design.
USB MIDI 2.0 / UMP SysEx8
Save MIDI and select USB mode. Use MIDI 2.0 Universal MIDI Packets (UMP), message type 0x5, group 0, SysEx8 stream ID 0. This is not MIDI 1.0 SysEx7. Do not add F0/F7, 7-bit packing, Base64, compression, newline or semicolon framing.
Logical message envelope
| Offset | Bytes | Meaning / value |
|---|---|---|
| 0 | 2 | 16-bit manufacturer ID, big-endian: 00 7D (research/beta only). |
| 2 | 1 | Device ID 00; endpoint selection supplies the address. |
| 3 | 1 | Sub-ID 1: 4C (L). |
| 4 | 1 | Sub-ID 2: 43 (C). |
| 5 | 1 | LC MIDI transport version 01. |
| 6 | 1 | Controller-chosen transaction ID, echoed in the response. |
| 7 | variable | UTF-8 bytes of one supported JSON command/reply, unchanged. |
00 7D 00 4C 43 01 2A followed by UTF-8 bytes of {"cmd":"api.info"}
The response repeats the seven-byte envelope and transaction ID, followed by JSON. Match endpoint, fixed header and transaction ID before accepting it. The ID is correlation, not authentication or replay protection. Client examples use 1–255 then wrap to 1. Keep one outstanding request per endpoint.
Packet assembly
Each SysEx8 UMP is 128 bits: four 32-bit words. The byte count includes the stream-ID byte, leaving at most 13 LC payload bytes. Use Complete for one packet, or Start, zero or more Continue packets and End.
| Status nibble | Meaning |
|---|---|
| 0 | Complete |
| 1 | Start |
| 2 | Continue |
| 3 | End |
First UMP byte: 50 (type 5, group 0). Second: status in the high nibble, payload byte count plus 1 in the low nibble. Third: stream ID 00. The final 13 bytes contain LC data and unused zero fill. Do not append unused fill to the assembled JSON.
A receive callback can contain only part of a message or several UMPs. Iterate all UMPs; track Start/Continue/End; bound assembly to 256 bytes; validate count, sequence, group and stream, then the LC header. The JSON portion including deliberate trailing spaces is limited to 239 bytes. Invalid envelopes or oversized MIDI requests can be silently discarded rather than answered with JSON.
Required USB padding
Before packetizing, join the seven-byte header and JSON. Calculate N = ceil(total_bytes / 13). If N is divisible by four, append ASCII spaces after the closing JSON brace until one additional UMP is required: pad total length to 13 * N + 1. A 51-byte payload therefore becomes 53 bytes, not 52.
This avoids the development USB MIDI class’s exact 64-byte OUT-boundary issue without changing the JSON object. Do not use NULs, newlines or another packing scheme. Use this padding with v1.7.0.
Multiple devices and identity
A USB hub presents each LC as a separate source/destination pair. Select the destination and matching source in the same Core MIDI entity. No configurable bus address or extra JSON device-address field is required. Never silently substitute the first LC when a stored identity is ambiguous.
The USB descriptor has a hardware-derived serial; UMP discovery exposes a hardware-derived Product Instance ID. Host endpoint IDs distinguish connected devices. Verify stability through unplug/replug, hub changes and the intended iOS version before using a host ID as a permanent installation identity.
USB / MIDI identifiers
| Item | v1.7.0 value |
|---|---|
| USB VID | 2FE3: development value, not an owned production allocation. |
| CDC / NCM / MIDI PIDs | 0001 / 0002 / 0003. |
| Product strings | KipperTie LCminiND 1.7.0, KipperTie LCcineND 1.7.0, KipperTie LCminiFX 1.7.0. |
| MIDI manufacturer ID | 00 7D: research/non-commercial beta value; production ID TBD. |
OS endpoint display names can differ. Confirm the selected device using api.info instead of matching a friendly name alone.
Native iOS / macOS integration
The intended USB MIDI controller is a native application using Apple’s Core MIDI MIDI 2.0 APIs, not a browser Web MIDI implementation. See Apple’s MIDI 2 integration overview.
- Create a long-lived Core MIDI client, a MIDI 2.0 input port with
MIDIInputPortCreateWithProtocol/kMIDIProtocol_2_0, and an output port. - Enumerate endpoints; show device identity in the picker. Get the selected destination’s entity and connect input to the source in that same entity before sending.
- Add four-word UMPs to a
MIDIEventListand send usingMIDISendEventList. Pass numeric UMP words, not a MIDI 1.0 byte stream. - When connected, send api.info. Suggested timeout: two seconds; retry that read-only handshake up to three times if the initial reply is missed.
- Read limits, status and optional damping. Use a separate single-request queue for each LC.
- Copy incoming UMPs promptly into a bounded parser/queue; update the UI on its UI thread, not in the high-priority receive callback.
- Handle endpoint removal/reconnect notifications, cancel stale requests and reselect ambiguous devices.
Validate the intended iOS version, USB hub and power arrangement, device count and background behaviour for your integration.
Code examples
These demonstrate actual command/wire formats, not a complete production application. Substitute the port/endpoint, maintain one request queue and add discovery, lifecycle, disconnect handling and error UI.
Python USB CDC
import json
import serial
# Substitute the actual port; macOS commonly uses /dev/cu.usbmodem...
with serial.Serial("/dev/ttyACM0", 115200, timeout=2) as port:
port.write(b'{"cmd":"api.info"}\n')
line = port.readline()
if not line.endswith(b"\n"):
raise TimeoutError("No complete LC reply")
reply = json.loads(line.decode("utf-8"))
if reply.get("status") != "ok":
raise RuntimeError(reply.get("reason", "LC command failed"))
print(reply)
Same-origin browser HTTP / NCM
// Run on the LC's own NCM page/origin, not an unrelated website.
// Await each response before starting another request.
async function sendLC(command) {
const response = await fetch('/api/v1/command', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(command),
cache: 'no-store'
});
const text = await response.text();
let reply;
try { reply = JSON.parse(text); }
catch { throw new Error('Non-JSON HTTP response: ' + response.status); }
if (!response.ok || reply.status !== 'ok') {
throw new Error(reply.reason || ('HTTP ' + response.status));
}
return reply;
}
const identity = await sendLC({ cmd: 'api.info' });
const limits = await sendLC({ cmd: 'api.limits' });
const state = await sendLC({ cmd: 'dev.status' });
Browser Bluetooth where supported
// Only for a browser/environment supporting Web Bluetooth.
// LC must already be in Bluetooth mode. Native iOS uses Core Bluetooth.
const SERVICE = '4ed4587e-5172-429c-8505-8cbe9c4f231f';
const RX = '523d6915-0ac2-40f6-a423-7d5b0a413d9b';
const TX = 'ac4493cf-becf-42de-9e3b-3ea076249af7';
const enc = new TextEncoder();
const dec = new TextDecoder();
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: [SERVICE] }], optionalServices: [SERVICE]
});
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SERVICE);
const rx = await service.getCharacteristic(RX);
const tx = await service.getCharacteristic(TX);
let pending = null;
tx.addEventListener('characteristicvaluechanged', event => {
if (!pending) return;
let reply;
try { reply = JSON.parse(dec.decode(event.target.value)); }
catch (error) { pending.reject(error); return; }
pending.resolve(reply);
});
await tx.startNotifications();
async function sendBLE(command) {
if (pending) throw new Error('An LC request is already pending');
let timer;
const result = new Promise((resolve, reject) => {
pending = { resolve, reject };
timer = setTimeout(() => reject(new Error('LC timeout')), 2000);
});
result.catch(() => {}); // Handle rejection while write awaits.
try {
await rx.writeValueWithResponse(enc.encode(JSON.stringify(command)));
const reply = await result;
if (reply.status !== 'ok') throw new Error(reply.reason || 'LC error');
return reply;
} finally {
clearTimeout(timer);
pending = null;
}
}
const info = await sendBLE({ cmd: 'api.info' });
// After a timeout/write failure, reconnect before sending again;
// a late old response must not be mistaken for a new reply.
Reference MIDI packetizer
This Python reference produces numeric UMP words for the byte layout and padding; it does not open Core MIDI. Implement equivalent packetization in your native send path.
import json
import struct
# Current engineering/beta manufacturer ID, not a production allocation.
PREFIX = bytes.fromhex("00 7d 00 4c 43 01")
def lc_request_umps(command, transaction_id):
# Client convention: use 1..255, then wrap back to 1.
if not 1 <= transaction_id <= 255:
raise ValueError("transaction_id must be 1..255")
body = json.dumps(command, separators=(",", ":"),
ensure_ascii=False).encode("utf-8")
frame = PREFIX + bytes([transaction_id]) + body
# Current USB-boundary workaround: avoid 4, 8, 12, ... UMPs.
count = (len(frame) + 12) // 13
if count % 4 == 0:
frame += b" " * (count * 13 + 1 - len(frame))
if not 1 <= len(frame) - 7 <= 239:
raise ValueError("JSON including padding exceeds 239 bytes")
words = []
for offset in range(0, len(frame), 13):
chunk = frame[offset:offset + 13]
last = offset + len(chunk) == len(frame)
if len(frame) <= 13:
status = 0 # Complete
elif offset == 0:
status = 1 # Start
elif last:
status = 3 # End
else:
status = 2 # Continue
# MT 5, group 0; count includes stream ID; stream ID 0.
packet = bytes([0x50, (status << 4) | (len(chunk) + 1), 0])
packet += chunk + bytes(13 - len(chunk))
words.append(struct.unpack(">4I", packet))
return words
# Numeric UMP words, not bytes to write to a serial port:
umps = lc_request_umps({"cmd": "api.info"}, 0x2A)
Read a complete calibration table
async function readCalibration(sendCommand, table = 'current') {
const values = [];
let offset = 0;
for (;;) {
const reply = await sendCommand({ cmd: 'cal.read', table, offset, count: 12 });
if (reply.status !== 'ok') throw new Error(reply.reason || 'cal.read failed');
if (!reply.available) return [];
values.push(...reply.values);
if (!reply.more) return values;
if (!Number.isInteger(reply.next) || reply.next <= offset) {
throw new Error('Invalid calibration pagination');
}
offset = reply.next;
}
}
// sendCommand sends one request and returns its parsed JSON reply.
Legacy and service commands
Legacy JSON remains available with older response shapes. Prefer v1 for new integrations. Physical transport-mode restrictions still apply.
| Legacy command | Example request | Reply shape / purpose |
|---|---|---|
| get_nd | {"cmd":"get_nd"} | {"nd":N} |
| set_nd | {"cmd":"set_nd","nd":48} | Status ok or err. |
| get_bt | {"cmd":"get_bt"} | {"bt":N,"chg":true}: percentage and charging. |
| get_fw_ver | {"cmd":"get_fw_ver"} | fw_ver and dev_type. |
| get_bt_name | {"cmd":"get_bt_name"} | bt_name. |
| set_bt_name | {"cmd":"set_bt_name","bt_name":"My LC"} | Status, with bt_name on success; optional reboot boolean. |
The following recognised names return not_implemented:
preset.list, preset.save, preset.load, preset.delete
ramp.start, ramp.stop, ramp.status
cal.status, cal.write, cal.restore
event.subscribe, event.unsubscribe
cfg.passkey.set, cfg.usb.set, sys.factory_reset, ota.status
These are not promised features. There is no unsolicited event subscription, public factory reset, JSON calibration write/restore or JSON USB-type selection.
CDC-only service/manufacturing text — not normal application control
Use only under explicit KipperTie service instructions. These are text strings, not JSON/MIDI/HTTP commands. Incorrect use can alter configuration or calibration. Do not expose them in a normal third-party UI.
| Format | v1.7.0 status |
|---|---|
-cmd port enable; / -cmd port disable; | Retired: reports that USB function is controlled by the menu. Not a CDC/NCM/MIDI selector. |
-cmd passkey 123456; | Store a six-digit Bluetooth passkey; follow the service restart procedure. |
-cmd set bt_name "My LC"; | Store Bluetooth name; public applications should use cfg.name.set. |
-w,v0,v1,...,v160; | Write a 161-value source calibration table. Service use only; not a public backup/restore API. |
Service replies are not JSON and must not be parsed using ordinary JSON reply assumptions.
Earlier firmware differences
This reference covers v1.7.0. Keep older behaviour version-scoped; a higher-looking number does not imply that all experiments were merged.
| Firmware line | Relevant difference |
|---|---|
| Public v1.5.4 | Established base JSON API. api:1 alone does not imply damping, USB startup setup, NCM or MIDI. |
| Separate v1.6.0 beta | input_api:1, remote source selection and selected/override policy; not in v1.7.0. |
| C/D development lines | Damping/transport experiments. Exact build/history matter; their older NCM-in-Local/Bluetooth policy is not the v1.7 policy. |
| v1.7.0 | Saved CDC/NCM/MIDI choice; USB control only in deliberately selected USB mode; Local at power-on; damping and immediate wheel input after OLED sleep. |
v1.6.0 beta source/policy reference — not v1.7.0 commands
Only firmware explicitly reporting input_api:1 advertises this feature. On that separate beta, mode is fine/step; source is local, ble or usb; policy is selected or override.
Selected makes the chosen input exclusive. Override permits local, USB and authenticated Bluetooth together, with the last accepted adjustment winning. It keeps Bluetooth available while powered, increasing power consumption. Source remains the displayed preference and becomes exclusive when policy returns to selected.
{"cmd":"ctrl.source.get"}
{"status":"ok","cmd":"ctrl.source.get","source":"usb","policy":"override"}
{"cmd":"ctrl.source.set","source":"usb","policy":"override"}
That beta accepts source, policy or both; at least one valid field is required. Its read/recovery commands, including ctrl.source.set, may be accepted over authenticated Bluetooth or initialised USB regardless of current policy. Other mutations need the selected input unless override is enabled. Its status/policy-rejection replies include source and policy, and those settings persist on that branch.
Do not use these commands to bootstrap or recover v1.7.0. It has no input_api flag or combined-control override, and can reject a transport before parsing any command. Select the physical control mode instead.
Implementation checklist
- Discover with api.info, then read api.limits and dev.status.
- Keep public codes, optical ND, stop labels and FX percentages distinct; use returned product limits and state.
- Distinguish API generation, visible firmware, numeric updater image version and MIDI envelope version.
- Connect the receive side first. Use one in-flight request per LC.
- Coalesce slider motion: retain the newest desired target while waiting, rather than flooding every intermediate event.
- No unsolicited control events are provided. Poll dev.status at a modest rate while visible, e.g. once per second. Poll softening state for applied-output feedback. Update existing UI values rather than rebuilding/flickering the interface.
- After timeout, reconnect and reconcile by reading. Do not blindly repeat relative adjustments, toggle, rename or reboot: the first request may already have run.
- For MIDI, match endpoint/header/transaction ID and apply the padding rule. For BLE/CDC, never associate a late old reply with a new request.
- Handle range, busy, unsupported commands, API errors, HTTP errors and disconnects separately.
- Separate target from applied state during damping; neither is measured optical output.
- Follow calibration pagination; never substitute diagnostic table entries for public control values.
- Confirm persistent writes, reboot and approved service actions with the user.
- Test the real OS, phone/desktop, hub, product, reconnect behaviour and multi-device targeting before production use.
Disclaimer
This reference is supplied for integration, testing and development. Applications must validate input, discover per-device limits, implement safe retries and test intended hardware/firmware.
