> ## 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.

# Tactile Contact Grid View

## Overview

`tactile_binary` is one of the outputs of tactile calibration: a contact-or-not verdict per taxel. This example lays it out as a 24x31 terminal grid. The grid shows the contact location in real time.

It's an **observation tool**, not another calibration workflow. Use it to verify calibration results, tune sensitivity, and track down a region that doesn't respond.

Matching examples: `examples/python/wuji_glove/8.tactile_contact_view.py` and `examples/c/wuji_glove/6_tactile_contact_view.c`.

For tactile field definitions, see [Tactile](/en/docs/wuji-glove/sdk-data-reference/tactile). This page covers only how to run it and what the measurements show.

<Warning>
  The grid rendering, layout mapping, parameter behavior, and uncalibrated appearance on this page are all measured. **The view under actual contact isn't** — producing contact requires someone wearing the glove and touching something.
</Warning>

## Prerequisites

### Hardware

| Item | Requirement |
| - | - |
| Wuji Glove | 1, **tactile calibration completed** |
| Connection | Ethernet |
| Terminal | At least 31 columns wide and 26 rows tall |

### Software

| Item | Requirement |
| - | - |
| Operating system | Ubuntu 22.04 LTS or later |
| Wuji SDK | v2026.8.31 or later |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    pip install wuji-sdk
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    cmake -S examples/c/wuji_glove -B build/glove \
      -DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
      -DWUJI_SDK_LIB="${SDK}/lib/libwuji_sdk_c.so"
    cmake --build build/glove --target 6_tactile_contact_view
    ```
  </Tab>
</Tabs>

### Before You Start

**Run tactile calibration first.** On an uncalibrated glove, `tactile_binary` is exactly zero and the grid stays empty forever. See [Tactile Calibration](/en/docs/wuji-glove/sdk-practice/tactile-calibration).

To tell whether a glove is calibrated, check whether the stream is all zeros:

```python theme={null}
frame = glove.tactile_binary().subscribe().recv()
print("calibrated" if any(v > 0 for v in frame.data) else "all zeros, likely uncalibrated")
```

## The Grid Is the Array's Physical Layout

This is the key to reading the whole view: **24 x 31 = 744**, exactly the channel count of the raw `tactile` stream. The grid isn't resampled. It's those 744 channels laid out as the sensor arranges them physically.

The mapping is row-major:

```text theme={null}
index = row * 31 + col
```

`tactile`, `tactile_binary`, and `tactile_residual` all carry 744 values, with invalid channels in identical positions (218 of them). All three views therefore show the same outline.

### Invalid Channels Trace the Hand's Outline

Invalid channels per row:

```text theme={null}
row  0: 27    row  6: 9     row 12: 0     row 18: 9
row  1: 18    row  7: 9     row 13: 2     row 19: 9
row  2: 14    row  8: 9     row 14: 6     row 20: 9
row  3: 14    row  9: 6     row 15: 5     row 21: 11
row  4: 14    row 10: 2     row 16: 4     row 22: 12
row  5: 11    row 11: 0     row 17: 6     row 23: 12
```

Edge rows hold most of them while middle rows hold almost none. Rendered, that reads as a hand: five fingers branching at the top and a palm filling the middle.

No column is entirely invalid, because 744 already reflects the hardware-masked column being removed.

## Legend

| Character | Test | Meaning |
| - | - | - |
| `#` | `v > 0.5` | Contact |
| `.` | `-0.5 <= v <= 0.5` | Hand surface, no contact |
| Space | `v < -0.5` | Invalid or masked taxel |

`-1.0` is the sentinel for an invalid channel, so the space test reads `v < -0.5` rather than `v == -1.0`, avoiding a floating-point equality check.

## Status Line

One line above the grid:

```text theme={null}
contacts=0   sensitivity=1.0 peak=0.000 row=0 col=14 len=744  (+/- sensitivity, q quit)
```

| Field | Meaning |
| - | - |
| `contacts` | Taxels with `v > 0.5` in this frame |
| `sensitivity` | Current sensitivity |
| `peak` | Highest value in this frame |
| `row` / `col` | Grid coordinates of the peak, from `peak_idx // 31` and `peak_idx % 31` |
| `len` | Frame length, normally 744 |

## Sensitivity Has Two Clamps, and Only One Is Real

This is the detail worth knowing on this page.

| Layer | Range | Behavior |
| - | - | - |
| Example's `clamp_sensitivity` | `[0.5, 3.0]` | Clamps to the boundary |
| SDK parameter layer | **No clamp** | Any value writes through |

Calling the SDK API directly:

```text theme={null}
set(1.5) -> reads back 1.5
set(3.0) -> reads back 3.0
set(5.0) -> reads back 5.0
set(0.1) -> reads back 0.1
```

