BKPT LabsDOCS/TRACE DOMAINS/CREATING CUSTOM TRACE DOMAINS BKPT DEBUG & VIEWALYZER RS
TRACE DOMAINS · PRACTICAL GUIDES

Creating custom Trace Domains

Trace Domain format v1. A practical authoring guide and complete field reference for BKPT Debug and ViewAlyzer.

A Trace Domain turns your knowledge of an embedded subsystem into a reusable .vadomain JSON file. It describes what can be observed, how values should be interpreted, which conditions deserve attention, and how to present the evidence. Engineers and AI-assisted tools can use this guide to build, validate and share those files.

You can make a domain for one Ethernet controller IP revision, a particular DMA descriptor layout, an application object in RAM, a USB middleware stack, an RTOS queue or a bench instrument. A useful domain does not need to cover an entire chip family. Its value comes from describing a specific subsystem accurately and stating exactly where it applies.

What a good community domain promises

  1. A clear scope. Name the supported controller, silicon revisions, middleware versions, firmware build options and memory layouts. A matching chip name alone does not establish that a middleware object has the expected meaning.
  2. Observable evidence. Choose registers and firmware values that answer real diagnostic questions. Prefer existing cumulative counters when a short-lived state could disappear between polls.
  3. Honest interpretation. An unavailable value is missing evidence. A sampled state is an observation at a particular time. Neither should be turned into a fabricated zero, an exact transition timestamp or proof that no fault occurred.
  4. Useful explanations. Findings should say what was observed, what it may mean, and what the engineer should inspect next. Include supporting channels instead of claiming a root cause that the observations cannot prove.
  5. Reproducible compatibility. Ship a small example, validated JSON, the required ELF/SVD assumptions and the conditions you tested. Keep hardware-specific vocabulary in your domain so other experts can contribute domains of their own.

The file declares data and supported operations. It cannot add arbitrary formulas, scripts, function calls or a new renderer. Optional typed actions must be declared separately from observations. An optional external-instrument program is executable software distributed alongside a package; it is not JSON logic.

Start here

GOAL READ NEXT
Make a first domainA first working domain
Check a file without hardwareValidate with the ViewAlyzer CLI
Observe controller registersSampled inputs and Ethernet example
Follow firmware objects and pointersStructured sources and Middleware example
Add editable controlsTyped actions
Generate a domain with an AI toolAuthoring with AI
Look up every supported propertyFile format and compatibility and Validation boundaries

Choose the observation contract first

Before writing JSON, make a short list of the questions your domain answers. For each value, identify its source, type, units, valid states and read effects.

SOURCE APPROPRIATE USE PREREQUISITES
peripheral + registerController status, hardware counters, configuration bitsCorrect device SVD, exact register names and safe read semantics
symbolA scalar already maintained by firmwareThe matching ELF and correct scalar type; a nonzero member offset must match that exact build
structuredRAM objects, middleware structs, pointers and fixed arraysMatching ELF with DWARF debug information and supported writable RAM allocations
ringOccupancy of fixed-stride descriptorsMatching ELF, verified array extent, ownership word and mask
Existing recorded channelsFirmware instrumentation or imported instrument dataChannel names, namespaces and units from the producing recorder

The same peripheral IP can appear behind different register names or addresses in different MCUs. SVD references let the application resolve addresses. They do not make an incorrect register interpretation portable. Likewise, named DWARF paths adapt to supported layout changes, but do not establish semantic compatibility between middleware releases.

Record compatibility notes in a companion README. The descriptor has no general-purpose compatibility, description, license, url or arbitrary metadata property at the top level. Unknown properties are rejected. Use the supported channel note/reason, section description and finding explain fields where appropriate.

A first working domain

Step 1: identify two existing firmware counters

This example assumes the matching firmware ELF contains two unsigned 32-bit globals named packets_completed and packet_errors. Use actual retained symbols from your firmware. You do not need to add recorder calls to observe existing RAM values.

These declarations illustrate the expected types; your firmware remains responsible for maintaining the counters:

#include <stdint.h>
uint32_t packets_completed;
uint32_t packet_errors;

Step 2: save the complete descriptor

Save the following as packet-counter.vadomain. JSON uses double quotes and has no comments or trailing commas. The filename is for people; id is the stable identity used by the applications.

{
  "id": "com.example.packet-counter",
  "name": "Packet Counter",
  "version": "1.0.0",
  "domain_api": 1,
  "publisher": "Example",
  "sampled_state": {
    "rate_hz": 20,
    "channels": [
      {"name": "packets.completed", "symbol": "packets_completed", "ty": "u32", "counter": "wrapping"},
      {"name": "packets.errors", "symbol": "packet_errors", "ty": "u32", "counter": "wrapping"}
    ]
  },
  "derived": [
    {"type": "rate", "name": "packets.rate", "source": "packets.completed", "unit": "packets/s"}
  ],
  "channels": [
    {"match": {"ns": "poll", "name_prefix": "packets."}, "display": "counter", "unit": "packets", "interpolation": "step"},
    {"match": {"ns": "derived", "name": "packets.rate"}, "display": "graph", "unit": "packets/s", "graph": true}
  ],
  "verdicts": [
    {"id": "packet-errors", "name": "Packet errors increased", "severity": "warning",
     "when": {"type": "counter_advanced", "channel": "packets.errors"},
     "explain": "The firmware error counter advanced. Inspect the driver's error reason and compare with traffic load.",
     "evidence": ["packets.errors", "packets.rate"]}
  ],
  "presentation": {
    "sections": [
      {"title": "Packets", "view": "cards", "fields": [
        {"channel": "packets.completed", "label": "Completed", "unit": "packets"},
        {"channel": "packets.errors", "label": "Errors", "unit": "packets"},
        {"channel": "packets.rate", "label": "Throughput", "unit": "packets/s"}
      ], "checks": ["packet-errors"]}
    ]
  }
}

The sampled inputs provide the evidence. The derived rate uses the completed counter. The finding observes error-counter advances and includes traffic rate as context. The presentation supplies readable cards; channel rules supply chart formatting.

Step 3: validate before loading

Use the CLI installed with ViewAlyzer or BKPT Debug:

viewalyzer-cli parse-trace-domain packet-counter.vadomain --pretty

The result is JSON. A valid file reports "valid": true and exits with status zero. Validation needs no probe, license activation or connected board. The ViewAlyzer executable must include the parse-trace-domain command; if your installed build does not recognize it, update ViewAlyzer. The Python SDK is optional.

Step 4: load it in BKPT Debug

  1. Open a project and select the ELF from the firmware build running on the target.
  2. Run BKPT Debug: Install Custom Trace Domain... from the Command Palette, or open BKPT Debug → Views → Trace Domains and choose Install custom trace domain....
  3. Select the file, then choose Enable for this workspace. Installation and workspace activation are separate steps.
  4. Start debugging and run the target. Inspect channel availability before interpreting values.
  5. Use Graph to show a value in Traces. Use the domain's Settings to choose inputs and requested polling rate. Findings appear in Trace Domains when valid observations satisfy their rules.

This RAM-only example needs no SVD. Register sources and register preconditions do need the matching SVD. Disabling a required input also makes dependent derived values and checks unavailable; hiding its graph does not disable sampling.

