LCcommand API

LCcommand API v1.0 – 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.

  1. Connect over Bluetooth LE or USB CDC.
  2. Send {"cmd":"api.info"} to confirm the firmware responds and to read api, input_api where present, fw_ver, and dev_type.
  3. Send {"cmd":"api.limits"} and use the returned limits for the current session.
  4. Send {"cmd":"dev.status"} to read the current ND, mode, source, policy, lock state, battery, charging state, and LCminiFX percentage where supported.
  5. If input_api:1 is present, use ctrl.source.get and ctrl.source.set to read or change the selected input and admission policy.
  6. Use ctrl.set, ctrl.adjust, ctrl.mode.set, and lock.set for user actions.
  7. Use cal.read for 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

Productdev_typeControl value rangePercentage control
LCminiNDLCminiND0 to 240No
LCcineNDLCcineND-24 to 144No
LCminiFXLCminiFX0 to 240Yes, 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:

  • mode controls local adjustment granularity and is fine or step.
  • source is the selected or preferred input and is local, ble, or usb.
  • policy controls which inputs may change the device and is selected or override.

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 + 96
  • nd = 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
ProductPublic API valueInternal valueDisplayed NDDisplayed stops
LCminiND / LCminiFXnd:096ND 0.62 stops
LCminiND / LCminiFXnd:48144ND 0.93 stops
LCminiND / LCminiFXnd:240336ND 2.17 stops
LCcineNDnd:-2472ND 0.451.5 stops
LCcineNDnd:096ND 0.62 stops
LCcineNDnd:144240ND 1.55 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.

ProductUSB product string
LCminiNDKipperTie LCminiND CDC
LCcineNDKipperTie LCcineND CDC
LCminiFXKipperTie LCminiFX CDC

Bluetooth LE UUIDs

ItemUUIDDirection
Service4ed4587e-5172-429c-8505-8cbe9c4f231fPrimary service
RX characteristic523d6915-0ac2-40f6-a423-7d5b0a413d9bApp writes request
TX characteristicac4493cf-becf-42de-9e3b-3ea076249af7Device 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"}
ReasonMeaning
bad_jsonRequest did not parse as a supported JSON object. Because there is no trustworthy command name, this error omits cmd.
unknown_cmdcmd was parsed but is not recognised.
bad_valueA required field is missing or a value is invalid.
rangeA numeric value is outside product limits.
not_supportedThe command exists but is unsupported on this product.
not_implementedReserved command name recognised but not implemented yet.
mode_requiredThe requested operation requires a different device mode.
storageA persistent storage operation failed.
busyAnother USB or Bluetooth JSON command is currently being processed. Wait for its response before retrying.
usb_mode_requiredUSB mutation rejected by the current source and policy.
btle_mode_requiredBluetooth 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, and dev.status
  • ctrl.get, ctrl.mode.get, ctrl.source.get, and ctrl.source.set
  • lock.get, cfg.name.get, and cal.read
  • Legacy reads get_nd, get_bt, get_fw_ver, and get_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 voltageAPI battery value
4170 mV or higher100
4136 mV95
4062 mV90
3980 mV85
3891 mV70
3829 mV60
3749 mV40
3691 mV25
3665 mV10
3633 mV7
3601 mV3
3550 mV1

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

CommandExample requestUse
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

CommandExample requestUse
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

CommandExample requestUse
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.

CommandExample requestUse
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 commandExample requestResponse 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.

CommandFormatPurpose
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.limits after every connection.
  • Do not show percentage controls unless api.limits returns pct:true.
  • Do not send another command until the previous response has been received.
  • Do not confuse fine/step mode, selected source, and admission policy.
  • Do not assume a connected transport is authorised to mutate state. Handle usb_mode_required and btle_mode_required, then use ctrl.source.set as 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.status when 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_supported as a broken connection. It may simply mean the connected product does not support that feature.
  • Do not use calibration values as control values. cal.read is for diagnostics only.
  • Do not rely on legacy commands for new integrations.

Implementation checklist

  • Call api.info, then api.limits, then dev.status after connecting.
  • Check for input_api:1 before showing source or policy controls. Keep the base API compatible with firmware that reports only api:1.
  • Store dev_type, nd_min, nd_max, percentage capability, and—when supported—the current source and policy for the session.
  • Use ctrl.source.get to read arbitration state and ctrl.source.set to change either or both values. Treat all values as case-sensitive.
  • Clamp UI controls locally, but still handle firmware range errors.
  • Hide percentage controls unless api.limits returns pct:true.
  • Treat not_supported as 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 busy by 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.status if the UI must track physical changes; firmware 1.6.0 does not emit unsolicited control events.
  • For cal.read, iterate using the returned next value until more: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.