The AY-Robots SO-100 hub page, the starting point for building, calibrating, recording and training on an SO-100 robot arm
Tactile sensingSO-100LeRobotVLASensorsData collection

Tactile Sensing for Low-Cost Robot Arms: What Touch Adds

AY-Robots ResearchAugust 23, 202619 min read

A DIGIT costs 355 USD, an AnySkin fingertip 128, a magnetic skin under 30. None of the five trainable policies has a touch input. Here is how tactile data gets in anyway.

Touch is the one modality the low-cost stack skipped

An SO-100 ships with joint encoders and whatever USB cameras you bolt onto it. That is its entire sensory world. Contact is never measured, only inferred: the policy watches a gripper close on a pixel blob and guesses something is held. When the guess is wrong the arm keeps executing a grasp it does not have, and you get a gripper that does not close on the thing it was aimed at.

Tactile sensing is supposed to fix that, and the hardware has existed for a decade. The useful question is narrower than "is touch good": what can you buy today, how does the signal get into a LeRobot dataset, and can any of the five policies you can actually fine-tune read it.

What you need to know

  • None of the five trainable policies has a tactile input. Camera images, a joint-state vector, a language instruction. That is the whole observation space.
  • Two workarounds exist: render the signal as a fake camera (an extra observation.images.* key), or append it to observation.state. Both appear in public SO-101 datasets on the Hub.
  • Prices on 24 August 2026: DIGIT 355 USD, GelSight Mini system 510 USD, a ready-made AnySkin fingertip for SO-100 and SO-101 128 USD. ReSkin puts a self-made magnetic skin under 30 USD.
  • Meta archived the reference open-source tactile stack: digit-interface, digit-design, TACTO, PyTouch and Sparsh are all read-only on GitHub today, and digit-interface has not taken a commit since 22 September 2021.
  • The real obstacle is action chunking, not the sensor. ACT defaults to chunkSize 100 and nActionSteps 100, so the arm runs 100 steps open loop after one look. Anything the skin feels inside that window changes nothing.

What a hobby-price tactile sensor actually is

Two families dominate at this price. Vision-based sensors put a tiny camera behind a silicone pad lit by coloured LEDs and read contact as a picture of the deformed gel. Magnetic skins mix magnetic particles into an elastomer and read deformation as flux change at magnetometers underneath. One gives you an image with microns of detail; the other a short vector at hundreds of hertz.

SensorSize (mm)Sensing areaFrame rateCost as publishedPrice on 24 Aug 2026
DIGIT (Lambeta et al., RA-L 2020)20 x 27 x 18, 20 g19 x 16 mm60 fps at 640 x 480~15 USD of parts at a batch of 1000; 300 USD commodity in 2023355 USD from GelSight, 4 to 6 weeks
GelSight Mini32 x 28.5 x 28, 20.8 g18.6 x 14.3 mm field of view25 fps, 8 MP camera499 USD commodity price in the 9DTact comparison table510 USD system, 560 USD robotics pack, 4 weeks
9DTact (Lin et al., RA-L)32.5 x 25.5 x 25.5, 20 g24 x 18 mm90 fps, OV5647 camera15 USD including two reusable molds, fully open sourceself-build only
ReSkin (CoRL 2021)2 to 3 mm skin20 x 20 mm~400 Hz, 5 magnetometersunder 30 USDself-build only
AnySkin2 mm skin, fingertip formfingertip-shaped100 Hz in the paper's runsderived from ReSkin128 USD as WowSkin for SO-100/SO-101
Where these numbers come from

Sensing areas, frame rates and the 9DTact and GelSight Mini dimensions are Table I of the 9DTact paper; the GelSight Mini gel, field of view and 25 fps are the Mini datasheet. DIGIT's 20 x 27 x 18 mm is from the DIGIT paper itself, which states "20 mm width x 27 mm height x 18 mm depth". Note the two sources disagree: 9DTact's Table I lists DIGIT at 36 x 26 x 33 mm, almost certainly measuring the housing and mount rather than the bare sensor. Take the larger figure if you are checking clearance on a printed jaw. Shop prices and lead times were read from the GelSight and WOWROBO stores on 24 August 2026 and will drift.