Step 5: load it in ViewAlyzer

  1. In the sidebar's Trace Domains section choose Load Domain File, select the .vadomain, and enable it.
  2. Select the matching ELF, target and probe in the capture configuration. Add the SVD when the domain uses registers.
  3. For polling-only capture, turn Software Trace off. Enable it when combining domain polling with supported firmware recorder data.
  4. Start capture. The domain's gear opens its cards and tables; its pencil opens the descriptor editor.
  5. Stop capture to review derived traces and findings in Analyzer. Saved observations remain read-only.

Each application keeps its own installation and enable settings. A valid descriptor does not select a target, supply an ELF or claim a probe. Use one application at a time with a probe.

Step 6: exercise a known condition

Observe a healthy interval, then exercise an error case through your application's normal test controls. Confirm the raw error counter changes, the finding names the expected observation, and the evidence agrees. Check behavior when a symbol or input is unavailable. Do not mistake a successful schema check for a hardware validation result.

Validate with the ViewAlyzer CLI

Call ViewAlyzer directly from a terminal, editor task or CI job:

viewalyzer-cli parse-trace-domain packet-counter.vadomain --pretty
viewalyzer-cli parse-trace-domain packet-counter.vadomain queue.vadomain
viewalyzer --headless --parse-trace-domain packet-counter.vadomain --pretty
viewalyzer-cli parse-trace-domain --schema --pretty

Use viewalyzer-cli.exe or viewalyzer.exe on Windows if needed, or the full path to the installed executable when it is not on your executable search path. Both executables use the same parser. This command also supplies the validation used by the applications and the optional SDK wrapper.

Pass one or more filenames. Both .vadomain and .json content are accepted. Output is always JSON; --pretty adds indentation. There is no silent repair or version conversion. Validation never reads target memory, sends an action or starts an instrument program.

EXIT STATUS MEANING
0Every supplied file passed validation
1At least one file has invalid JSON, an invalid field or an incompatible relationship
2A file could not be read, or the command arguments were invalid

If a batch contains both unreadable and invalid files, exit status 2 takes precedence. Every readable file is still checked.

The report contains schema_version (the CLI response format), domain_api (the supported descriptor format), valid (whether every file passed) and files. Each file has file, valid and errors; a valid file also has id. Each error includes a readable path, a standard JSON Pointer pointer, and a specific message. Array indices are zero-based. An empty pointer refers to the whole document. JSON syntax errors also include line and column.

For example, a zero polling rate produces a report like this:

{
  "schema_version": 2,
  "domain_api": 1,
  "valid": false,
  "files": [
    {
      "file": "queue.vadomain",
      "valid": false,
      "errors": [
        {
          "path": "$[\"sampled_state\"][\"rate_hz\"]",
          "pointer": "/sampled_state/rate_hz",
          "message": "rate_hz must be greater than zero and at most 1000; got 0"
        }
      ]
    }
  ]
}

Errors identify unknown fields, wrong types, unsupported enum choices, missing required values, invalid references, conflicting sources, unsafe action policies and incompatible field combinations. Correct the named fields and run the command again. Structural errors are reported first; after those are fixed, semantic relationships are checked. Duplicate JSON keys, non-finite numbers and invalid Unicode are rejected. Defaults are interpreted without modifying the file. Prefer omitting unused optional fields to writing null; only nullable fields accept explicit null.

Editor schema export

parse-trace-domain --schema returns a JSON envelope containing schema_version, domain_api and schema. Extract its schema property when configuring an editor's JSON Schema association. The schema describes structure, property types and required fields. Run parse-trace-domain for the complete check, including channel references, dependency ordering, duplicate identifiers, read policies, ring extents and action encodings. Configure schema associations externally: $schema is not accepted inside the descriptor.

Optional Python SDK wrapper

Python applications can call the same native command through ViewAlyzer SDK 1.4.0 or later. Install both ViewAlyzer and the SDK; the SDK finds the executable or accepts an explicit binary path. It contains no separate descriptor schema or validation rules.

python -m pip install --upgrade viewalyzer-sdk
from viewalyzer_sdk import ViewAlyzer

va = ViewAlyzer()  # Or ViewAlyzer(binary="/path/to/viewalyzer-cli")
report = va.validate_trace_domain("packet-counter.vadomain")
if not report["valid"]:
    for file in report["files"]:
        for issue in file["errors"]:
            print(file["file"], issue["path"], issue["message"])

schema = va.trace_domain_schema()["schema"]

validate_trace_domain(path) returns the native report unchanged, including invalid-file diagnostics. trace_domain_schema() returns the native schema envelope. Both use the client's normal query timeout. Missing executables, invocation failures and command error envelopes raise ViewAlyzerError. Generate JSON with your usual JSON library, save it, and pass the path to this wrapper or directly to the CLI.

File format and compatibility

Save the descriptor as UTF-8 JSON with the .vadomain extension. Use valid JSON: no comments or trailing commas. A package is a folder containing the descriptor and any supporting files, such as an external instrument recorder.

domain_api: 1 selects the complete version-one format described here: sampled sources, pointer/member/array traversal, derived channels, findings, cards and tables, graph choices, RAM read groups, typed actions and instrument declarations. Omitting domain_api also selects version one. Always include it in shared files so the required format is explicit.

version is your domain's own release string. It is independent of domain_api, the SDK package version and the applications' version numbers. Increment your domain version when you publish changes. Change domain_api only when adopting a new documented format supported by the reader. Do not change the number to bypass a compatibility error.

A descriptor is rejected if it uses an unsupported API version, an unknown field, or an invalid value. Check the error shown when installing the file, or run viewalyzer-cli domains to inspect loading errors. For a version error, use a compatible application version or a descriptor supplied for your installed version.

Top level

This is a complete minimal descriptor. Add the sections your domain needs; omit unused sections.

{
    "id": "com.example.device",
    "name": "Device Monitor",
    "version": "1.0.0",
    "domain_api": 1,
    "publisher": "Example"
}
FIELD REQUIRED MEANING
idYesStable domain identifier. Use a reverse-DNS name such as com.example.device. ViewAlyzer uses it to identify installed versions and overrides.
nameYesDisplay name in domain lists and findings.
versionYesYour descriptor's version string, such as 1.0.0.
domain_apiNo; defaults to 1Descriptor format version.
publisherNoAuthor or organization shown in the domain list.
channelsNoRules for channel units, grouping, names, and display style.
sampled_stateNoRegisters and firmware values to observe through the debug probe.
derivedNoChannels calculated from observed values.
verdictsNoConditions that produce findings with explanations and evidence.
presentationNoCollapsible cards or tables grouping sampled and derived values.
actionsNoExplicit typed operations, executable only in BKPT Debug.
instrumentNoExternal instrument recorder to run with a ViewAlyzer capture.
series_kindsNoAdditional event kinds whose numeric values should appear as series.
namespacesNoAdditional namespaces whose objects should be treated as data channels.

Channel display rules

Each entry in channels selects channels by namespace and name. The first matching rule wins, so put exact-name rules before broader prefix rules. Styling applies to the channel's presentation; recorded sample values are preserved.

This rule displays a sampled ring occupancy as a level bar:

