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.
Files changed (51) hide show
  1. atech/__init__.py +71 -0
  2. atech/build.py +82 -0
  3. atech/catalog/__init__.py +43 -0
  4. atech/catalog/data/boards/14port.yaml +181 -0
  5. atech/catalog/data/boards/8port.yaml +103 -0
  6. atech/catalog/data/modules/aht20/aht20.cpp +131 -0
  7. atech/catalog/data/modules/aht20/aht20.h +71 -0
  8. atech/catalog/data/modules/aht20/i2c_hardware.cpp +77 -0
  9. atech/catalog/data/modules/aht20/i2c_hardware.h +61 -0
  10. atech/catalog/data/modules/aht20/i2c_interface.h +186 -0
  11. atech/catalog/data/modules/aht20/module.yaml +63 -0
  12. atech/catalog/data/modules/button/button.cpp +61 -0
  13. atech/catalog/data/modules/button/button.h +38 -0
  14. atech/catalog/data/modules/button/module.yaml +44 -0
  15. atech/catalog/data/modules/dc_motor/dc_motor.cpp +95 -0
  16. atech/catalog/data/modules/dc_motor/dc_motor.h +40 -0
  17. atech/catalog/data/modules/dc_motor/module.yaml +49 -0
  18. atech/catalog/data/modules/neopixel/module.yaml +50 -0
  19. atech/catalog/data/modules/neopixel/neopixel.cpp +96 -0
  20. atech/catalog/data/modules/neopixel/neopixel.h +48 -0
  21. atech/catalog/data/modules/pir/module.yaml +57 -0
  22. atech/catalog/data/modules/pir/pir.cpp +67 -0
  23. atech/catalog/data/modules/pir/pir.h +74 -0
  24. atech/catalog/data/modules/rotary_encoder/module.yaml +69 -0
  25. atech/catalog/data/modules/rotary_encoder/rotary_encoder.cpp +373 -0
  26. atech/catalog/data/modules/rotary_encoder/rotary_encoder.h +169 -0
  27. atech/catalog/data/modules/speaker/module.yaml +84 -0
  28. atech/catalog/data/modules/speaker/speaker.cpp +606 -0
  29. atech/catalog/data/modules/speaker/speaker.h +222 -0
  30. atech/catalog/data/modules/st7735_tft/module.yaml +68 -0
  31. atech/catalog/data/modules/st7735_tft/st7735_tft.cpp +232 -0
  32. atech/catalog/data/modules/st7735_tft/st7735_tft.h +165 -0
  33. atech/catalog/data/modules/stepper_motor/module.yaml +57 -0
  34. atech/catalog/data/modules/stepper_motor/stepper_motor.cpp +88 -0
  35. atech/catalog/data/modules/stepper_motor/stepper_motor.h +111 -0
  36. atech/catalog/loader.py +154 -0
  37. atech/catalog/models.py +164 -0
  38. atech/cli.py +248 -0
  39. atech/codegen.py +256 -0
  40. atech/errors.py +23 -0
  41. atech/placement.py +174 -0
  42. atech/project.py +367 -0
  43. atech/runtime/__init__.py +34 -0
  44. atech/runtime/board.py +99 -0
  45. atech/runtime/models.py +73 -0
  46. atech/runtime/transport.py +287 -0
  47. atech/upload.py +76 -0
  48. atech-1.0.0a1.dist-info/METADATA +248 -0
  49. atech-1.0.0a1.dist-info/RECORD +51 -0
  50. atech-1.0.0a1.dist-info/WHEEL +4 -0
  51. 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