The camera-in-a-fingertip family

GelSight is the original. The 2017 Sensors paper describes a fingertip sensor with an 18 by 14 mm sensing field and a spatial resolution "around 20 to 30 microns", a minimum perceivable force mostly under 0.05 N, and CNN force regression with R-squared above 0.9. Read that range carefully: the same paper puts compact GelSight devices for robot fingers at 30 to 100 microns generally, and 1 to 2 microns only for builds optimised for resolution over size. DIGIT shrank the format to 20 by 27 by 18 mm and 20 grams, running an Omnivision OVM7692 at 640 by 480 and 60 fps for about 15 USD of parts at a batch of 1000.

The catch is the gel. GelSight rates the Mini's 4.25 mm Lambertian silicone cartridge at 1000 coin presses, 0 to 30 degrees C, and 25 fps. That is slower than the 30 fps most people record an episode at, so the streams will not line up without resampling.

The magnetic skin family

ReSkin takes the opposite trade. Five MLX90393 magnetometers, four spaced 7 mm around a central one, cover 20 by 20 mm and stream 20 values (temperature plus Bx, By, Bz per chip) at roughly 400 Hz. The paper claims under 30 USD, a 2 to 3 mm skin, 1 mm contact localisation at 90 percent accuracy, and a comparison table rating the skin durable beyond 50,000 contacts. Read that last one as a claim that the learned model still predicts, not a wear-out point: the same paper runs 50,000 indentations and watches error climb unless the no-load baseline is re-measured. AnySkin rebuilds ReSkin as a self-adhering 2 mm skin that swaps in 12 seconds on average, and reports a policy losing only 13 percent when moved to a different physical skin.

Optical versus magnetic on a 5 DoF arm
Optical (GelSight, DIGIT, 9DTact)
  • Output is an image, so it drops into a vision pipeline with no architecture change.
  • You can see the thread pitch of an M3 screw, not just that contact happened.
  • Shear and slip are readable from marker motion, the signal that predicts a dropped object.
  • Sparsh and PyTouch give you pretrained encoders that expect this kind of image.
Magnetic (ReSkin, AnySkin)
  • Output is a 15-value vector, small enough to append to the state vector unchanged.
  • Hundreds of hertz instead of 25 to 90, and 2 mm thick instead of a 30 mm cube.
  • The zero drifts. The AnySkin visualiser has a recalibrate key for it, and that drift becomes silent distribution shift in a recorded dataset.
  • No spatial detail. A deformation field, not a picture of the surface.

None of the five trainable policies has a tactile input

Worth being blunt about. Across the five policies you can fine-tune here the observation space is identical: camera images, a joint-state vector, and for the VLA models a language instruction. No touch channel, no force channel, no contact flag. A vision-language-action model is called that for a reason.

The AY-Robots policy comparison table showing GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT with parameters, GPU tier, inference latency and minimum episodes
The five trainable policies. Every one takes images plus joint state, and there is no tactile column because there is no tactile input.

The door is not shut. The signal just has to disguise itself as something the policy already accepts. Here is where each one lets you put it.

PolicyRoute in for tactileHard constraint
ACTextra image key, or extra observation.state dimsIts docstring says only equal-shaped images are supported. Defaults: chunkSize 100, nActionSteps 100.
SmolVLAextra image key, or extra state dimsImages are resized with padding to 512 x 512. max_state_dim is 32, so a 6 DoF arm has 26 spare dims.
Pi0.5extra state dims are safe, extra cameras are notPhysical Intelligence's openpi defines three fixed image slots (base_0_rgb, left_wrist_0_rgb, right_wrist_0_rgb), so two cameras leaves one. lerobot's pi05 derives image keys from the dataset and resizes to 224 x 224.
GR00T N1.7 and N1.5an extra key under "video" in meta/modality.json, or a state index rangeNVIDIA's SO-100 example uses two video keys. A LeRobot v3.0 dataset crashes the loader and must be converted to v2.1.
The trap that eats a day