{
    "match": { "ns": "poll", "name": "eth.rx_ring" },
    "unit": "desc",
    "display": "level",
    "graph": true,
    "group": "DMA",
    "min": 0,
    "max": 4,
    "warn": 3,
    "interpolation": "step"
}
FIELD MEANING
match.nsChannel namespace; see the table below.
match.nameExact channel name. Takes precedence over name_prefix within the same rule.
match.name_prefixSelects names beginning with this text. For example, eth. selects Ethernet channels named with that prefix. Omit both name fields to select the whole namespace.
unitSuffix shown beside values, such as pkts, Hz, or m/s2.
displayDisplay style: graph, bar, level, scatter, impulse, enum, gauge, counter, table, histogram, toggle, register, angle, task, or isr. Overrides the firmware's display hint.
graphOptional boolean default Graph choice. A saved user choice takes precedence. Does not affect sampling or history.
groupString label grouping charts. Distinct from a presentation field's nested group array.
min, max, warnFixed scale and warning threshold for a level bar.
namesValue labels, such as {"0": "Stopped", "1": "Running"}. Per-channel labels chosen in the app take precedence.
interpolationstep or linear. Use step for sampled states and counters so the chart holds each observed value until the next sample. A per-chart selection takes precedence.
NAMESPACE VALUES
pollDomain inputs and manually polled firmware symbols.
derivedValues calculated by the derived section.
user_traceUser traces emitted by the firmware recorder.
externalImported or captured instrument samples.

Graph defaults

In both apps, each value's Graph control chooses whether to display its history in Trace/Traces. It stays available in cards/tables either way. The first matching channel rule wins; if that rule omits graph, use the automatic default rather than searching later rules.

With presentation, declared counters, ring occupancy and derived rates start graphed; other fields start in cards/tables. Without presentation, the legacy enabled graph default is preserved. Use explicit graph: true for motion or other useful signals, and graph: false for addresses, configuration codes or duplicate representations. Users can override any default. Reset graphs restores these defaults without changing expansion or sampling settings. Preferences are separate in each app; BKPT Debug also scopes them to the workspace.

Sampled inputs

sampled_state tells the debug probe which target values to read while the firmware runs. Register sources use the device's SVD file. Firmware symbols, structure members, and rings use the ELF from the firmware build. Domains that only read firmware memory need no SVD unless they also declare register preconditions.

For desktop domain-only capture, enable an applicable sampled domain and turn Software Trace off. No recorder control block is required. Enable Software Trace for combined recorder capture. RAM snapshots still require recorder firmware.

This section observes an existing firmware counter without adding logging calls:

{
    "sampled_state": {
        "rate_hz": 20,
        "channels": [
            {
                "name": "sensor.samples",
                "symbol": "imu_sample_count",
                "ty": "u32",
                "counter": "wrapping"
            }
        ]
    }
}
FIELD MEANING
svd_deviceDevice SVD name, without .svd. ViewAlyzer searches VA_SVD_DIR, its svd/ folder, and STM32CubeCLT installations. Use the exact peripheral and register names in that file.
target_device_prefixList of target-name prefixes to which the domain applies, such as ["STM32U575"]. An empty list permits any target. A mismatch skips sampling with a reason.
rate_hzRequested samples per second; defaults to 20. Must be positive and no greater than 1000. Actual acquisition can be slower.
coalesce_gap_bytesMaximum gap between registers that may be included in one combined read. Defaults to 0, allowing only adjacent registers to be combined. A larger value permits reading intervening bytes; use it only when those bytes are safe to read.
preconditionRegister condition required for every input in this section.
channelsList of sampled inputs.

Input fields

Give each input a unique name and one source: a peripheral/register pair, a symbol, a ring, or a structured path.

FIELD MEANING
nameChannel name used by display rules, derived channels, and findings.
peripheral, registerExact names in the selected SVD. Supply both for a register source.
symbolGlobal symbol in the matching firmware ELF.
tyScalar type: u8, u16, u32, i8, i16, i32, or f32. Defaults to u32.
offset_bytesByte offset within a symbol; defaults to 0. The selected value must fit within that symbol's size.
ringDescriptor-ring source whose value is the number of matching entries. See Rings below.
structuredNamed structure members, explicit pointer steps and fixed-array indices.
countermonotonic, wrapping, or clear_on_read. Omit for values that are not counters.
safetyRead policy; defaults to read-safe. See Read policies below.
preconditionAdditional register condition required for this input.
reasonExplanation shown when an input cannot be sampled.
noteContext for interpreting the value, such as cache behavior or counter reset conditions.

Use monotonic for an increasing total. Use wrapping for a counter that rolls over at the width of ty, so its rollover can be accounted for in comparisons. With clear_on_read, each sample is the increment since the previous read; use value_at_or_above to detect a nonzero interval. Reading a clear-on-read counter consumes its value, so only observe it when doing so is compatible with the firmware's use of that counter.

For a fixed member offset, verify the layout against the exact firmware build. This example reads a 16-bit member two bytes into lwip_stats:

{
    "name": "lwip.link_recv",
    "symbol": "lwip_stats",
    "offset_bytes": 2,
    "ty": "u16",
    "counter": "wrapping"
}

Preconditions

A precondition tests (register & mask) == equals. Both mask and equals are hexadecimal or decimal strings. Use a condition to check, for example, that a peripheral clock is enabled before reading its registers.

{
    "peripheral": "RCC_S",
    "register": "RCC_AHB5ENR",
    "mask": "0x02000000",
    "equals": "0x02000000",
    "description": "Ethernet peripheral clock is disabled"
}

The register names and mask above are device-specific. Replace them with values from your device's SVD and reference manual. A failed condition makes the affected inputs unavailable and displays description.

In ViewAlyzer captures, preconditions are evaluated at capture start. Start capture after the required peripheral initialization; a condition that fails at startup disables its inputs for that capture.

Read policies

SAFETY BEHAVIOR
read-safe or safeRead when applicable preconditions pass.
conditionalRead only with a declared precondition, either on the input or on sampled_state.
intrusiveNot sampled. Use reason to explain how a read would disturb the target.
unsafeNot sampled. Use reason to explain the risk to target state.
unsupportedNot sampled. Use reason to explain why the value is inaccessible through the debug port.

Sampling never executes a write action. Typed actions are a separate, explicit BKPT Debug operation; desktop views remain read-only. Reads can still have device-specific effects; choose sources and conditions using the device documentation and the firmware's ownership of each resource.

Rings

A ring source reads a fixed-size array located by an ELF symbol and counts entries whose ownership bits match the requested state.

{
    "name": "eth.rx_ring",
    "ring": {
        "symbol": "DMARxDscrTab",
        "entries": 4,
        "stride_bytes": 24,
        "own_word_offset": 12,
        "own_mask": "0x80000000",
        "count_set": false
    },
    "note": "Receive descriptors awaiting reclamation by the application"
}

entries is the array length; stride_bytes is the size of each entry. own_word_offset locates the ownership word within each entry. count_set: true counts entries where word & own_mask is nonzero; false counts entries where it is zero. The example's layout and ownership convention must match your firmware.

The ring requires a matching ELF, and its total size must fit within 65,535 bytes. Set the entry count, stride, offset, and mask to match the application's actual array layout.

Structured sources

Use structured to select members by name and follow pointers explicitly. This avoids hard-coding member offsets. The matching firmware ELF must contain debug information describing the types.

This input follows the global pointer device_root and reads its state member:

{
    "name": "device.state",
    "ty": "u32",
    "structured": {
        "symbol": "device_root",
        "steps": [
            { "op": "deref" },
            { "op": "member", "name": "state" }
        ]
    }
}