Bypassing the example, `glove.set("algorithms.tactile_binary.sensitivity", 5.0)` writes and reads back successfully. The example's `[0.5, 3.0]` is a **recommended range**, not a boundary the SDK enforces.

That's why passing `--sensitivity 20.0` shows `3.0` on the status line. The example clamps before it writes:

```text theme={null}
=== sensitivity=20.0 ===
contacts=0   sensitivity=3.0 peak=0.000 ...
```

### This Parameter Writes to the Device

`SENSITIVITY_PARAM = "algorithms.tactile_binary.sensitivity"` goes out through `glove.set()` and changes the `tactile_binary` stream itself, not the local display. Every other subscriber sees the change at the same moment.

Reading it before it's ever been set raises rather than returning a default:

```text theme={null}
WujiException: Parameter not set: algorithms.tactile_binary.sensitivity
```

The example deliberately wraps `set_sensitivity` in a try so a failed write doesn't take the view down. An observation tool crashing mid-run is worse than a reading being off.

## Render Throttling

The stream runs at 130 Hz and rendering is throttled to 10 Hz:

```python theme={null}
RENDER_INTERVAL_S = 0.1

if now - self._last_render < RENDER_INTERVAL_S:
    return
```

Callbacks fire per frame on an SDK worker thread, and without throttling, terminal output drags that thread down. **This is the standard shape for rendering inside a callback**: decide quickly whether to draw, and return immediately when you shouldn't.

## Runtime Handling

| Branch point | Behavior | What to do |
| - | - | - |
| No tactile calibration | The grid is all `.`, with `contacts=0` and a constant `peak=0.000` | Run tactile calibration first |
| Glove not worn | Same as above | Put the glove on and touch something |
| Sensitivity outside \[0.5, 3.0] | The example clamps before writing | For values outside that range, call `set()` directly instead |
| Sensitivity write fails | Prints one line to stderr and the view continues | Check the device connection |
| Non-interactive terminal | Keyboard control is unavailable, so it runs out `--seconds` or waits for `Ctrl-C` | Preset a value with `--sensitivity` |
| `--seconds 0` | Runs until `q` or `Ctrl-C` | That's the default |
| Frame length isn't 744 | Anything beyond is treated as invalid | The example guards with `limit = min(len(data), 24*31)` |

In an interactive terminal, `+` and `-` adjust sensitivity and `q` quits.

## Integration Steps

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    import sys
    import time
    from wuji_sdk import SdkManager, DeviceType, TactileBinary, WujiException

    TACTILE_ROWS, TACTILE_COLS = 24, 31
    RENDER_INTERVAL_S = 0.1          # stream at 130 Hz, rendering throttled to 10 Hz

    last_render = 0.0

    def on_binary(frame: TactileBinary) -> None:
        global last_render
        now = time.monotonic()
        # This runs on an SDK worker thread. Return immediately when not drawing
        if now - last_render < RENDER_INTERVAL_S:
            return
        last_render = now

        data = frame.data
        limit = min(len(data), TACTILE_ROWS * TACTILE_COLS)
        contacts = sum(1 for i in range(limit) if data[i] > 0.5)

        lines = ["\x1b[H\x1b[2J", f"contacts={contacts} len={len(data)}"]
        for row in range(TACTILE_ROWS):
            # Row-major: index = row * 31 + col
            cells = []
            for col in range(TACTILE_COLS):
                idx = row * TACTILE_COLS + col
                v = data[idx] if idx < limit else -1.0
                cells.append("#" if v > 0.5 else (" " if v < -0.5 else "."))
            lines.append("".join(cells))
        sys.stdout.write("\n".join(lines) + "\n")

    manager = SdkManager.instance()

    # 1. Connect
    devices = [d for d in manager.scan() if d.device_type == DeviceType.WujiGlove]
    glove = manager.connect(sn=devices[0].sn, device_name="my_glove")

    # 2. Push the sensitivity. This is a device parameter and affects
    #    every subscriber of the stream. Wrap it so a failure doesn't kill the view
    try:
        glove.set("algorithms.tactile_binary.sensitivity", 1.0)
    except WujiException as exc:
        print(f"(failed to set sensitivity: {exc})", file=sys.stderr)

    # 3. Subscribe
    sub = glove.tactile_binary().subscribe_with_callback(on_binary)

    time.sleep(30)

    sub.close()
    manager.disconnect_all()
    ```
  </Tab>

  <Tab title="C">
    ```c theme={null}
    #include "wuji_sdk.h"

    #define TACTILE_ROWS 24
    #define TACTILE_COLS 31

    /* Callbacks fire on an SDK worker thread, so throttle rendering */
    static void on_binary(WujiFrameKind kind, const WujiTactileBinary *f, void *ud) {
        if (kind != WUJI_FRAME_KIND_OK || !f) return;

        size_t limit = f->data_len < TACTILE_ROWS * TACTILE_COLS
                     ? f->data_len : TACTILE_ROWS * TACTILE_COLS;
        int contacts = 0;
        for (size_t i = 0; i < limit; i++) {
            if (f->data[i] > 0.5f) contacts++;
        }

        printf("\x1b[H\x1b[2J" "contacts=%d len=%zu\n", contacts, f->data_len);
        for (int row = 0; row < TACTILE_ROWS; row++) {
            for (int col = 0; col < TACTILE_COLS; col++) {
                size_t idx = (size_t)row * TACTILE_COLS + col;
                float v = idx < limit ? f->data[idx] : -1.0f;
                putchar(v > 0.5f ? '#' : (v < -0.5f ? ' ' : '.'));
            }
            putchar('\n');
        }
        fflush(stdout);
    }

    /* 1. Initialize, discover, connect. See the callback subscription page */

    /* 2. Subscribe to tactile_binary */
    struct WujiSub *sub = NULL;
    wuji_glove_subscribe_tactile_binary(dev, on_binary, NULL, &sub);

    /* 3. Close the subscription first, then release the device */
    wuji_sub_close(sub);
    wuji_dev_disconnect(dev);
    wuji_dev_release(dev);
    wuji_shutdown();
    ```
  </Tab>
</Tabs>

## Running It

<Tabs>
  <Tab title="Python">
    ```bash theme={null}
    python examples/python/wuji_glove/8.tactile_contact_view.py --sn <serial number>
    python examples/python/wuji_glove/8.tactile_contact_view.py --sn <serial number> \
      --seconds 60 --sensitivity 1.5
    ```
  </Tab>

  <Tab title="C">
    ```bash theme={null}
    ./build/glove/6_tactile_contact_view
    ```
  </Tab>
</Tabs>

Measured output, uncalibrated and unworn:

```text theme={null}
contacts=0   sensitivity=1.0 peak=0.000 row=0 col=14 len=744  (+/- sensitivity, q quit)
              ....
      ....   ......   ...
     ......  ......  .....
     ......  ......  .....
     ......  ......  .....
 ... ......  ......  .....