The ACT config docstring in lerobot says: "If there are multiple keys beginning with observation.images. they are treated as multiple camera views. Right now we only support all images having the same shape." A tactile stream is almost never the same shape as your RGB cameras. One public Hub run mixed 1280x720, 640x480 and 400x400 and trained anyway, but that author's next revision moved tactile out of the images into observation.state. Going the image route, resize the tactile frame to match your cameras before you record. The plugin mechanics are in the LeRobot third-party cameras and sensors docs.

The two encodings people actually use

You do not have to take this on trust. Public SO-101 datasets with an AnySkin on the gripper, recorded at 30 fps in LeRobot v2.1, show both encodings.

Encoding 1: tactile as a fake camera

The magnetic readings are rendered into a picture and written as an ordinary video feature. To the loader it is a camera. To the policy it is a camera. Nothing in the training code knows otherwise.

json
// meta/info.json, oordonez/single_arm_so101_USB_anyskin
// robot_type so101_follower, fps 30, 10 episodes, 5126 frames, LeRobot v2.1

"observation.state":             { "dtype": "float32", "shape": [6] }
"action":                        { "dtype": "float32", "shape": [6] }
"observation.images.global_top": { "dtype": "video",   "shape": [720, 1280, 3] }
"observation.images.front":      { "dtype": "video",   "shape": [480, 640, 3] }
"observation.images.anyskin1":   { "dtype": "video",   "shape": [400, 400, 3] }
The tactile stream is just another video key, and its shape matches neither camera.

Encoding 2: tactile as extra state dimensions

The second route appends the raw magnetometer values to the proprioceptive vector. Five magnetometers times three axes is 15, and 6 joints plus 15 is 21. The same author published the matched ablation with those dimensions removed, same task and same 3365 frames.

json
// oordonez/rev3_ACT_single_arm_so101_USB_anyskin_raw  (10 episodes, 3365 frames)
"observation.state": { "dtype": "float32", "shape": [21] }   // 6 joints + 15 tactile

// oordonez/rev3_ACT_single_arm_so101_USB_NO_anyskin   (10 episodes, 3365 frames)
"observation.state": { "dtype": "float32", "shape": [6] }    // the control run

// trained config.json, both runs: "type": "act", chunk_size 100, n_action_steps 100
A matched pair: identical task and frame count, with and without the 15 tactile dimensions.

Ten episodes is far below what these policies need. The minimum here is 30 for SmolVLA and 50 for ACT, GR00T N1.7 and Pi0.5. Treat those datasets as a schema reference, not evidence that tactile helped.

Getting a tactile stream into a LeRobot recording