member selects a struct or union member; deref follows a pointer. For a global struct instead of a pointer, start with member. Paths contain 1–16 steps. Use the input's ty to match the final scalar type.

A path also accepts {"op":"index","index":1} for a fixed-size C array. Bounds and element sizes come from DWARF. Pointer indexing, variable-length/multidimensional arrays and out-of-bounds indices are rejected. Use an explicit deref step when an array element is a pointer.

Structured sources require a 32-bit little-endian ELF with matching type information. Reads must remain within writable allocated ELF sections. Missing symbols or types, unsupported bitfields, mismatched scalar types, null pointers, out-of-range addresses, and failed reads make the input unavailable with a reason. Structured sources do not support clear_on_read.

Pointers and the selected value are read again for each observation. A detected pointer change produces a gap, so rates and checks do not span two different object instances. Separately read fields are not an atomic snapshot, and pointer checks cannot detect every change between reads, including reuse at the same address. Keep the selected ELF matched to the flashed firmware.

Optional RAM read groups

sampled_state.ram_read_groups declares bounded opportunities to read related RAM fields together. It is an acquisition hint: a consumer may keep separate reads. It is not a promise of higher achieved rate or an atomic snapshot.

{
    "id": "queue_header",
    "channels": ["queue.used", "queue.capacity"],
    "max_bytes": 64,
    "allow_padding": true
}
FIELD REQUIRED MEANING
idYesNonempty group ID, unique within ram_read_groups
channelsYes2–32 distinct declared sampled inputs; each input may belong to only one group
max_bytesYesMaximum aligned read span, 4–4096 bytes and a multiple of four
allow_paddingNo; default falseExplicitly permits reading padding and unselected RAM fields inside the bounded span

At most 32 groups are allowed. Members must be readable symbol or structured inputs; registers, rings, clear_on_read, unsafe, intrusive and unsupported inputs cannot belong. ELF-derived bounds still apply. A group cannot authorize reading MMIO or memory outside supported allocations. Without allow_padding, every byte in the aligned span must belong to a selected field.

Selecting fewer than two members allows individual reads; selecting none reads nothing for that group. Unselected fields never become observations merely because their bytes were inside a permitted read. Groups do not change dependency requirements for derived values or findings. Pointer validation and failed-read handling still apply.

Derived channels

Each entry in derived calculates a channel from a sampled or recorded value. Derived channels use the derived namespace and can be displayed, queried, or used by findings. Put dependencies before the derived channels that use them.

This array extracts a named state from a register and calculates a counter's rate:

[
    {
        "type": "field",
        "name": "eth.rx_dma_state",
        "source": "eth.dma_debug",
        "shift": 8,
        "mask": "0xF",
        "values": { "0": "Stopped", "4": "Suspended", "7": "Transferring" }
    },
    {
        "type": "rate",
        "name": "eth.tx_rate",
        "source": "eth.tx_good",
        "unit": "pkts/s"
    }
]
TYPE FIELDS RESULT
fieldname, source, shift, mask, optional values(value >> shift) & mask, retains observation cadence for sampled domains; recorded on change for externally recorded channels. values maps numeric values to state labels.
ratename, source, optional unitCounter advance per second between adjacent samples. Negative deltas are clamped to zero.
mapname, source, table, optional unitNumeric lookup, such as {"4": 104, "5": 208} to convert configuration codes to Hz. Unmapped values pass through. Sampled domains retain observation cadence; externally recorded values are recorded on change.

For a domain with sampled_state, every source must refer to a declared input or an earlier derived channel. Names must be unique. Derived channels are available during live observation in BKPT Debug and when ViewAlyzer finalizes or opens a recording.

Findings and verdicts

Each entry in verdicts defines a condition, severity, explanation, and supporting channels. ViewAlyzer shows the resulting findings in the Analyzer. CLI users can retrieve them with query verdicts. BKPT Debug shows them in its Trace Domains view.

{
    "id": "app-not-draining",
    "name": "Application not draining",
    "severity": "warning",
    "when": {
        "type": "counter_stalled",
        "channel": "lwip.link_recv",
        "while_advances": "eth.rx_good",
        "window_ms": 250
    },
    "explain": "Frames continued arriving while the stack stopped accepting them. Check the receive task and buffer pool.",
    "evidence": ["lwip.link_recv", "eth.rx_good", "eth.rx_ring"]
}
FIELD MEANING
idUnique, stable identifier for the rule.
nameShort finding title.
severityerror, warning (default), suspicious, flag, or info.
whenDetector and its inputs, from the table below.
explainExplanation shown with the finding. Describe what happened and what to check next.
evidenceChannels to include with their values at the finding's onset and end. Named states use their labels.

Detectors

WHEN.TYPE FIELDS CONDITION
sustained_at_or_abovechannel, value, sustained_msObserved value stays at or above the threshold for the duration.
sustained_at_or_belowchannel, value, sustained_msObserved value stays at or below the threshold for the duration.
counter_advancedchannelCounter increases. Use for cumulative counts, such as an error total. Wrapping counters are compared using their unwrapped total.
value_at_or_abovechannel, valueA sample reaches the threshold. Useful for clear_on_read counters, where each sample already represents an interval.
value_inchannel, valuesAn observed value matches one of the listed discrete codes. Repeated observations form intervals, not counts of operations.
counter_stalledchannel, while_advances, window_ms, optional min_advanceOne counter stops while the other advances over the window. min_advance defaults to 1 and sets the required advance of while_advances.
ratio_belowchannel, reference, ratio, sustained_msThe channel stays below reference * ratio for the duration while both values are available.

In a domain with sampled inputs, declare every channel used by a detector or listed as evidence. Choose the observation and duration to suit the expected behavior: a 20 Hz poll may miss a state that changes and returns between two samples. A cumulative counter can preserve evidence of those short events. The absence of a finding does not establish that no brief event occurred.

Cards and tables

presentation.sections contains 1–12 sections, each with 1–32 fields.

SECTION FIELD MEANING
titleRequired nonempty heading
viewRequired cards or table
descriptionOptional explanatory text
collapsedOptional initial state. Omitted: tables closed, cards open. Saved expansion choices take precedence
fieldsRequired array of fields below
checksOptional verdict IDs associated with this section
PRESENTATION FIELD MEANING
channelRequired declared sampled or derived channel name
labelRequired nonempty display label
groupOptional array of up to four nonempty labels, each at most 128 UTF-8 bytes. Omitted/empty means directly in the section
referenceOptional channel supplying positive capacity/total
unitOptional display unit
actionOptional declared action ID; editable only in BKPT Debug

For example, a field with "group": ["Characteristic", "Identity"] appears under those two nested disclosures. Groups start closed. Direct fields precede child groups, and sibling groups follow their first appearance in the fields array. The app does not infer hierarchy from channel names or device type.

Expansion is remembered separately from Graph choices. Closing a section or group does not stop reads, hide history, disable checks or change Graph choices. Expand all/Collapse all change expansion; Reset graphs restores only the graph defaults. Each app keeps its own preferences. Renaming a section or group can reset that branch's expansion choice.

For graph defaults, the first matching channel rule is decisive. If it omits graph, use the automatic fallback, not a later rule: with presentation, declared counters, rings and derived rates start graphed; other channels do not. Without presentation, channels retain the legacy enabled default. Put exact rules before broad prefixes when they need different defaults.

