Device pack manifest.json
Each device pack is a folder under assets/devices/ (bundled) or the on-disk device library. The pack must contain a manifest.json at its root. This document describes the manifest format; the machine-readable subset is in device-manifest.schema.json.
Layout
devices/
docs/ ← this documentation (not a device pack)
products/<pack_id>/manifest.json
vendors/<pack_id>/manifest.json
protocols/<pack_id>/manifest.json
generic/<pack_id>/manifest.jsonThe pack folder id (e.g. products/pixel_buds_2a) is the runtime device id. Paths in the manifest are relative to that folder.
Top-level fields
| Field | Description |
|---|---|
name | Display name in the device library and scan UI. |
description | Short summary. |
author | Author or vendor label. |
manifest_version | Integer manifest format version (which keys and semantics the app expects). Current value: 1. |
manifest_version_note | Optional. Only if you need a short label when bumping manifest_version (e.g. "added device_name_regex"); omit on normal packs. |
version | Integer pack content revision; shown in the library UI, bumped when you edit a pack, and compared with the ESP32 copy for sync (OUTDATED when the phone copy is higher). |
notes | Longer free-form notes (pairing, testing, provenance). |
author_url, model_url, source_url | Optional links (URI). |
profile | Relative path to BLE profile JSON (e.g. ble.json). |
uuids | Relative path to UUID map (default uuids.json when omitted). |
vars | Relative path to JSON merged into the Lua vars global (e.g. vars.json). |
assets | icon, graphics, optional icon_tint for scan row / Lua. |
apps | Companion app store links (google, apple, direct[]). |
roles | observer, central, peripheral — see below. |
Additional top-level keys are allowed and ignored by the app unless documented elsewhere (e.g. catalog for Fast Pair).
Roles
Observer (roles.observer)
Runs on scan when advertisement conditions match. Lower priority runs first.
| Field | Description |
|---|---|
entry | Relative path to observer Lua (e.g. observer/adv_decode.lua). Omit for manifest-only packs: scan_conditions + assets.icon apply overlay icon without Lua. |
priority | Integer; default 50. |
scan_conditions | When to run this observer — see Scan conditions. |
Fingerprinting (Lua parse() or manifest-only icon) runs only when the canonical advertisement payload for that device changes, not on every duplicate ADV.
Central (roles.central)
Scripts after connect / on the Inspect sheet. Each item in scripts[]:
| Field | Description |
|---|---|
id | Stable script id. |
title | UI label. |
kind | full_menu (Scripts tab + menu JSON) or quick_action (one-shot Inspect action). |
entry | Relative path to central Lua. |
menu | Menu JSON path (required for full_menu). |
priority | Ordering when multiple scripts match. |
auto_run_on_connect | Run automatically after GATT discovery when matched. |
vars | Per-script string map merged into Lua vars. |
match | fingerprint (key → allowed values), services (GATT UUIDs), require_discovered_services (default true), fingerprint_or_services. |
Central matching uses fingerprint overlay keys and discovered GATT services, not scan_conditions.
When match.services is non-empty, require_discovered_services defaults to true: the script does not appear on the Inspect sheet until GATT discovery (or a persisted session log) confirms those UUIDs. Set "require_discovered_services": false only if the script should match optimistically before discovery.
Peripheral (roles.peripheral)
Advertiser / peripheral simulation in the app.
| Field | Description |
|---|---|
advertisement | Path to advertisement profile JSON. |
entry | Peripheral Lua entry. |
interface | UI interface JSON. |
Scan conditions
Used only under roles.observer.scan_conditions. Evaluated against the parsed advertisement before observer Lua runs.
Combining rules
- Top-level keys on one object are ANDed.
one_of: array of nested condition objects; at least one branch must match (OR). Each branch is ANDed internally. Branches may nestone_of.- Most fields accept a single value or a JSON array; within that field, any array element may match (OR).
Condition keys
| Key | Matches |
|---|---|
company_id | Manufacturer company ID (16-bit). Prefer four hex digits (004C, 05A7). |
manufacturer_data_prefix_hex | Hex prefix of manufacturer specific data. |
service_uuid_128 | Full 128-bit service UUID in the AD service list. |
service_uuid_16 | 16-bit service UUID in the AD service list (e.g. fe2c). |
service_data_uuid_128 | 128-bit UUID key in service data AD. |
service_data_uuid_16 | 16-bit UUID key in service data AD (0x16). |
device_name_contains | Case-sensitive substring of the advertised device name. |
device_name_regex | Kotlin regex on the advertised device name (see below). |
fingerprint_has_entry_id | Prior fingerprint entry id from another observer (chained decode). |
fingerprint_entry_attributes | Attribute map on that entry; requires fingerprint_has_entry_id. |
ad_type | AD type byte(s), used with ad_type_data_hex. |
ad_type_data_hex | Hex prefix or regex on payload(s) for ad_type. |
has_gap_device_type_hints | true if Appearance, Class of Device, or service UUID list is present. |
Observer Lua parse(input).manufacturer_data keys and first_company_id use the same 4-digit uppercase hex company id format (e.g. "0075", "004C"). See lua-mobile.md.
device_name_contains vs device_name_regex
Use device_name_contains for simple substring checks:
"scan_conditions": {
"device_name_contains": "Pixel Buds 2a"
}"device_name_contains": ["WH-CH720N", "LE-WH-CH720N"]Use device_name_regex when you need anchors, character classes, or alternation:
"scan_conditions": {
"device_name_regex": "^QCAR-[0-9A-F]{4}$"
}"scan_conditions": {
"device_name_regex": ["^WH-CH720N$", "^LE-WH-CH720N$"]
}| Topic | Behavior |
|---|---|
| Target | Complete / Short Local Name from the advertisement (device_name in Lua). Empty if absent. |
| Match mode | Regex(pattern).containsMatchIn(name) — use ^ and $ for full-string match. |
| Case | Case-sensitive; use (?i) in the pattern for ignore-case. |
| Invalid regex | Treated as non-match (no crash). |
With device_name_contains | If both are set, both must pass. |
Chained observers (one_of + fingerprint)
Example: run only when Apple meta continuity was decoded first:
"scan_conditions": {
"fingerprint_has_entry_id": "apple_meta",
"fingerprint_entry_attributes": {
"continuity_has_nearbyinfo": "true"
}
}Example: company id only (manifest-only icon)
"roles": {
"observer": {
"priority": 20,
"scan_conditions": {
"company_id": "05A7"
}
}
}Requires assets.icon when entry is omitted.
JSON Schema
device-manifest.schema.json validates the structured parts of the manifest (especially scan_conditions). The app does not load this file at runtime; it documents the contract for authors and editors.
Point your editor’s JSON Schema setting at that file when editing a pack’s manifest.json, or use it in CI if you add manifest linting.
Version fields and ESP32 sync
manifest_version: format of the JSON file (bump when adding breaking manifest keys).version: content revision of the pack folder. The app incrementsversionwhen you save edits; ESP32 sync compares the phone copy withmanifest.jsonon the device (SYNCEDwhen equal,OUTDATEDwhen the phone is higher).- Legacy dotted strings in old
versionvalues (e.g."1.0.0") are still read from ESP32 using the first numeric segment (1) until the pack is re-synced.