LeRobot has a documented plugin path and it treats tactile sensors as cameras. Any installed package named lerobot_camera_ is auto-discovered and the camera type it registers works from the CLI like a webcam. The only tactile plugin listed in the upstream docs today is one for Xense sensors, registering the type xense with rectified, difference, depth, marker, force and mesh outputs. Check its README before planning around that list: a LeRobot robot camera expects a single HWC image, so only the image-shaped outputs travel down the normal recording path and the force fields stay reachable only from your own code. For DIGIT, GelSight or AnySkin, you write the shim yourself.

  1. 1
    Get the sensor talking on its own first

    Connect the AnySkin magnetometer board to the Adafruit QT Py over the QWIIC cable, find the port, run the visualiser. Press B to re-zero when the baseline drifts.

    bash
    pip install anyskin
    
    # Linux
    ls /dev/ | grep -e ACM -e USB      # usually /dev/ttyACM0
    sudo chmod a+rw /dev/ttyACM0
    
    # macOS
    ls /dev/ | grep cu.usb            # usually cu.usbmodem*
    
    anyskin_viz /dev/ttyACM0
  2. 2
    For an optical sensor, confirm the raw stream

    DIGIT enumerates as a USB camera. The archived digit-interface package still installs and defaults to VGA at 30 fps. GelSight Mini has its own GPL-3.0 SDK with live-view and marker-tracking demos.

    python
    # pip install digit-interface
    from digit_interface import Digit
    
    d = Digit("D12345")   # serial number printed on the sensor
    d.connect()
    frame = d.get_frame()  # numpy array, default VGA 640x480 @ 30fps
    d.show_view()
    d.disconnect()
  3. 3
    Decide the encoding before you record

    Image or state. If image, resize the tactile frame to match your cameras. If state, extend the robot's observation_features so the extra dimensions are named.

    python
    @property
    def observation_features(self) -> dict:
        return {**self._motors_ft, **self._cameras_ft,
                "tactile": (15,)}   # 5 magnetometers x 3 axes
    
    def get_observation(self) -> dict:
        obs = super().get_observation()
        obs["tactile"] = self.skin.get_sample()
        return obs
  4. 4
    Record with the sensor named like a camera

    The documented plugin invocation. Note the Xense plugin is not on PyPI, so it installs from a clone, and its config takes a sensor serial number and output types rather than width and height. Swap in whatever your own plugin registers.

    bash
    # the generic pattern from the LeRobot docs
    pip install lerobot_camera_<name>
    
    # the Xense plugin specifically: not on PyPI, install the SDK then the clone
    pip install "xensesdk>=2.0.0"
    git clone https://github.com/xensedyl/lerobot-camera-xense
    pip install -e lerobot-camera-xense
    
    lerobot-record \
      --robot.type=so101_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.cameras="{ front: {type: opencv, width: 640, height: 480, fps: 30},
                         tactile: {type: xense, serial_number: OG001319,
                                   output_types: [rectify], fps: 30} }" \
      --dataset.repo_id=${HF_USER}/so101-tactile \
      --dataset.num_episodes=50
  5. 5
    Read the feature table before spending GPU money

    The cheapest bug-catch in the pipeline. Heading for GR00T, convert a v3.0 dataset to v2.1 now.

    bash
    python -c "import json;d=json.load(open('meta/info.json'));\
    [print(k, v['dtype'], v['shape']) for k,v in d['features'].items()]"
Two hardware traps specific to the SO-100

Power. The SO-100 and SO-101 run Feetech STS3215 servos on 7.4 V. Feeding them 12 V destroys them, and a tactile sensor arriving with a 12 V supply is an easy way to make that mistake at the bench. Clearance. A DIGIT is a 20 x 27 x 18 mm block, a GelSight Mini 32 x 28.5 x 28 mm. Bolting either to a printed jaw changes gripper geometry, so your old calibration and every earlier dataset stop describing the same robot. A 2 mm skin is far less invasive.

The honest limit: action chunking throws most of the signal away

Suppose you solved all of that and the fine-tuning run converges. There is still a structural problem, and it is not the sensor. It is action chunking. These policies do not run a closed loop at sensor rate. They look once, emit a chunk of future actions, and execute that chunk open loop.

PolicyInference per action stepChunk defaultsOpen-loop window at 30 fps
ACT20 mschunkSize 100, nActionSteps 100 in the training form100 steps, about 3.3 s
SmolVLA245 msnot in the form; lerobot config defaults to 50 and 5050 steps, about 1.7 s
Pi0.5485 msnot in the form; lerobot config defaults to 50 and 5050 steps, about 1.7 s
GR00T N1.7152 msnot in the formset by the trained checkpoint
GR00T N1.5165 msnot in the formset by the trained checkpoint

Now compare the phenomenon you bought the sensor for. AnySkin streams at 100 Hz and ReSkin at roughly 400 Hz because slip is a tens-of-milliseconds event. At ACT's default nActionSteps of 100 the arm commits to about three and a third seconds of motion from one glance at the skin, and every tactile sample inside that window is recorded and ignored. Lowering nActionSteps below chunkSize buys more frequent re-planning at the cost of more inference calls. That is the knob that decides whether touch can close a loop at all.