Fields may add reference (another channel containing a positive capacity/total) and unit, for example {"channel":"queue.used","reference":"queue.capacity","label":"Control queue","unit":"messages"}. Both observations must be available; missing or invalid capacity does not render as a successful empty queue. Sections may add checks: ["verdict-id"] to surface associated observations and retained incident history in Debug. These are observed incidents, not declarations that a fault remains active. Replay uses the history available at the selected time. The desktop analyzer continues to provide its full findings report when capture stops.

{"presentation":{"sections":[{"title":"Device","view":"cards","description":"Current sampled state","fields":[{"channel":"device.state","label":"State"}]}]}}

Each section uses cards or table; 1–12 sections and 1–32 fields per section. Fields reference declared input or derived channels and use their existing enum names. There is no HTML, script or arbitrary layout execution. Both BKPT Debug and ViewAlyzer render these same sections with neutral rectangular observations. Debug shows live values alongside checks and retained findings. ViewAlyzer shows live values while capturing. Its dedicated domain view follows the hovered or pinned trace cursor in saved recordings, or the recording end when no cursor is selected. Existing trace views retain histories. Samples older than three requested periods (at least 250 ms) display as unavailable in snapshots.

Typed actions

In BKPT Debug, a presentation field can reference a declared action, for example {"channel":"device.state","label":"State","action":"set_state"}. Editing a draft never writes: the user must explicitly select Send. ViewAlyzer keeps all observations read-only, even when the descriptor declares an action. Actions have a unique identifier id, a visible label, input, target and mandatory completion. Unknown properties, target kinds and policies are rejected. At most 32 actions are accepted per domain. No scripts, addresses, expressions or function calls can be submitted by the editor.

Inputs and encoding

INPUT.TY ENCODING AND BOUNDS
u8, u16, u32, i8, i16, i32Little-endian integer; decimal or nonnegative 0x hexadecimal; optional min/max within the type
f32Finite little-endian float32; optional min/max; normal float32 rounding, overflow and underflow-to-zero rejected
booltrue or false, one byte 1 or 0
bytesComplete hex byte pairs, contiguous or separated by whitespace; mandatory max_bytes in 1–256
utf8UTF-8 text; mandatory max_bytes in 1–256 and termination: "none" or "nul"; bound includes the terminator; embedded NUL rejected

Optional choices is a list of up to 64 { "value": "1", "label": "Enabled" } entries. Values must pass the same encoder and be distinct after encoding. With choices present, only those encoded values are accepted. Inputs are never silently truncated or wrapped. The same type, range and encoding rules apply when validating a draft and when sending it. The editor sends strings, not encoded bytes.

RAM symbols and members

{"id":"set_limit","label":"Limit","input":{"ty":"u16","min":0,"max":500},
 "target":{"kind":"ram","source":{"symbol":"settings","steps":[{"op":"member","name":"limit"}]},"execution":"running_scalar"},
 "completion":"readback"}

The matching ELF/DWARF determines the exact type, size, member/array offsets and writable allocated SRAM bounds. It must match the input type; integer widths or signedness are not reinterpreted. Const, union, bitfield and pointer-valued leaves are rejected. Buffers require an exactly sized one-dimensional byte/char array. A shorter buffer value is zero-padded to that storage capacity. Completion reports fresh readback, including a mismatch if firmware overwrote or did not retain it.

execution defaults to halted. Pointer traversal is halted-only, bounded to ELF allocations, read fresh and rechecked before writing; null/changing pointers fail. running_scalar explicitly permits aligned 1/2/4-byte scalar edits at fixed paths without dereferencing pointers. This is a background debug-bus operation, not a transaction with firmware. The application owns concurrency with interrupts, DMA and other cores; halting one core does not stop those other bus masters.

SVD registers and fields

{"id":"clear_flags","label":"Clear flags","input":{"ty":"u32"},
 "target":{"kind":"register","peripheral":"PORT","register":"STATUS","field":"READY","execution":"running_scalar"},
 "completion":"transport"}

BKPT Debug resolves names and access policies from the selected SVD. Only unsigned integer/bool inputs and unambiguous 8/16/32-bit register layouts are supported. Register/field access rights, writable masks, reserved bits, numeric/enumerated write constraints and read effects are enforced. Normal partial writes preserve unrelated bits with read-modify-write and require a halted core and side-effect-free readable register. Running actions allow direct writes only. Homogeneous oneToClear, oneToSet and oneToToggle effects write zero to untouched flags; mixed effects, zero-to effects, write-once, writeAsRead, ambiguous derived/alias layouts and missing field masks are refused.

completion: "readback" requires a safe read. "transport" reports the probe's write acknowledgement only, with no claim about the resulting register value. A descriptor cannot override SVD restrictions. SVD accuracy remains a prerequisite.

Application mailbox

{"id":"set_state","label":"State",
 "input":{"ty":"u8","choices":[{"value":"0","label":"Off"},{"value":"1","label":"On"}]},
 "target":{"kind":"mailbox","symbol":"domain_command","operation":1,"timeout_ms":3000},
 "completion":"application"}

Mailbox commands require running firmware. The firmware callback runs in its appropriate application context, validates operation/length/value independently, and invokes the real application API. Observation mirrors are not command targets. Operation mappings and payload interpretation belong to the descriptor/firmware. Timeout is 100–5000 ms and the maximum payload is 64 bytes.

Command ABI 1 is a word-aligned 104-byte object with these exact DWARF member names and offsets. Every integer is uint32_t, little-endian; payload is uint8_t[64].

OFFSET MEMBER OWNER
0, 4, 8magic = 0x56414331, abi = 1, capacity = 64Firmware initialization
12request_seqHost publishes last
16, 20, 24request_nonce, operation, lengthHost stages
28payloadHost stages
92, 96, 100response_seq, response_nonce, statusFirmware completion

An idle mailbox has equal request/response sequences. The host stages and verifies the body before publishing a new nonzero sequence. Firmware consumes only a new sequence, copies and validates the bounded request, then publishes status and nonce before response sequence using appropriate memory barriers. Status 0 means accepted; other values are application errors. Host completion requires both sequence and nonce to match. One host owns a mailbox; concurrent independent writers are unsupported.

Host lifecycle and compatibility

BKPT Debug keeps drafts separate from sampled values and writes only on explicit Send. It shows pending, accepted, rejected and unknown completion separately. Overlapping destinations are serialized by refusing concurrent requests. A timed-out or disconnected write may have executed: it is never retried automatically. A busy mailbox refuses another command until firmware acknowledges the previous sequence.

Send is bound to the loaded descriptor revision, debug session, ELF, target state and destination resolution. The host rechecks these at queued IO dispatch, validates the ELF file hash for DWARF targets, and compares sampled ELF load-image bytes with target memory. A mismatch or unavailable signature blocks writes. This detects ordinary stale firmware; sparse signatures are not full-image attestation. Modified RAM load images or patched code may also fail the signature check. Keep the matching firmware/ELF/source set. The host never silently halts, resumes, resets or reflashes.

History, recordings and disconnected/saved-task views are read-only. Changing the domain or session discards drafts and cannot replay a prior Send. The desktop renders observations with a visible read-only limitation; its analyzer does not execute actions. No action is executed while parsing, recording, restoring a layout or applying a descriptor to a recording.