...........  ......  .....
...........  ......  .....
...........  ......  .....
...........  ......  ..... ...
...................  ..........
...............................
...............................
.... .................... .....
     .................... .....
.    .................... .....
.    ..........................
.    ..................... ...
.    .....................
.    .....................
.    .....................
.    ...................
.    ..................
.    ..................
```

Measured on a right-hand Wuji Glove resting on a desk, unworn and uncalibrated.

The outline reads clearly: five fingers branching at the top, a palm filling the middle, and the thumb running down the left. `contacts=0` and `peak=0.000` are what an uncalibrated glove looks like — **the outline comes from the distribution of invalid channels and has nothing to do with calibration**, so seeing a hand shape doesn't mean the stream carries data.

Once calibrated and worn, contact regions turn into `#`, `contacts` grows with the contact area, `peak` rises, and `row` / `col` point at the hardest-pressed spot.

## Adapting It

### Use It to Verify Calibration Quality

After calibrating, press each region in turn and watch whether the matching part of the grid lights up. A region that never lights no matter how you press wasn't covered during calibration, so redo that motion. This localizes the problem far better than a single `dead_pct` number.

### Tune Sensitivity to Find a Working Point

Adjust live with `+` and `-`. While holding a press, lower it gradually until false positives just stop, and that's the working point for this glove. Record it in your application config.

### For Your Own Threshold, Read the Residual

The threshold behind `tactile_binary` is computed by the SDK from sensitivity. For full control, use `tactile_residual`, which gives a continuous value and leaves the threshold to you. See [Tactile Residual Grid View](/en/docs/wuji-glove/sdk-practice/tactile-residual-view).

### Map Grid Coordinates Back to the Point Cloud

The `row` / `col` on the status line converts back to a channel index with `row * 31 + col`, which then maps to a spatial coordinate in `tactile_point_cloud` to locate the contact in 3D. The point cloud holds only the 526 valid points, so skip invalid channels when converting.

## Related

* [Tactile Calibration](/en/docs/wuji-glove/sdk-practice/tactile-calibration): the hard requirement for this page
* [Tactile Residual Grid View](/en/docs/wuji-glove/sdk-practice/tactile-residual-view): the same grid showing a continuous signal
* [Subscribing With Callbacks](/en/docs/wuji-glove/sdk-practice/subscribe-callback): where 744, 526, and 218 come from
* [Common Data Types](/en/docs/wuji-sdk/data-reference/): the `TactileBinary` structure


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