Remote inference plus tactile is the worst combination

The inference latency figures above are model time only. Cloud inference adds public-internet round trips: survivable for slow pick-and-place, not for reactive contact control. Touch is exactly the modality whose value is reactive. If you want tactile feedback rather than context, the policy has to run next to the servos. That is physics, not a platform limitation.

Two ways to run the experiment

All of this is doable on your own hardware with open source. It is also about a week of yak shaving before the first useful gradient step.

  1. Buy the sensor: 128 USD for a ready-made AnySkin fingertip that fits an SO-100 or SO-101 jaw, 355 USD for a DIGIT with a 4 to 6 week lead time, or 15 USD of parts for a 9DTact if you cast your own silicone.
  2. Write the driver shim: package it as lerobot_camera_<name> for CLI auto-discovery, or subclass the robot and extend observation_features.
  3. Record 50 or more episodes with lerobot-record, keeping the tactile stream frame-locked to the cameras.
  4. Pick an encoding, verify meta/info.json, convert to v2.1 if you are targeting GR00T.
  5. Rent a GPU, install the trainer, fight CUDA. Isaac-GR00T's launch_finetune.py exposes no seed, so GR00T runs are not bit-for-bit reproducible.
  6. Run the ablation: train with and without the tactile dimensions on the same episodes, or you have learned nothing.
Budget for the archived stack

Meta's tactile ecosystem is frozen. digit-interface, digit-design, TACTO, PyTouch and Sparsh are all archived and read-only; digit-interface's last commit is 22 September 2021, though the package still installs from PyPI at version 0.2.1. They still work; nobody is fixing them for your Python version. 9DTact and AnySkin are live and MIT-licensed.

What the published work claims touch buys you

The research side is healthier than the tooling. The useful results cluster into four claims, and measured is worth separating from asserted.

  • Slip detection works and is cheap. AnySkin reports 92 percent accuracy on unseen objects with an LSTM over 100 Hz magnetic data, the one result reproducible on an SO-100 in an afternoon.
  • Tactile representations transfer. Sparsh pretrains on over 460,000 tactile images and reports 95.1 percent average improvement over task-specific end-to-end training on the six-task TacBench suite.
  • Sensor instances are interchangeable, which used to be the blocker. AnySkin shows a 13 percent drop when the physical skin is swapped, in 12 seconds.
  • Fusing touch into a VLA is a research direction, not a shipped feature. Tactile-VLA (July 2025) adds a hybrid position-force controller and a tactile reasoning module, arguing the vision-language backbone already holds semantic knowledge about physical interaction. A fork, not a dropdown.
The AY-Robots Arena leaderboard, a sortable table of 85 vision-language-action models with 332 benchmark results, each value linked to its paper or model card
The Arena tracks 85 VLA models and 332 benchmark results. Scan the observation spaces: vision, state and language across the board.

That is the honest summary as of August 2026. Browse the Arena and the pattern holds: the models that got scaled, released and benchmarked are vision-language-action models. Tactile-augmented variants publish encouraging numbers; none is a checkpoint you can fine-tune this afternoon. Compare how fast VLAs themselves went from paper to downloadable weights.

The tactile signal you already own

Before spending anything, there is a crude force proxy already on the arm. The STS3215 exposes a read-only Present_Load register and lerobot's Feetech table has the address. It is not tactile sensing: it is whole-joint torque with the gripper's own friction folded in, and it says nothing about where contact happened. But it costs nothing and tells you whether the gripper is pressing on something.

python
# from lerobot/src/lerobot/motors/feetech/tables.py (STS/SMS series)
#   "Present_Load":    (60, 2)   # address 60, 2 bytes, read-only
#   "Present_Current": (69, 2)   # address 69, 2 bytes, read-only
#   sign-magnitude encoding: Present_Load uses bit 10 as the sign bit
#   sts3215 model number: 777