External instrument recorders

An instrument section configures a separate recorder program that runs during a ViewAlyzer capture. The program streams the external-series format; its samples are aligned with and merged into the recording. Include the program and its required assets in the package folder. Instrument recording is configured in ViewAlyzer.

{
    "instrument": {
        "cmd": "\"{dir}/bin/recorder\" --stream --rate {param.rate}",
        "warmup_s": 4,
        "sync_name": "jsync",
        "params": [
            {
                "key": "rate",
                "label": "Sample rate",
                "type": "number",
                "default": "1000",
                "min": 1,
                "max": 10000
            }
        ]
    }
}
FIELD MEANING
cmdRecorder command. {dir} expands to the descriptor's folder; {param.key} expands to a parameter value.
warmup_sSeconds to wait after launching the recorder before target capture starts; default 2, constrained to 0–30 at runtime.
sync_nameName of the recording's synchronization channel; default jsync.
paramsEditable parameter fields shown beneath the domain in the sidebar. Each has a key, optional label, type (number or string, default string), and a string default (default empty). Numeric fields may include min and max.

Set the parameters and enable Record with capture in the sidebar before capturing. See Instrument time sync for synchronization setup and alignment quality.

Worked example

This complete descriptor observes a firmware counter, calculates its rate, and raises a warning when the observed rate stays at or below 10 samples per second for at least one second. It requires a matching ELF containing a 32-bit unsigned global named imu_sample_count that increments once per sample. Change the symbol and threshold to match your application.

Save the following as sensor-monitor.vadomain, then install it using Load Domain File in ViewAlyzer or Install custom trace domain… in BKPT Debug.

{
    "id": "com.example.sensor-monitor",
    "name": "Sensor Monitor",
    "version": "1.0.0",
    "domain_api": 1,
    "sampled_state": {
        "rate_hz": 20,
        "channels": [
            {
                "name": "sensor.samples",
                "symbol": "imu_sample_count",
                "ty": "u32",
                "counter": "wrapping"
            }
        ]
    },
    "derived": [
        {
            "type": "rate",
            "name": "sensor.sample_rate",
            "source": "sensor.samples",
            "unit": "samples/s"
        }
    ],
    "verdicts": [
        {
            "id": "low-sample-rate",
            "name": "Sensor sample rate is low",
            "severity": "warning",
            "when": {
                "type": "sustained_at_or_below",
                "channel": "sensor.sample_rate",
                "value": 10,
                "sustained_ms": 1000
            },
            "explain": "The observed sample rate stayed at or below 10 samples/s for at least one second. Check the sensor configuration and acquisition task.",
            "evidence": ["sensor.samples", "sensor.sample_rate"]
        }
    ],
    "channels": [
        {
            "match": { "ns": "poll", "name": "sensor.samples" },
            "unit": "samples",
            "display": "counter",
            "group": "Sensor"
        },
        {
            "match": { "ns": "derived", "name": "sensor.sample_rate" },
            "unit": "samples/s",
            "display": "graph",
            "group": "Sensor",
            "interpolation": "step"
        }
    ]
}

Enable the domain, select the matching ELF, and capture while the firmware runs. In ViewAlyzer, stop capture to inspect the derived rate in Trace and any findings in Analyzer. In BKPT Debug, follow the channels in Traces and findings in Trace Domains.

Troubleshooting

SYMPTOM WHAT TO CHECK
Descriptor will not loadCheck JSON syntax, field names, display names, and domain_api. Omit unused sections instead of supplying incomplete objects.
Unknown input or evidence channelMatch names exactly and define each derived dependency before its consumer.
Register input is unavailableCheck the selected SVD, peripheral/register names, target match, peripheral clock, and precondition message.
Firmware input is unavailableSelect the ELF from the flashed build. Check symbol names, type information, member layout, and pointer availability.
A finding does not appearCheck that the domain and its required inputs are enabled, valid samples exist, and enough time has elapsed for the rule's duration. Review the actual sampling rate.
Channel styling is missingCheck match.ns, spelling, and rule order. Put exact matches before prefix matches.

For a hardware example with controller registers, structured sources, derived fields, and presentation sections, use the downloadable domain in the USBX on ThreadX walkthrough.

Ethernet example

The downloadable Ethernet example combines a guarded hardware counter, a decoded status word, a descriptor ring and a sustained-backlog finding. Its ExampleMCU, CLOCK and ETH register names are illustrative, not a claim of compatibility with any real device.

To adapt it to a specific Ethernet IP:

  1. Choose the exact controller revision and firmware driver configuration. Verify register names and access policies in the selected SVD and reference manual.
  2. Verify how clock, reset and counter modes affect observation. Encode valid modes with preconditions and explain failures in their descriptions.
  3. Verify the ring symbol's actual element size, entry count, ownership word and polarity. Descriptor ownership may mean DMA-owned or CPU-owned depending on the IP and direction.
  4. Set a target-name prefix for intended devices. Document additional IP and firmware compatibility constraints separately.
  5. Choose a backlog duration meaningful at the achieved polling rate. Ring occupancy alone does not prove packet loss or identify the stalled task.
{
  "id": "com.example.mac-monitor",
  "name": "Example Ethernet MAC",
  "version": "1.0.0",
  "domain_api": 1,
  "sampled_state": {
    "svd_device": "ExampleMCU",
    "target_device_prefix": ["ExampleMCU"],
    "rate_hz": 10,
    "precondition": {"peripheral": "CLOCK", "register": "ENABLE", "mask": "0x1", "equals": "0x1", "description": "The Ethernet clock must be enabled."},
    "channels": [
      {"name": "eth.rx_good", "peripheral": "ETH", "register": "RX_GOOD", "counter": "wrapping", "safety": "conditional",
       "precondition": {"peripheral": "ETH", "register": "COUNTER_CONTROL", "mask": "0x4", "equals": "0", "description": "Disable reset-on-read before observing this counter."}},
      {"name": "eth.status", "peripheral": "ETH", "register": "STATUS", "note": "This example assumes STATUS has no read side effects."},
      {"name": "eth.rx_pending", "ring": {"symbol": "rx_descriptors", "entries": 4, "stride_bytes": 24, "own_word_offset": 12, "own_mask": "0x80000000", "count_set": false}},
      {"name": "eth.payload", "safety": "unsupported", "reason": "Payload contents are outside this monitor's observation contract."}
    ]
  },
  "derived": [
    {"type": "field", "name": "eth.state", "source": "eth.status", "shift": 1, "mask": "0x3", "values": {"0": "Stopped", "1": "Running", "2": "Suspended", "3": "Error"}},
    {"type": "rate", "name": "eth.rx_rate", "source": "eth.rx_good", "unit": "frames/s"}
  ],
  "verdicts": [
    {"id": "rx-backlog", "name": "Receive backlog", "when": {"type": "sustained_at_or_above", "channel": "eth.rx_pending", "value": 3, "sustained_ms": 500},
     "explain": "At least three receive descriptors stayed pending across the observed window. Check whether the receive task is reclaiming descriptors.", "evidence": ["eth.rx_pending", "eth.rx_rate", "eth.state"]}
  ],
  "channels": [
    {"match": {"ns": "poll", "name": "eth.rx_pending"}, "display": "level", "unit": "descriptors", "min": 0, "max": 4, "warn": 3, "interpolation": "step"}
  ],
  "presentation": {"sections": [
    {"title": "Receive path", "view": "table", "collapsed": false, "fields": [
      {"channel": "eth.state", "label": "DMA state"},
      {"channel": "eth.rx_pending", "label": "Pending descriptors"},
      {"channel": "eth.rx_rate", "label": "Receive rate", "unit": "frames/s"}
    ], "checks": ["rx-backlog"]}
  ]}
}

