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.
@@ -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>