flexitac 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flexitac-0.2.0/LICENSE +21 -0
- flexitac-0.2.0/PKG-INFO +164 -0
- flexitac-0.2.0/README.md +140 -0
- flexitac-0.2.0/examples/__init__.py +1 -0
- flexitac-0.2.0/examples/stream_frames.py +47 -0
- flexitac-0.2.0/examples/visualize_heatmap.py +61 -0
- flexitac-0.2.0/flexitac/__init__.py +6 -0
- flexitac-0.2.0/flexitac/find_port.py +55 -0
- flexitac-0.2.0/flexitac/firmware/__init__.py +1 -0
- flexitac-0.2.0/flexitac/firmware/template.ino +116 -0
- flexitac-0.2.0/flexitac/flash.py +160 -0
- flexitac-0.2.0/flexitac/logging_utils.py +20 -0
- flexitac-0.2.0/flexitac/py.typed +0 -0
- flexitac-0.2.0/flexitac/sensor.py +175 -0
- flexitac-0.2.0/flexitac.egg-info/PKG-INFO +164 -0
- flexitac-0.2.0/flexitac.egg-info/SOURCES.txt +23 -0
- flexitac-0.2.0/flexitac.egg-info/dependency_links.txt +1 -0
- flexitac-0.2.0/flexitac.egg-info/entry_points.txt +5 -0
- flexitac-0.2.0/flexitac.egg-info/requires.txt +15 -0
- flexitac-0.2.0/flexitac.egg-info/top_level.txt +2 -0
- flexitac-0.2.0/pyproject.toml +130 -0
- flexitac-0.2.0/setup.cfg +4 -0
- flexitac-0.2.0/tests/test_find_port.py +33 -0
- flexitac-0.2.0/tests/test_flash.py +55 -0
- flexitac-0.2.0/tests/test_sensor.py +76 -0
flexitac-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wesley Maa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flexitac-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flexitac
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python runtime and flashing tools for FlexiTac tactile sensors
|
|
5
|
+
Author-email: Wesley Maa <wesley.maa@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/WT-MM/PyFlexiTac.git
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pyserial
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: ruff; extra == "dev"
|
|
15
|
+
Requires-Dist: mypy; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest; extra == "dev"
|
|
17
|
+
Requires-Dist: matplotlib-stubs; extra == "dev"
|
|
18
|
+
Provides-Extra: examples
|
|
19
|
+
Requires-Dist: matplotlib; extra == "examples"
|
|
20
|
+
Provides-Extra: all
|
|
21
|
+
Requires-Dist: flexitac[dev]; extra == "all"
|
|
22
|
+
Requires-Dist: flexitac[examples]; extra == "all"
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# flexitac
|
|
26
|
+
|
|
27
|
+
Python interface for [FlexiTac](https://flexitac.github.io/) tactile sensors:
|
|
28
|
+
flash Arduino firmware, then read framed sensor data over serial.
|
|
29
|
+
|
|
30
|
+
Defaults target the standard FlexiTac 12x32 sensor (12 rows wired to mux
|
|
31
|
+
channels 4-15). Override `--rows`, `--cols`, and `--mux-offset` for variants.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# recommended
|
|
37
|
+
uv sync # core deps only
|
|
38
|
+
uv sync --extra examples # include matplotlib for the heatmap CLI
|
|
39
|
+
uv sync --extra dev # dev tooling (ruff, mypy, pytest)
|
|
40
|
+
|
|
41
|
+
# or with pip
|
|
42
|
+
pip install flexitac
|
|
43
|
+
pip install 'flexitac[examples]' # include matplotlib
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Reading frames
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from flexitac import FlexiTacSensor
|
|
50
|
+
|
|
51
|
+
with FlexiTacSensor("/dev/ttyUSB0") as sensor: # rows=12, cols=32 by default
|
|
52
|
+
for frame in sensor:
|
|
53
|
+
print(frame.normalized.shape, frame.normalized.max())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`sensor.read()` returns a `FlexiTacFrame(seq, timestamp_s, raw, normalized)`.
|
|
57
|
+
The first read auto-calibrates by collecting `init_frames` (default 30) and
|
|
58
|
+
storing the per-pixel median as the baseline. Call `sensor.calibrate()` to
|
|
59
|
+
recalibrate.
|
|
60
|
+
|
|
61
|
+
## CLI tools
|
|
62
|
+
|
|
63
|
+
All CLI commands are installed as entry points:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
flexitac-stream --port /dev/ttyUSB0 # stream frames & print stats
|
|
67
|
+
flexitac-heatmap --port /dev/ttyUSB0 # live matplotlib heatmap
|
|
68
|
+
flexitac-flash # flash firmware (auto-detects board)
|
|
69
|
+
flexitac-find-port # identify sensor port by unplug
|
|
70
|
+
|
|
71
|
+
# with uv (no install needed)
|
|
72
|
+
uv run flexitac-stream --port /dev/ttyUSB0
|
|
73
|
+
uv run flexitac-heatmap --port /dev/ttyUSB0
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `flexitac-stream`
|
|
77
|
+
|
|
78
|
+
Stream frames and print FPS / signal stats:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
flexitac-stream --port /dev/ttyUSB0 --frames 30
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
frame= 10 fps= 85.3 raw_max=104 norm_max=0.000
|
|
86
|
+
frame= 20 fps= 87.1 raw_max=109 norm_max=0.123
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
What to check:
|
|
90
|
+
|
|
91
|
+
- **`fps`** stabilizes near your expected rate (~100+ fps for a 12x32 sensor
|
|
92
|
+
at 2 Mbps). If it's 0 or you get `TimeoutError`, the firmware isn't sending
|
|
93
|
+
framed data -- confirm `--rows`/`--cols`/`--baud` match what you flashed.
|
|
94
|
+
- **`raw_max`** is in `[0, 255]` and changes when you press the sensor.
|
|
95
|
+
A flat 0 or flat 255 indicates a wiring issue, not a flashing issue.
|
|
96
|
+
- **`norm_max`** rises toward 1.0 under contact and stays near 0 at rest.
|
|
97
|
+
|
|
98
|
+
### `flexitac-heatmap`
|
|
99
|
+
|
|
100
|
+
Live heatmap visualization (requires `matplotlib` -- install with the `examples` extra):
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
flexitac-heatmap --port /dev/ttyUSB0
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Press the sensor pad -- bright spots should track your touch.
|
|
107
|
+
|
|
108
|
+
### `flexitac-find-port`
|
|
109
|
+
|
|
110
|
+
Not sure which `/dev/tty*` your sensor is on? Run:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
flexitac-find-port
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
It snapshots ports, asks you to unplug the sensor, then reports whichever port
|
|
117
|
+
disappeared.
|
|
118
|
+
|
|
119
|
+
### `flexitac-flash`
|
|
120
|
+
|
|
121
|
+
Requires [`arduino-cli`](https://arduino.github.io/arduino-cli/latest/installation/).
|
|
122
|
+
First-time setup:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
brew install arduino-cli
|
|
126
|
+
# or use the upstream install script:
|
|
127
|
+
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
|
128
|
+
|
|
129
|
+
# then install the Arduino AVR core
|
|
130
|
+
arduino-cli core update-index
|
|
131
|
+
arduino-cli core install arduino:avr # AVR core for Uno/Nano/Mega/etc.
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# auto-detects the port + FQBN if exactly one Arduino is plugged in
|
|
136
|
+
flexitac-flash
|
|
137
|
+
|
|
138
|
+
# override geometry / wiring for non-standard sensors
|
|
139
|
+
flexitac-flash --rows 16 --cols 32 --mux-offset 0
|
|
140
|
+
|
|
141
|
+
# if unable to autodetect, pass --port and --fqbn explicitly:
|
|
142
|
+
flexitac-flash --port /dev/ttyUSB0 --fqbn arduino:avr:uno
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Defaults: `rows=12`, `cols=32`, `baud=2000000`, `mux-offset=4` (standard
|
|
146
|
+
FlexiTac 12x32 sensor wired to mux channels 4-15). The firmware is generated
|
|
147
|
+
from `flexitac/firmware/template.ino` by substituting `ROW_COUNT`,
|
|
148
|
+
`COLUMN_COUNT`, `BAUD_RATE`, and `MUX_CHANNEL_OFFSET`. To customize pin
|
|
149
|
+
assignments, edit the template directly.
|
|
150
|
+
|
|
151
|
+
## Wire protocol
|
|
152
|
+
|
|
153
|
+
Each frame: marker `0xAA 0x55` followed by `rows * cols` uint8 ADC samples,
|
|
154
|
+
streamed continuously at the configured baud rate.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv sync --extra dev
|
|
160
|
+
|
|
161
|
+
make format # ruff format + autofix
|
|
162
|
+
make static-checks # ruff + mypy
|
|
163
|
+
make test # pytest
|
|
164
|
+
```
|
flexitac-0.2.0/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# flexitac
|
|
2
|
+
|
|
3
|
+
Python interface for [FlexiTac](https://flexitac.github.io/) tactile sensors:
|
|
4
|
+
flash Arduino firmware, then read framed sensor data over serial.
|
|
5
|
+
|
|
6
|
+
Defaults target the standard FlexiTac 12x32 sensor (12 rows wired to mux
|
|
7
|
+
channels 4-15). Override `--rows`, `--cols`, and `--mux-offset` for variants.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# recommended
|
|
13
|
+
uv sync # core deps only
|
|
14
|
+
uv sync --extra examples # include matplotlib for the heatmap CLI
|
|
15
|
+
uv sync --extra dev # dev tooling (ruff, mypy, pytest)
|
|
16
|
+
|
|
17
|
+
# or with pip
|
|
18
|
+
pip install flexitac
|
|
19
|
+
pip install 'flexitac[examples]' # include matplotlib
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Reading frames
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from flexitac import FlexiTacSensor
|
|
26
|
+
|
|
27
|
+
with FlexiTacSensor("/dev/ttyUSB0") as sensor: # rows=12, cols=32 by default
|
|
28
|
+
for frame in sensor:
|
|
29
|
+
print(frame.normalized.shape, frame.normalized.max())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`sensor.read()` returns a `FlexiTacFrame(seq, timestamp_s, raw, normalized)`.
|
|
33
|
+
The first read auto-calibrates by collecting `init_frames` (default 30) and
|
|
34
|
+
storing the per-pixel median as the baseline. Call `sensor.calibrate()` to
|
|
35
|
+
recalibrate.
|
|
36
|
+
|
|
37
|
+
## CLI tools
|
|
38
|
+
|
|
39
|
+
All CLI commands are installed as entry points:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
flexitac-stream --port /dev/ttyUSB0 # stream frames & print stats
|
|
43
|
+
flexitac-heatmap --port /dev/ttyUSB0 # live matplotlib heatmap
|
|
44
|
+
flexitac-flash # flash firmware (auto-detects board)
|
|
45
|
+
flexitac-find-port # identify sensor port by unplug
|
|
46
|
+
|
|
47
|
+
# with uv (no install needed)
|
|
48
|
+
uv run flexitac-stream --port /dev/ttyUSB0
|
|
49
|
+
uv run flexitac-heatmap --port /dev/ttyUSB0
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `flexitac-stream`
|
|
53
|
+
|
|
54
|
+
Stream frames and print FPS / signal stats:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
flexitac-stream --port /dev/ttyUSB0 --frames 30
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
frame= 10 fps= 85.3 raw_max=104 norm_max=0.000
|
|
62
|
+
frame= 20 fps= 87.1 raw_max=109 norm_max=0.123
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
What to check:
|
|
66
|
+
|
|
67
|
+
- **`fps`** stabilizes near your expected rate (~100+ fps for a 12x32 sensor
|
|
68
|
+
at 2 Mbps). If it's 0 or you get `TimeoutError`, the firmware isn't sending
|
|
69
|
+
framed data -- confirm `--rows`/`--cols`/`--baud` match what you flashed.
|
|
70
|
+
- **`raw_max`** is in `[0, 255]` and changes when you press the sensor.
|
|
71
|
+
A flat 0 or flat 255 indicates a wiring issue, not a flashing issue.
|
|
72
|
+
- **`norm_max`** rises toward 1.0 under contact and stays near 0 at rest.
|
|
73
|
+
|
|
74
|
+
### `flexitac-heatmap`
|
|
75
|
+
|
|
76
|
+
Live heatmap visualization (requires `matplotlib` -- install with the `examples` extra):
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
flexitac-heatmap --port /dev/ttyUSB0
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Press the sensor pad -- bright spots should track your touch.
|
|
83
|
+
|
|
84
|
+
### `flexitac-find-port`
|
|
85
|
+
|
|
86
|
+
Not sure which `/dev/tty*` your sensor is on? Run:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
flexitac-find-port
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
It snapshots ports, asks you to unplug the sensor, then reports whichever port
|
|
93
|
+
disappeared.
|
|
94
|
+
|
|
95
|
+
### `flexitac-flash`
|
|
96
|
+
|
|
97
|
+
Requires [`arduino-cli`](https://arduino.github.io/arduino-cli/latest/installation/).
|
|
98
|
+
First-time setup:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
brew install arduino-cli
|
|
102
|
+
# or use the upstream install script:
|
|
103
|
+
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
|
104
|
+
|
|
105
|
+
# then install the Arduino AVR core
|
|
106
|
+
arduino-cli core update-index
|
|
107
|
+
arduino-cli core install arduino:avr # AVR core for Uno/Nano/Mega/etc.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# auto-detects the port + FQBN if exactly one Arduino is plugged in
|
|
112
|
+
flexitac-flash
|
|
113
|
+
|
|
114
|
+
# override geometry / wiring for non-standard sensors
|
|
115
|
+
flexitac-flash --rows 16 --cols 32 --mux-offset 0
|
|
116
|
+
|
|
117
|
+
# if unable to autodetect, pass --port and --fqbn explicitly:
|
|
118
|
+
flexitac-flash --port /dev/ttyUSB0 --fqbn arduino:avr:uno
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Defaults: `rows=12`, `cols=32`, `baud=2000000`, `mux-offset=4` (standard
|
|
122
|
+
FlexiTac 12x32 sensor wired to mux channels 4-15). The firmware is generated
|
|
123
|
+
from `flexitac/firmware/template.ino` by substituting `ROW_COUNT`,
|
|
124
|
+
`COLUMN_COUNT`, `BAUD_RATE`, and `MUX_CHANNEL_OFFSET`. To customize pin
|
|
125
|
+
assignments, edit the template directly.
|
|
126
|
+
|
|
127
|
+
## Wire protocol
|
|
128
|
+
|
|
129
|
+
Each frame: marker `0xAA 0x55` followed by `rows * cols` uint8 ADC samples,
|
|
130
|
+
streamed continuously at the configured baud rate.
|
|
131
|
+
|
|
132
|
+
## Development
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
uv sync --extra dev
|
|
136
|
+
|
|
137
|
+
make format # ruff format + autofix
|
|
138
|
+
make static-checks # ruff + mypy
|
|
139
|
+
make test # pytest
|
|
140
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Example scripts for FlexiTac sensors, installable as CLI entry points."""
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Stream FlexiTac frames and print FPS / signal stats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from flexitac import FlexiTacSensor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(description="Stream FlexiTac frames.")
|
|
13
|
+
parser.add_argument("--port", required=True)
|
|
14
|
+
parser.add_argument("--rows", type=int, default=12)
|
|
15
|
+
parser.add_argument("--cols", type=int, default=32)
|
|
16
|
+
parser.add_argument("--baud", type=int, default=2_000_000)
|
|
17
|
+
parser.add_argument("--frames", type=int, default=0, help="0 = run forever")
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--per-row",
|
|
20
|
+
action="store_true",
|
|
21
|
+
help="Print per-row raw mean / baseline (useful when some rows look dead)",
|
|
22
|
+
)
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
|
|
25
|
+
started = time.monotonic()
|
|
26
|
+
with FlexiTacSensor(args.port, rows=args.rows, cols=args.cols, baud=args.baud) as sensor:
|
|
27
|
+
sensor.calibrate()
|
|
28
|
+
if args.per_row:
|
|
29
|
+
assert sensor.baseline is not None
|
|
30
|
+
print("baseline per row:", [round(float(v), 1) for v in sensor.baseline.mean(axis=1)])
|
|
31
|
+
|
|
32
|
+
for i, frame in enumerate(sensor, start=1):
|
|
33
|
+
fps = i / max(time.monotonic() - started, 1e-6)
|
|
34
|
+
print(
|
|
35
|
+
f"frame={i:6d} fps={fps:6.1f} raw_max={int(frame.raw.max()):3d} "
|
|
36
|
+
f"norm_max={float(frame.normalized.max()):.3f}"
|
|
37
|
+
)
|
|
38
|
+
if args.per_row and i % 20 == 0:
|
|
39
|
+
row_max = frame.raw.max(axis=1)
|
|
40
|
+
print(" raw_max per row:", [int(v) for v in row_max])
|
|
41
|
+
if args.frames and i >= args.frames:
|
|
42
|
+
break
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Live heatmap of FlexiTac normalized frames using matplotlib."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import matplotlib
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from flexitac import FlexiTacSensor
|
|
12
|
+
|
|
13
|
+
for _backend in ("TkAgg", "Qt5Agg", "MacOSX"):
|
|
14
|
+
try:
|
|
15
|
+
matplotlib.use(_backend)
|
|
16
|
+
break
|
|
17
|
+
except Exception: # noqa: BLE001
|
|
18
|
+
continue
|
|
19
|
+
|
|
20
|
+
from matplotlib import animation, pyplot as plt # noqa: E402
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main() -> int:
|
|
24
|
+
parser = argparse.ArgumentParser(description="Live FlexiTac heatmap.")
|
|
25
|
+
parser.add_argument("--port", required=True)
|
|
26
|
+
parser.add_argument("--rows", type=int, default=12)
|
|
27
|
+
parser.add_argument("--cols", type=int, default=32)
|
|
28
|
+
parser.add_argument("--baud", type=int, default=2_000_000)
|
|
29
|
+
parser.add_argument("--cmap", default="viridis")
|
|
30
|
+
args = parser.parse_args()
|
|
31
|
+
|
|
32
|
+
sensor = FlexiTacSensor(args.port, rows=args.rows, cols=args.cols, baud=args.baud).open()
|
|
33
|
+
sensor.calibrate()
|
|
34
|
+
|
|
35
|
+
fig, ax = plt.subplots(figsize=(8, 4.5))
|
|
36
|
+
im = ax.imshow(np.zeros((args.rows, args.cols), dtype=np.float32), cmap=args.cmap, vmin=0.0, vmax=1.0)
|
|
37
|
+
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
38
|
+
title = ax.set_title("FlexiTac")
|
|
39
|
+
|
|
40
|
+
started = time.monotonic()
|
|
41
|
+
count = 0
|
|
42
|
+
|
|
43
|
+
def update(_: int) -> list:
|
|
44
|
+
nonlocal count
|
|
45
|
+
frame = sensor.read_latest()
|
|
46
|
+
count += 1
|
|
47
|
+
im.set_data(frame.normalized)
|
|
48
|
+
fps = count / max(time.monotonic() - started, 1e-6)
|
|
49
|
+
title.set_text(f"FlexiTac | seq={frame.seq} fps={fps:.1f}")
|
|
50
|
+
return [im, title]
|
|
51
|
+
|
|
52
|
+
_anim = animation.FuncAnimation(fig, update, interval=16, blit=True, cache_frame_data=False)
|
|
53
|
+
try:
|
|
54
|
+
plt.show()
|
|
55
|
+
finally:
|
|
56
|
+
sensor.close()
|
|
57
|
+
return 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Identify the FlexiTac serial port by snapshotting before/after unplug."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import platform
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from serial.tools import list_ports
|
|
10
|
+
|
|
11
|
+
from flexitac.logging_utils import configure_logging
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def list_serial_ports() -> set[str]:
|
|
15
|
+
"""Return currently available serial port device paths."""
|
|
16
|
+
if platform.system() == "Windows":
|
|
17
|
+
return {port.device for port in list_ports.comports()}
|
|
18
|
+
return {str(path) for path in Path("/dev").glob("tty*")}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def find_port(*, settle_s: float = 0.5) -> str:
|
|
22
|
+
"""Prompt the user to unplug their sensor and return the port that vanished."""
|
|
23
|
+
logger = configure_logging(verbose=False)
|
|
24
|
+
|
|
25
|
+
before = list_serial_ports()
|
|
26
|
+
logger.info("Detected %d serial ports.", len(before))
|
|
27
|
+
input("Unplug the FlexiTac USB cable, then press Enter... ")
|
|
28
|
+
|
|
29
|
+
time.sleep(settle_s)
|
|
30
|
+
after = list_serial_ports()
|
|
31
|
+
diff = sorted(before - after)
|
|
32
|
+
|
|
33
|
+
if len(diff) == 1:
|
|
34
|
+
port = diff[0]
|
|
35
|
+
logger.info("FlexiTac port: %s", port)
|
|
36
|
+
logger.info("Reconnect the USB cable.")
|
|
37
|
+
return port
|
|
38
|
+
if not diff:
|
|
39
|
+
raise OSError("No port disappeared. Make sure you actually unplugged the device.")
|
|
40
|
+
raise OSError(f"Multiple ports disappeared: {diff}. Unplug only the FlexiTac.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main(argv: list[str] | None = None) -> int:
|
|
44
|
+
"""CLI entrypoint for ``flexitac-find-port``."""
|
|
45
|
+
del argv
|
|
46
|
+
try:
|
|
47
|
+
find_port()
|
|
48
|
+
return 0
|
|
49
|
+
except OSError as exc:
|
|
50
|
+
configure_logging(verbose=False).error("%s", exc)
|
|
51
|
+
return 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Firmware helpers for template rendering and profile metadata."""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#define BAUD_RATE 2000000
|
|
2
|
+
#define ROW_COUNT 12
|
|
3
|
+
#define COLUMN_COUNT 32
|
|
4
|
+
|
|
5
|
+
#define PIN_ADC_INPUT A0
|
|
6
|
+
#define PIN_SHIFT_REGISTER_DATA 2
|
|
7
|
+
#define PIN_SHIFT_REGISTER_CLOCK 3
|
|
8
|
+
#define PIN_MUX_CHANNEL_0 4
|
|
9
|
+
#define PIN_MUX_CHANNEL_1 5
|
|
10
|
+
#define PIN_MUX_CHANNEL_2 6
|
|
11
|
+
#define PIN_MUX_CHANNEL_3 7
|
|
12
|
+
#define PIN_MUX_INHIBIT_0 8
|
|
13
|
+
#define PIN_MUX_INHIBIT_1 9
|
|
14
|
+
|
|
15
|
+
#define START_MARKER_0 0xAA
|
|
16
|
+
#define START_MARKER_1 0x55
|
|
17
|
+
|
|
18
|
+
#define SET_SR_DATA_HIGH() PORTD|=B00000100
|
|
19
|
+
#define SET_SR_DATA_LOW() PORTD&=~B00000100
|
|
20
|
+
#define SET_SR_CLK_HIGH() PORTD|=B00001000
|
|
21
|
+
#define SET_SR_CLK_LOW() PORTD&=~B00001000
|
|
22
|
+
|
|
23
|
+
#define ROWS_PER_MUX 16
|
|
24
|
+
#define MUX_COUNT 1
|
|
25
|
+
#define CHANNEL_PINS_PER_MUX 4
|
|
26
|
+
#define MUX_CHANNEL_OFFSET 4
|
|
27
|
+
|
|
28
|
+
#ifndef cbi
|
|
29
|
+
#define cbi(sfr, bit) (_SFR_BYTE(sfr) &= ~_BV(bit))
|
|
30
|
+
#endif
|
|
31
|
+
#ifndef sbi
|
|
32
|
+
#define sbi(sfr, bit) (_SFR_BYTE(sfr) |= _BV(bit))
|
|
33
|
+
#endif
|
|
34
|
+
|
|
35
|
+
int current_enabled_mux = MUX_COUNT - 1;
|
|
36
|
+
|
|
37
|
+
void setup()
|
|
38
|
+
{
|
|
39
|
+
Serial.begin(BAUD_RATE);
|
|
40
|
+
pinMode(PIN_ADC_INPUT, INPUT);
|
|
41
|
+
pinMode(PIN_SHIFT_REGISTER_DATA, OUTPUT);
|
|
42
|
+
pinMode(PIN_SHIFT_REGISTER_CLOCK, OUTPUT);
|
|
43
|
+
pinMode(PIN_MUX_CHANNEL_0, OUTPUT);
|
|
44
|
+
pinMode(PIN_MUX_CHANNEL_1, OUTPUT);
|
|
45
|
+
pinMode(PIN_MUX_CHANNEL_2, OUTPUT);
|
|
46
|
+
pinMode(PIN_MUX_CHANNEL_3, OUTPUT);
|
|
47
|
+
pinMode(PIN_MUX_INHIBIT_0, OUTPUT);
|
|
48
|
+
pinMode(PIN_MUX_INHIBIT_1, OUTPUT);
|
|
49
|
+
|
|
50
|
+
sbi(ADCSRA, ADPS2);
|
|
51
|
+
cbi(ADCSRA, ADPS1);
|
|
52
|
+
cbi(ADCSRA, ADPS0);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
void loop()
|
|
56
|
+
{
|
|
57
|
+
Serial.write(START_MARKER_0);
|
|
58
|
+
Serial.write(START_MARKER_1);
|
|
59
|
+
|
|
60
|
+
for (int i = 0; i < ROW_COUNT; i++)
|
|
61
|
+
{
|
|
62
|
+
setRow(i);
|
|
63
|
+
shiftColumn(true);
|
|
64
|
+
shiftColumn(false);
|
|
65
|
+
|
|
66
|
+
for (int j = 0; j < COLUMN_COUNT; j++)
|
|
67
|
+
{
|
|
68
|
+
int raw_reading = analogRead(PIN_ADC_INPUT);
|
|
69
|
+
byte send_reading = (byte)(raw_reading >> 2);
|
|
70
|
+
|
|
71
|
+
Serial.write(send_reading);
|
|
72
|
+
shiftColumn(false);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void setRow(int row_number)
|
|
78
|
+
{
|
|
79
|
+
if ((row_number % ROWS_PER_MUX) == 0)
|
|
80
|
+
{
|
|
81
|
+
digitalWrite(PIN_MUX_INHIBIT_0 + current_enabled_mux, HIGH);
|
|
82
|
+
current_enabled_mux++;
|
|
83
|
+
if (current_enabled_mux >= MUX_COUNT)
|
|
84
|
+
{
|
|
85
|
+
current_enabled_mux = 0;
|
|
86
|
+
}
|
|
87
|
+
digitalWrite(PIN_MUX_INHIBIT_0 + current_enabled_mux, LOW);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
int mux_channel = row_number + MUX_CHANNEL_OFFSET;
|
|
91
|
+
for (int i = 0; i < CHANNEL_PINS_PER_MUX; i++)
|
|
92
|
+
{
|
|
93
|
+
if (bitRead(mux_channel, i))
|
|
94
|
+
{
|
|
95
|
+
digitalWrite(PIN_MUX_CHANNEL_0 + i, HIGH);
|
|
96
|
+
}
|
|
97
|
+
else
|
|
98
|
+
{
|
|
99
|
+
digitalWrite(PIN_MUX_CHANNEL_0 + i, LOW);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
void shiftColumn(boolean is_first)
|
|
105
|
+
{
|
|
106
|
+
if (is_first)
|
|
107
|
+
{
|
|
108
|
+
SET_SR_DATA_HIGH();
|
|
109
|
+
}
|
|
110
|
+
SET_SR_CLK_HIGH();
|
|
111
|
+
SET_SR_CLK_LOW();
|
|
112
|
+
if (is_first)
|
|
113
|
+
{
|
|
114
|
+
SET_SR_DATA_LOW();
|
|
115
|
+
}
|
|
116
|
+
}
|