The unavailable payload row illustrates how a domain can explain an observation it cannot provide. It does not read the payload. coalesce_gap_bytes remains zero; increasing it would explicitly authorize reads across otherwise undeclared register gaps.

Middleware example

The downloadable middleware example follows a queue pointer, selects a fixed array element, presents occupancy against capacity, and compares observed progress with expected progress. It needs no register SVD because it has no register sources or preconditions.

The names are an example contract for your firmware, not built-in queue or RTOS knowledge. Assume retained globals with DWARF matching these declarations:

#include <stdint.h>
struct queue_state {
    uint32_t used;
    uint32_t capacity;
    uint32_t state;
};
struct queue_slot { uint32_t mode; };
struct queue_state *active_queue;
struct queue_slot queue_slots[2];
uint32_t queue_enqueued;
uint32_t queue_drained;

The pointed-to queue must occupy a supported writable ELF allocation. Your firmware initializes and updates it; declaring a symbol alone does not produce observations. A null pointer displays as unavailable. The state and mode mappings belong to this example's firmware convention. Replace them with your middleware's documented values.

{
  "id": "com.example.middleware-queue",
  "name": "Middleware Queue",
  "version": "1.0.0",
  "domain_api": 1,
  "sampled_state": {
    "rate_hz": 20,
    "channels": [
      {"name": "queue.used", "structured": {"symbol": "active_queue", "steps": [{"op": "deref"}, {"op": "member", "name": "used"}]}},
      {"name": "queue.capacity", "structured": {"symbol": "active_queue", "steps": [{"op": "deref"}, {"op": "member", "name": "capacity"}]}},
      {"name": "queue.state", "structured": {"symbol": "active_queue", "steps": [{"op": "deref"}, {"op": "member", "name": "state"}]}},
      {"name": "queue.mode", "structured": {"symbol": "queue_slots", "steps": [{"op": "index", "index": 1}, {"op": "member", "name": "mode"}]}},
      {"name": "queue.enqueued", "symbol": "queue_enqueued", "counter": "wrapping"},
      {"name": "queue.drained", "symbol": "queue_drained", "counter": "wrapping"}
    ],
    "ram_read_groups": [{"id": "queue_header", "channels": ["queue.used", "queue.capacity", "queue.state"], "max_bytes": 64, "allow_padding": true}]
  },
  "derived": [
    {"type": "rate", "name": "queue.drain_rate", "source": "queue.drained", "unit": "messages/s"},
    {"type": "map", "name": "queue.expected_rate", "source": "queue.mode", "table": {"0": 0, "1": 50, "2": 100}, "unit": "messages/s"}
  ],
  "channels": [
    {"match": {"ns": "poll", "name": "queue.state"}, "display": "enum", "names": {"0": "Idle", "1": "Active", "2": "Fault"}, "graph": false}
  ],
  "verdicts": [
    {"id": "drain-stalled", "name": "Consumer stopped progressing", "when": {"type": "counter_stalled", "channel": "queue.drained", "while_advances": "queue.enqueued", "window_ms": 1000, "min_advance": 10}, "evidence": ["queue.used", "queue.capacity"]},
    {"id": "fault-state", "name": "Firmware reports a fault", "severity": "error", "when": {"type": "value_in", "channel": "queue.state", "values": [2]}, "explain": "Inspect the firmware fault reason before restarting this queue."},
    {"id": "slow-drain", "name": "Drain rate below expectation", "when": {"type": "ratio_below", "channel": "queue.drain_rate", "reference": "queue.expected_rate", "ratio": 0.8, "sustained_ms": 2000}, "evidence": ["queue.mode"]}
  ],
  "presentation": {"sections": [
    {"title": "Queue", "view": "cards", "fields": [
      {"channel": "queue.used", "reference": "queue.capacity", "label": "Occupancy", "unit": "messages"},
      {"channel": "queue.state", "label": "State", "group": ["Status"]},
      {"channel": "queue.drain_rate", "label": "Drain rate", "unit": "messages/s", "group": ["Performance"]},
      {"channel": "queue.expected_rate", "label": "Expected rate", "unit": "messages/s", "group": ["Performance"]}
    ], "checks": ["drain-stalled", "fault-state", "slow-drain"]}
  ]}
}

For USBX, ThreadX, lwIP, FreeRTOS or another middleware library, inspect the exact headers, configuration and ELF. Do not guess member names from a different version. A pending request, waiting thread or full queue can be normal; combine observations and use an explanation that reflects what the evidence establishes.

Complete actions example

The downloadable actions example includes all three action targets: a bounded RAM scalar edit, an application mailbox request, and an SVD-defined flag write. The RAM scalar must actually be safe for asynchronous firmware access. The mailbox must implement the documented ABI. The illustrative PORT.STATUS.READY field must have suitable write-one semantics and permissions in the selected device's SVD; it is not valid for an arbitrary status register.

{
  "id": "com.example.device-controls",
  "name": "Device Controls",
  "version": "1.0.0",
  "domain_api": 1,
  "sampled_state": {"channels": [
    {"name": "settings.limit", "symbol": "sample_limit", "ty": "u16"},
    {"name": "device.enabled", "symbol": "device_enabled", "ty": "u8"},
    {"name": "device.flags", "peripheral": "PORT", "register": "STATUS"}
  ]},
  "actions": [
    {"id": "set_limit", "label": "Set sample limit", "input": {"ty": "u16", "min": 0, "max": 500},
     "target": {"kind": "ram", "source": {"symbol": "sample_limit", "steps": []}, "execution": "running_scalar"}, "completion": "readback"},
    {"id": "set_enabled", "label": "Enable device", "input": {"ty": "u8", "choices": [{"value": "0", "label": "Off"}, {"value": "1", "label": "On"}]},
     "target": {"kind": "mailbox", "symbol": "domain_command", "operation": 1, "timeout_ms": 3000}, "completion": "application"},
    {"id": "clear_ready", "label": "Clear ready flag", "input": {"ty": "u32", "choices": [{"value": "1", "label": "Clear"}]},
     "target": {"kind": "register", "peripheral": "PORT", "register": "STATUS", "field": "READY", "execution": "running_scalar"}, "completion": "transport"}
  ],
  "presentation": {"sections": [
    {"title": "Controls", "view": "cards", "fields": [
      {"channel": "settings.limit", "label": "Sample limit", "action": "set_limit"},
      {"channel": "device.enabled", "label": "Device enabled", "action": "set_enabled"},
      {"channel": "device.flags", "label": "Flags", "action": "clear_ready"}
    ]}
  ]}
}

All three actions remain explicit user operations in BKPT Debug. ViewAlyzer displays the associated observations read-only. Schema validation does not establish whether an action's destination is compatible with the user's ELF or SVD.

External instrument example

The downloadable instrument example assumes an independently supplied sensor-recorder program on the user's executable search path. It must write the documented external-series NDJSON stream to standard output, send diagnostics to standard error and participate in the required time synchronization. This example does not include that program.

