atech 1.0.0a1__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.
- atech-1.0.0a1/.gitignore +46 -0
- atech-1.0.0a1/CLAUDE.md +194 -0
- atech-1.0.0a1/IMPLEMENTATION_PLAN.md +180 -0
- atech-1.0.0a1/PKG-INFO +248 -0
- atech-1.0.0a1/PRD.md +299 -0
- atech-1.0.0a1/README.md +218 -0
- atech-1.0.0a1/examples/blink_button/README.md +51 -0
- atech-1.0.0a1/examples/blink_button/project.yaml +21 -0
- atech-1.0.0a1/examples/custom_module/README.md +57 -0
- atech-1.0.0a1/examples/custom_module/my_modules/heartbeat/heartbeat.cpp +19 -0
- atech-1.0.0a1/examples/custom_module/my_modules/heartbeat/heartbeat.h +24 -0
- atech-1.0.0a1/examples/custom_module/my_modules/heartbeat/module.yaml +31 -0
- atech-1.0.0a1/examples/custom_module/project.yaml +13 -0
- atech-1.0.0a1/examples/flappy_bird/build_and_flash.py +101 -0
- atech-1.0.0a1/examples/flappy_bird/project.yaml +93 -0
- atech-1.0.0a1/examples/speaker_music/README.md +81 -0
- atech-1.0.0a1/examples/speaker_music/project.yaml +55 -0
- atech-1.0.0a1/pyproject.toml +72 -0
- atech-1.0.0a1/src/atech/__init__.py +71 -0
- atech-1.0.0a1/src/atech/build.py +82 -0
- atech-1.0.0a1/src/atech/catalog/__init__.py +43 -0
- atech-1.0.0a1/src/atech/catalog/data/boards/14port.yaml +181 -0
- atech-1.0.0a1/src/atech/catalog/data/boards/8port.yaml +103 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/aht20/aht20.cpp +131 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/aht20/aht20.h +71 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/aht20/i2c_hardware.cpp +77 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/aht20/i2c_hardware.h +61 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/aht20/i2c_interface.h +186 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/aht20/module.yaml +63 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/button/button.cpp +61 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/button/button.h +38 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/button/module.yaml +44 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/dc_motor/dc_motor.cpp +95 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/dc_motor/dc_motor.h +40 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/dc_motor/module.yaml +49 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/neopixel/module.yaml +50 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/neopixel/neopixel.cpp +96 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/neopixel/neopixel.h +48 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/pir/module.yaml +57 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/pir/pir.cpp +67 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/pir/pir.h +74 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/rotary_encoder/module.yaml +69 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/rotary_encoder/rotary_encoder.cpp +373 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/rotary_encoder/rotary_encoder.h +169 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/speaker/module.yaml +84 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/speaker/speaker.cpp +606 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/speaker/speaker.h +222 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/st7735_tft/module.yaml +68 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/st7735_tft/st7735_tft.cpp +232 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/st7735_tft/st7735_tft.h +165 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/stepper_motor/module.yaml +57 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/stepper_motor/stepper_motor.cpp +88 -0
- atech-1.0.0a1/src/atech/catalog/data/modules/stepper_motor/stepper_motor.h +111 -0
- atech-1.0.0a1/src/atech/catalog/loader.py +154 -0
- atech-1.0.0a1/src/atech/catalog/models.py +164 -0
- atech-1.0.0a1/src/atech/cli.py +248 -0
- atech-1.0.0a1/src/atech/codegen.py +256 -0
- atech-1.0.0a1/src/atech/errors.py +23 -0
- atech-1.0.0a1/src/atech/placement.py +174 -0
- atech-1.0.0a1/src/atech/project.py +367 -0
- atech-1.0.0a1/src/atech/runtime/__init__.py +34 -0
- atech-1.0.0a1/src/atech/runtime/board.py +99 -0
- atech-1.0.0a1/src/atech/runtime/models.py +73 -0
- atech-1.0.0a1/src/atech/runtime/transport.py +287 -0
- atech-1.0.0a1/src/atech/upload.py +76 -0
atech-1.0.0a1/.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
|
|
8
|
+
# Virtualenvs
|
|
9
|
+
.venv/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Build / dist
|
|
14
|
+
build/
|
|
15
|
+
dist/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
*.egg
|
|
18
|
+
|
|
19
|
+
# Tooling caches
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.mypy_cache/
|
|
22
|
+
.ruff_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
htmlcov/
|
|
26
|
+
coverage.xml
|
|
27
|
+
|
|
28
|
+
# IDE
|
|
29
|
+
.vscode/
|
|
30
|
+
.idea/
|
|
31
|
+
.claude/
|
|
32
|
+
*.swp
|
|
33
|
+
|
|
34
|
+
# Secrets — never commit publishing tokens / credentials.
|
|
35
|
+
# Prefer storing the PyPI token in $HOME (~/.pypirc) or a CI secret store,
|
|
36
|
+
# outside the repo entirely. These are a belt-and-suspenders backstop.
|
|
37
|
+
.env
|
|
38
|
+
.env.*
|
|
39
|
+
.pypirc
|
|
40
|
+
*.token
|
|
41
|
+
secrets.yaml
|
|
42
|
+
secrets.yml
|
|
43
|
+
|
|
44
|
+
# OS
|
|
45
|
+
.DS_Store
|
|
46
|
+
Thumbs.db
|
atech-1.0.0a1/CLAUDE.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# CLAUDE.md — writing firmware for an Atech board
|
|
2
|
+
|
|
3
|
+
You are helping a user build firmware for an **Atech motherboard** — a
|
|
4
|
+
small ESP32-S3 board with numbered ports that accept snap-in electrical
|
|
5
|
+
modules. They have the hardware; you write the code.
|
|
6
|
+
|
|
7
|
+
The `atech` Python SDK is the bridge. It picks pins, assembles drivers,
|
|
8
|
+
generates a `main.cpp` and `platformio.ini`, compiles with PlatformIO, and
|
|
9
|
+
flashes the board. **Your job is to write the behavior** — the user
|
|
10
|
+
code block that's spliced into `loop()` (and `setup()` if needed) to make
|
|
11
|
+
the modules do what the user asked.
|
|
12
|
+
|
|
13
|
+
## The 60-second mental model
|
|
14
|
+
|
|
15
|
+
A project is one YAML file:
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
name: my_project
|
|
19
|
+
board: 8port # 8port | 14port
|
|
20
|
+
modules:
|
|
21
|
+
- {id: button, instance: btn, port: 3}
|
|
22
|
+
- {id: neopixel, instance: led, port: 4}
|
|
23
|
+
code:
|
|
24
|
+
setup: |
|
|
25
|
+
// optional — runs once
|
|
26
|
+
loop: |
|
|
27
|
+
// your behavior — runs forever
|
|
28
|
+
if (btn.wasPressed()) {
|
|
29
|
+
led.setAll(255, 0, 0);
|
|
30
|
+
led.show();
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`atech build .` turns that into a working PlatformIO project with all the
|
|
35
|
+
plumbing wired up. `atech upload .` flashes it.
|
|
36
|
+
|
|
37
|
+
## Your workflow when the user asks for something
|
|
38
|
+
|
|
39
|
+
1. **List the catalog first.** Always start here. Don't guess what modules exist:
|
|
40
|
+
```bash
|
|
41
|
+
atech list-modules
|
|
42
|
+
atech list-modules --category sensor
|
|
43
|
+
atech list-boards
|
|
44
|
+
```
|
|
45
|
+
If the user wants a temperature reading and no temperature module is in
|
|
46
|
+
the catalog, say so. **Do not invent modules.**
|
|
47
|
+
|
|
48
|
+
2. **Read each chosen module's manifest** to know the public API:
|
|
49
|
+
```bash
|
|
50
|
+
python -c "from atech import get_module; m = get_module('button'); print(m.usage)"
|
|
51
|
+
```
|
|
52
|
+
Or read `src/atech/catalog/data/modules/<id>/module.yaml` directly. The
|
|
53
|
+
`usage:` field is a short C++ snippet showing every method you can
|
|
54
|
+
call from user code.
|
|
55
|
+
|
|
56
|
+
3. **Pick ports.** On the 8-port board, valid ports are 1, 2, 3, 4, 5, 7
|
|
57
|
+
(6 and 8 are reserved for the reset button and USB-C). Adjacent pairs
|
|
58
|
+
for double-width modules: `[1,2]` and `[3,4]`. Check `get_board(...)`
|
|
59
|
+
for specifics.
|
|
60
|
+
|
|
61
|
+
4. **Write the YAML** with `code.loop` (and `code.setup` if you need
|
|
62
|
+
one-time initialization beyond what each module's `templates.setup`
|
|
63
|
+
already does).
|
|
64
|
+
|
|
65
|
+
5. **Validate before building:**
|
|
66
|
+
```bash
|
|
67
|
+
atech validate .
|
|
68
|
+
```
|
|
69
|
+
If it complains, fix the placement. Reserved ports, overlapping
|
|
70
|
+
modules, and non-adjacent double-width pairs all surface here.
|
|
71
|
+
|
|
72
|
+
6. **Build:**
|
|
73
|
+
```bash
|
|
74
|
+
atech build .
|
|
75
|
+
```
|
|
76
|
+
On success it prints the path to `firmware.bin` and leaves a full
|
|
77
|
+
PlatformIO project under `./build/`.
|
|
78
|
+
|
|
79
|
+
7. **Flash:**
|
|
80
|
+
```bash
|
|
81
|
+
atech upload .
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Writing the `code:` block — the part you actually own
|
|
85
|
+
|
|
86
|
+
Every module placed on the board becomes a C++ global variable at the
|
|
87
|
+
name you gave as `instance`. The `usage:` field in each module manifest
|
|
88
|
+
is your source of truth for what methods exist on that variable.
|
|
89
|
+
|
|
90
|
+
```yaml
|
|
91
|
+
modules:
|
|
92
|
+
- {id: button, instance: btn, port: 3}
|
|
93
|
+
code:
|
|
94
|
+
loop: |
|
|
95
|
+
// `btn` is a `ButtonModule` global. See atech list-modules.
|
|
96
|
+
if (btn.wasPressed()) { /* edge */ }
|
|
97
|
+
if (btn.isPressed()) { /* level */ }
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Things to remember:
|
|
101
|
+
|
|
102
|
+
- **You're writing into a real Arduino `loop()`** — non-blocking only.
|
|
103
|
+
Use `millis()` for timing, never `delay()` longer than a few ms.
|
|
104
|
+
- **`Serial` is wired to USB-CDC** so events you `Serial.println(...)`
|
|
105
|
+
show up in `atech monitor`. The wire format the runtime SDK
|
|
106
|
+
understands is one JSON object per line (see the auto-generated module
|
|
107
|
+
loop snippets for the exact shape).
|
|
108
|
+
- **Don't hand-edit `main.cpp`.** Re-run `atech build`. The generator
|
|
109
|
+
splices your `code:` block in the right place and keeps the project
|
|
110
|
+
reproducible.
|
|
111
|
+
- **Don't hardcode GPIO numbers.** They come from the port assignment.
|
|
112
|
+
If you need a pin number for something exotic, ask the user to use the
|
|
113
|
+
Python API and pull it from `BoardSpec.port(port_id).pins`.
|
|
114
|
+
|
|
115
|
+
## Bringing your own module (rare but supported)
|
|
116
|
+
|
|
117
|
+
If the user has a sensor or actuator no bundled module covers, drop a
|
|
118
|
+
folder next to `project.yaml`:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
my_project/
|
|
122
|
+
├── project.yaml # add `modules_path: ./my_modules` at the top
|
|
123
|
+
└── my_modules/
|
|
124
|
+
└── <id>/
|
|
125
|
+
├── module.yaml # manifest (templates, usage, lib_deps)
|
|
126
|
+
├── <id>.h # driver header
|
|
127
|
+
└── <id>.cpp # driver source
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
See [`examples/custom_module/`](examples/custom_module/) for a working
|
|
131
|
+
example. The manifest schema is identical to the bundled modules —
|
|
132
|
+
copy/paste one and edit.
|
|
133
|
+
|
|
134
|
+
When you write the module's `usage:` block, **think about what the
|
|
135
|
+
*next* agent will need to know to use this module**. Give the typical
|
|
136
|
+
method calls with the `{{ instance }}` placeholder.
|
|
137
|
+
|
|
138
|
+
## Things to NEVER do
|
|
139
|
+
|
|
140
|
+
- ❌ Invent modules that aren't in `atech list-modules`.
|
|
141
|
+
- ❌ Skip `atech validate` and go straight to `atech build`. Physics catches you.
|
|
142
|
+
- ❌ Hand-edit `build/src/main.cpp`. It'll be overwritten on the next build.
|
|
143
|
+
- ❌ Use `delay(1000)` in the loop. Use `millis()` deltas.
|
|
144
|
+
- ❌ Hardcode pin numbers in the `code:` block. Use the instance API.
|
|
145
|
+
- ❌ Reach into the runtime SDK (`atech.runtime.transport`) for raw
|
|
146
|
+
serial bytes. Use `Board.connect()` / `board.events()` / `board.send()`.
|
|
147
|
+
- ❌ Depend on atech.dev or any Atech cloud feature. The SDK is fully
|
|
148
|
+
offline.
|
|
149
|
+
|
|
150
|
+
## Quick reference
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
atech list-boards
|
|
154
|
+
atech list-modules [--category X]
|
|
155
|
+
atech new <name> --board <id> # scaffolds project.yaml
|
|
156
|
+
atech validate [project-dir]
|
|
157
|
+
atech build [project-dir]
|
|
158
|
+
atech upload [project-dir] [--port X]
|
|
159
|
+
atech monitor # stream events after flashing
|
|
160
|
+
atech ports # see candidate USB devices
|
|
161
|
+
atech send <key> <value> # send one action to the board
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from atech import Project, list_modules, get_module, Board
|
|
166
|
+
|
|
167
|
+
# Inspect what's available
|
|
168
|
+
list_modules(category="input")
|
|
169
|
+
get_module("button").usage # the C++ snippet
|
|
170
|
+
|
|
171
|
+
# Assemble
|
|
172
|
+
p = (
|
|
173
|
+
Project(board="8port", name="thing")
|
|
174
|
+
.add("button", port=3, instance="btn")
|
|
175
|
+
.add("neopixel", port=4, instance="led")
|
|
176
|
+
.set_loop("""
|
|
177
|
+
if (btn.wasPressed()) { led.setAll(0, 0, 255); led.show(); }
|
|
178
|
+
""")
|
|
179
|
+
)
|
|
180
|
+
print(p.describe()) # human/agent-readable summary of instances + their APIs
|
|
181
|
+
p.build()
|
|
182
|
+
p.upload()
|
|
183
|
+
|
|
184
|
+
# After flashing — talk to the board
|
|
185
|
+
with Board.connect() as board:
|
|
186
|
+
for ev in board.events():
|
|
187
|
+
print(ev.key, ev.value)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## When in doubt
|
|
191
|
+
|
|
192
|
+
`atech list-modules` is the source of truth for what's buildable today.
|
|
193
|
+
If the user wants something that isn't in there, your job is to *say so
|
|
194
|
+
clearly* and offer to build a custom module — not to fabricate one.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# Atech Python SDK — Implementation Plan
|
|
2
|
+
|
|
3
|
+
Sequenced plan to ship `atech` v1.0 — the comprehensive open SDK described in [PRD.md](PRD.md).
|
|
4
|
+
|
|
5
|
+
## Layout
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
python-sdk/
|
|
9
|
+
├── pyproject.toml
|
|
10
|
+
├── README.md
|
|
11
|
+
├── PRD.md
|
|
12
|
+
├── IMPLEMENTATION_PLAN.md
|
|
13
|
+
├── LICENSE # MIT
|
|
14
|
+
├── CLAUDE.md # how Claude Code uses the SDK
|
|
15
|
+
├── examples/
|
|
16
|
+
│ └── thermometer/ # one end-to-end demo project
|
|
17
|
+
│ ├── project.yaml
|
|
18
|
+
│ └── README.md
|
|
19
|
+
├── src/atech/
|
|
20
|
+
│ ├── __init__.py # top-level re-exports
|
|
21
|
+
│ ├── _version.py
|
|
22
|
+
│ ├── errors.py # SDK-wide exception types
|
|
23
|
+
│ ├── catalog/ # bundled data + loader
|
|
24
|
+
│ │ ├── __init__.py
|
|
25
|
+
│ │ ├── loader.py # load_boards / load_modules from package data
|
|
26
|
+
│ │ ├── models.py # BoardSpec, ModuleSpec, PortSpec, …
|
|
27
|
+
│ │ └── data/
|
|
28
|
+
│ │ ├── boards/
|
|
29
|
+
│ │ │ ├── 8port.yaml
|
|
30
|
+
│ │ │ └── 14port.yaml
|
|
31
|
+
│ │ └── modules/
|
|
32
|
+
│ │ ├── button/
|
|
33
|
+
│ │ │ ├── module.yaml
|
|
34
|
+
│ │ │ ├── button.h
|
|
35
|
+
│ │ │ └── button.cpp
|
|
36
|
+
│ │ ├── neopixel/
|
|
37
|
+
│ │ │ ├── module.yaml
|
|
38
|
+
│ │ │ └── …
|
|
39
|
+
│ │ └── aht20/
|
|
40
|
+
│ │ ├── module.yaml
|
|
41
|
+
│ │ └── …
|
|
42
|
+
│ ├── placement.py # validator + structured issues
|
|
43
|
+
│ ├── project.py # Project class (assembly + YAML)
|
|
44
|
+
│ ├── codegen.py # template stitching → PlatformIO project
|
|
45
|
+
│ ├── build.py # `pio run` wrapper
|
|
46
|
+
│ ├── upload.py # `pio run -t upload` wrapper
|
|
47
|
+
│ ├── runtime/ # v0.1 serial SDK, preserved
|
|
48
|
+
│ │ ├── __init__.py
|
|
49
|
+
│ │ ├── board.py
|
|
50
|
+
│ │ ├── transport.py
|
|
51
|
+
│ │ └── models.py
|
|
52
|
+
│ └── cli.py
|
|
53
|
+
└── tests/
|
|
54
|
+
├── test_catalog.py
|
|
55
|
+
├── test_placement.py
|
|
56
|
+
├── test_project.py
|
|
57
|
+
├── test_codegen.py
|
|
58
|
+
├── test_runtime_models.py
|
|
59
|
+
├── test_runtime_transport.py
|
|
60
|
+
└── test_runtime_board.py
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Phases
|
|
64
|
+
|
|
65
|
+
### Phase 0 — Repackage (no behavior change)
|
|
66
|
+
- Move `src/atech/{board,transport,models}.py` → `src/atech/runtime/`.
|
|
67
|
+
- Top-level `atech/__init__.py` re-exports the runtime symbols at the old paths for one minor version (`atech.Board`, `atech.Event`, etc.) so the v0.1 API stays callable while internals reorganize.
|
|
68
|
+
- Move corresponding tests under `tests/` to `test_runtime_*.py`.
|
|
69
|
+
|
|
70
|
+
### Phase 1 — Catalog
|
|
71
|
+
- `catalog/models.py`: Pydantic models for `BoardSpec`, `PortSpec`, `AdjacentPair`, `ModuleSpec`, `ModuleTemplates`, `EventDecl`, `ActionDecl`. Mirrors the existing `motherboard/board.yaml` and the new per-module YAML.
|
|
72
|
+
- `catalog/loader.py`: `load_boards()` and `load_modules()` read from package data via `importlib.resources`. Cache results.
|
|
73
|
+
- `catalog/data/boards/8port.yaml`: copied from `backend/motherboard/board.yaml`, trimmed to just the 8-port entry. Reformatted so the SDK loader matches the schema.
|
|
74
|
+
- `catalog/data/modules/<id>/module.yaml`: new schema (see §"Module manifest schema" below). Seed with three modules whose drivers already exist in the backend:
|
|
75
|
+
- `button` — single GPIO input, size=1
|
|
76
|
+
- `neopixel` — single GPIO output, size=1
|
|
77
|
+
- `aht20` — I²C sensor, size=1
|
|
78
|
+
- Each module bundles its `.h` / `.cpp` files copied directly from `backend/src/lib/athera_modules/esp32/modules/`.
|
|
79
|
+
|
|
80
|
+
### Phase 2 — Placement validator
|
|
81
|
+
- `placement.py`: pure function `validate(board: BoardSpec, placements: list[Placement]) -> list[PlacementIssue]`.
|
|
82
|
+
- Issue types: `ReservedPort`, `UnknownPort`, `Overlap`, `NotAdjacent` (double-width module), `MissingSecondaryPort`, `IncompatibleInterface`.
|
|
83
|
+
- Ported from the backend's `placement_optimizer.py` but pared back to validation only — the open SDK does *not* auto-correct or solve, it reports.
|
|
84
|
+
|
|
85
|
+
### Phase 3 — Project
|
|
86
|
+
- `project.py`: dataclass-style `Project` with `add` / `remove` / `validate` / `generate` / `build` / `upload` / `to_yaml` / `from_yaml`.
|
|
87
|
+
- Project state on disk is a single `project.yaml`:
|
|
88
|
+
```yaml
|
|
89
|
+
name: weather
|
|
90
|
+
board: 8port
|
|
91
|
+
modules:
|
|
92
|
+
- id: aht20
|
|
93
|
+
instance: aht20_1
|
|
94
|
+
port: 1
|
|
95
|
+
- id: neopixel
|
|
96
|
+
instance: status_led
|
|
97
|
+
port: 3
|
|
98
|
+
```
|
|
99
|
+
- `Project.add` accepts either an int (single port) or a tuple (double-width); raises if the placement fails validation.
|
|
100
|
+
|
|
101
|
+
### Phase 4 — Codegen
|
|
102
|
+
- `codegen.py`: takes a validated `Project`, returns the in-memory representation of the PlatformIO tree, and can write it to disk.
|
|
103
|
+
- Per-module Jinja2 templates from the manifest produce three fragments: `globals`, `setup`, `loop`. The codegen splices them into a single `main.cpp` skeleton. No LLM, no regex post-processing — keep it dumb on purpose.
|
|
104
|
+
- `platformio.ini` emitted from `BoardSpec.platformio_env` + accumulated `lib_deps` from every selected module.
|
|
105
|
+
- Drivers (`.h` / `.cpp`) are copied per-instance into `lib/atech_<module>/` so PlatformIO's library finder picks them up.
|
|
106
|
+
- Pin assignments come from `BoardSpec.ports[port_id].pins` and are exposed to templates as `{{ pin_a }}`, `{{ pin_b }}`, `{{ instance_name }}`.
|
|
107
|
+
|
|
108
|
+
### Phase 5 — Build / upload
|
|
109
|
+
- `build.py`: `run_build(project_dir) -> BuildResult` shells out to `pio run` (using the bundled PlatformIO from the venv). Captures stdout/stderr; surfaces the path to `firmware.bin` on success.
|
|
110
|
+
- `upload.py`: `run_upload(project_dir, port=None) -> UploadResult` shells out to `pio run -t upload --upload-port <port>`. If `port` is None, calls `runtime.discover_ports()` and picks the first match.
|
|
111
|
+
- Both stream PlatformIO output to the user's terminal in CLI mode; return logs as strings in library mode.
|
|
112
|
+
|
|
113
|
+
### Phase 6 — CLI
|
|
114
|
+
Subcommands:
|
|
115
|
+
- `atech ports` *(preserved)*
|
|
116
|
+
- `atech monitor` *(preserved)*
|
|
117
|
+
- `atech send` *(preserved)*
|
|
118
|
+
- `atech list-boards`
|
|
119
|
+
- `atech list-modules [--category X]`
|
|
120
|
+
- `atech new <name> --board <id>`
|
|
121
|
+
- `atech validate <project-dir>`
|
|
122
|
+
- `atech build <project-dir>`
|
|
123
|
+
- `atech upload <project-dir> [--port X]`
|
|
124
|
+
|
|
125
|
+
### Phase 7 — Tests
|
|
126
|
+
- All catalog / placement / project / codegen tests use the bundled data + `tmp_path` + string comparisons. No PlatformIO or hardware required.
|
|
127
|
+
- Build/upload tests are gated on `ATECH_INTEGRATION=1` and a connected board.
|
|
128
|
+
- Runtime tests stay against `MockTransport`.
|
|
129
|
+
|
|
130
|
+
### Phase 8 — Docs
|
|
131
|
+
- README: install, quick-start, the Claude Code story, links to PRD.
|
|
132
|
+
- CLAUDE.md: a short, deliberate cheatsheet for Claude Code in a user's repo — "to build a hardware project, do this".
|
|
133
|
+
- `examples/thermometer/`: a 30-second demo that gets a user from clone → flashed firmware.
|
|
134
|
+
|
|
135
|
+
## Module manifest schema (Phase 1 deliverable)
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
id: aht20
|
|
139
|
+
name: "Temperature & Humidity Sensor (AHT20)"
|
|
140
|
+
category: sensor # sensor | input | display | actuator | led | connectivity | audio
|
|
141
|
+
size: 1 # 1 or 2 ports
|
|
142
|
+
interface: i2c # i2c | gpio | analog | uart
|
|
143
|
+
i2c_address: 0x38 # only when interface=i2c
|
|
144
|
+
driver:
|
|
145
|
+
header: aht20.h
|
|
146
|
+
source: aht20.cpp
|
|
147
|
+
class_name: AHT20
|
|
148
|
+
templates:
|
|
149
|
+
globals: |
|
|
150
|
+
AHT20 {{ instance }};
|
|
151
|
+
setup: |
|
|
152
|
+
{{ instance }}.begin({{ pin_a }}, {{ pin_b }});
|
|
153
|
+
loop: |
|
|
154
|
+
if (millis() - {{ instance }}_last >= 1000) {
|
|
155
|
+
{{ instance }}_last = millis();
|
|
156
|
+
float t = {{ instance }}.readTemperature();
|
|
157
|
+
Serial.printf("{\"type\":\"event\",\"payload\":{\"event_type\":\"sensor\",\"key\":\"temperature\",\"value\":%.2f,\"source\":\"aht20\"}}\n", t);
|
|
158
|
+
}
|
|
159
|
+
globals_extra: |
|
|
160
|
+
unsigned long {{ instance }}_last = 0;
|
|
161
|
+
events:
|
|
162
|
+
- key: temperature
|
|
163
|
+
type: sensor
|
|
164
|
+
value_type: float
|
|
165
|
+
unit: "°C"
|
|
166
|
+
description: "Ambient temperature in Celsius"
|
|
167
|
+
actions: []
|
|
168
|
+
lib_deps: [] # PlatformIO dep strings; empty when bundled driver is self-contained
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Out for v1
|
|
172
|
+
- Module-authoring framework (custom modules outside the bundled catalog).
|
|
173
|
+
- AI module selection.
|
|
174
|
+
- Async runtime API.
|
|
175
|
+
- WiFi / BLE transports.
|
|
176
|
+
- MCP server (P2 in the PRD; can ship in 1.1).
|
|
177
|
+
|
|
178
|
+
## Hard dependencies
|
|
179
|
+
- **PlatformIO** is a runtime dep declared in `pyproject.toml` so `pip install atech` brings it in. Users still need USB-UART drivers on Windows for boards that don't use native ESP32-S3 USB.
|
|
180
|
+
- **Backend firmware modules** (`backend/src/lib/athera_modules/`) are the source of the bundled drivers. M1 copies a subset; M4 should track the backend catalog 1:1.
|
atech-1.0.0a1/PKG-INFO
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: atech
|
|
3
|
+
Version: 1.0.0a1
|
|
4
|
+
Summary: Open-source Python SDK for Atech motherboards: catalog, codegen, build, flash, and runtime control.
|
|
5
|
+
Project-URL: Homepage, https://atech.dev
|
|
6
|
+
Project-URL: Repository, https://github.com/atechdotdev/python-sdk
|
|
7
|
+
Project-URL: Documentation, https://github.com/atechdotdev/python-sdk#readme
|
|
8
|
+
Author: Atech
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: atech,claude-code,esp32,firmware,hardware,iot,platformio,serial
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
19
|
+
Classifier: Topic :: System :: Hardware
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: jinja2>=3.1
|
|
22
|
+
Requires-Dist: platformio>=6.1
|
|
23
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
24
|
+
Requires-Dist: pyserial>=3.5
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# atech
|
|
32
|
+
|
|
33
|
+
**The open-source way to write firmware for [Atech](https://atech.dev)
|
|
34
|
+
boards — designed for humans + coding agents working together.**
|
|
35
|
+
|
|
36
|
+
You have an Atech motherboard and you want it to do something. With this
|
|
37
|
+
SDK installed, you can ask Claude Code (or any coding agent that can run
|
|
38
|
+
Python) — *"make my board light up red when the button is pressed"* — and
|
|
39
|
+
the agent has everything it needs: a catalog of available modules, the
|
|
40
|
+
public API of each driver, deterministic codegen, a real compiler, and
|
|
41
|
+
an upload command. No cloud account, no AI prompts in the SDK, no
|
|
42
|
+
proprietary glue.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install atech
|
|
48
|
+
# or
|
|
49
|
+
uv add atech
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
PlatformIO is pulled in as a dep, so a single install builds and flashes.
|
|
53
|
+
|
|
54
|
+
## What it gives a coding agent
|
|
55
|
+
|
|
56
|
+
A coding agent working in a user's repo gets four things:
|
|
57
|
+
|
|
58
|
+
1. **A discoverable catalog** of bundled modules with rich metadata.
|
|
59
|
+
```bash
|
|
60
|
+
atech list-modules
|
|
61
|
+
atech list-modules --category sensor
|
|
62
|
+
python -c "from atech import get_module; print(get_module('button').usage)"
|
|
63
|
+
```
|
|
64
|
+
Each module's `usage:` field is a short C++ snippet showing the public
|
|
65
|
+
API — no need for the agent to grep through driver headers.
|
|
66
|
+
|
|
67
|
+
2. **A small declarative project format** (`project.yaml`) that the agent
|
|
68
|
+
writes or edits directly:
|
|
69
|
+
```yaml
|
|
70
|
+
name: my_thing
|
|
71
|
+
board: 8port
|
|
72
|
+
modules:
|
|
73
|
+
- {id: button, instance: btn, port: 3}
|
|
74
|
+
- {id: neopixel, instance: led, port: 4}
|
|
75
|
+
code:
|
|
76
|
+
loop: |
|
|
77
|
+
if (btn.wasPressed()) { led.setAll(255, 0, 0); led.show(); }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
3. **Mechanical, deterministic codegen.** `atech build` turns the YAML
|
|
81
|
+
into a vanilla PlatformIO project, compiles it, and produces a
|
|
82
|
+
`firmware.bin`. No LLM in this path — the agent owns the behavior,
|
|
83
|
+
the SDK owns the plumbing.
|
|
84
|
+
|
|
85
|
+
4. **An upload + monitor flow** that doesn't require the user to know
|
|
86
|
+
PlatformIO internals: `atech upload` / `atech monitor`.
|
|
87
|
+
|
|
88
|
+
## 60-second example
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
atech new thermo --board 8port
|
|
92
|
+
cd thermo
|
|
93
|
+
# Tell Claude (or write yourself):
|
|
94
|
+
# "edit project.yaml to add a button on port 3 and a neopixel on port 4,
|
|
95
|
+
# and in code.loop, cycle the neopixel through six colours on each press"
|
|
96
|
+
atech validate .
|
|
97
|
+
atech build .
|
|
98
|
+
atech upload .
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or, fully programmatic:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from atech import Project
|
|
105
|
+
|
|
106
|
+
(
|
|
107
|
+
Project(board="8port", name="thermo")
|
|
108
|
+
.add("button", port=3, instance="btn")
|
|
109
|
+
.add("neopixel", port=4, instance="led")
|
|
110
|
+
.set_loop("""
|
|
111
|
+
if (btn.wasPressed()) {
|
|
112
|
+
static uint8_t hue = 0;
|
|
113
|
+
hue += 60;
|
|
114
|
+
led.setAll(hue, 0, 255 - hue);
|
|
115
|
+
led.show();
|
|
116
|
+
}
|
|
117
|
+
""")
|
|
118
|
+
.build()
|
|
119
|
+
.upload()
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Bringing your own module
|
|
124
|
+
|
|
125
|
+
If the bundled catalog doesn't cover what the user has, drop a folder
|
|
126
|
+
next to `project.yaml`:
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
my_project/
|
|
130
|
+
├── project.yaml # add `modules_path: ./my_modules`
|
|
131
|
+
└── my_modules/
|
|
132
|
+
└── lidar_v2/
|
|
133
|
+
├── module.yaml # id, templates, usage, lib_deps
|
|
134
|
+
├── lidar_v2.h
|
|
135
|
+
└── lidar_v2.cpp
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Same shape as the SDK's
|
|
139
|
+
[`src/atech/catalog/data/modules/`](src/atech/catalog/data/modules/) tree.
|
|
140
|
+
The SDK merges your modules with the bundled catalog at build time; you
|
|
141
|
+
never have to fork. See [`examples/custom_module/`](examples/custom_module/)
|
|
142
|
+
for a working example.
|
|
143
|
+
|
|
144
|
+
## Examples
|
|
145
|
+
|
|
146
|
+
- [`examples/blink_button/`](examples/blink_button/) — minimal user
|
|
147
|
+
behavior hook: a button cycles an LED grid through colours.
|
|
148
|
+
- [`examples/speaker_music/`](examples/speaker_music/) — write your own
|
|
149
|
+
music: note-by-note melodies, RTTTL ringtone strings, and chords on the
|
|
150
|
+
`speaker` module.
|
|
151
|
+
- [`examples/custom_module/`](examples/custom_module/) — a user-authored
|
|
152
|
+
module (`heartbeat`) placed in a sibling folder, then used by the project.
|
|
153
|
+
|
|
154
|
+
All build end-to-end with `atech build .`.
|
|
155
|
+
|
|
156
|
+
## The full Python API
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
import atech
|
|
160
|
+
|
|
161
|
+
# Discovery — what the agent should call first
|
|
162
|
+
atech.list_boards()
|
|
163
|
+
atech.list_modules(category="sensor")
|
|
164
|
+
atech.get_board("8port")
|
|
165
|
+
atech.get_module("button").usage # C++ snippet showing the public API
|
|
166
|
+
|
|
167
|
+
# Project assembly
|
|
168
|
+
p = atech.Project(board="8port", name="weather")
|
|
169
|
+
p.add("button", port=3, instance="btn")
|
|
170
|
+
p.add("dc_motor", ports=(1, 2)) # double-width = adjacent pair
|
|
171
|
+
p.set_setup("Serial.println(\"booting\");")
|
|
172
|
+
p.set_loop("if (btn.wasPressed()) { Serial.println(\"hi\"); }")
|
|
173
|
+
p.use_modules_from("./my_modules") # custom catalog directory
|
|
174
|
+
p.validate() # -> list[PlacementIssue]
|
|
175
|
+
p.describe() # human/agent summary + per-module usage
|
|
176
|
+
p.generate(out_dir="./build") # -> Path to PlatformIO tree
|
|
177
|
+
p.build() # -> BuildResult
|
|
178
|
+
p.upload(port="/dev/ttyACM0") # -> UploadResult
|
|
179
|
+
p.to_yaml(); atech.Project.from_yaml(text); atech.Project.load(path)
|
|
180
|
+
|
|
181
|
+
# Runtime — after flashing
|
|
182
|
+
with atech.Board.connect() as board:
|
|
183
|
+
for ev in board.events():
|
|
184
|
+
print(ev.key, ev.value)
|
|
185
|
+
board.send("led", {"r": 255, "g": 0, "b": 0})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## CLI
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
atech list-boards
|
|
192
|
+
atech list-modules [--category X]
|
|
193
|
+
atech new <name> --board <id> # scaffolds project.yaml + README
|
|
194
|
+
atech validate [project-dir]
|
|
195
|
+
atech build [project-dir]
|
|
196
|
+
atech upload [project-dir] [--port X]
|
|
197
|
+
|
|
198
|
+
atech ports # candidate USB devices
|
|
199
|
+
atech monitor [--port X] [--type T,T] [--key K,K] [--json]
|
|
200
|
+
atech send <key> <value> [--port X]
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## How a coding agent should drive it
|
|
204
|
+
|
|
205
|
+
[`CLAUDE.md`](CLAUDE.md) — drop a copy into your project root and your
|
|
206
|
+
coding agent will follow it. It covers: how to discover modules, how to
|
|
207
|
+
write the `code:` block, what *not* to do (invent modules, skip
|
|
208
|
+
validation, hand-edit generated files, use `delay()` in the loop).
|
|
209
|
+
|
|
210
|
+
## Architecture (short version)
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
┌────────────────────────────────────────────────────────────────────┐
|
|
214
|
+
│ atech.Project ← agent assembles a hardware spec │
|
|
215
|
+
├────────────────────────────────────────────────────────────────────┤
|
|
216
|
+
│ atech.placement ← validates adjacency / reserved / overlap │
|
|
217
|
+
│ atech.catalog ← bundled + user-authored modules │
|
|
218
|
+
│ atech.codegen ← deterministic template stitching │
|
|
219
|
+
│ atech.build / upload ← `pio run` wrappers │
|
|
220
|
+
├────────────────────────────────────────────────────────────────────┤
|
|
221
|
+
│ atech.runtime.Board ← serial client for the flashed board │
|
|
222
|
+
└────────────────────────────────────────────────────────────────────┘
|
|
223
|
+
│ │
|
|
224
|
+
PlatformIO pyserial
|
|
225
|
+
│ │
|
|
226
|
+
ESP32 flash ESP32 USB-CDC
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The closed Atech hosted product imports this SDK and layers AI module
|
|
230
|
+
selection, OTA, sharing, and signing on top. The open SDK is everything
|
|
231
|
+
needed to build firmware locally; nothing in this package phones home.
|
|
232
|
+
|
|
233
|
+
## Status
|
|
234
|
+
|
|
235
|
+
`v1.0.0a1` — alpha. Two boards (`8port`, `14port`) and nine bundled
|
|
236
|
+
modules: `button`, `rotary_encoder`, `pir` (motion), `aht20`
|
|
237
|
+
(temp/humidity), `neopixel`, `st7735_tft` (colour display), `speaker`
|
|
238
|
+
(I2S audio), `dc_motor`, and `stepper_motor`. More ship through the
|
|
239
|
+
catalog as they're ported in — run `atech list-modules` for the live list.
|
|
240
|
+
The runtime serial API is stable and preserves the v0.1.x surface under
|
|
241
|
+
`atech.runtime`.
|
|
242
|
+
|
|
243
|
+
See [`PRD.md`](PRD.md) and [`IMPLEMENTATION_PLAN.md`](IMPLEMENTATION_PLAN.md)
|
|
244
|
+
for the longer story.
|
|
245
|
+
|
|
246
|
+
## License
|
|
247
|
+
|
|
248
|
+
MIT.
|