chumicro-knobs 0.1.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.
- chumicro_knobs-0.1.0/.gitignore +114 -0
- chumicro_knobs-0.1.0/LICENSE +22 -0
- chumicro_knobs-0.1.0/PKG-INFO +212 -0
- chumicro_knobs-0.1.0/README.md +185 -0
- chumicro_knobs-0.1.0/VERSION +1 -0
- chumicro_knobs-0.1.0/docs/api.md +32 -0
- chumicro_knobs-0.1.0/docs/guide.md +350 -0
- chumicro_knobs-0.1.0/docs/index.md +43 -0
- chumicro_knobs-0.1.0/docs/testing.md +103 -0
- chumicro_knobs-0.1.0/examples/circuitpython_encoder_volume.py +65 -0
- chumicro_knobs-0.1.0/examples/micropython_encoder_volume.py +65 -0
- chumicro_knobs-0.1.0/pyproject.toml +54 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/__init__.py +22 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/_adapters/__init__.py +0 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/_adapters/base.py +75 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/_adapters/cp.py +62 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/_adapters/mp.py +125 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/analog.py +115 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/encoder.py +149 -0
- chumicro_knobs-0.1.0/src/chumicro_knobs/testing.py +69 -0
- chumicro_knobs-0.1.0/tests/_mp_helpers.py +190 -0
- chumicro_knobs-0.1.0/tests/conftest.py +15 -0
- chumicro_knobs-0.1.0/tests/test_analog.py +237 -0
- chumicro_knobs-0.1.0/tests/test_encoder.py +344 -0
- chumicro_knobs-0.1.0/tests/test_mp_adapter.py +416 -0
- chumicro_knobs-0.1.0/tests/test_source_selection_host.py +58 -0
- chumicro_knobs-0.1.0/tests/test_sources.py +94 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
.idea/*
|
|
3
|
+
!.idea/modules.xml
|
|
4
|
+
!.idea/chumicro.iml
|
|
5
|
+
!.idea/runConfigurations/
|
|
6
|
+
!.idea/inspectionProfiles/
|
|
7
|
+
.venv/
|
|
8
|
+
.tools/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.coverage
|
|
14
|
+
coverage.xml
|
|
15
|
+
htmlcov/
|
|
16
|
+
build/
|
|
17
|
+
dist/
|
|
18
|
+
site/
|
|
19
|
+
.site/
|
|
20
|
+
*.egg-info/
|
|
21
|
+
|
|
22
|
+
# Generated by docs build from support/docs/ and zensical
|
|
23
|
+
libraries/*/docs/stylesheets/
|
|
24
|
+
libraries/*/docs/img/
|
|
25
|
+
libraries/*/docs/assets/
|
|
26
|
+
workbench/*/docs/stylesheets/
|
|
27
|
+
workbench/*/docs/img/
|
|
28
|
+
workbench/*/docs/assets/
|
|
29
|
+
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
.scratch/
|
|
32
|
+
|
|
33
|
+
# Generated runtime-config msgpack from `chumicro-workspace deploy-example`
|
|
34
|
+
# — the `_generated/` build-artifact convention, never committed beside
|
|
35
|
+
# the tracked example source.
|
|
36
|
+
libraries/*/examples/_generated/
|
|
37
|
+
|
|
38
|
+
# Pre-upgrade backups of edited curated library trees, written by
|
|
39
|
+
# `chumicro-workspace library update` before it replaces a tree —
|
|
40
|
+
# tool-managed, never committed; recover local edits from here.
|
|
41
|
+
_library-backups/
|
|
42
|
+
|
|
43
|
+
# Agent-tooling scratch dir (worktrees, per-user settings). Track only:
|
|
44
|
+
# * the `skills` symlink so Claude Code auto-discovers the canonical skills
|
|
45
|
+
# tree at `.github/skills/` from the location it expects (`.claude/skills/`).
|
|
46
|
+
# * `settings.json` — team-shared agent permissions for chumicro dev tooling
|
|
47
|
+
# (chumicro-workspace / chumicro-deploy / chumicro-repl). `.local.json`
|
|
48
|
+
# stays ignored for per-user overrides.
|
|
49
|
+
# * `surfaces/` - the browser-surface toolkit (Decision 0100). It is a generated
|
|
50
|
+
# copy of a canonical shared with sibling repos, so it must stay tracked to be
|
|
51
|
+
# diffable against that canonical.
|
|
52
|
+
# * `rules/` — path-scoped rules that load only when an agent opens a matching
|
|
53
|
+
# file. Team-shared, same as AGENTS.md.
|
|
54
|
+
.claude/*
|
|
55
|
+
!.claude/agents
|
|
56
|
+
!.claude/rules
|
|
57
|
+
!.claude/skills
|
|
58
|
+
!.claude/settings.json
|
|
59
|
+
!.claude/surfaces
|
|
60
|
+
|
|
61
|
+
# .agents/ — install tree the add-skill tool writes when pulling downloaded
|
|
62
|
+
# marketplace skills from `skills-lock.json`. The canonical, tracked copy of
|
|
63
|
+
# every downloaded skill lives under `.github/skills/`; this is a redundant
|
|
64
|
+
# per-checkout install tree, so track the manifest plus `.github/skills/` and
|
|
65
|
+
# ignore this duplicate.
|
|
66
|
+
.agents/
|
|
67
|
+
|
|
68
|
+
devices.yml
|
|
69
|
+
|
|
70
|
+
# Legacy: chumicro-dev-config.toml. Retired in Phase 4 of the unification
|
|
71
|
+
# workstream — networking-library functional-test conftests now read from
|
|
72
|
+
# workspace.yml via `chumicro_workspace.compose_runtime_config`. Kept
|
|
73
|
+
# gitignored for one cycle so contributors with a left-over copy from the
|
|
74
|
+
# pre-Phase-4 era don't accidentally commit it.
|
|
75
|
+
chumicro-dev-config.toml
|
|
76
|
+
|
|
77
|
+
# workspace.yml — workspace machinery (library_sources, deploy_targets,
|
|
78
|
+
# quality, environments). Materialised by ``python scripts/run.py setup``
|
|
79
|
+
# from `_workspace_template/workspace.yml` (this repo's specific opinions
|
|
80
|
+
# — tracked, ships as the starter) or chumicro-workspace's workbench-owned
|
|
81
|
+
# starter. Gitignored so the auto-populated ``library_sources:`` block
|
|
82
|
+
# (paths into a sibling chumicro/ checkout) doesn't reach git. Root-
|
|
83
|
+
# anchored so `_workspace_template/workspace.yml` stays trackable.
|
|
84
|
+
/workspace.yml
|
|
85
|
+
|
|
86
|
+
# secrets.toml — workspace-wide credentials + device defaults that flow
|
|
87
|
+
# into runtime_config.msgpack at deploy time. Materialised on first
|
|
88
|
+
# ``setup`` from `_workspace_template/secrets.toml` (this repo's specific
|
|
89
|
+
# opinions — tracked, ships as the starter) or the workbench-owned
|
|
90
|
+
# starter. Never committed — contains real wifi passwords / broker auth.
|
|
91
|
+
# Root-anchored so `_workspace_template/secrets.toml` stays trackable.
|
|
92
|
+
/secrets.toml
|
|
93
|
+
|
|
94
|
+
# .envrc — direnv hook for per-checkout shell setup (e.g. auto-sourcing
|
|
95
|
+
# the venv on cd). Per-user / per-machine; not part of the canonical
|
|
96
|
+
# workflow.
|
|
97
|
+
.envrc
|
|
98
|
+
|
|
99
|
+
# Firmware blobs. `chumicro-deploy flash-firmware` and friends default
|
|
100
|
+
# to `./firmware.bin` for the downloaded image; the file is build /
|
|
101
|
+
# board artifact, not source. Use `.scratch/` for one-off testing or
|
|
102
|
+
# pass an explicit destination to keep blobs out of the working tree.
|
|
103
|
+
/firmware.bin
|
|
104
|
+
firmware-*.bin
|
|
105
|
+
*.uf2
|
|
106
|
+
|
|
107
|
+
.claude/settings.local.json
|
|
108
|
+
|
|
109
|
+
# Built guides site (guides.mkdocs.yml); the docs deploy publishes it.
|
|
110
|
+
.guides-site/
|
|
111
|
+
.site-root/
|
|
112
|
+
|
|
113
|
+
# Playwright MCP session output: console logs and page snapshots.
|
|
114
|
+
.playwright-mcp/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ChuMicro
|
|
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.
|
|
22
|
+
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: chumicro-knobs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rotary encoders and analog knobs, read as a position that holds still
|
|
5
|
+
Project-URL: Homepage, https://github.com/ChuMicro/ChuMicro
|
|
6
|
+
Project-URL: Documentation, https://chumicro.com/ChuMicro/knobs/stable/
|
|
7
|
+
Project-URL: Source, https://github.com/ChuMicro/ChuMicro/tree/main/libraries/knobs
|
|
8
|
+
Project-URL: Issues, https://github.com/ChuMicro/ChuMicro/issues
|
|
9
|
+
Project-URL: Bundle, https://github.com/ChuMicro/ChuMicro-Bundle
|
|
10
|
+
Author: ChuMicro
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: adc,circuitpython,embedded,esp32,microcontroller,micropython,potentiometer,quadrature,rotary-encoder,rp2040
|
|
14
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
20
|
+
Classifier: Topic :: System :: Hardware
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: chumicro-test-harness; extra == 'test'
|
|
24
|
+
Requires-Dist: chumicro-timing; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest; extra == 'test'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# chumicro-knobs
|
|
29
|
+
|
|
30
|
+
<img src="https://raw.githubusercontent.com/ChuMicro/ChuMicro/main/support/docs/chumicro_tip.png"
|
|
31
|
+
align="left" width="64" style="margin-right: 16px; margin-bottom: 8px;">
|
|
32
|
+
|
|
33
|
+
**Rotary encoders and analog knobs, read as a number that holds still.**
|
|
34
|
+
|
|
35
|
+
Turn a shaft and `encoder.position` counts the clicks. Turn a potentiometer and `knob.value` lands on a step and stays there. A fast spin arrives whole even when your loop was busy elsewhere, a parked wiper keeps reporting the same number, and the same code runs on CircuitPython, MicroPython, and your laptop.
|
|
36
|
+
|
|
37
|
+
<br clear="left">
|
|
38
|
+
|
|
39
|
+
> Part of the [ChuMicro](https://github.com/ChuMicro/ChuMicro) family: small, focused Python libraries for microcontrollers and laptops. [Browse all libraries.](https://github.com/ChuMicro/ChuMicro/tree/main/libraries)
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# CircuitPython (after `circup bundle-add ChuMicro/ChuMicro-Bundle`)
|
|
45
|
+
circup install chumicro_knobs
|
|
46
|
+
|
|
47
|
+
# MicroPython
|
|
48
|
+
mpremote mip install github:ChuMicro/ChuMicro-Bundle/chumicro_knobs
|
|
49
|
+
|
|
50
|
+
# CPython
|
|
51
|
+
pip install chumicro-knobs
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
For bundle setup, pre-compiled `.mpy` bundles, the experimental channel, and details on PyPI naming, see the [ChuMicro install guide](https://chumicro.com/ChuMicro/guides/install/).
|
|
55
|
+
|
|
56
|
+
## Quick example
|
|
57
|
+
|
|
58
|
+
Wire the encoder's two signal pins to GPIO pins and its common pin to GND. The internal pull-ups are switched on for you, so no extra parts are needed.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import board
|
|
62
|
+
from chumicro_knobs import Encoder
|
|
63
|
+
from chumicro_timing import ticks_ms
|
|
64
|
+
|
|
65
|
+
volume = Encoder(board.GP16, board.GP17, bounds=(0, 20))
|
|
66
|
+
|
|
67
|
+
while True:
|
|
68
|
+
now = ticks_ms()
|
|
69
|
+
volume.check(now)
|
|
70
|
+
|
|
71
|
+
if volume.just_moved: # true only on the tick the shaft moved
|
|
72
|
+
print("volume", volume.position) # walks 0 to 20 and stops at both ends
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The loop never pauses, so the rest of your program keeps running between turns. `just_moved` is true for exactly one pass, which means you can read it as many times as you like without it firing twice.
|
|
76
|
+
|
|
77
|
+
## A fast spin arrives whole
|
|
78
|
+
|
|
79
|
+
An encoder reports movement as pulses on two signal pins, and one brisk flick of the wrist sends dozens of them. If your loop stalls on a socket read or a flash write, a plain pin read looks at the wrong moments and most of the turn is gone. The counting here happens outside your loop, so a tick that arrives late still reads the whole spin.
|
|
80
|
+
|
|
81
|
+
On CircuitPython that counting is `rotaryio`, running in the firmware's own C, which on RP2040 boards is a state machine in the PIO block. On MicroPython the library installs an interrupt on both signal pins and decodes the pulses itself. Your program sets up neither one; it reads `position` on whichever runtime it happens to be on.
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
volume.check(now)
|
|
85
|
+
if volume.just_moved:
|
|
86
|
+
print(volume.delta) # every detent of the spin, even the ones during the stall
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## What's included
|
|
90
|
+
|
|
91
|
+
### Core
|
|
92
|
+
|
|
93
|
+
| Symbol | Description |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `Encoder(pin_a, pin_b, detent_steps=4, bounds=None, wrap=False)` | One rotary encoder on two signal pins, counted in detents |
|
|
96
|
+
| `AnalogKnob(pin, steps=100, deadband=512)` | One potentiometer or slider on one analog pin, read as a step number |
|
|
97
|
+
| `knob.check(now_ms)` | Take one reading; returns `True` when the number changed |
|
|
98
|
+
| `knob.handle(now_ms)` | Call `on_change` when the tick earned it |
|
|
99
|
+
| `knob.deinit()` | Hand the pins back, along with any interrupt the library installed |
|
|
100
|
+
|
|
101
|
+
### Readings, refreshed by `check(now_ms)`
|
|
102
|
+
|
|
103
|
+
| Symbol | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `encoder.position` | Detents counted so far, held inside `bounds` when there are any. Assign to it to restore a saved value |
|
|
106
|
+
| `encoder.delta` | Detents this tick added to `position`, negative the other way round |
|
|
107
|
+
| `encoder.just_moved` | `True` only on the tick `position` changed |
|
|
108
|
+
| `knob.value` | Where the knob points, `0` at one end of the sweep and `steps - 1` at the other |
|
|
109
|
+
| `knob.delta` | Steps this tick added to `value`, negative the other way round |
|
|
110
|
+
| `knob.raw` | The settled 0 to 65535 reading `value` was worked out from |
|
|
111
|
+
| `knob.just_moved` | `True` only on the tick `value` changed |
|
|
112
|
+
|
|
113
|
+
### Callbacks, dispatched by `handle(now_ms)`
|
|
114
|
+
|
|
115
|
+
| Symbol | Description |
|
|
116
|
+
|---|---|
|
|
117
|
+
| `encoder.on_change` | Called with the signed detent change when the shaft turns |
|
|
118
|
+
| `knob.on_change` | Called with the new step number when the knob moves |
|
|
119
|
+
|
|
120
|
+
### Defaults you can import
|
|
121
|
+
|
|
122
|
+
| Symbol | Description |
|
|
123
|
+
|---|---|
|
|
124
|
+
| `DEFAULT_DETENT_STEPS` | `4`, the pulses one click of a detented encoder produces |
|
|
125
|
+
| `DEFAULT_STEPS` | `100` positions across a full sweep of an analog knob |
|
|
126
|
+
| `DEFAULT_DEADBAND` | `512`, how far a reading moves before `value` follows it |
|
|
127
|
+
| `RAW_RANGE` | `65536`, the raw scale every runtime reports a conversion on |
|
|
128
|
+
|
|
129
|
+
### Testing
|
|
130
|
+
|
|
131
|
+
| Symbol | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `chumicro_knobs.testing.FakeEncoderSource` | A shaft your test turns by hand, so encoder logic runs with no board |
|
|
134
|
+
| `chumicro_knobs.testing.FakeAnalogSource` | A wiper your test parks where it likes, so the deadband is testable too |
|
|
135
|
+
|
|
136
|
+
## The reading holds still
|
|
137
|
+
|
|
138
|
+
A potentiometer's voltage is never exactly steady. The low bits of a 12-bit converter wander a couple of counts under a parked wiper, which is 32 counts once the reading is scaled to the 0 to 65535 range every runtime reports on, and a noisier part on a long lead wanders several times that. A program that prints the raw reading shows a knob somebody is fiddling with.
|
|
139
|
+
|
|
140
|
+
`deadband` is how far the reading has to move before `value` follows it, and quantizing into `steps` gives the number a size a person can aim at:
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
brightness = AnalogKnob(board.A0, steps=10) # reports 0 to 9
|
|
144
|
+
fine = AnalogKnob(board.A1, steps=256, deadband=128) # a finer sweep, a tighter deadband
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The default 512 sits well above the wander and well under the 655 counts one step spans at 100 steps. Keep `deadband` under the width of one step, which is `65536 // steps` counts, so every step stays reachable including the ones at the ends of the sweep.
|
|
148
|
+
|
|
149
|
+
## Where this fits
|
|
150
|
+
|
|
151
|
+
Depends on nothing. The timestamp you hand to `check()` comes from wherever your loop already gets one, and [`chumicro-timing`](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/timing)'s `ticks_ms()` is the usual source. Used directly in user apps; nothing downstream depends on it.
|
|
152
|
+
|
|
153
|
+
Pairs with [`chumicro-runner`](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/runner), which deals out turns to every service in your program:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
runner.add(volume) # check() and handle() are the runner's contract
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
A rotary encoder's push switch is a button on its own pin, so an encoder with a click reads its shaft here and its switch with [`chumicro-buttons`](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/buttons).
|
|
160
|
+
|
|
161
|
+
## Platform support
|
|
162
|
+
|
|
163
|
+
Works on CPython, MicroPython, and CircuitPython.
|
|
164
|
+
|
|
165
|
+
### RP2040 wants adjacent pins
|
|
166
|
+
|
|
167
|
+
`rotaryio` on RP2040 boards reads the two signal pins with one PIO state machine, which requires them to sit next to each other in GPIO numbering. `board.GP16` and `board.GP17` work; `board.GP16` and `board.GP20` do not. Other CircuitPython ports and every MicroPython port take any two pins.
|
|
168
|
+
|
|
169
|
+
`pin_a`, `pin_b`, and `pin` need real hardware, so on a laptop they raise and point you at the fakes below.
|
|
170
|
+
|
|
171
|
+
## Testing your code
|
|
172
|
+
|
|
173
|
+
The `chumicro_knobs.testing` module provides `FakeEncoderSource` and `FakeAnalogSource`, hand-driven stand-ins for the hardware, so knob logic is an ordinary unit test with no board:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
from chumicro_knobs import Encoder
|
|
177
|
+
from chumicro_knobs.testing import FakeEncoderSource
|
|
178
|
+
|
|
179
|
+
source = FakeEncoderSource()
|
|
180
|
+
volume = Encoder(source=source, bounds=(0, 20))
|
|
181
|
+
|
|
182
|
+
source.turn(3)
|
|
183
|
+
volume.check(0)
|
|
184
|
+
|
|
185
|
+
assert volume.position == 3
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Examples
|
|
189
|
+
|
|
190
|
+
| Example | What it shows |
|
|
191
|
+
|---|---|
|
|
192
|
+
| [`circuitpython_encoder_volume.py`](https://github.com/ChuMicro/ChuMicro/blob/main/libraries/knobs/examples/circuitpython_encoder_volume.py) | An encoder for volume and a potentiometer for brightness on CircuitPython |
|
|
193
|
+
| [`micropython_encoder_volume.py`](https://github.com/ChuMicro/ChuMicro/blob/main/libraries/knobs/examples/micropython_encoder_volume.py) | The same two knobs on MicroPython |
|
|
194
|
+
|
|
195
|
+
## Contributing
|
|
196
|
+
|
|
197
|
+
Issues, bug reports, and pull requests are welcome, and so is "I ran it on this board and here's what happened", some of the most useful feedback a hardware project can get. Development happens in the [ChuMicro repository](https://github.com/ChuMicro/ChuMicro), whose contributing guide covers setup and the test workflow.
|
|
198
|
+
|
|
199
|
+
## Docs
|
|
200
|
+
|
|
201
|
+
📖 **[Stable docs](https://chumicro.com/ChuMicro/knobs/stable/)** · **[Experimental docs](https://chumicro.com/ChuMicro/knobs/experimental/)**
|
|
202
|
+
|
|
203
|
+
## Find this library
|
|
204
|
+
|
|
205
|
+
- **PyPI:** [chumicro-knobs](https://pypi.org/project/chumicro-knobs/)
|
|
206
|
+
- **Bundle:** [ChuMicro-Bundle](https://github.com/ChuMicro/ChuMicro-Bundle/tree/main/chumicro_knobs) (CircuitPython & MicroPython)
|
|
207
|
+
- **Experimental bundle:** [ChuMicro-Bundle-Experimental](https://github.com/ChuMicro/ChuMicro-Bundle-Experimental/tree/main/chumicro_knobs)
|
|
208
|
+
- **Source:** [libraries/knobs](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/knobs)
|
|
209
|
+
|
|
210
|
+
## License
|
|
211
|
+
|
|
212
|
+
[MIT](https://github.com/ChuMicro/ChuMicro/blob/main/LICENSE)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# chumicro-knobs
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/ChuMicro/ChuMicro/main/support/docs/chumicro_tip.png"
|
|
4
|
+
align="left" width="64" style="margin-right: 16px; margin-bottom: 8px;">
|
|
5
|
+
|
|
6
|
+
**Rotary encoders and analog knobs, read as a number that holds still.**
|
|
7
|
+
|
|
8
|
+
Turn a shaft and `encoder.position` counts the clicks. Turn a potentiometer and `knob.value` lands on a step and stays there. A fast spin arrives whole even when your loop was busy elsewhere, a parked wiper keeps reporting the same number, and the same code runs on CircuitPython, MicroPython, and your laptop.
|
|
9
|
+
|
|
10
|
+
<br clear="left">
|
|
11
|
+
|
|
12
|
+
> Part of the [ChuMicro](https://github.com/ChuMicro/ChuMicro) family: small, focused Python libraries for microcontrollers and laptops. [Browse all libraries.](https://github.com/ChuMicro/ChuMicro/tree/main/libraries)
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# CircuitPython (after `circup bundle-add ChuMicro/ChuMicro-Bundle`)
|
|
18
|
+
circup install chumicro_knobs
|
|
19
|
+
|
|
20
|
+
# MicroPython
|
|
21
|
+
mpremote mip install github:ChuMicro/ChuMicro-Bundle/chumicro_knobs
|
|
22
|
+
|
|
23
|
+
# CPython
|
|
24
|
+
pip install chumicro-knobs
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For bundle setup, pre-compiled `.mpy` bundles, the experimental channel, and details on PyPI naming, see the [ChuMicro install guide](https://chumicro.com/ChuMicro/guides/install/).
|
|
28
|
+
|
|
29
|
+
## Quick example
|
|
30
|
+
|
|
31
|
+
Wire the encoder's two signal pins to GPIO pins and its common pin to GND. The internal pull-ups are switched on for you, so no extra parts are needed.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import board
|
|
35
|
+
from chumicro_knobs import Encoder
|
|
36
|
+
from chumicro_timing import ticks_ms
|
|
37
|
+
|
|
38
|
+
volume = Encoder(board.GP16, board.GP17, bounds=(0, 20))
|
|
39
|
+
|
|
40
|
+
while True:
|
|
41
|
+
now = ticks_ms()
|
|
42
|
+
volume.check(now)
|
|
43
|
+
|
|
44
|
+
if volume.just_moved: # true only on the tick the shaft moved
|
|
45
|
+
print("volume", volume.position) # walks 0 to 20 and stops at both ends
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The loop never pauses, so the rest of your program keeps running between turns. `just_moved` is true for exactly one pass, which means you can read it as many times as you like without it firing twice.
|
|
49
|
+
|
|
50
|
+
## A fast spin arrives whole
|
|
51
|
+
|
|
52
|
+
An encoder reports movement as pulses on two signal pins, and one brisk flick of the wrist sends dozens of them. If your loop stalls on a socket read or a flash write, a plain pin read looks at the wrong moments and most of the turn is gone. The counting here happens outside your loop, so a tick that arrives late still reads the whole spin.
|
|
53
|
+
|
|
54
|
+
On CircuitPython that counting is `rotaryio`, running in the firmware's own C, which on RP2040 boards is a state machine in the PIO block. On MicroPython the library installs an interrupt on both signal pins and decodes the pulses itself. Your program sets up neither one; it reads `position` on whichever runtime it happens to be on.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
volume.check(now)
|
|
58
|
+
if volume.just_moved:
|
|
59
|
+
print(volume.delta) # every detent of the spin, even the ones during the stall
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## What's included
|
|
63
|
+
|
|
64
|
+
### Core
|
|
65
|
+
|
|
66
|
+
| Symbol | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `Encoder(pin_a, pin_b, detent_steps=4, bounds=None, wrap=False)` | One rotary encoder on two signal pins, counted in detents |
|
|
69
|
+
| `AnalogKnob(pin, steps=100, deadband=512)` | One potentiometer or slider on one analog pin, read as a step number |
|
|
70
|
+
| `knob.check(now_ms)` | Take one reading; returns `True` when the number changed |
|
|
71
|
+
| `knob.handle(now_ms)` | Call `on_change` when the tick earned it |
|
|
72
|
+
| `knob.deinit()` | Hand the pins back, along with any interrupt the library installed |
|
|
73
|
+
|
|
74
|
+
### Readings, refreshed by `check(now_ms)`
|
|
75
|
+
|
|
76
|
+
| Symbol | Description |
|
|
77
|
+
|---|---|
|
|
78
|
+
| `encoder.position` | Detents counted so far, held inside `bounds` when there are any. Assign to it to restore a saved value |
|
|
79
|
+
| `encoder.delta` | Detents this tick added to `position`, negative the other way round |
|
|
80
|
+
| `encoder.just_moved` | `True` only on the tick `position` changed |
|
|
81
|
+
| `knob.value` | Where the knob points, `0` at one end of the sweep and `steps - 1` at the other |
|
|
82
|
+
| `knob.delta` | Steps this tick added to `value`, negative the other way round |
|
|
83
|
+
| `knob.raw` | The settled 0 to 65535 reading `value` was worked out from |
|
|
84
|
+
| `knob.just_moved` | `True` only on the tick `value` changed |
|
|
85
|
+
|
|
86
|
+
### Callbacks, dispatched by `handle(now_ms)`
|
|
87
|
+
|
|
88
|
+
| Symbol | Description |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `encoder.on_change` | Called with the signed detent change when the shaft turns |
|
|
91
|
+
| `knob.on_change` | Called with the new step number when the knob moves |
|
|
92
|
+
|
|
93
|
+
### Defaults you can import
|
|
94
|
+
|
|
95
|
+
| Symbol | Description |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `DEFAULT_DETENT_STEPS` | `4`, the pulses one click of a detented encoder produces |
|
|
98
|
+
| `DEFAULT_STEPS` | `100` positions across a full sweep of an analog knob |
|
|
99
|
+
| `DEFAULT_DEADBAND` | `512`, how far a reading moves before `value` follows it |
|
|
100
|
+
| `RAW_RANGE` | `65536`, the raw scale every runtime reports a conversion on |
|
|
101
|
+
|
|
102
|
+
### Testing
|
|
103
|
+
|
|
104
|
+
| Symbol | Description |
|
|
105
|
+
|---|---|
|
|
106
|
+
| `chumicro_knobs.testing.FakeEncoderSource` | A shaft your test turns by hand, so encoder logic runs with no board |
|
|
107
|
+
| `chumicro_knobs.testing.FakeAnalogSource` | A wiper your test parks where it likes, so the deadband is testable too |
|
|
108
|
+
|
|
109
|
+
## The reading holds still
|
|
110
|
+
|
|
111
|
+
A potentiometer's voltage is never exactly steady. The low bits of a 12-bit converter wander a couple of counts under a parked wiper, which is 32 counts once the reading is scaled to the 0 to 65535 range every runtime reports on, and a noisier part on a long lead wanders several times that. A program that prints the raw reading shows a knob somebody is fiddling with.
|
|
112
|
+
|
|
113
|
+
`deadband` is how far the reading has to move before `value` follows it, and quantizing into `steps` gives the number a size a person can aim at:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
brightness = AnalogKnob(board.A0, steps=10) # reports 0 to 9
|
|
117
|
+
fine = AnalogKnob(board.A1, steps=256, deadband=128) # a finer sweep, a tighter deadband
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The default 512 sits well above the wander and well under the 655 counts one step spans at 100 steps. Keep `deadband` under the width of one step, which is `65536 // steps` counts, so every step stays reachable including the ones at the ends of the sweep.
|
|
121
|
+
|
|
122
|
+
## Where this fits
|
|
123
|
+
|
|
124
|
+
Depends on nothing. The timestamp you hand to `check()` comes from wherever your loop already gets one, and [`chumicro-timing`](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/timing)'s `ticks_ms()` is the usual source. Used directly in user apps; nothing downstream depends on it.
|
|
125
|
+
|
|
126
|
+
Pairs with [`chumicro-runner`](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/runner), which deals out turns to every service in your program:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
runner.add(volume) # check() and handle() are the runner's contract
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
A rotary encoder's push switch is a button on its own pin, so an encoder with a click reads its shaft here and its switch with [`chumicro-buttons`](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/buttons).
|
|
133
|
+
|
|
134
|
+
## Platform support
|
|
135
|
+
|
|
136
|
+
Works on CPython, MicroPython, and CircuitPython.
|
|
137
|
+
|
|
138
|
+
### RP2040 wants adjacent pins
|
|
139
|
+
|
|
140
|
+
`rotaryio` on RP2040 boards reads the two signal pins with one PIO state machine, which requires them to sit next to each other in GPIO numbering. `board.GP16` and `board.GP17` work; `board.GP16` and `board.GP20` do not. Other CircuitPython ports and every MicroPython port take any two pins.
|
|
141
|
+
|
|
142
|
+
`pin_a`, `pin_b`, and `pin` need real hardware, so on a laptop they raise and point you at the fakes below.
|
|
143
|
+
|
|
144
|
+
## Testing your code
|
|
145
|
+
|
|
146
|
+
The `chumicro_knobs.testing` module provides `FakeEncoderSource` and `FakeAnalogSource`, hand-driven stand-ins for the hardware, so knob logic is an ordinary unit test with no board:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from chumicro_knobs import Encoder
|
|
150
|
+
from chumicro_knobs.testing import FakeEncoderSource
|
|
151
|
+
|
|
152
|
+
source = FakeEncoderSource()
|
|
153
|
+
volume = Encoder(source=source, bounds=(0, 20))
|
|
154
|
+
|
|
155
|
+
source.turn(3)
|
|
156
|
+
volume.check(0)
|
|
157
|
+
|
|
158
|
+
assert volume.position == 3
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Examples
|
|
162
|
+
|
|
163
|
+
| Example | What it shows |
|
|
164
|
+
|---|---|
|
|
165
|
+
| [`circuitpython_encoder_volume.py`](https://github.com/ChuMicro/ChuMicro/blob/main/libraries/knobs/examples/circuitpython_encoder_volume.py) | An encoder for volume and a potentiometer for brightness on CircuitPython |
|
|
166
|
+
| [`micropython_encoder_volume.py`](https://github.com/ChuMicro/ChuMicro/blob/main/libraries/knobs/examples/micropython_encoder_volume.py) | The same two knobs on MicroPython |
|
|
167
|
+
|
|
168
|
+
## Contributing
|
|
169
|
+
|
|
170
|
+
Issues, bug reports, and pull requests are welcome, and so is "I ran it on this board and here's what happened", some of the most useful feedback a hardware project can get. Development happens in the [ChuMicro repository](https://github.com/ChuMicro/ChuMicro), whose contributing guide covers setup and the test workflow.
|
|
171
|
+
|
|
172
|
+
## Docs
|
|
173
|
+
|
|
174
|
+
📖 **[Stable docs](https://chumicro.com/ChuMicro/knobs/stable/)** · **[Experimental docs](https://chumicro.com/ChuMicro/knobs/experimental/)**
|
|
175
|
+
|
|
176
|
+
## Find this library
|
|
177
|
+
|
|
178
|
+
- **PyPI:** [chumicro-knobs](https://pypi.org/project/chumicro-knobs/)
|
|
179
|
+
- **Bundle:** [ChuMicro-Bundle](https://github.com/ChuMicro/ChuMicro-Bundle/tree/main/chumicro_knobs) (CircuitPython & MicroPython)
|
|
180
|
+
- **Experimental bundle:** [ChuMicro-Bundle-Experimental](https://github.com/ChuMicro/ChuMicro-Bundle-Experimental/tree/main/chumicro_knobs)
|
|
181
|
+
- **Source:** [libraries/knobs](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/knobs)
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
[MIT](https://github.com/ChuMicro/ChuMicro/blob/main/LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.1.0
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## `chumicro_knobs.encoder`
|
|
4
|
+
|
|
5
|
+
`Encoder` reads one rotary encoder: detents counted into a running `position`, the signed `delta` for each tick, and the optional `bounds` and `wrap` that keep the count inside a range you choose.
|
|
6
|
+
|
|
7
|
+
::: chumicro_knobs.encoder
|
|
8
|
+
|
|
9
|
+
## `chumicro_knobs.analog`
|
|
10
|
+
|
|
11
|
+
`AnalogKnob` reads one potentiometer or slider: a converter held still by a deadband and quantized into steps, published as `value`, `delta`, and the settled `raw` reading behind them.
|
|
12
|
+
|
|
13
|
+
::: chumicro_knobs.analog
|
|
14
|
+
|
|
15
|
+
## `chumicro_knobs.testing`
|
|
16
|
+
|
|
17
|
+
`FakeEncoderSource` and `FakeAnalogSource` stand in for the hardware so knob logic can be tested on a host. Turn the shaft or park the wiper, tick the knob, assert on what it made of them.
|
|
18
|
+
|
|
19
|
+
::: chumicro_knobs.testing
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<div class="chumicro-footer" markdown>
|
|
24
|
+
|
|
25
|
+
[← Home](index.md)
|
|
26
|
+
|
|
27
|
+
[Source](https://github.com/ChuMicro/ChuMicro/tree/main/libraries/knobs) · \
|
|
28
|
+
[PyPI](https://pypi.org/project/chumicro-knobs/) · \
|
|
29
|
+
[Bundle](https://github.com/ChuMicro/ChuMicro-Bundle) · \
|
|
30
|
+
[Experimental Bundle](https://github.com/ChuMicro/ChuMicro-Bundle-Experimental)
|
|
31
|
+
|
|
32
|
+
</div>
|