{
  "id": "com.example.bench-sensor",
  "name": "Bench Sensor",
  "version": "1.0.0",
  "domain_api": 1,
  "namespaces": ["external"],
  "series_kinds": ["external_trace"],
  "instrument": {
    "cmd": "sensor-recorder --stream --rate {param.rate}",
    "warmup_s": 2,
    "sync_name": "sync",
    "params": [{"key": "rate", "label": "Sample rate", "type": "number", "default": "1000", "min": 1, "max": 10000}]
  },
  "channels": [
    {"match": {"ns": "external", "name": "current"}, "unit": "mA", "display": "graph", "group": "Bench", "interpolation": "linear"}
  ],
  "verdicts": [
    {"id": "high-current", "name": "High current", "when": {"type": "sustained_at_or_above", "channel": "current", "value": 200, "sustained_ms": 500}, "evidence": ["current"]}
  ]
}

An instrument declaration launches software when recording is enabled. Distribute trusted recorder binaries or source with installation instructions for supported operating systems. Quote command paths containing spaces and account for platform executable names. Offline validation checks the declaration, not executable existence, process output, synchronization quality or the device connection. BKPT Debug's sampled-domain workflow does not run instrument sidecars.

Validation boundaries

The file is checked in stages. An offline pass establishes that the descriptor has the supported shape and internally consistent references. The application later resolves real hardware and firmware information.

CHECK OFFLINE CLI APPLICATION OR TARGET REQUIRED
JSON syntax, types, unknown properties, supported formatYesNo
Source exclusivity, limits, names, derived ordering and referencesYesNo
Action input bounds, choice encodings and completion policiesYesNo
Whether a selected SVD contains the named register or fieldNoSelected device SVD
Register read/write effects and actual access restrictionsNoCorrect SVD and hardware context
Whether symbols, members, array indices and scalar types matchNoMatching ELF with supported debug information
Runtime pointer validity and current valuesNoLive target
Achieved polling rate, cache coherence and firmware concurrencyNoLive target and application-specific evaluation
Whether diagnostic thresholds describe a real faultNoDomain expertise and representative tests
Instrument executable, stream and synchronizationNoInstrument and recorder program

Numeric and structural limits

The following details matter when generating files programmatically. Integers must be JSON integers, not strings, booleans or decimal tokens such as 1.0, except fields explicitly described as numeric strings.

FIELD OR OBJECT CONSTRAINT
domain_apiCurrent supported format, 1; omit to use the default
sampled_state.rate_hzFinite number greater than zero and at most 1000; default 20
coalesce_gap_bytesUnsigned 64-bit integer; default zero
offset_bytesUnsigned 32-bit integer; default zero; nonzero only with symbol
Precondition mask / equalsUnsigned decimal or 0x strings; mask fits 32 bits; all set bits in equals must be inside the mask
Ring entries, stride_bytes, own_word_offsetUnsigned 16-bit integers; entries positive; stride at least four; ownership offset plus four fits the stride; entries times stride at most 65,535
Ring own_mask / sampled typeNonzero unsigned 32-bit decimal/hex string; ring channel uses u32
Sampled structured.symbol / member namesASCII C identifiers, at most 256 bytes; 1–16 steps
Structured indexUnsigned 32-bit integer; actual array bounds checked against the ELF
Derived field shift / maskShift 0–63; unsigned 64-bit decimal/hex mask applied after shifting
Derived map tableNonempty object with signed 64-bit decimal integer keys and finite numeric values
Duration valuesUnsigned 64-bit integer milliseconds; window_ms positive; sustained_ms may be zero
min_advance / ratioFinite and positive / finite and nonnegative, respectively
value_in.values1–256 finite numbers
Presentation1–12 sections, 1–32 fields each; nonempty titles and labels; up to four group levels with nonblank labels no larger than 128 UTF-8 bytes
Action ID and target C identifiersAt most 128 ASCII bytes; action IDs unique within the domain
RAM action pathZero to 16 steps; zero selects the root scalar or buffer, unlike a sampled structured source
Actions and choicesAt most 32 actions; at most 64 choices per input; choice labels nonempty and encoded choice values unique
Buffer capacitymax_bytes 1–256 for bytes/utf8; mailbox payload at most 64 bytes
MailboxNonzero unsigned 32-bit operation; unsigned integer timeout 100–5000 ms
Instrument parameter keyOne or more ASCII letters, digits, underscores or hyphens

The sampled types are u8, u16, u32, i8, i16, i32 and f32; the default is u32. bool is an action input type, not a sampled type. Unknown enum values are errors. Channel names and identifiers are case-sensitive. Sampled and derived names must be unique within their domain, and verdict IDs must be unique within their own list. An action ID uses a C identifier, so use set_limit, not set-limit.

Arrays default to empty when optional. Optional text such as publisher, explanation and section description defaults to empty. Fields represented as optional values may accept null, including the top-level sampled_state, presentation and instrument, source alternatives, notes, channel-style options and action bounds. A required scalar or list does not become optional by assigning null.

The parser checks format validity, not editorial quality. Give domains, channels and findings useful names even where the format allows an empty string. Use step or linear for interpolation. Include a reason for every deliberately unavailable input. A reverse-DNS identifier should belong to a namespace you can maintain; the parser's basic identifier check does not prove ownership.

Authoring with AI

Give an AI assistant this guide, the intended diagnostic question, the supported target and IP revision, relevant SVD entries, exact firmware headers and the matching build configuration. Provide only information you are entitled to share. Require an inventory of source names and assumptions before generating a descriptor.

A useful generation workflow is:

  1. Identify observable inputs and their evidence sources. Mark unresolved names or semantics as open questions instead of inventing them.
  2. Generate one complete version-one descriptor with supported properties only. Keep declared sampled inputs separate from display rules and derived values.
  3. Run viewalyzer-cli parse-trace-domain your-domain.vadomain and fix the fields named by its diagnostics. Revalidate after every change.
  4. Check symbol/type resolution in the application. Resolve unavailable inputs using the matching ELF/SVD rather than deleting evidence from a diagnostic rule until it passes.
  5. Test healthy behavior, a known abnormal condition and unavailable data. Review explanations for claims stronger than the samples support.
  6. Have a domain expert review read side effects, ownership, counter semantics, action policies and thresholds before sharing the pack.

Do not ask the file to evaluate expressions such as queue.used / queue.capacity, add executable callbacks, invent detector names or embed arbitrary register addresses. Use the listed derived operators, a presentation reference, named SVD registers and supported source paths.

Share and maintain a domain

Distribute the .vadomain with a README containing its purpose, tested devices and IP revisions, firmware or middleware versions, relevant build options, ELF/SVD requirements, observation limits, units and counter semantics. Include test steps and the domain's license and attribution. Keep licenses and extra documentation in companion files rather than adding unsupported JSON properties.

Use a stable id so an update represents the same domain, and increment version as the domain evolves. Two installations with the same ID can replace or override each other; fork to a distinct ID for a different compatibility contract. Remove machine-specific paths and credentials from examples. Select symbols rather than copying addresses from one linked firmware image.

For each release, rerun validation and test the intended application workflows. State exactly which firmware and device combinations were exercised. A specialized, well-tested domain for one Ethernet IP revision is more useful than an unverified claim to support every Ethernet controller.