> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wuji.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Diagnostics and Logs

## joint\_diagnostics (SUB)

**Joint diagnostics stream**: subscribe to per-joint status word, current, bus voltage, temperature, and error code. Frames are variable-length and contain only online joints — identify each entry by its `nid`. This stream and `joint_states` ride the same device stream, so lowering the push rate of one lowers both — see [joint\_states (SUB)](/en/docs/wuji-hand/latest/sdk-reference/joint-control/#joint_states-sub).

```python theme={null}
sub = hand.joint_diagnostics().subscribe() # → Subscription[JointDiagnosticsFrame]
frame = await sub.recv_async()
for j in frame.joints:
    print(j.nid, j.vbus_v_fb, j.mcu_temp_c_fb, f"0x{j.error_code_current:04X}")
sub.close()
```

Decode `error_code_current` with the static method `WujiHand2.describe_error(code)`. The returned object carries `code`, the error name, `desc`, `cause`, `resolution`, `severity` (`Warning` / `DeferredStop` / `ImmediateStop` / `Fatal`), and `clear_policy` (`AutoClear` / `ManualClear` / `NonClearable`). Use `cause` and `resolution` to identify and address the fault. Unknown codes return `None`. Printing a diagnostics entry object shows error codes in hex.

<Info>
  For joint angles, use `position` from the `joint_states` stream — the diagnostics stream doesn't carry joint angles.
</Info>

## comm\_diag (GET, 1 Hz)

**Communication diagnostics**: returns the throughput / error rate of the 5 fingers, once per second.

```python theme={null}
diag = hand.comm_diag().get() # → HandCommunicationDiagnostics
for finger in diag.fingers: # throughput / error rate for each of the 5 fingers
    print(finger.tx_kbps, finger.rx_kbps, finger.error_per_sec)
```

Use it to troubleshoot communication between the joints and the control board.

## Flash Log Export (Diagnostics)

```python theme={null}
# This call blocks until the export finishes.
result = hand.export_flash_logs("./logs")
print(result["path"])      # str: absolute path to the exported JSONL file
print(result["frames"])    # int: number of exported log records
```

The SDK writes one `flash_<serial>_<YYYY-MM-DD_HHMMSS>.jsonl` file to `out_dir`. Omit `out_dir` to use `~/.wuji/logs`.

The SDK writes to a temporary file and makes the JSONL file available only after the export finishes. If the export fails, the SDK removes the temporary file, so the output directory contains no partial export.

<Info>
  Starting with firmware v2.6.0, one device supports concurrent exports. On earlier firmware, export one at a time.

  When multiple sessions export logs from the same device, the SDK retries automatically. If the export still can't finish, the call reports that the device is busy. The SDK removes the temporary files and doesn't keep an unfinished log.
</Info>

<Warning>
  Flash log export requires compatible device firmware and Wuji SDK versions. If the versions are incompatible, the export fails without creating log files. Update the device firmware and the Wuji SDK together, then retry.
</Warning>

## Data Types

### JointDiagnosticsFrame / JointDiagnosticsEntry

**Joint diagnostics frame**: frame type of the `hand.joint_diagnostics().subscribe()` stream. Variable-length, online joints only.

```python theme={null}
class JointDiagnosticsFrame:
    header: FrameHeader # seq / timestamp_us / frame_id
    num_joints: int
    joints: list[JointDiagnosticsEntry]
    comm: Hand2CommSummary # frame-level comm health summary, 1 Hz refresh (see Hand2CommSummary)

class JointDiagnosticsEntry:
    nid: int # node ID — identifies the joint across frames
    status_word: StatusWord # status word
    current: float # current (A)
    vbus_v_fb: float # bus voltage (V)
    mcu_temp_c_fb: float # MCU temperature (°C)
    error_code_current: int # current error code (integer value, shown in hex when the entry is printed) — decode with describe_error()
    comm_response_rate_pct: int # bus response rate over the last second, 0–100
    comm_timeout_total: int # cumulative bus timeouts for this joint
```

### HandCommunicationDiagnostics

**Communication diagnostics** data.

```python theme={null}
class HandCommunicationDiagnostics:
    fingers: list[FingerCommunicationDiagnostics] # length 5

class FingerCommunicationDiagnostics:
    tx_frame_total: int
    rx_frame_total: int
    tx_kbps: int
    rx_kbps: int
    error_per_sec: int
    crc_error_total: int
    frame_format_error_total: int
    uart_hw_error_total: int
    transfer_stats: list[TransferStats]
    nodes: list[NodeDiagnostics] # per-node: online / ms_since_last_response / response_rate_pct
```

### StatusWord

**Status word**: the type of the `status_word` field on [JointDiagnosticsEntry](#jointdiagnosticsframe-/-jointdiagnosticsentry), the public status view exposed by the SDK.

| Field | Type | Description |
| - | - | - |
| `ext_state` | `int` | Extended state value (such as Init / Ready / Enabled / Stopped) |
| `ext_state_name` | `str` | Semantic name of `ext_state` |
| `position_limit_active` | `bool` | Position limit triggered |
| `velocity_limit_active` | `bool` | Velocity limit triggered |
| `current_limit_active` | `bool` | Current limit triggered |

### Hand2CommSummary

**Frame-level communication health summary**: the type of the `comm` field on [JointDiagnosticsFrame](#jointdiagnosticsframe-/-jointdiagnosticsentry), combining the device-internal bus view with the SDK-local end-to-end (Ethernet) view. The device-internal part comes from a snapshot the SDK refreshes automatically at 1 Hz — no polling required.

| Field | Type | Description |
| - | - | - |
| `age_ms` | `int` | Age of the device-internal snapshot in milliseconds. `65535` = never sampled successfully (the device-internal fields are zeros). `65534` = saturated, the snapshot is 65.5 s or older |
| `e2e_received` | `int` | Frames received across all subscribed streams (cumulative) |
| `e2e_lost` | `int` | Frames lost on the Ethernet segment, detected via sequence gaps (cumulative) |
| `e2e_reordered` | `int` | Late/reordered frames (cumulative) |
| `e2e_duplicates` | `int` | Duplicate frames (cumulative) |
| `e2e_window_loss_x100` | `int` | Loss rate over the last second, in 0.01% units |
| `rpc_total` | `int` | Requests sent (cumulative). **`0` means this transport does not report RPC statistics** (only the wuji-proto transport does, and a live wuji-proto connection has always sent at least one request), so a zero here is not the same as "no retries happened" |
| `rpc_retries` | `int` | Request retransmissions (cumulative) |
| `rpc_timeouts` | `int` | Requests that ultimately timed out (cumulative) |
| `comm_get_failures` | `int` | Failed internal snapshot refreshes (cumulative) |
| `sdk_dropped` | `int` | Frames dropped inside the SDK because one of its in-process hops fell behind (cumulative) — either your subscription consumer or the internal stream handler. Distinct from `e2e_lost`, which is network loss |

<Info>
  **Reading the device-internal fields.** `age_ms` and the per-joint `comm_*` fields
  all come from one snapshot the SDK refreshes at 1 Hz. The refresh is deliberately
  skipped while a firmware upgrade is running, so a briefly stale snapshot is expected
  rather than a fault. Tell the two cases apart with
  `comm_get_failures`: a growing `age_ms` while `comm_get_failures` stays flat means the
  refresh is being skipped on purpose, while `comm_get_failures` increasing means the refresh
  itself is failing.
</Info>

<Warning>
  **Counters saturate, they do not wrap.** Every counter in this table stops at its
  maximum (65535 for the 16-bit ones — `e2e_reordered`, `e2e_duplicates`,
  `rpc_retries`, `rpc_timeouts`, `comm_get_failures` — and 4294967295 for the 32-bit ones)
  and stays there. On a 1 kHz stream a long-running session can reach those ceilings, so
  read these as *deltas between two samples* rather than as absolute lifetime totals.
</Warning>

## Exception Handling

All SDK operation errors raise a unified `WujiException` whose message carries an error-type prefix (`Disconnected`, `Timeout`, `PathNotFound`, `SchemaMismatch`, `SerializeError`, …). Use `try` / `except` as needed.

```python theme={null}
from wuji_sdk import WujiException

try:
    hand.joint_states().subscribe()
except WujiException as e:
    print(f"SDK error: {e}")
```

For the full list of exception types, see the [Wuji SDK appendix — Python Exception Types](/en/docs/wuji-sdk/appendix/#python-exception-types). For reconnecting after a drop, see [Wuji SDK best practices — Reconnection Strategy](/en/docs/wuji-sdk/advanced/#reconnection-strategy).

### Device Fault Codes

`error_code_current` in the joint diagnostics and `error_code` in the error history are both `u16` device fault codes. Decode them with `WujiHand2.describe_error()`.

Device fault codes lay their hex digits out as `0xSCNN`, but don't decode the digits yourself — the `severity` and `clear_policy` fields returned by `describe_error()` carry the same information as plain strings and stay correct if the layout ever changes. Treat the code as an opaque identifier: log it, show it to the user, quote it in a bug report. When decoding succeeds, use the returned `severity` and `clear_policy` to decide how to react. When `describe_error()` returns `None` (an unknown code), don't access those fields — keep the original value for logging and bug reports.

<Tabs>
  <Tab title="Python">
    Python: See the Python API examples earlier on this page.
  </Tab>

  <Tab title="C">
    The C API matches the Python API on this page and uses the `wuji_hand_2_` prefix. For initialization conventions, memory management, error codes, and build setup, see the [C API Reference](/en/docs/wuji-sdk/data-reference/).

    ### Subscribe to Joint Diagnostics

    `wuji_hand_2_subscribe_joint_diagnostics` has the same callback shape as `wuji_hand_2_subscribe_joint_states`. Each `WujiJointDiagnosticsEntry` carries `status_word`, `current`, `vbus_v_fb`, `mcu_temp_c_fb`, `error_code_current`, and per-joint bus comm quality (`comm_response_rate_pct`, `comm_timeout_total`). The frame-level `comm` field (`WujiHand2CommSummary`) adds end-to-end stream loss, RPC retry and timeout counters, and the tactile online bitmap.

    ### Decode Joint Status Words

    `wuji_hand_2_decode_joint_status` decodes `WujiJointDiagnosticsEntry.status_word` into `WujiJointStatus`. `ext_state` is `0` for initializing, `1` for ready, `2` for enabled, or `3` for stopped. `enabled` is `true` only when `ext_state == 2`. The remaining fields show whether the position, velocity, and current limits are active. The function doesn't require a device connection.

    ```c theme={null}
    void print_joint_status(uint32_t status_word) {
        WujiJointStatus status = {0};
        if (wuji_hand_2_decode_joint_status(status_word, &status) != WUJI_STATUS_OK) {
            return;
        }

        printf("state=%u enabled=%d limits=%d/%d/%d\n",
               status.ext_state,
               status.enabled,
               status.position_limit_active,
               status.velocity_limit_active,
               status.current_limit_active);
    }
    ```

    The function returns `WUJI_STATUS_ERR_INVALID_ARG` when `out` is `NULL`.

    ### Flash Log Export

    C SDK v2026.8.31 and later provides the Flash log export API. After calling `wuji_hand_2_export_flash_logs()`, the current thread waits until the SDK reads the device logs and writes the output file before the function returns a result struct with `path` and `frame_count`:

    ```c theme={null}
    WujiFlashLogExport result = {0};
    if (wuji_hand_2_export_flash_logs(dev, NULL, &result) == WUJI_STATUS_OK) {
        printf("%u frames -> %s\n", result.frame_count, result.path);
        wuji_hand_2_flash_log_export_free(&result);
    } else {
        fprintf(stderr, "export failed: %s\n", wuji_last_error());
    }
    ```

    Pass `NULL` as `out_dir` to write `flash_<serial>_<date>_<time>.jsonl` under `~/.wuji/logs/`, or pass a directory path to choose the destination. Each call creates a new file. Exports created within the same second receive a numbered suffix instead of replacing an existing file.

    The returned frame count can be `0` when the device log ring is empty. The empty output file is still a successful export.

    Handle common export errors as follows:

    * C returns `WUJI_STATUS_ERR_INVALID_ARG` when `dev` or `out` is `NULL`, or when `out_dir` isn't valid UTF-8.
    * C returns `WUJI_STATUS_ERR_UNSUPPORTED` when the device isn't Wuji Hand 2 or its firmware doesn't support the current protocol.
    * C returns `WUJI_STATUS_ERR_TIMEOUT` if another export is running or if log data is overwritten during export and repeated retries still can't complete. Check `wuji_last_error()` for details.
    * C returns `WUJI_STATUS_ERR_DISCONNECTED` if the device disconnects during export.
    * C returns `WUJI_STATUS_ERR_PROTOCOL` when the device reports a flash-log protocol error or returns malformed data.
    * C returns `WUJI_STATUS_ERR_INTERNAL` when a file-system I/O operation fails. Check `wuji_last_error()` for details.

    For other status values, call `wuji_last_error()` to get the cause. A failed export doesn't create an output file.

    Call `wuji_hand_2_flash_log_export_free` only after a successful export.
  </Tab>
</Tabs>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.