atech 1.0.0a1__py3-none-any.whl
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/__init__.py +71 -0
- atech/build.py +82 -0
- atech/catalog/__init__.py +43 -0
- atech/catalog/data/boards/14port.yaml +181 -0
- atech/catalog/data/boards/8port.yaml +103 -0
- atech/catalog/data/modules/aht20/aht20.cpp +131 -0
- atech/catalog/data/modules/aht20/aht20.h +71 -0
- atech/catalog/data/modules/aht20/i2c_hardware.cpp +77 -0
- atech/catalog/data/modules/aht20/i2c_hardware.h +61 -0
- atech/catalog/data/modules/aht20/i2c_interface.h +186 -0
- atech/catalog/data/modules/aht20/module.yaml +63 -0
- atech/catalog/data/modules/button/button.cpp +61 -0
- atech/catalog/data/modules/button/button.h +38 -0
- atech/catalog/data/modules/button/module.yaml +44 -0
- atech/catalog/data/modules/dc_motor/dc_motor.cpp +95 -0
- atech/catalog/data/modules/dc_motor/dc_motor.h +40 -0
- atech/catalog/data/modules/dc_motor/module.yaml +49 -0
- atech/catalog/data/modules/neopixel/module.yaml +50 -0
- atech/catalog/data/modules/neopixel/neopixel.cpp +96 -0
- atech/catalog/data/modules/neopixel/neopixel.h +48 -0
- atech/catalog/data/modules/pir/module.yaml +57 -0
- atech/catalog/data/modules/pir/pir.cpp +67 -0
- atech/catalog/data/modules/pir/pir.h +74 -0
- atech/catalog/data/modules/rotary_encoder/module.yaml +69 -0
- atech/catalog/data/modules/rotary_encoder/rotary_encoder.cpp +373 -0
- atech/catalog/data/modules/rotary_encoder/rotary_encoder.h +169 -0
- atech/catalog/data/modules/speaker/module.yaml +84 -0
- atech/catalog/data/modules/speaker/speaker.cpp +606 -0
- atech/catalog/data/modules/speaker/speaker.h +222 -0
- atech/catalog/data/modules/st7735_tft/module.yaml +68 -0
- atech/catalog/data/modules/st7735_tft/st7735_tft.cpp +232 -0
- atech/catalog/data/modules/st7735_tft/st7735_tft.h +165 -0
- atech/catalog/data/modules/stepper_motor/module.yaml +57 -0
- atech/catalog/data/modules/stepper_motor/stepper_motor.cpp +88 -0
- atech/catalog/data/modules/stepper_motor/stepper_motor.h +111 -0
- atech/catalog/loader.py +154 -0
- atech/catalog/models.py +164 -0
- atech/cli.py +248 -0
- atech/codegen.py +256 -0
- atech/errors.py +23 -0
- atech/placement.py +174 -0
- atech/project.py +367 -0
- atech/runtime/__init__.py +34 -0
- atech/runtime/board.py +99 -0
- atech/runtime/models.py +73 -0
- atech/runtime/transport.py +287 -0
- atech/upload.py +76 -0
- atech-1.0.0a1.dist-info/METADATA +248 -0
- atech-1.0.0a1.dist-info/RECORD +51 -0
- atech-1.0.0a1.dist-info/WHEEL +4 -0
- atech-1.0.0a1.dist-info/entry_points.txt +2 -0
atech/__init__.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Atech SDK — open-source Python interface to Atech motherboards.
|
|
2
|
+
|
|
3
|
+
Two complementary halves:
|
|
4
|
+
|
|
5
|
+
* **Build** — assemble a project, validate placement, generate firmware,
|
|
6
|
+
compile with PlatformIO, flash to a board. See :class:`Project`.
|
|
7
|
+
* **Runtime** — talk to a flashed board over USB serial. See
|
|
8
|
+
:class:`atech.runtime.Board` (also re-exported as :class:`Board` here).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from atech.build import BuildResult
|
|
12
|
+
from atech.catalog import (
|
|
13
|
+
BoardSpec,
|
|
14
|
+
ModuleSpec,
|
|
15
|
+
get_board,
|
|
16
|
+
get_module,
|
|
17
|
+
list_boards,
|
|
18
|
+
list_modules,
|
|
19
|
+
)
|
|
20
|
+
from atech.errors import (
|
|
21
|
+
AtechError,
|
|
22
|
+
BuildError,
|
|
23
|
+
CatalogError,
|
|
24
|
+
PlacementError,
|
|
25
|
+
UploadError,
|
|
26
|
+
)
|
|
27
|
+
from atech.placement import Placement, PlacementIssue
|
|
28
|
+
from atech.project import Project, ProjectModule
|
|
29
|
+
from atech.runtime import (
|
|
30
|
+
Action,
|
|
31
|
+
Board,
|
|
32
|
+
Event,
|
|
33
|
+
MockTransport,
|
|
34
|
+
NoBoardFoundError,
|
|
35
|
+
SerialTransport,
|
|
36
|
+
discover_ports,
|
|
37
|
+
)
|
|
38
|
+
from atech.upload import UploadResult
|
|
39
|
+
|
|
40
|
+
__version__ = "1.0.0a1"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Build half
|
|
44
|
+
"Project",
|
|
45
|
+
"ProjectModule",
|
|
46
|
+
"Placement",
|
|
47
|
+
"PlacementIssue",
|
|
48
|
+
"BoardSpec",
|
|
49
|
+
"ModuleSpec",
|
|
50
|
+
"BuildResult",
|
|
51
|
+
"UploadResult",
|
|
52
|
+
"list_boards",
|
|
53
|
+
"list_modules",
|
|
54
|
+
"get_board",
|
|
55
|
+
"get_module",
|
|
56
|
+
# Runtime half
|
|
57
|
+
"Board",
|
|
58
|
+
"Event",
|
|
59
|
+
"Action",
|
|
60
|
+
"SerialTransport",
|
|
61
|
+
"MockTransport",
|
|
62
|
+
"discover_ports",
|
|
63
|
+
"NoBoardFoundError",
|
|
64
|
+
# Errors
|
|
65
|
+
"AtechError",
|
|
66
|
+
"BuildError",
|
|
67
|
+
"CatalogError",
|
|
68
|
+
"PlacementError",
|
|
69
|
+
"UploadError",
|
|
70
|
+
"__version__",
|
|
71
|
+
]
|
atech/build.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""PlatformIO build wrapper.
|
|
2
|
+
|
|
3
|
+
Shells out to ``pio run`` in the given project directory. Captures the build
|
|
4
|
+
artifact path on success and surfaces logs on failure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
from atech.errors import BuildError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class BuildResult:
|
|
20
|
+
"""Outcome of a PlatformIO build."""
|
|
21
|
+
|
|
22
|
+
project_dir: Path
|
|
23
|
+
success: bool
|
|
24
|
+
firmware_path: Optional[Path]
|
|
25
|
+
stdout: str
|
|
26
|
+
stderr: str
|
|
27
|
+
returncode: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _pio_executable() -> str:
|
|
31
|
+
exe = shutil.which("pio") or shutil.which("platformio")
|
|
32
|
+
if not exe:
|
|
33
|
+
raise BuildError(
|
|
34
|
+
"PlatformIO CLI not found on PATH.\n"
|
|
35
|
+
"\n"
|
|
36
|
+
"PlatformIO is bundled with `atech` — installing the SDK installs it too,\n"
|
|
37
|
+
"so this usually means atech was installed into a different environment\n"
|
|
38
|
+
"than the one you're running from. Fixes, in order of likelihood:\n"
|
|
39
|
+
" 1. Activate the venv where you installed atech, then retry.\n"
|
|
40
|
+
" 2. Reinstall into the current environment: pip install --force-reinstall atech\n"
|
|
41
|
+
" 3. Install PlatformIO directly: pip install platformio\n"
|
|
42
|
+
"\n"
|
|
43
|
+
"Verify with: python -c \"import platformio; print(platformio.__version__)\""
|
|
44
|
+
)
|
|
45
|
+
return exe
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def run_build(project_dir: Path, *, capture: bool = True) -> BuildResult:
|
|
49
|
+
"""Run ``pio run`` in ``project_dir`` and return the result."""
|
|
50
|
+
project_dir = Path(project_dir).expanduser().resolve()
|
|
51
|
+
if not (project_dir / "platformio.ini").is_file():
|
|
52
|
+
raise BuildError(
|
|
53
|
+
f"no platformio.ini at {project_dir}. Run Project.generate() first."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
cmd = [_pio_executable(), "run", "-d", str(project_dir)]
|
|
57
|
+
proc = subprocess.run(
|
|
58
|
+
cmd,
|
|
59
|
+
capture_output=capture,
|
|
60
|
+
text=True,
|
|
61
|
+
)
|
|
62
|
+
firmware = _locate_firmware(project_dir) if proc.returncode == 0 else None
|
|
63
|
+
return BuildResult(
|
|
64
|
+
project_dir=project_dir,
|
|
65
|
+
success=proc.returncode == 0,
|
|
66
|
+
firmware_path=firmware,
|
|
67
|
+
stdout=proc.stdout or "",
|
|
68
|
+
stderr=proc.stderr or "",
|
|
69
|
+
returncode=proc.returncode,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _locate_firmware(project_dir: Path) -> Optional[Path]:
|
|
74
|
+
"""Find the firmware.bin PlatformIO emitted, if any."""
|
|
75
|
+
build_root = project_dir / ".pio" / "build"
|
|
76
|
+
if not build_root.is_dir():
|
|
77
|
+
return None
|
|
78
|
+
for env_dir in build_root.iterdir():
|
|
79
|
+
candidate = env_dir / "firmware.bin"
|
|
80
|
+
if candidate.is_file():
|
|
81
|
+
return candidate
|
|
82
|
+
return None
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Bundled catalog of Atech boards and modules."""
|
|
2
|
+
|
|
3
|
+
from atech.catalog.loader import (
|
|
4
|
+
get_board,
|
|
5
|
+
get_module,
|
|
6
|
+
iter_module_files,
|
|
7
|
+
list_boards,
|
|
8
|
+
list_modules,
|
|
9
|
+
load_boards,
|
|
10
|
+
load_modules,
|
|
11
|
+
load_modules_from,
|
|
12
|
+
module_dir,
|
|
13
|
+
)
|
|
14
|
+
from atech.catalog.models import (
|
|
15
|
+
ActionDecl,
|
|
16
|
+
BoardSpec,
|
|
17
|
+
EventDecl,
|
|
18
|
+
ModuleDriver,
|
|
19
|
+
ModuleSpec,
|
|
20
|
+
ModuleTemplates,
|
|
21
|
+
Pin,
|
|
22
|
+
PortSpec,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"ActionDecl",
|
|
27
|
+
"BoardSpec",
|
|
28
|
+
"EventDecl",
|
|
29
|
+
"ModuleDriver",
|
|
30
|
+
"ModuleSpec",
|
|
31
|
+
"ModuleTemplates",
|
|
32
|
+
"Pin",
|
|
33
|
+
"PortSpec",
|
|
34
|
+
"get_board",
|
|
35
|
+
"get_module",
|
|
36
|
+
"iter_module_files",
|
|
37
|
+
"list_boards",
|
|
38
|
+
"list_modules",
|
|
39
|
+
"load_boards",
|
|
40
|
+
"load_modules",
|
|
41
|
+
"load_modules_from",
|
|
42
|
+
"module_dir",
|
|
43
|
+
]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Atech 14-Port motherboard
|
|
2
|
+
#
|
|
3
|
+
# Portrait board with three columns of slots. Left column (ports 1-6) and
|
|
4
|
+
# right column (ports 9-14) run top→bottom; a single isolated slot (port 7)
|
|
5
|
+
# sits top-middle. The Reset button (port 12) and USB-C jack (port 8) are
|
|
6
|
+
# reserved and break adjacency in their columns.
|
|
7
|
+
#
|
|
8
|
+
# Sourced from backend/motherboard/board.yaml (14port_board).
|
|
9
|
+
|
|
10
|
+
id: 14port
|
|
11
|
+
name: "Atech 14-Port Board"
|
|
12
|
+
|
|
13
|
+
microcontroller:
|
|
14
|
+
type: esp32
|
|
15
|
+
variant: ESP32-S3
|
|
16
|
+
flash_size_kb: 8192
|
|
17
|
+
ram_size_kb: 512
|
|
18
|
+
|
|
19
|
+
platformio_env: esp32-s3-devkitc-1
|
|
20
|
+
platformio_framework: arduino
|
|
21
|
+
|
|
22
|
+
# Reserved port ids that exist on the layout but cannot host a module.
|
|
23
|
+
reserved:
|
|
24
|
+
- port_8 # USB-C jack (bottom-middle)
|
|
25
|
+
- port_12 # Reset button (right column)
|
|
26
|
+
|
|
27
|
+
# 2D physical layout (matches the frontend renderer). Numbers are slot ids
|
|
28
|
+
# (one-based), "" is a physical gap, text labels are reserved positions.
|
|
29
|
+
# Portrait orientation: port_7 is top-middle, USB-C (port_8) is bottom-middle.
|
|
30
|
+
layout:
|
|
31
|
+
- ["9", "10", "11", "Restart", "13", "14"] # right column, top→bottom
|
|
32
|
+
- ["7", "", "", "", "", "USB-C"] # middle: 7 top, USB-C bottom
|
|
33
|
+
- ["1", "2", "3", "4", "5", "6"] # left column, top→bottom
|
|
34
|
+
|
|
35
|
+
notes: |
|
|
36
|
+
12 module slots in 3 columns: left (1-6), middle (just port_7), right
|
|
37
|
+
(9-14). Top edge ports: 1, 7, 9. Bottom edge ports: 6, 14. Corners:
|
|
38
|
+
top-left=port_1, top-right=port_9, bottom-left=port_6, bottom-right=port_14.
|
|
39
|
+
Port_7 is isolated in the middle column with NO adjacent slots — ideal for
|
|
40
|
+
a single central component (main display, primary sensor, IMU). For
|
|
41
|
+
4-wheeled vehicles, place motors at the four corner ports. Double-width
|
|
42
|
+
modules (size=2) must use one adjacent pair: left side [1,2], [2,3], [3,4],
|
|
43
|
+
[4,5], [5,6]; right side [9,10], [10,11], [13,14]. The Reset button at
|
|
44
|
+
port_12 breaks adjacency between ports 11 and 13.
|
|
45
|
+
|
|
46
|
+
ports:
|
|
47
|
+
- id: port_1
|
|
48
|
+
name: "Port 1"
|
|
49
|
+
type: universal
|
|
50
|
+
slot_number: 1
|
|
51
|
+
side: left
|
|
52
|
+
pins:
|
|
53
|
+
- {gpio: 9, function: signal}
|
|
54
|
+
- {gpio: 8, function: signal_b}
|
|
55
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
56
|
+
|
|
57
|
+
- id: port_2
|
|
58
|
+
name: "Port 2"
|
|
59
|
+
type: universal
|
|
60
|
+
slot_number: 2
|
|
61
|
+
side: left
|
|
62
|
+
pins:
|
|
63
|
+
- {gpio: 5, function: signal}
|
|
64
|
+
- {gpio: 4, function: signal_b}
|
|
65
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
66
|
+
|
|
67
|
+
- id: port_3
|
|
68
|
+
name: "Port 3"
|
|
69
|
+
type: universal
|
|
70
|
+
slot_number: 3
|
|
71
|
+
side: left
|
|
72
|
+
pins:
|
|
73
|
+
- {gpio: 17, function: signal}
|
|
74
|
+
- {gpio: 18, function: signal_b}
|
|
75
|
+
compatible_interfaces: [i2c, digital, gpio, uart]
|
|
76
|
+
|
|
77
|
+
- id: port_4
|
|
78
|
+
name: "Port 4"
|
|
79
|
+
type: universal
|
|
80
|
+
slot_number: 4
|
|
81
|
+
side: left
|
|
82
|
+
pins:
|
|
83
|
+
- {gpio: 16, function: signal}
|
|
84
|
+
- {gpio: 15, function: signal_b}
|
|
85
|
+
compatible_interfaces: [i2c, digital, gpio, uart]
|
|
86
|
+
|
|
87
|
+
- id: port_5
|
|
88
|
+
name: "Port 5"
|
|
89
|
+
type: universal
|
|
90
|
+
slot_number: 5
|
|
91
|
+
side: left
|
|
92
|
+
pins:
|
|
93
|
+
- {gpio: 11, function: signal}
|
|
94
|
+
- {gpio: 10, function: signal_b}
|
|
95
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
96
|
+
|
|
97
|
+
- id: port_6
|
|
98
|
+
name: "Port 6"
|
|
99
|
+
type: universal
|
|
100
|
+
slot_number: 6
|
|
101
|
+
side: left
|
|
102
|
+
pins:
|
|
103
|
+
- {gpio: 13, function: signal}
|
|
104
|
+
- {gpio: 12, function: signal_b}
|
|
105
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
106
|
+
|
|
107
|
+
- id: port_7
|
|
108
|
+
name: "Port 7"
|
|
109
|
+
type: universal
|
|
110
|
+
slot_number: 7
|
|
111
|
+
side: top
|
|
112
|
+
pins:
|
|
113
|
+
- {gpio: 6, function: signal}
|
|
114
|
+
- {gpio: 7, function: signal_b}
|
|
115
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
116
|
+
|
|
117
|
+
# Port 8 reserved for USB-C (bottom-middle, not a module slot)
|
|
118
|
+
|
|
119
|
+
- id: port_9
|
|
120
|
+
name: "Port 9"
|
|
121
|
+
type: universal
|
|
122
|
+
slot_number: 9
|
|
123
|
+
side: right
|
|
124
|
+
pins:
|
|
125
|
+
- {gpio: 40, function: signal}
|
|
126
|
+
- {gpio: 41, function: signal_b}
|
|
127
|
+
compatible_interfaces: [i2c, digital, gpio]
|
|
128
|
+
|
|
129
|
+
- id: port_10
|
|
130
|
+
name: "Port 10"
|
|
131
|
+
type: universal
|
|
132
|
+
slot_number: 10
|
|
133
|
+
side: right
|
|
134
|
+
pins:
|
|
135
|
+
- {gpio: 1, function: signal}
|
|
136
|
+
- {gpio: 2, function: signal_b}
|
|
137
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
138
|
+
|
|
139
|
+
- id: port_11
|
|
140
|
+
name: "Port 11"
|
|
141
|
+
type: universal
|
|
142
|
+
slot_number: 11
|
|
143
|
+
side: right
|
|
144
|
+
pins:
|
|
145
|
+
- {gpio: 43, function: signal}
|
|
146
|
+
- {gpio: 44, function: signal_b}
|
|
147
|
+
compatible_interfaces: [i2c, digital, gpio]
|
|
148
|
+
|
|
149
|
+
# Port 12 reserved for Reset button (right column, not a module slot)
|
|
150
|
+
|
|
151
|
+
- id: port_13
|
|
152
|
+
name: "Port 13"
|
|
153
|
+
type: universal
|
|
154
|
+
slot_number: 13
|
|
155
|
+
side: right
|
|
156
|
+
pins:
|
|
157
|
+
- {gpio: 39, function: signal}
|
|
158
|
+
- {gpio: 38, function: signal_b}
|
|
159
|
+
compatible_interfaces: [i2c, digital, gpio]
|
|
160
|
+
|
|
161
|
+
- id: port_14
|
|
162
|
+
name: "Port 14"
|
|
163
|
+
type: universal
|
|
164
|
+
slot_number: 14
|
|
165
|
+
side: right
|
|
166
|
+
pins:
|
|
167
|
+
- {gpio: 36, function: signal}
|
|
168
|
+
- {gpio: 35, function: signal_b}
|
|
169
|
+
compatible_interfaces: [i2c, digital, gpio]
|
|
170
|
+
|
|
171
|
+
adjacent_port_pairs:
|
|
172
|
+
# Left column (vertically adjacent)
|
|
173
|
+
- ["port_1", "port_2"]
|
|
174
|
+
- ["port_2", "port_3"]
|
|
175
|
+
- ["port_3", "port_4"]
|
|
176
|
+
- ["port_4", "port_5"]
|
|
177
|
+
- ["port_5", "port_6"]
|
|
178
|
+
# Right column (vertically adjacent, skipping Reset at port_12)
|
|
179
|
+
- ["port_9", "port_10"]
|
|
180
|
+
- ["port_10", "port_11"]
|
|
181
|
+
- ["port_13", "port_14"]
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Atech 8-Port motherboard
|
|
2
|
+
#
|
|
3
|
+
# Two rows of port columns flanking a reserved Restart button (port 6) and
|
|
4
|
+
# USB-C jack (port 8). Six general-purpose module slots: 1-5 and 7. Double-
|
|
5
|
+
# width modules can span [1,2] or [3,4]; the gap below row 2 separates the
|
|
6
|
+
# pairs, and Restart/USB-C make 5-7 non-adjacent.
|
|
7
|
+
#
|
|
8
|
+
# Sourced from backend/motherboard/board.yaml (8port_board).
|
|
9
|
+
|
|
10
|
+
id: 8port
|
|
11
|
+
name: "Atech 8-Port Board"
|
|
12
|
+
|
|
13
|
+
microcontroller:
|
|
14
|
+
type: esp32
|
|
15
|
+
variant: ESP32-S3
|
|
16
|
+
flash_size_kb: 8192
|
|
17
|
+
ram_size_kb: 512
|
|
18
|
+
|
|
19
|
+
platformio_env: esp32-s3-devkitc-1
|
|
20
|
+
platformio_framework: arduino
|
|
21
|
+
|
|
22
|
+
# Reserved port ids that exist on the layout but cannot host a module.
|
|
23
|
+
reserved:
|
|
24
|
+
- port_6
|
|
25
|
+
- port_8
|
|
26
|
+
|
|
27
|
+
# 2D physical layout (matches the frontend renderer). Numbers are slot ids
|
|
28
|
+
# (one-based), "" is a physical gap, text labels are reserved positions.
|
|
29
|
+
layout:
|
|
30
|
+
- ["1", "2", "", "3", "4"]
|
|
31
|
+
- ["5", "Restart", "", "7", "USB-C"]
|
|
32
|
+
|
|
33
|
+
notes: |
|
|
34
|
+
6 module slots arranged as right column (1, 2, 3, 4) and left column (5, 7).
|
|
35
|
+
Two adjacent pairs for double-width modules: [1,2] and [3,4]. Port 5 and
|
|
36
|
+
port 7 are NOT adjacent — the Restart button at port 6 and the USB-C jack
|
|
37
|
+
at port 8 break that column. Corners (useful for vehicle motor placement):
|
|
38
|
+
top-left=port_5, top-right=port_1, bottom-left=port_7, bottom-right=port_4.
|
|
39
|
+
|
|
40
|
+
ports:
|
|
41
|
+
- id: port_1
|
|
42
|
+
name: "Port 1"
|
|
43
|
+
type: universal
|
|
44
|
+
slot_number: 1
|
|
45
|
+
side: right
|
|
46
|
+
pins:
|
|
47
|
+
- {gpio: 5, function: signal}
|
|
48
|
+
- {gpio: 4, function: signal_b}
|
|
49
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
50
|
+
|
|
51
|
+
- id: port_2
|
|
52
|
+
name: "Port 2"
|
|
53
|
+
type: universal
|
|
54
|
+
slot_number: 2
|
|
55
|
+
side: right
|
|
56
|
+
pins:
|
|
57
|
+
- {gpio: 7, function: signal}
|
|
58
|
+
- {gpio: 6, function: signal_b}
|
|
59
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
60
|
+
|
|
61
|
+
- id: port_3
|
|
62
|
+
name: "Port 3"
|
|
63
|
+
type: universal
|
|
64
|
+
slot_number: 3
|
|
65
|
+
side: right
|
|
66
|
+
pins:
|
|
67
|
+
- {gpio: 9, function: signal}
|
|
68
|
+
- {gpio: 10, function: signal_b}
|
|
69
|
+
compatible_interfaces: [i2c, digital, gpio, analog]
|
|
70
|
+
|
|
71
|
+
- id: port_4
|
|
72
|
+
name: "Port 4"
|
|
73
|
+
type: universal
|
|
74
|
+
slot_number: 4
|
|
75
|
+
side: right
|
|
76
|
+
pins:
|
|
77
|
+
- {gpio: 1, function: signal}
|
|
78
|
+
- {gpio: 2, function: signal_b}
|
|
79
|
+
compatible_interfaces: [i2c, digital, gpio]
|
|
80
|
+
|
|
81
|
+
- id: port_5
|
|
82
|
+
name: "Port 5"
|
|
83
|
+
type: universal
|
|
84
|
+
slot_number: 5
|
|
85
|
+
side: left
|
|
86
|
+
pins:
|
|
87
|
+
- {gpio: 43, function: signal}
|
|
88
|
+
- {gpio: 44, function: signal_b}
|
|
89
|
+
compatible_interfaces: [i2c, digital, gpio]
|
|
90
|
+
|
|
91
|
+
- id: port_7
|
|
92
|
+
name: "Port 7"
|
|
93
|
+
type: universal
|
|
94
|
+
slot_number: 7
|
|
95
|
+
side: left
|
|
96
|
+
pins:
|
|
97
|
+
- {gpio: 15, function: signal}
|
|
98
|
+
- {gpio: 16, function: signal_b}
|
|
99
|
+
compatible_interfaces: [i2c, digital, gpio, analog, uart]
|
|
100
|
+
|
|
101
|
+
adjacent_port_pairs:
|
|
102
|
+
- ["port_1", "port_2"]
|
|
103
|
+
- ["port_3", "port_4"]
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file aht20.cpp
|
|
3
|
+
* @brief AHT20 Temperature & Humidity Sensor implementation for Athera
|
|
4
|
+
*
|
|
5
|
+
* Background FreeRTOS task triggers measurements every ~2s.
|
|
6
|
+
* Public methods return cached values for non-blocking access.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#include "aht20.h"
|
|
10
|
+
#include <Arduino.h>
|
|
11
|
+
|
|
12
|
+
static void measureTaskTrampoline(void* param) {
|
|
13
|
+
((AHT20*)param)->_measureTaskLoop();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
AHT20::AHT20(I2CInterface* i2c, uint8_t address)
|
|
17
|
+
: _i2c(i2c)
|
|
18
|
+
, _address(address)
|
|
19
|
+
, _measureTask(nullptr)
|
|
20
|
+
, _temperature(0)
|
|
21
|
+
, _humidity(0)
|
|
22
|
+
{
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
bool AHT20::begin() {
|
|
26
|
+
// Wait for sensor power-on
|
|
27
|
+
vTaskDelay(pdMS_TO_TICKS(100));
|
|
28
|
+
|
|
29
|
+
if (!isConnected()) {
|
|
30
|
+
Serial.println("AHT20 not responding!");
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check calibration status and init if needed
|
|
35
|
+
if (!_initCalibration()) {
|
|
36
|
+
Serial.println("AHT20 calibration failed!");
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Start background measurement task on Core 0
|
|
41
|
+
xTaskCreatePinnedToCore(
|
|
42
|
+
measureTaskTrampoline,
|
|
43
|
+
"AHT20",
|
|
44
|
+
2048,
|
|
45
|
+
this,
|
|
46
|
+
1, // Lower priority than IMU
|
|
47
|
+
&_measureTask,
|
|
48
|
+
0
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
Serial.println("AHT20 initialized");
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
float AHT20::readTemperature() { return _temperature; }
|
|
56
|
+
float AHT20::readHumidity() { return _humidity; }
|
|
57
|
+
|
|
58
|
+
bool AHT20::isConnected() {
|
|
59
|
+
_i2c->beginTransmission(_address);
|
|
60
|
+
return _i2c->endTransmission() == 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ========== Background task ==========
|
|
64
|
+
|
|
65
|
+
void AHT20::_measureTaskLoop() {
|
|
66
|
+
// Initial delay to let sensor settle
|
|
67
|
+
vTaskDelay(pdMS_TO_TICKS(500));
|
|
68
|
+
|
|
69
|
+
while (true) {
|
|
70
|
+
if (_triggerMeasurement()) {
|
|
71
|
+
vTaskDelay(pdMS_TO_TICKS(80)); // Measurement takes ~75ms
|
|
72
|
+
_readMeasurement();
|
|
73
|
+
}
|
|
74
|
+
vTaskDelay(pdMS_TO_TICKS(2000)); // Measure every ~2s
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ========== Private methods ==========
|
|
79
|
+
|
|
80
|
+
bool AHT20::_initCalibration() {
|
|
81
|
+
// Read status byte
|
|
82
|
+
if (_i2c->requestFrom(_address, (size_t)1) < 1) return false;
|
|
83
|
+
uint8_t status = _i2c->read();
|
|
84
|
+
|
|
85
|
+
// If not calibrated (bit 3), send init command
|
|
86
|
+
if (!(status & 0x08)) {
|
|
87
|
+
_i2c->beginTransmission(_address);
|
|
88
|
+
_i2c->write((uint8_t)0xBE);
|
|
89
|
+
_i2c->write((uint8_t)0x08);
|
|
90
|
+
_i2c->write((uint8_t)0x00);
|
|
91
|
+
if (_i2c->endTransmission() != 0) return false;
|
|
92
|
+
vTaskDelay(pdMS_TO_TICKS(10));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
bool AHT20::_triggerMeasurement() {
|
|
99
|
+
_i2c->beginTransmission(_address);
|
|
100
|
+
_i2c->write((uint8_t)0xAC);
|
|
101
|
+
_i2c->write((uint8_t)0x33);
|
|
102
|
+
_i2c->write((uint8_t)0x00);
|
|
103
|
+
return _i2c->endTransmission() == 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bool AHT20::_readMeasurement() {
|
|
107
|
+
if (_i2c->requestFrom(_address, (size_t)7) < 7) return false;
|
|
108
|
+
|
|
109
|
+
uint8_t data[7];
|
|
110
|
+
for (int i = 0; i < 7; i++) {
|
|
111
|
+
data[i] = _i2c->read();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check busy bit
|
|
115
|
+
if (data[0] & 0x80) return false;
|
|
116
|
+
|
|
117
|
+
// Humidity: data[1], data[2], upper 4 bits of data[3]
|
|
118
|
+
uint32_t humRaw = ((uint32_t)data[1] << 12) |
|
|
119
|
+
((uint32_t)data[2] << 4) |
|
|
120
|
+
((uint32_t)data[3] >> 4);
|
|
121
|
+
|
|
122
|
+
// Temperature: lower 4 bits of data[3], data[4], data[5]
|
|
123
|
+
uint32_t tempRaw = (((uint32_t)data[3] & 0x0F) << 16) |
|
|
124
|
+
((uint32_t)data[4] << 8) |
|
|
125
|
+
(uint32_t)data[5];
|
|
126
|
+
|
|
127
|
+
_humidity = (humRaw / 1048576.0f) * 100.0f;
|
|
128
|
+
_temperature = (tempRaw / 1048576.0f) * 200.0f - 50.0f;
|
|
129
|
+
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file aht20.h
|
|
3
|
+
* @brief AHT20 Temperature & Humidity Sensor for Athera
|
|
4
|
+
*
|
|
5
|
+
* Integrated temperature and humidity sensor with I2C interface.
|
|
6
|
+
* Uses a background FreeRTOS task to periodically trigger measurements
|
|
7
|
+
* and cache results — public getters return cached values instantly.
|
|
8
|
+
*
|
|
9
|
+
* Specifications:
|
|
10
|
+
* - Temperature: -40 to +125°C
|
|
11
|
+
* - Humidity: 0-100% RH
|
|
12
|
+
* - Interface: I2C (400kHz)
|
|
13
|
+
* - Address: 0x38
|
|
14
|
+
* - Measurement time: ~75ms
|
|
15
|
+
*
|
|
16
|
+
* Athera Connector:
|
|
17
|
+
* - Line A: SDA
|
|
18
|
+
* - Line B: SCL
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
#ifndef AHT20_MODULE_H
|
|
22
|
+
#define AHT20_MODULE_H
|
|
23
|
+
|
|
24
|
+
#include <i2c_interface.h>
|
|
25
|
+
#include "i2c_hardware.h" // exposes WireI2C so generated globals can construct a bus
|
|
26
|
+
#include "freertos/FreeRTOS.h"
|
|
27
|
+
#include "freertos/task.h"
|
|
28
|
+
|
|
29
|
+
class AHT20 {
|
|
30
|
+
public:
|
|
31
|
+
static constexpr uint8_t ADDR_DEFAULT = 0x38;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @brief Construct AHT20 sensor
|
|
35
|
+
* @param i2c Pointer to I2C interface
|
|
36
|
+
* @param address I2C address (default 0x38)
|
|
37
|
+
*/
|
|
38
|
+
AHT20(I2CInterface* i2c, uint8_t address = ADDR_DEFAULT);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @brief Initialize sensor and start background measurement task
|
|
42
|
+
* @return true on success
|
|
43
|
+
*/
|
|
44
|
+
bool begin();
|
|
45
|
+
|
|
46
|
+
/** @brief Read cached temperature in Celsius */
|
|
47
|
+
float readTemperature();
|
|
48
|
+
|
|
49
|
+
/** @brief Read cached relative humidity in %RH */
|
|
50
|
+
float readHumidity();
|
|
51
|
+
|
|
52
|
+
/** @brief Check if sensor is responding */
|
|
53
|
+
bool isConnected();
|
|
54
|
+
|
|
55
|
+
// Background task function (public for static callback, do not call directly)
|
|
56
|
+
void _measureTaskLoop();
|
|
57
|
+
|
|
58
|
+
private:
|
|
59
|
+
I2CInterface* _i2c;
|
|
60
|
+
uint8_t _address;
|
|
61
|
+
TaskHandle_t _measureTask;
|
|
62
|
+
|
|
63
|
+
volatile float _temperature;
|
|
64
|
+
volatile float _humidity;
|
|
65
|
+
|
|
66
|
+
bool _initCalibration();
|
|
67
|
+
bool _triggerMeasurement();
|
|
68
|
+
bool _readMeasurement();
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
#endif // AHT20_MODULE_H
|