Skip to content

Commit 134935f

Browse files
committed
add initial scripts for haptic coin and tactile fsr sensor
1 parent 680c1f3 commit 134935f

8 files changed

Lines changed: 1223 additions & 0 deletions

File tree

Demo/Sensors/README.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# AmazingHand — Sensors
2+
3+
## Files
4+
5+
| File | Description |
6+
|---|---|
7+
| `config.toml` | Shared config — GPIO pins, SPI settings, sensor channels, logging and visualisation defaults |
8+
| `ads1256.py` | ADS1256 driver — 24-bit, 8-channel |
9+
| `tactile_sensing.py` | `TactileSensor` wrapper — reads config.toml and drives the ADS1256 |
10+
| `tactile_sensing_post_visualize.py` | Offline Bokeh visualisation of logged CSV data |
11+
| `haptic_coin.py` | `HapticCoin` PWM class for the QYF-740 coin motor |
12+
13+
---
14+
15+
## FSR Tactile Sensing (ADS1256)
16+
17+
### Hardware
18+
19+
- **ADC HAT**: [Waveshare High-Precision AD HAT](https://www.waveshare.com/wiki/High-Precision_AD_HAT)
20+
- ADS1256: 24-bit, 8 single-ended channels, SPI
21+
- **Sensors**: FSR (Force Sensitive Resistors) wired in a voltage-divider circuit with a fixed resistor (default 10 kΩ) to each ADC input channel.
22+
23+
### SPI Wiring (BCM pin numbers, set in `config.toml`)
24+
25+
| Signal | BCM | Pi header pin |
26+
|---|---|---|
27+
| MOSI (DIN) | GPIO 10 | Pin 19 |
28+
| MISO (DOUT) | GPIO 9 | Pin 21 |
29+
| SCLK | GPIO 11 | Pin 23 |
30+
| CS | GPIO 22 | Pin 15 |
31+
| RST | GPIO 18 | Pin 12 |
32+
| DRDY | GPIO 17 | Pin 11 |
33+
34+
Enable SPI on the Pi:
35+
```bash
36+
sudo raspi-config # Interface Options → SPI → Enable
37+
```
38+
39+
### Install dependencies
40+
41+
```bash
42+
pixi install
43+
```
44+
45+
To add a single package interactively:
46+
47+
```bash
48+
pixi add --pypi lgpio
49+
```
50+
51+
### Run
52+
53+
**GUI (default):**
54+
```bash
55+
pixi run python -m Demo.Sensors.tactile_sensing
56+
```
57+
58+
**Terminal only (no GUI window):**
59+
```bash
60+
pixi run python -m Demo.Sensors.tactile_sensing --terminal
61+
```
62+
63+
See all flags:
64+
```bash
65+
pixi run python -m Demo.Sensors.tactile_sensing --help
66+
```
67+
68+
### Offline Visualization
69+
70+
```bash
71+
pixi run python Demo/Sensors/tactile_sensing_post_visualize.py
72+
# or point at a specific file:
73+
pixi run python Demo/Sensors/tactile_sensing_post_visualize.py --file Demo/Sensors/logs/tactile_20260101_120000.csv
74+
```
75+
76+
CSV schema (long format — one row per channel per sample):
77+
78+
```
79+
sensor_time, channel, raw, volts, force_norm
80+
```
81+
82+
### Tuning `fsr_r_fixed`
83+
84+
The `fsr_r_fixed` parameter (default 10 000 Ω) must match the resistor you place in series with each FSR to form the voltage divider. Larger values increase sensitivity at low force; smaller values increase the measurable force range.
85+
86+
---
87+
88+
## Haptic Coin (QYF-740)
89+
90+
### Pin conflict warning
91+
92+
The ADS1256 tactile sensing stack claims the following BCM GPIO pins. **Do not assign the HapticCoin to any of these:**
93+
94+
| BCM | Use |
95+
|---|---|
96+
| GPIO 8 | SPI0 CE0 (kernel) |
97+
| GPIO 9 | SPI0 MISO |
98+
| GPIO 10 | SPI0 MOSI |
99+
| GPIO 11 | SPI0 SCLK |
100+
| GPIO 17 | ADS1256 DRDY |
101+
| GPIO 18 | ADS1256 RST |
102+
| GPIO 22 | ADS1256 CS |
103+
| GPIO 23 | ADS1256 CS_DAC |
104+
105+
Safe choices for the PWM output include GPIO 12, 13 (hardware PWM), 24, 25, 26, 27, or 35.
106+
107+
### Wiring — [RPi 5 pinout](https://vilros.com/pages/raspberry-pi-5-pinout)
108+
109+
![Raspberry Pi 5 GPIO Pinout](images/rpi5_pinout.jpg)
110+
111+
| Motor wire | Pi pin |
112+
|---|---|
113+
| GND | Pin 6 |
114+
| VCC | Pin 2 (5 V) |
115+
| PWM | Pin 35 (GPIO 19) |
116+
117+
### Usage
118+
119+
```python
120+
from Demo.Sensors.haptic_coin import HapticCoin
121+
122+
motor = HapticCoin(gpio_pin=19)
123+
motor.vibrate_once(intensity=0.8, duration_s=0.5)
124+
motor.cleanup()
125+
```
126+
127+
Run the example test script as a module so relative imports resolve:
128+
129+
```bash
130+
pixi run python -m Demo.Sensors.haptic_test
131+
```

Demo/Sensors/ads1256.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
"""
2+
ADS1256 driver — 24-bit, 8-channel ADC (lgpio + spidev, no RPi.GPIO).
3+
4+
Hardware pin and SPI settings are read from config.toml in the same directory.
5+
"""
6+
7+
import time
8+
from pathlib import Path
9+
10+
import lgpio
11+
import spidev
12+
13+
# ---------------------------------------------------------------------------
14+
# Load hardware settings from config.toml
15+
# ---------------------------------------------------------------------------
16+
import tomllib
17+
18+
_CONFIG_PATH = Path(__file__).parent / "config.toml"
19+
with _CONFIG_PATH.open("rb") as _f:
20+
_hw = tomllib.load(_f)["hardware"]
21+
22+
_RST_PIN : int = _hw["rst_pin"]
23+
_CS_PIN : int = _hw["cs_pin"]
24+
_CS_DAC_PIN : int = _hw["cs_dac_pin"]
25+
_DRDY_PIN : int = _hw["drdy_pin"]
26+
_SPI_MAX_SPEED_HZ : int = _hw["spi_max_speed_hz"]
27+
_SPI_MODE : int = _hw["spi_mode"]
28+
29+
# ---------------------------------------------------------------------------
30+
# Hardware handles
31+
# ---------------------------------------------------------------------------
32+
_SPI = spidev.SpiDev(0, 0)
33+
_h: lgpio.lgpio | None = None
34+
35+
36+
# ---------------------------------------------------------------------------
37+
# Low-level GPIO / SPI helpers
38+
# ---------------------------------------------------------------------------
39+
40+
def _gpio_write(pin: int, value: int) -> None:
41+
lgpio.gpio_write(_h, pin, value)
42+
43+
44+
def _gpio_read(pin: int) -> int:
45+
return lgpio.gpio_read(_h, pin)
46+
47+
48+
def _delay_ms(ms: float) -> None:
49+
time.sleep(ms / 1000.0)
50+
51+
52+
def _spi_write(data: list[int]) -> None:
53+
_SPI.writebytes(data)
54+
55+
56+
def _spi_read(n: int) -> list[int]:
57+
return _SPI.readbytes(n)
58+
59+
60+
# ---------------------------------------------------------------------------
61+
# Hardware lifecycle
62+
# ---------------------------------------------------------------------------
63+
64+
def _hw_init() -> None:
65+
global _h
66+
_h = lgpio.gpiochip_open(0)
67+
lgpio.gpio_claim_output(_h, _RST_PIN)
68+
lgpio.gpio_claim_output(_h, _CS_DAC_PIN)
69+
lgpio.gpio_claim_output(_h, _CS_PIN)
70+
lgpio.gpio_claim_input(_h, _DRDY_PIN)
71+
_SPI.max_speed_hz = _SPI_MAX_SPEED_HZ
72+
_SPI.mode = _SPI_MODE
73+
74+
75+
def _hw_exit() -> None:
76+
global _h
77+
_SPI.close()
78+
if _h is not None:
79+
lgpio.gpiochip_close(_h)
80+
_h = None
81+
82+
83+
# ---------------------------------------------------------------------------
84+
# ADS1256 register / command constants
85+
# ---------------------------------------------------------------------------
86+
87+
GAIN = {
88+
"ADS1256_GAIN_1": 0,
89+
"ADS1256_GAIN_2": 1,
90+
"ADS1256_GAIN_4": 2,
91+
"ADS1256_GAIN_8": 3,
92+
"ADS1256_GAIN_16": 4,
93+
"ADS1256_GAIN_32": 5,
94+
"ADS1256_GAIN_64": 6,
95+
}
96+
97+
DRATE = {
98+
"ADS1256_30000SPS": 0xF0,
99+
"ADS1256_15000SPS": 0xE0,
100+
"ADS1256_7500SPS": 0xD0,
101+
"ADS1256_3750SPS": 0xC0,
102+
"ADS1256_2000SPS": 0xB0,
103+
"ADS1256_1000SPS": 0xA1,
104+
"ADS1256_500SPS": 0x92,
105+
"ADS1256_100SPS": 0x82,
106+
"ADS1256_60SPS": 0x72,
107+
"ADS1256_50SPS": 0x63,
108+
"ADS1256_30SPS": 0x53,
109+
"ADS1256_25SPS": 0x43,
110+
"ADS1256_15SPS": 0x33,
111+
"ADS1256_10SPS": 0x20,
112+
"ADS1256_5SPS": 0x13,
113+
"ADS1256_2d5SPS": 0x03,
114+
}
115+
116+
REG = {
117+
"REG_STATUS": 0,
118+
"REG_MUX": 1,
119+
"REG_ADCON": 2,
120+
"REG_DRATE": 3,
121+
"REG_IO": 4,
122+
"REG_OFC0": 5,
123+
"REG_OFC1": 6,
124+
"REG_OFC2": 7,
125+
"REG_FSC0": 8,
126+
"REG_FSC1": 9,
127+
"REG_FSC2": 10,
128+
}
129+
130+
CMD = {
131+
"CMD_WAKEUP": 0x00,
132+
"CMD_RDATA": 0x01,
133+
"CMD_RDATAC": 0x03,
134+
"CMD_SDATAC": 0x0F,
135+
"CMD_RREG": 0x10,
136+
"CMD_WREG": 0x50,
137+
"CMD_SELFCAL": 0xF0,
138+
"CMD_SELFOCAL": 0xF1,
139+
"CMD_SELFGCAL": 0xF2,
140+
"CMD_SYSOCAL": 0xF3,
141+
"CMD_SYSGCAL": 0xF4,
142+
"CMD_SYNC": 0xFC,
143+
"CMD_STANDBY": 0xFD,
144+
"CMD_RESET": 0xFE,
145+
}
146+
147+
_STATUS_DRDY = 0x04
148+
ADC_MAX = 0x7FFFFF # 24-bit signed positive full-scale
149+
150+
# ADS1256 internal Vref = 2.5 V; with PGA=1 full-scale input = ±2×Vref = ±5 V
151+
REF_VOLTAGE = 2.5
152+
153+
154+
# ---------------------------------------------------------------------------
155+
# Driver class
156+
# ---------------------------------------------------------------------------
157+
158+
class ADS1256:
159+
"""Low-level ADS1256 driver."""
160+
161+
def __init__(self):
162+
self.rst_pin = _RST_PIN
163+
self.cs_pin = _CS_PIN
164+
self.drdy_pin = _DRDY_PIN
165+
166+
# --- SPI / GPIO primitives ---
167+
168+
def _write_cmd(self, cmd: int):
169+
_gpio_write(self.cs_pin, 0)
170+
_spi_write([cmd])
171+
_gpio_write(self.cs_pin, 1)
172+
173+
def _write_reg(self, reg: int, data: int):
174+
_gpio_write(self.cs_pin, 0)
175+
_spi_write([CMD["CMD_WREG"] | reg, 0x00, data])
176+
_gpio_write(self.cs_pin, 1)
177+
178+
def _read_reg(self, reg: int) -> int:
179+
_gpio_write(self.cs_pin, 0)
180+
_spi_write([CMD["CMD_RREG"] | reg, 0x00])
181+
data = _spi_read(1)
182+
_gpio_write(self.cs_pin, 1)
183+
return data[0]
184+
185+
def _wait_drdy(self, timeout: int = 400_000):
186+
for _ in range(timeout):
187+
if _gpio_read(self.drdy_pin) == 0:
188+
return
189+
raise TimeoutError("ADS1256 DRDY timeout — check wiring / power")
190+
191+
# --- Init helpers ---
192+
193+
def _reset(self):
194+
_gpio_write(self.rst_pin, 1)
195+
_delay_ms(200)
196+
_gpio_write(self.rst_pin, 0)
197+
_delay_ms(200)
198+
_gpio_write(self.rst_pin, 1)
199+
200+
def _read_chip_id(self) -> int:
201+
self._wait_drdy()
202+
return self._read_reg(REG["REG_STATUS"]) >> 4
203+
204+
def _config_adc(self, gain: int, drate: int):
205+
self._wait_drdy()
206+
buf = [
207+
(0 << 3) | _STATUS_DRDY | (0 << 1), # STATUS
208+
0x08, # MUX default
209+
(0 << 5) | (0 << 3) | gain, # ADCON
210+
drate, # DRATE
211+
]
212+
_gpio_write(self.cs_pin, 0)
213+
_spi_write([CMD["CMD_WREG"] | 0, 0x03])
214+
_spi_write(buf)
215+
_gpio_write(self.cs_pin, 1)
216+
_delay_ms(1)
217+
218+
def _set_channel(self, channel: int):
219+
if channel > 7:
220+
raise ValueError(f"ADS1256 single-ended channel must be 0–7, got {channel}")
221+
self._write_reg(REG["REG_MUX"], (channel << 4) | 0x08)
222+
223+
def _set_diff_channel(self, channel: int):
224+
pairs = [(0, 1), (2, 3), (4, 5), (6, 7)]
225+
if channel >= len(pairs):
226+
raise ValueError(f"ADS1256 differential channel must be 0–3, got {channel}")
227+
pos, neg = pairs[channel]
228+
self._write_reg(REG["REG_MUX"], (pos << 4) | neg)
229+
230+
def _read_adc_data(self) -> int:
231+
self._wait_drdy()
232+
_gpio_write(self.cs_pin, 0)
233+
_spi_write([CMD["CMD_RDATA"]])
234+
buf = _spi_read(3)
235+
_gpio_write(self.cs_pin, 1)
236+
raw = ((buf[0] << 16) & 0xFF0000) | ((buf[1] << 8) & 0xFF00) | (buf[2] & 0xFF)
237+
if raw & 0x800000: # two's-complement sign extension
238+
raw -= 0x1000000
239+
return raw
240+
241+
# --- Public API ---
242+
243+
def init(self, gain_key: str = "ADS1256_GAIN_1",
244+
drate_key: str = "ADS1256_30000SPS") -> None:
245+
_hw_init()
246+
self._reset()
247+
chip_id = self._read_chip_id()
248+
if chip_id != 3:
249+
raise RuntimeError(f"ADS1256 chip ID mismatch: got {chip_id}, expected 3")
250+
self._config_adc(GAIN[gain_key], DRATE[drate_key])
251+
252+
def get_channel_value(self, channel: int, diff: bool = False) -> int:
253+
"""Return signed 24-bit raw count for the given channel."""
254+
if diff:
255+
self._set_diff_channel(channel)
256+
else:
257+
self._set_channel(channel)
258+
self._write_cmd(CMD["CMD_SYNC"])
259+
self._write_cmd(CMD["CMD_WAKEUP"])
260+
return self._read_adc_data()
261+
262+
def exit(self) -> None:
263+
_hw_exit()

0 commit comments

Comments
 (0)