Tactile Residual Grid View
SDK Practice
Overview
tactile_binary has already decided whether something touched the glove. tactile_residual hands you the continuous signal from the step before that: a signed residual per taxel, where positive means pressed harder than the calibrated baseline, near zero means no contact, and negative means lighter than baseline.
That’s the difference — the threshold belongs to the consumer. tactile_binary uses a threshold the SDK derives from sensitivity, while tactile_residual makes no judgment and leaves the line for you to draw.
This example lays the residual out as a 24x31 grid, shows intensity with a six-level character ramp, and lets you adjust the threshold live to watch the grid respond. It’s an observation tool, not another calibration workflow.
Matching examples: examples/python/wuji_glove/9.tactile_residual_view.py and examples/c/wuji_glove/7_tactile_residual_view.c.
For API and field definitions, see Tactile.
The grid rendering, character ramp rules, threshold behavior, and uncalibrated appearance on this page are all measured. The view under actual contact isn’t — producing a residual requires someone wearing the glove and touching something.
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 | Linux or a Mac with Apple silicon, meeting the system requirements |
| Wuji SDK | v2026.8.31 or later |
| Python | 3.10 or later |
| C toolchain | CMake 3.16 or later, plus the extracted SDK package |
Python
C
pip install wuji-sdkLIB="libwuji_sdk_c.so"
if [ "$(uname -s)" = "Darwin" ]; then LIB="libwuji_sdk_c.dylib"; fi
cmake -S examples/c/wuji_glove -B build/glove \
-DWUJI_SDK_INCLUDE_DIR="${SDK}/include" \
-DWUJI_SDK_LIB="${SDK}/lib/${LIB}"
cmake --build build/glove --target 7_tactile_residual_view Before You Start
Run tactile calibration first. The residual is computed against a calibrated baseline, and without that baseline there’s no residual — this stream measures exactly zero on an uncalibrated glove. See Tactile Calibration.
How the Residual Relates to the Binary Stream
Both streams come from the same model, and tactile_binary is tactile_residual after one threshold. Take the former when you want a ready-made verdict, the latter when you want to set the criterion yourself.
The Grid Matches the Binary View Exactly
tactile_residual also carries 744 values, with the same 218 invalid channels in the same places and the same row-major mapping of index = row * 31 + col. 24 x 31 = 744, so the grid is the sensor array’s physical layout.
The hand outline is therefore identical between the two views, and switching between them never shifts anything. For where the channel counts come from, see Subscribing With Callbacks.
Six-Level Character Ramp
This is the main display difference from the binary view:
| Character | Test | Meaning |
|---|---|---|
| Space | abs(v - (-1.0)) < 1e-6 | Invalid or masked taxel |
# | threshold > 0 and v >= threshold | At or above your threshold |
. | v <= 0 or threshold <= 0 | No contact, or the fallback when the threshold is zero |
. : - = + * | 0 < v < threshold | Six intensity levels below the threshold, rising to the right |
The ramp maps proportionally:
RAMP = ".:-=+*"
idx = int(value / threshold * len(RAMP))
return RAMP[min(idx, len(RAMP) - 1)] A value in the first sixth of the threshold shows ., the second sixth shows :, and so on, with * meaning nearly at the threshold. The ramp is relative to the threshold, so adjusting the threshold redistributes the whole scale. It isn’t an absolute measure.
The invalid-channel test uses an approximate float comparison rather than == -1.0, avoiding an equality check:
MASKED_VALUE = -1.0
MASKED_EPS = 1e-6
if abs(value - MASKED_VALUE) < MASKED_EPS:
return " " Threshold Behavior
| Input | Status line shows | Note |
|---|---|---|
0.5 | 0.5 | Default |
0.0 | 0.0 | Valid, and every valid channel renders as . |
-1.0 | 0.0 | Clamped to the lower bound |
The clamp has a floor only:
def clamp_threshold(value: float) -> float:
return max(0.0, float(value)) There’s no ceiling. That differs from sensitivity in the binary view, which the example clamps to [0.5, 3.0]. The residual threshold is your own criterion, and the example doesn’t bound it for you.
threshold = 0 is a special case: in cell_for, the value <= 0.0 or threshold <= 0.0 branch renders every valid channel as . and # never appears. To “show everything,” use a very small positive number rather than zero.
In an interactive terminal, + and - step by 0.1 (THRESHOLD_STEP = 0.1).
Status Line
above=0 threshold=0.5 peak=+0.000 row=0 col=14 seq=657713 len=744 (+/- threshold, q quit) | Field | Meaning |
|---|---|
above | Taxels at or above the threshold |
threshold | Current threshold |
peak | Highest residual this frame, signed |
row / col | Grid coordinates of the peak |
seq | Frame sequence number, absent from the binary view |
len | Frame length, normally 744 |
The sign on peak matters. A negative peak means a whole region reads lighter than the calibrated baseline, which usually points at a loose fit or a hand posture far from the one used during calibration.
seq confirms the stream is alive: a number that stops moving means no new frames.
Render Throttling
Throttled to 10 Hz, same as the binary view:
RENDER_INTERVAL_S = 0.1 The stream runs at 130 Hz with callbacks firing per frame on an SDK worker thread. Without throttling, that thread spends itself on terminal output.
Integration Steps
Complete examples: Python tactile_residual view and C tactile_residual view.
Reuse imports, manager setup, C initialization, connection error handling, and callbacks from Python example and C example. Check the status of each C call. On failure, release existing resources and exit as shown in the examples.
1. Connect to the Target Glove
Connect to the target glove by serial number. Don’t depend on discovery order when several devices share the network.
Python
C
glove = manager.connect(sn=serial_number, device_name="my_glove")WujiConnectTarget target = {
.kind = WUJI_CONNECT_TARGET_KIND_SN, .value = serial_number,
};
struct WujiDevice *dev = NULL;
wuji_connect(&target, "glove_0", NULL, &dev); 2. Subscribe to the Residual Stream
The residual stream requires no device parameter writes. Read the signed tactile-unit array in the callback, apply application thresholds, and throttle rendering.
Python
C
sub = glove.tactile_residual().subscribe_with_callback(on_residual)struct WujiSub *sub = NULL;
wuji_glove_subscribe_tactile_residual(dev, on_residual, NULL, &sub); 3. Run for a Fixed Duration and Clean Up
When the view exits, close the subscription before disconnecting. Don’t run blocking rendering directly on the callback thread.
Python
C
try:
time.sleep(seconds)
finally:
sub.close()
manager.disconnect_all()sleep(seconds);
wuji_sub_close(sub);
wuji_dev_disconnect(dev);
wuji_dev_release(dev);
wuji_shutdown(); Running It
Python
C
python examples/python/wuji_glove/9.tactile_residual_view.py --sn <serial number>
python examples/python/wuji_glove/9.tactile_residual_view.py --sn <serial number> \
--seconds 60 --threshold 0.8./build/glove/7_tactile_residual_view Measured output, uncalibrated and unworn:
above=0 threshold=0.5 peak=+0.000 row=0 col=14 seq=557357 len=744 (+/- threshold, q quit)
....
.... ...... ...
...... ...... .....
...... ...... .....
...... ...... .....
... ...... ...... .....
........... ...... .....
........... ...... .....
........... ...... .....
........... ...... ..... ...
................... ..........
...............................
...............................
.... .................... .....
.................... .....
. .................... .....
. ..........................
. ..................... ...
. .....................
. .....................
. .....................
. ...................
. ..................
. .................. Measured on a right-hand Wuji Glove resting on a desk, unworn and uncalibrated.
The outline matches the binary view exactly, because invalid channels sit in the same places. peak=+0.000 and above=0 are what an uncalibrated glove looks like, while seq keeps incrementing to show the stream itself is alive — a stream carrying frames isn’t the same as a stream carrying content, which is what the seq field is for.
Once calibrated and worn, contact regions render as the . : - = + * ramp by intensity, places at or above the threshold turn into #, and peak gives the strongest current residual.
Notes
| Branch point | Behavior | What to do |
|---|---|---|
| No tactile calibration | The whole grid is . with a constant peak=+0.000 | Run tactile calibration first |
| Glove not worn | Same as above | Put the glove on and touch something |
Negative threshold | Clamped to 0.0 | Use a positive number |
threshold set to 0 | # never appears and everything is . | Use a very small positive number to show everything |
peak stays negative | The whole glove reads lighter than baseline | Check the fit, or recalibrate |
| Non-interactive terminal | Keyboard control is unavailable | Preset a value with --threshold |
--seconds 0 | Runs until q or Ctrl-C | That’s the default |
Adapting It
Find Your Threshold From peak
Use the example as follows: hold the lightest contact your application cares about, watch where peak settles, and set the threshold just below that. It beats guessing, and it beats tuning sensitivity by trial and error.
Read the Ramp for Contact Edges
# tells you only where the threshold was crossed, while the ramp shows how contact falls off toward the edges. For grasp-posture analysis or contact-area estimation, that gradient carries more than a binary verdict.
A Negative Residual Is a Signal
A persistently negative peak means the whole glove reads lighter than its calibrated baseline, most often because it’s worn looser than during calibration. Refit the glove rather than lowering the threshold to compensate.
For a Verdict, Use the Binary Stream
Writing your own threshold logic costs real work: noise handling, temporal filtering, hysteresis. If all you need is whether something touched the glove, use tactile_binary, where the SDK has already done that. The residual earns its place when your criterion differs from the default.
Related
- Tactile Calibration: the hard requirement here, since the residual is computed against a calibrated baseline
- Tactile Contact Grid View: the same grid after the SDK’s own decision
- Subscribing With Callbacks: where 744, 526, and 218 come from
- Common Data Types: the
TactileResidualstructure
ドキュメント