Skip to content

Commit b100c1b

Browse files
committed
Add Core API for low-level pin control
1 parent 15a0003 commit b100c1b

File tree

5 files changed

+282
-0
lines changed

5 files changed

+282
-0
lines changed

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
* [Utilities](#utilities)
6464
* [sleep](#sleep)
6565
* [map](#map)
66+
* [Core](#core)
6667
* [Supported Python features](#supported-python-features)
6768
* [License](#license)
6869

@@ -483,6 +484,36 @@ mapped = map(512, 0, 1023, 0.0, 5.0) # 2.5-ish
483484
print(mapped)
484485
```
485486

487+
### Core
488+
489+
The **Core** module exposes low-level helpers that map directly to the Arduino
490+
API. Use these when you need to configure a pin or interact with a sensor that
491+
doesn't yet have a dedicated Reduino abstraction.
492+
493+
| Helper | Description |
494+
| ------ | ----------- |
495+
| `pin_mode(pin, mode)` | Configure a pin for `INPUT`, `INPUT_PULLUP`, or `OUTPUT`. |
496+
| `digital_write(pin, value)` | Write `HIGH`/`LOW` to a digital pin. |
497+
| `analog_write(pin, value)` | Output a PWM value (0–255). |
498+
| `digital_read(pin)` | Read a digital pin (returns `0` or `1`). |
499+
| `analog_read(pin)` | Read an analogue value (0–1023 on most boards). |
500+
501+
Constants `INPUT`, `OUTPUT`, `INPUT_PULLUP`, `HIGH`, and `LOW` mirror the
502+
Arduino macros so your code matches what you would write in C++.
503+
504+
```python
505+
from Reduino import target
506+
target("COM3")
507+
508+
from Reduino.Core import pin_mode, digital_write, digital_read, OUTPUT, HIGH, LOW
509+
510+
pin_mode(7, OUTPUT)
511+
digital_write(7, HIGH)
512+
513+
if digital_read(2) == HIGH:
514+
digital_write(7, LOW)
515+
```
516+
486517
---
487518

488519
## Supported Python features

src/Reduino/Core/__init__.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Host-side stubs for the low-level Reduino Core helpers.
2+
3+
These helpers provide a minimal in-memory simulation of the Arduino
4+
``pinMode``/``digitalWrite``/``analogWrite`` family so that importing
5+
``Reduino.Core`` succeeds when running documentation examples or unit tests
6+
on a host machine. The transpiler recognises these names and replaces them
7+
with the corresponding Arduino calls during code generation.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
from typing import Dict, Union
13+
14+
__all__ = [
15+
"pin_mode",
16+
"digital_write",
17+
"analog_write",
18+
"digital_read",
19+
"analog_read",
20+
"INPUT",
21+
"OUTPUT",
22+
"INPUT_PULLUP",
23+
"HIGH",
24+
"LOW",
25+
]
26+
27+
28+
INPUT = "INPUT"
29+
OUTPUT = "OUTPUT"
30+
INPUT_PULLUP = "INPUT_PULLUP"
31+
HIGH = 1
32+
LOW = 0
33+
34+
_PinKey = Union[int, str]
35+
36+
_pin_modes: Dict[_PinKey, str] = {}
37+
_digital_values: Dict[_PinKey, int] = {}
38+
_analog_values: Dict[_PinKey, int] = {}
39+
40+
41+
def _normalise_pin(pin: _PinKey) -> _PinKey:
42+
if isinstance(pin, str) and pin.isdigit():
43+
return int(pin)
44+
return pin
45+
46+
47+
def pin_mode(pin: _PinKey, mode: str) -> None:
48+
"""Record the configured ``mode`` for ``pin`` in the host simulation."""
49+
50+
key = _normalise_pin(pin)
51+
_pin_modes[key] = mode
52+
if mode == INPUT_PULLUP and key not in _digital_values:
53+
_digital_values[key] = HIGH
54+
55+
56+
def digital_write(pin: _PinKey, value: Union[int, bool]) -> None:
57+
"""Store the logical ``value`` for ``pin`` in the host simulation."""
58+
59+
key = _normalise_pin(pin)
60+
_digital_values[key] = HIGH if bool(value) else LOW
61+
62+
63+
def analog_write(pin: _PinKey, value: Union[int, float]) -> None:
64+
"""Store the PWM ``value`` (0-255) for ``pin`` in the host simulation."""
65+
66+
key = _normalise_pin(pin)
67+
scaled = int(round(float(value)))
68+
_analog_values[key] = max(0, min(255, scaled))
69+
70+
71+
def digital_read(pin: _PinKey) -> int:
72+
"""Return the previously stored logical value for ``pin``."""
73+
74+
key = _normalise_pin(pin)
75+
if key in _digital_values:
76+
return _digital_values[key]
77+
if _pin_modes.get(key) == INPUT_PULLUP:
78+
return HIGH
79+
return LOW
80+
81+
82+
def analog_read(pin: _PinKey) -> int:
83+
"""Return the previously stored analogue value for ``pin``."""
84+
85+
key = _normalise_pin(pin)
86+
return _analog_values.get(key, 0)
87+

src/Reduino/transpile/parser.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ def _merge_element_types(types: List[str]) -> str:
162162
"abs": "int",
163163
"max": "int",
164164
"min": "int",
165+
"digital_read": "int",
166+
"analog_read": "int",
165167
}
166168

167169
def _eval_const(expr: str, env: dict):
@@ -370,6 +372,40 @@ def _literal_length(node: ast.AST) -> Optional[int]:
370372
return len(bound)
371373
return None
372374

375+
def _render_pin_argument(node: ast.AST) -> str:
376+
expr = emit(node)
377+
if isinstance(node, ast.Constant) and isinstance(node.value, str):
378+
candidate = node.value.strip()
379+
if candidate.isdigit():
380+
return candidate
381+
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", candidate):
382+
return candidate
383+
return expr
384+
385+
def _call_argument(
386+
call: ast.Call, position: int, keyword: Optional[str]
387+
) -> Optional[ast.AST]:
388+
value: Optional[ast.AST] = None
389+
has_positional = len(call.args) > position
390+
has_keyword = False if keyword is None else any(
391+
kw.arg == keyword for kw in call.keywords
392+
)
393+
if has_positional and has_keyword:
394+
raise ValueError("duplicate argument provided")
395+
if has_positional:
396+
value = call.args[position]
397+
elif has_keyword:
398+
for kw in call.keywords:
399+
if kw.arg == keyword:
400+
value = kw.value
401+
break
402+
return value
403+
404+
def _ensure_allowed_keywords(call: ast.Call, allowed: Set[str]) -> None:
405+
for kw in call.keywords:
406+
if kw.arg not in allowed:
407+
raise ValueError("unsupported keyword arguments in call")
408+
373409
def emit(n: ast.AST) -> str:
374410
if isinstance(n, ast.Constant):
375411
if isinstance(n.value, bool):
@@ -695,6 +731,49 @@ def emit(n: ast.AST) -> str:
695731

696732
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name):
697733
fname = n.func.id
734+
if fname == "pin_mode":
735+
_ensure_allowed_keywords(n, {"pin", "mode"})
736+
pin_node = _call_argument(n, 0, "pin")
737+
mode_node = _call_argument(n, 1, "mode")
738+
if pin_node is None or mode_node is None:
739+
raise ValueError("pin_mode requires pin and mode arguments")
740+
pin_expr = _render_pin_argument(pin_node)
741+
mode_expr = emit(mode_node)
742+
return f"pinMode({pin_expr}, {mode_expr})"
743+
if fname == "digital_write":
744+
_ensure_allowed_keywords(n, {"pin", "value"})
745+
pin_node = _call_argument(n, 0, "pin")
746+
value_node = _call_argument(n, 1, "value")
747+
if pin_node is None or value_node is None:
748+
raise ValueError(
749+
"digital_write requires pin and value arguments"
750+
)
751+
pin_expr = _render_pin_argument(pin_node)
752+
value_expr = emit(value_node)
753+
return f"digitalWrite({pin_expr}, {value_expr})"
754+
if fname == "analog_write":
755+
_ensure_allowed_keywords(n, {"pin", "value"})
756+
pin_node = _call_argument(n, 0, "pin")
757+
value_node = _call_argument(n, 1, "value")
758+
if pin_node is None or value_node is None:
759+
raise ValueError("analog_write requires pin and value arguments")
760+
pin_expr = _render_pin_argument(pin_node)
761+
value_expr = emit(value_node)
762+
return f"analogWrite({pin_expr}, {value_expr})"
763+
if fname == "digital_read":
764+
_ensure_allowed_keywords(n, {"pin"})
765+
pin_node = _call_argument(n, 0, "pin")
766+
if pin_node is None:
767+
raise ValueError("digital_read requires a pin argument")
768+
pin_expr = _render_pin_argument(pin_node)
769+
return f"digitalRead({pin_expr})"
770+
if fname == "analog_read":
771+
_ensure_allowed_keywords(n, {"pin"})
772+
pin_node = _call_argument(n, 0, "pin")
773+
if pin_node is None:
774+
raise ValueError("analog_read requires a pin argument")
775+
pin_expr = _render_pin_argument(pin_node)
776+
return f"analogRead({pin_expr})"
698777
if fname == "str" and len(n.args) == 1 and not n.keywords:
699778
return f"String({emit(n.args[0])})"
700779
if fname in {"int", "float"} and len(n.args) == 1 and not n.keywords:
@@ -1237,6 +1316,7 @@ def _merge_return_types(types: List[str], has_void: bool) -> str:
12371316
RE_IMPORT_SLEEP = re.compile(r"^\s*from\s+Reduino\.Utils\s+import\s+sleep\s*$")
12381317
RE_IMPORT_SERIAL = re.compile(r"^\s*from\s+Reduino\.Communication\s+import\s+SerialMonitor\s*$")
12391318
RE_IMPORT_TARGET = re.compile(r"^\s*from\s+Reduino\s+import\s+target\s*$")
1319+
RE_IMPORT_CORE = re.compile(r"^\s*from\s+Reduino\.Core\s+import\s+.*$")
12401320
RE_IMPORT_ULTRASONIC = re.compile(r"^\s*from\s+Reduino\.Sensors\s+import\s+Ultrasonic\s*$")
12411321
RE_IMPORT_BUTTON = re.compile(r"^\s*from\s+Reduino\.Sensors\s+import\s+Button\s*$")
12421322
RE_IMPORT_LCD = re.compile(r"^\s*from\s+Reduino\.Displays\s+import\s+LCD\s*$")
@@ -3936,6 +4016,7 @@ def parse(src: str) -> Program:
39364016
or RE_IMPORT_SLEEP.match(text)
39374017
or RE_IMPORT_SERIAL.match(text)
39384018
or RE_IMPORT_TARGET.match(text)
4019+
or RE_IMPORT_CORE.match(text)
39394020
or RE_IMPORT_ULTRASONIC.match(text)
39404021
or RE_IMPORT_BUTTON.match(text)
39414022
or RE_IMPORT_POTENTIOMETER.match(text)

tests/test_emitter.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,39 @@ def test_emit_generates_setup_and_loop(src, norm) -> None:
3535
assert "// no loop actions" in loop_section or loop_section.strip() == "{\n}\n"
3636

3737

38+
def test_emit_core_primitives(src, norm) -> None:
39+
cpp = compile_source(
40+
src(
41+
"""
42+
from Reduino.Core import (
43+
pin_mode,
44+
digital_write,
45+
analog_write,
46+
digital_read,
47+
analog_read,
48+
INPUT,
49+
OUTPUT,
50+
HIGH,
51+
LOW,
52+
)
53+
54+
pin_mode(7, OUTPUT)
55+
digital_write(7, HIGH)
56+
analog_write(6, 42)
57+
value = digital_read(5)
58+
analog_value = analog_read(A0)
59+
"""
60+
)
61+
)
62+
63+
text = norm(cpp)
64+
assert "pinMode(7, OUTPUT);" in text
65+
assert "digitalWrite(7, HIGH);" in text
66+
assert "analogWrite(6, 42);" in text
67+
assert "value = digitalRead(5);" in text
68+
assert "analog_value = analogRead(A0);" in text
69+
70+
3871
def test_emit_infinite_loop_moves_body_to_loop(src, norm) -> None:
3972
cpp = compile_source(
4073
src(

tests/test_parser.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
ButtonPoll,
2929
ForRangeLoop,
3030
FunctionDef,
31+
ExprStmt,
3132
LedDecl,
3233
LedOff,
3334
LedToggle,
@@ -135,6 +136,55 @@ def test_parser_handles_buzzer_primitives(src) -> None:
135136
assert melody.tempo == pytest.approx(180)
136137

137138

139+
def test_parser_supports_core_primitives(src) -> None:
140+
code = src(
141+
"""
142+
from Reduino.Core import (
143+
pin_mode,
144+
digital_write,
145+
analog_write,
146+
digital_read,
147+
analog_read,
148+
INPUT,
149+
OUTPUT,
150+
INPUT_PULLUP,
151+
HIGH,
152+
LOW,
153+
)
154+
155+
pin_mode(7, OUTPUT)
156+
digital_write(7, HIGH)
157+
analog_write(6, 42)
158+
value = digital_read(5)
159+
analog_value = analog_read(A0)
160+
"""
161+
)
162+
163+
program = _parse(code)
164+
165+
exprs = [
166+
node.expr
167+
for node in program.setup_body
168+
if isinstance(node, ExprStmt)
169+
]
170+
assert exprs[:3] == [
171+
"pinMode(7, OUTPUT)",
172+
"digitalWrite(7, HIGH)",
173+
"analogWrite(6, 42)",
174+
]
175+
176+
assignments = [
177+
node.expr
178+
for node in program.setup_body
179+
if isinstance(node, VarAssign)
180+
]
181+
assert assignments == ["digitalRead(5)", "analogRead(A0)"]
182+
183+
decls = {decl.name: decl.c_type for decl in program.global_decls}
184+
assert decls["value"] == "int"
185+
assert decls["analog_value"] == "int"
186+
187+
138188
def test_parser_buzzer_optional_arguments(src) -> None:
139189
code = src(
140190
"""

0 commit comments

Comments
 (0)