load = bus.sync_read("Present_Load")      # motor name -> signed value
grip = load["gripper"]
contact = abs(grip) > threshold           # calibrate the threshold per arm
Zero-cost contact detection: coarse and arm-specific, still better than guessing from pixels.
Why this is worth trying first

Present_Load is one scalar, so appending it to observation.state takes a 6-dimensional vector to 7 and cannot break any dimension budget. If a one-number contact signal does not help, a 15-dimensional skin probably will not either, and you learned that for free. See the dataset docs and our guide to collecting high-quality VLA training data.

What we would actually do

Start with Present_Load and an ablation. If that moves the needle, buy a 128 USD AnySkin fingertip rather than a 355 USD DIGIT: a 2 mm skin does not change gripper geometry or force a re-calibration of the arm. Record 50 episodes with and without, then train ACT on the SO-100 both ways. ACT is the right first probe: cheapest run here, trained from scratch so no pretrained prior fights your new input dimensions, and at 20 ms per action step the only one of the five fast enough for a reactive loop to mean anything.

Only then consider an optical sensor, and only if the task needs surface geometry rather than contact and slip. With a 100-step chunk you are giving the policy tactile context when it plans, not tactile feedback while it moves. If the arm is not built yet, the SO-100 setup guide and the recording tutorial come first, and /live lets you drive a real arm with no signup before you buy anything.

Compare the five policies before you add a sensor

Parameters, GPU tier, inference latency and minimum episodes for GR00T N1.7, GR00T N1.5, Pi0.5, SmolVLA and ACT. The observation space each one accepts decides where a tactile stream can go.

See the comparison
Can I train GR00T N1.7 or Pi0.5 with a tactile sensor?

Only by disguising the signal. GR00T's meta/modality.json takes arbitrary keys under "video" and index ranges under "state". For Pi0.5 it depends on the implementation: openpi has three fixed image slots, while lerobot's pi05 derives image keys from the dataset. Extra state dimensions are the safer route either way, and GR00T needs LeRobot v2.0 or v2.1, so a v3.0 dataset must be converted down first.

GelSight Mini or DIGIT for an SO-100?

Neither, for most SO-100 tasks. A GelSight Mini is 32 x 28.5 x 28 mm, about 25 cubic centimetres of rigid block on a printed jaw; DIGIT is 20 x 27 x 18 mm by its own paper, nearer 10, though the 9DTact comparison table measures it at 36 x 26 x 33. Either way the jaw geometry changes, which invalidates your calibration and makes older datasets describe a different robot. DIGIT is smaller, faster (60 fps against 25) and cheaper today (355 USD against 510). But on an arm costing 110 to 150 EUR in parts, a 2 mm magnetic skin is the proportionate choice.

How many extra dimensions can I add to observation.state?

SmolVLA and Pi0.5 in lerobot both set max_state_dim to 32, so a 6 DoF arm leaves 26 spare and a five-magnetometer AnySkin uses 15. ACT has no such cap. Add the dimensions in the dataset before training, because the checkpoint's config.json freezes the input shape.

Will tactile data make my policy generalise better?

There is no published evidence for that on SO-100 or SO-101 hardware, so do not assume it. The strongest tactile results are narrow perception results: slip detection at 92 percent on unseen objects, force regression above 0.9 R-squared. Run the ablation on your own task and believe your own numbers.

Does the tactile sensor need the same frame rate as my cameras?

It has to be resampled to the dataset fps, which in the public AnySkin SO-101 datasets is 30. That is a downsample for a 400 Hz magnetic skin and an upsample for a 25 fps GelSight Mini. Upsampling is the worse case: a repeated frame gets presented as a new observation, and duplicated rows are a known way to teach a policy that nothing is changing.

Cheapest way to find out whether touch matters for my task?

Read Present_Load off the gripper servo, append it to observation.state, and train ACT twice on the same episodes. About 1 to 3 USD per run on a 24 GB card, no new hardware. If a scalar contact signal does nothing, a fifteen-dimensional one probably will not either.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started