pulsemaker-simulator 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pulsemaker_simulator-1.0.0/PKG-INFO +94 -0
- pulsemaker_simulator-1.0.0/README.md +79 -0
- pulsemaker_simulator-1.0.0/pulsemaker/__init__.py +47 -0
- pulsemaker_simulator-1.0.0/pulsemaker/buttons.py +31 -0
- pulsemaker_simulator-1.0.0/pulsemaker/cli.py +35 -0
- pulsemaker_simulator-1.0.0/pulsemaker/display.py +99 -0
- pulsemaker_simulator-1.0.0/pulsemaker/icons.py +65 -0
- pulsemaker_simulator-1.0.0/pulsemaker/music.py +38 -0
- pulsemaker_simulator-1.0.0/pulsemaker/pins.py +29 -0
- pulsemaker_simulator-1.0.0/pulsemaker/rover.py +57 -0
- pulsemaker_simulator-1.0.0/pulsemaker/sensors.py +49 -0
- pulsemaker_simulator-1.0.0/pulsemaker_rover/__init__.py +6 -0
- pulsemaker_simulator-1.0.0/pulsemaker_simulator.egg-info/PKG-INFO +94 -0
- pulsemaker_simulator-1.0.0/pulsemaker_simulator.egg-info/SOURCES.txt +17 -0
- pulsemaker_simulator-1.0.0/pulsemaker_simulator.egg-info/dependency_links.txt +1 -0
- pulsemaker_simulator-1.0.0/pulsemaker_simulator.egg-info/entry_points.txt +2 -0
- pulsemaker_simulator-1.0.0/pulsemaker_simulator.egg-info/top_level.txt +2 -0
- pulsemaker_simulator-1.0.0/pyproject.toml +28 -0
- pulsemaker_simulator-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pulsemaker-simulator
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python and MicroPython runtime library for PulseMaker Simulator (DBE CAPS Coding & Robotics)
|
|
5
|
+
Author: PulseMaker Simulator Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: robotics,simulator,micropython,education,stem,caps
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Education
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# PulseMaker Python Library
|
|
17
|
+
|
|
18
|
+
The official Python / MicroPython companion library for the **PulseMaker Simulator** (DBE CAPS Coding & Robotics curriculum for South African schools, Grades R–7).
|
|
19
|
+
|
|
20
|
+
This library allows code generated or written in the PulseMaker browser simulator to run on any computer or MicroPython microcontroller without modifications.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install via pip:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pulsemaker-simulator
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install locally in editable mode:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# From the project root
|
|
36
|
+
pip install -e ./python
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Example (Intermediate Phase 5x5 Board)
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pulsemaker import display, music, pin0, pin1, pin2, button_a, button_b, sensors, Icon
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
# Check simulated environment sensors
|
|
48
|
+
if sensors.temperature() > 30:
|
|
49
|
+
display.show(Icon.SUN)
|
|
50
|
+
music.pitch(880, duration=300)
|
|
51
|
+
print("Heatwave Alert! Temp > 30C")
|
|
52
|
+
else:
|
|
53
|
+
display.show(Icon.SMILE)
|
|
54
|
+
print("Temperature Normal")
|
|
55
|
+
|
|
56
|
+
# Wait and clear
|
|
57
|
+
time.sleep_ms(1000)
|
|
58
|
+
display.clear()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Senior Phase Example (2D Autonomous Rover)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pulsemaker_rover import Rover
|
|
67
|
+
import time
|
|
68
|
+
|
|
69
|
+
rover = Rover()
|
|
70
|
+
|
|
71
|
+
for _ in range(10):
|
|
72
|
+
dist = rover.ultrasonic.distance_cm()
|
|
73
|
+
print(f"Ultrasonic distance: {dist} cm")
|
|
74
|
+
|
|
75
|
+
if dist < 65:
|
|
76
|
+
# Avoid obstacle: spin right
|
|
77
|
+
rover.motors(v_left=65, v_right=-65)
|
|
78
|
+
else:
|
|
79
|
+
# Drive forward
|
|
80
|
+
rover.motors(v_left=70, v_right=70)
|
|
81
|
+
|
|
82
|
+
time.sleep_ms(200)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Features
|
|
88
|
+
|
|
89
|
+
- **5x5 LED Matrix Display**: Visual terminal rendering with ANSI color highlighting (`display.show`, `display.set_pixel`, `display.clear`).
|
|
90
|
+
- **Icons**: Pre-packaged icons including `Icon.HEART`, `Icon.SMILE`, `Icon.SAD`, `Icon.SUN`, `Icon.ARROW_N`, and `Icon.CHECK`.
|
|
91
|
+
- **Piezo Sound Synthesizer**: Native audio via PC speaker / `winsound` on Windows and terminal fallbacks on macOS/Linux.
|
|
92
|
+
- **Sensors & Pins**: Virtual inputs for Temperature, Light Level, Push Buttons A & B, and Edge Pins P0, P1, P2.
|
|
93
|
+
- **MicroPython Compatibility**: Automatically injects `time.sleep_ms()` into Python's built-in `time` module so MicroPython scripts run unmodified in standard CPython.
|
|
94
|
+
- **2D Rover Kinematics**: Complete differential drive robotics model with raycasting and line sensors (`pulsemaker_rover.Rover`).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# PulseMaker Python Library
|
|
2
|
+
|
|
3
|
+
The official Python / MicroPython companion library for the **PulseMaker Simulator** (DBE CAPS Coding & Robotics curriculum for South African schools, Grades R–7).
|
|
4
|
+
|
|
5
|
+
This library allows code generated or written in the PulseMaker browser simulator to run on any computer or MicroPython microcontroller without modifications.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Install via pip:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install pulsemaker-simulator
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or install locally in editable mode:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# From the project root
|
|
21
|
+
pip install -e ./python
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Quick Example (Intermediate Phase 5x5 Board)
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pulsemaker import display, music, pin0, pin1, pin2, button_a, button_b, sensors, Icon
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
# Check simulated environment sensors
|
|
33
|
+
if sensors.temperature() > 30:
|
|
34
|
+
display.show(Icon.SUN)
|
|
35
|
+
music.pitch(880, duration=300)
|
|
36
|
+
print("Heatwave Alert! Temp > 30C")
|
|
37
|
+
else:
|
|
38
|
+
display.show(Icon.SMILE)
|
|
39
|
+
print("Temperature Normal")
|
|
40
|
+
|
|
41
|
+
# Wait and clear
|
|
42
|
+
time.sleep_ms(1000)
|
|
43
|
+
display.clear()
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Senior Phase Example (2D Autonomous Rover)
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from pulsemaker_rover import Rover
|
|
52
|
+
import time
|
|
53
|
+
|
|
54
|
+
rover = Rover()
|
|
55
|
+
|
|
56
|
+
for _ in range(10):
|
|
57
|
+
dist = rover.ultrasonic.distance_cm()
|
|
58
|
+
print(f"Ultrasonic distance: {dist} cm")
|
|
59
|
+
|
|
60
|
+
if dist < 65:
|
|
61
|
+
# Avoid obstacle: spin right
|
|
62
|
+
rover.motors(v_left=65, v_right=-65)
|
|
63
|
+
else:
|
|
64
|
+
# Drive forward
|
|
65
|
+
rover.motors(v_left=70, v_right=70)
|
|
66
|
+
|
|
67
|
+
time.sleep_ms(200)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Features
|
|
73
|
+
|
|
74
|
+
- **5x5 LED Matrix Display**: Visual terminal rendering with ANSI color highlighting (`display.show`, `display.set_pixel`, `display.clear`).
|
|
75
|
+
- **Icons**: Pre-packaged icons including `Icon.HEART`, `Icon.SMILE`, `Icon.SAD`, `Icon.SUN`, `Icon.ARROW_N`, and `Icon.CHECK`.
|
|
76
|
+
- **Piezo Sound Synthesizer**: Native audio via PC speaker / `winsound` on Windows and terminal fallbacks on macOS/Linux.
|
|
77
|
+
- **Sensors & Pins**: Virtual inputs for Temperature, Light Level, Push Buttons A & B, and Edge Pins P0, P1, P2.
|
|
78
|
+
- **MicroPython Compatibility**: Automatically injects `time.sleep_ms()` into Python's built-in `time` module so MicroPython scripts run unmodified in standard CPython.
|
|
79
|
+
- **2D Rover Kinematics**: Complete differential drive robotics model with raycasting and line sensors (`pulsemaker_rover.Rover`).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker Simulator - Python & MicroPython Runtime
|
|
3
|
+
DBE CAPS Coding & Robotics Platform
|
|
4
|
+
"""
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
import builtins
|
|
8
|
+
|
|
9
|
+
from .display import display, Display
|
|
10
|
+
from .icons import Icon, IconImage
|
|
11
|
+
from .music import music, Music
|
|
12
|
+
from .pins import pin0, pin1, pin2, Pin
|
|
13
|
+
from .buttons import button_a, button_b, Button
|
|
14
|
+
from .sensors import sensors, Sensors
|
|
15
|
+
from .rover import Rover
|
|
16
|
+
|
|
17
|
+
# 1. MicroPython Compatibility Shim:
|
|
18
|
+
# MicroPython provides time.sleep_ms(ms), whereas standard CPython only provides time.sleep(seconds).
|
|
19
|
+
# We dynamically attach sleep_ms to the time module so standard Python scripts run without error.
|
|
20
|
+
if not hasattr(time, "sleep_ms"):
|
|
21
|
+
time.sleep_ms = lambda ms: time.sleep(ms / 1000.0) # type: ignore
|
|
22
|
+
|
|
23
|
+
# 2. Inject Icon into Python builtins as a safety fallback
|
|
24
|
+
# so transpiled code using Icon.SUN works even if 'Icon' was omitted from the import line.
|
|
25
|
+
if not hasattr(builtins, "Icon"):
|
|
26
|
+
setattr(builtins, "Icon", Icon)
|
|
27
|
+
|
|
28
|
+
__version__ = "1.0.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"display",
|
|
32
|
+
"Display",
|
|
33
|
+
"Icon",
|
|
34
|
+
"IconImage",
|
|
35
|
+
"music",
|
|
36
|
+
"Music",
|
|
37
|
+
"pin0",
|
|
38
|
+
"pin1",
|
|
39
|
+
"pin2",
|
|
40
|
+
"Pin",
|
|
41
|
+
"button_a",
|
|
42
|
+
"button_b",
|
|
43
|
+
"Button",
|
|
44
|
+
"sensors",
|
|
45
|
+
"Sensors",
|
|
46
|
+
"Rover",
|
|
47
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker Push Buttons A & B
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
class Button:
|
|
7
|
+
def __init__(self, name: str):
|
|
8
|
+
self.name = name
|
|
9
|
+
self._pressed = False
|
|
10
|
+
|
|
11
|
+
def is_pressed(self) -> bool:
|
|
12
|
+
"""
|
|
13
|
+
Check if the button is currently pressed.
|
|
14
|
+
Can be influenced by environment variables PULSEMAKER_BUTTON_A=1 / PULSEMAKER_BUTTON_B=1.
|
|
15
|
+
"""
|
|
16
|
+
env_val = os.environ.get(f"PULSEMAKER_BUTTON_{self.name.upper()}", "")
|
|
17
|
+
if env_val in ("1", "true", "True"):
|
|
18
|
+
return True
|
|
19
|
+
return self._pressed
|
|
20
|
+
|
|
21
|
+
def set_pressed(self, pressed: bool = True) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Simulate pressing or releasing this button.
|
|
24
|
+
"""
|
|
25
|
+
self._pressed = bool(pressed)
|
|
26
|
+
|
|
27
|
+
def __repr__(self) -> str:
|
|
28
|
+
return f"<Button {self.name}: {'PRESSED' if self.is_pressed() else 'RELEASED'}>"
|
|
29
|
+
|
|
30
|
+
button_a = Button("A")
|
|
31
|
+
button_b = Button("B")
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker CLI Utility
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import runpy
|
|
6
|
+
from pulsemaker import __version__, display, Icon, music
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
if len(sys.argv) > 1:
|
|
10
|
+
cmd = sys.argv[1]
|
|
11
|
+
if cmd in ("-v", "--version"):
|
|
12
|
+
print(f"pulsemaker version {__version__}")
|
|
13
|
+
return
|
|
14
|
+
elif cmd in ("-h", "--help"):
|
|
15
|
+
print("PulseMaker Python Simulator CLI")
|
|
16
|
+
print("Usage:")
|
|
17
|
+
print(" pulsemaker run <script.py> Run a PulseMaker script")
|
|
18
|
+
print(" pulsemaker demo Run a built-in hardware demo")
|
|
19
|
+
return
|
|
20
|
+
elif cmd == "run" and len(sys.argv) > 2:
|
|
21
|
+
filepath = sys.argv[2]
|
|
22
|
+
print(f"Running PulseMaker script: {filepath}")
|
|
23
|
+
runpy.run_path(filepath, run_name="__main__")
|
|
24
|
+
return
|
|
25
|
+
elif cmd == "demo":
|
|
26
|
+
print("=== PulseMaker Demo ===")
|
|
27
|
+
display.show(Icon.SUN)
|
|
28
|
+
music.pitch(880, 250)
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
print(f"PulseMaker Python Runtime v{__version__} is ready.")
|
|
32
|
+
print("Run with '--help' for options.")
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker 5x5 LED Matrix Display Controller
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Union, List
|
|
6
|
+
from .icons import IconImage
|
|
7
|
+
|
|
8
|
+
class Display:
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self._matrix: List[List[int]] = [[0 for _ in range(5)] for _ in range(5)]
|
|
11
|
+
self._supports_color = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
|
|
12
|
+
encoding = getattr(sys.stdout, "encoding", "utf-8") or "utf-8"
|
|
13
|
+
self._is_utf8 = "utf" in encoding.lower()
|
|
14
|
+
|
|
15
|
+
def show(self, image: Union[IconImage, List[List[int]], str]) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Display an icon preset or a 5x5 brightness matrix (0-255).
|
|
18
|
+
"""
|
|
19
|
+
if isinstance(image, IconImage):
|
|
20
|
+
self._matrix = [row[:] for row in image.matrix]
|
|
21
|
+
name = image.name
|
|
22
|
+
elif isinstance(image, list):
|
|
23
|
+
self._matrix = [row[:] for row in image]
|
|
24
|
+
name = "CUSTOM"
|
|
25
|
+
else:
|
|
26
|
+
name = str(image)
|
|
27
|
+
|
|
28
|
+
self._render(header=f"Icon.{name}")
|
|
29
|
+
|
|
30
|
+
def clear(self) -> None:
|
|
31
|
+
"""
|
|
32
|
+
Turn off all 25 LEDs on the matrix.
|
|
33
|
+
"""
|
|
34
|
+
self._matrix = [[0 for _ in range(5)] for _ in range(5)]
|
|
35
|
+
self._render(header="CLEARED")
|
|
36
|
+
|
|
37
|
+
def set_pixel(self, x: int, y: int, brightness: int = 255) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Set brightness of a single pixel (x: 0-4, y: 0-4, brightness: 0-255).
|
|
40
|
+
"""
|
|
41
|
+
if 0 <= x < 5 and 0 <= y < 5:
|
|
42
|
+
self._matrix[y][x] = max(0, min(255, brightness))
|
|
43
|
+
self._render(header=f"pixel({x}, {y}) = {brightness}")
|
|
44
|
+
|
|
45
|
+
def _render(self, header: str = "") -> None:
|
|
46
|
+
"""
|
|
47
|
+
Render 5x5 LED matrix in the terminal safely across Windows (cp1252/utf-8) and Linux/macOS.
|
|
48
|
+
"""
|
|
49
|
+
use_unicode = self._is_utf8
|
|
50
|
+
|
|
51
|
+
# Characters for ON, MEDIUM, OFF
|
|
52
|
+
if use_unicode:
|
|
53
|
+
on_char = "■ "
|
|
54
|
+
med_char = "▫ "
|
|
55
|
+
off_char = "· "
|
|
56
|
+
t_left, t_right = "┌", "┐"
|
|
57
|
+
b_left, b_right = "└", "┘"
|
|
58
|
+
h_bar, v_bar = "─", "│"
|
|
59
|
+
else:
|
|
60
|
+
# Safe ASCII fallback for Windows cp1252 / legacy CMD
|
|
61
|
+
on_char = "# "
|
|
62
|
+
med_char = "* "
|
|
63
|
+
off_char = ". "
|
|
64
|
+
t_left, t_right = "+", "+"
|
|
65
|
+
b_left, b_right = "+", "+"
|
|
66
|
+
h_bar, v_bar = "-", "|"
|
|
67
|
+
|
|
68
|
+
title = f"[ PulseBit 5x5: {header} ]"
|
|
69
|
+
total_width = 32
|
|
70
|
+
left_pad = (total_width - len(title) - 2) // 2
|
|
71
|
+
right_pad = total_width - len(title) - 2 - left_pad
|
|
72
|
+
|
|
73
|
+
border_top = t_left + (h_bar * max(1, left_pad)) + title + (h_bar * max(1, right_pad)) + t_right
|
|
74
|
+
border_bot = b_left + (h_bar * (len(border_top) - 2)) + b_right
|
|
75
|
+
|
|
76
|
+
lines = [border_top]
|
|
77
|
+
for y in range(5):
|
|
78
|
+
row_chars = []
|
|
79
|
+
for x in range(5):
|
|
80
|
+
b = self._matrix[y][x]
|
|
81
|
+
if b > 150:
|
|
82
|
+
char = f"\033[91m{on_char}\033[0m" if self._supports_color else on_char
|
|
83
|
+
elif b > 0:
|
|
84
|
+
char = f"\033[33m{med_char}\033[0m" if self._supports_color else med_char
|
|
85
|
+
else:
|
|
86
|
+
char = f"\033[90m{off_char}\033[0m" if self._supports_color else off_char
|
|
87
|
+
row_chars.append(char)
|
|
88
|
+
lines.append(f"{v_bar} {''.join(row_chars)} {v_bar}")
|
|
89
|
+
lines.append(border_bot)
|
|
90
|
+
|
|
91
|
+
output = "\n" + "\n".join(lines) + "\n"
|
|
92
|
+
try:
|
|
93
|
+
print(output, flush=True)
|
|
94
|
+
except UnicodeEncodeError:
|
|
95
|
+
# Emergency ASCII fallback
|
|
96
|
+
safe_output = output.encode("ascii", "replace").decode("ascii")
|
|
97
|
+
print(safe_output, flush=True)
|
|
98
|
+
|
|
99
|
+
display = Display()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker 5x5 LED Matrix Icon Presets
|
|
3
|
+
"""
|
|
4
|
+
from typing import List
|
|
5
|
+
|
|
6
|
+
class IconImage:
|
|
7
|
+
def __init__(self, name: str, matrix: List[List[int]]):
|
|
8
|
+
self.name = name
|
|
9
|
+
self.matrix = matrix
|
|
10
|
+
|
|
11
|
+
def __repr__(self) -> str:
|
|
12
|
+
return f"<Icon.{self.name}>"
|
|
13
|
+
|
|
14
|
+
class _IconMeta(type):
|
|
15
|
+
def __getattr__(cls, name: str):
|
|
16
|
+
raise AttributeError(f"Icon '{name}' does not exist on PulseMaker Icon set.")
|
|
17
|
+
|
|
18
|
+
class Icon(metaclass=_IconMeta):
|
|
19
|
+
HEART = IconImage("HEART", [
|
|
20
|
+
[0, 255, 0, 255, 0],
|
|
21
|
+
[255, 255, 255, 255, 255],
|
|
22
|
+
[255, 255, 255, 255, 255],
|
|
23
|
+
[0, 255, 255, 255, 0],
|
|
24
|
+
[0, 0, 255, 0, 0],
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
SMILE = IconImage("SMILE", [
|
|
28
|
+
[0, 255, 0, 255, 0],
|
|
29
|
+
[0, 255, 0, 255, 0],
|
|
30
|
+
[0, 0, 0, 0, 0],
|
|
31
|
+
[255, 0, 0, 0, 255],
|
|
32
|
+
[0, 255, 255, 255, 0],
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
SAD = IconImage("SAD", [
|
|
36
|
+
[0, 255, 0, 255, 0],
|
|
37
|
+
[0, 0, 0, 0, 0],
|
|
38
|
+
[0, 255, 255, 255, 0],
|
|
39
|
+
[255, 0, 0, 0, 255],
|
|
40
|
+
[0, 0, 0, 0, 0],
|
|
41
|
+
])
|
|
42
|
+
|
|
43
|
+
SUN = IconImage("SUN", [
|
|
44
|
+
[255, 0, 255, 0, 255],
|
|
45
|
+
[0, 255, 255, 255, 0],
|
|
46
|
+
[255, 255, 255, 255, 255],
|
|
47
|
+
[0, 255, 255, 255, 0],
|
|
48
|
+
[255, 0, 255, 0, 255],
|
|
49
|
+
])
|
|
50
|
+
|
|
51
|
+
ARROW_N = IconImage("ARROW_N", [
|
|
52
|
+
[0, 0, 255, 0, 0],
|
|
53
|
+
[0, 255, 255, 255, 0],
|
|
54
|
+
[255, 0, 255, 0, 255],
|
|
55
|
+
[0, 0, 255, 0, 0],
|
|
56
|
+
[0, 0, 255, 0, 0],
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
CHECK = IconImage("CHECK", [
|
|
60
|
+
[0, 0, 0, 0, 255],
|
|
61
|
+
[0, 0, 0, 255, 0],
|
|
62
|
+
[255, 0, 255, 0, 0],
|
|
63
|
+
[0, 255, 0, 0, 0],
|
|
64
|
+
[0, 0, 0, 0, 0],
|
|
65
|
+
])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker Piezo Buzzer Sound Synthesizer
|
|
3
|
+
"""
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
class Music:
|
|
8
|
+
def __init__(self):
|
|
9
|
+
self._winsound = None
|
|
10
|
+
if sys.platform == "win32":
|
|
11
|
+
try:
|
|
12
|
+
import winsound
|
|
13
|
+
self._winsound = winsound
|
|
14
|
+
except ImportError:
|
|
15
|
+
self._winsound = None
|
|
16
|
+
|
|
17
|
+
def pitch(self, frequency: int, duration: int = 300) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Play a musical frequency (Hz) for a duration (milliseconds).
|
|
20
|
+
"""
|
|
21
|
+
freq = int(max(37, min(32767, frequency)))
|
|
22
|
+
dur_ms = int(max(10, duration))
|
|
23
|
+
|
|
24
|
+
played = False
|
|
25
|
+
if self._winsound:
|
|
26
|
+
try:
|
|
27
|
+
# Windows hardware/driver beep
|
|
28
|
+
self._winsound.Beep(freq, dur_ms)
|
|
29
|
+
played = True
|
|
30
|
+
except Exception:
|
|
31
|
+
played = False
|
|
32
|
+
|
|
33
|
+
if not played:
|
|
34
|
+
# Fallback for systems without hardware beep
|
|
35
|
+
print(f"[PulseMaker Buzzer ♫ {freq} Hz for {dur_ms} ms]", flush=True)
|
|
36
|
+
time.sleep(dur_ms / 1000.0)
|
|
37
|
+
|
|
38
|
+
music = Music()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker GPIO Edge Connector Pins (P0, P1, P2)
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
class Pin:
|
|
6
|
+
def __init__(self, name: str):
|
|
7
|
+
self.name = name
|
|
8
|
+
self.value = 0
|
|
9
|
+
|
|
10
|
+
def write_digital(self, value: int) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Write digital state (1 for HIGH, 0 for LOW).
|
|
13
|
+
"""
|
|
14
|
+
self.value = 1 if value else 0
|
|
15
|
+
state_str = "HIGH (1)" if self.value else "LOW (0)"
|
|
16
|
+
print(f"[PulseMaker Pin {self.name} -> {state_str}]", flush=True)
|
|
17
|
+
|
|
18
|
+
def read_digital(self) -> int:
|
|
19
|
+
"""
|
|
20
|
+
Read current digital state (0 or 1).
|
|
21
|
+
"""
|
|
22
|
+
return self.value
|
|
23
|
+
|
|
24
|
+
def __repr__(self) -> str:
|
|
25
|
+
return f"<Pin {self.name}: {self.value}>"
|
|
26
|
+
|
|
27
|
+
pin0 = Pin("P0")
|
|
28
|
+
pin1 = Pin("P1")
|
|
29
|
+
pin2 = Pin("P2")
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker 2D Autonomous Rover Controller (Grade 7 Senior Phase)
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
class UltrasonicSensor:
|
|
8
|
+
def __init__(self, initial_dist: int = 120):
|
|
9
|
+
self._distance = initial_dist
|
|
10
|
+
|
|
11
|
+
def distance_cm(self) -> int:
|
|
12
|
+
"""
|
|
13
|
+
Return simulated ultrasonic distance in cm.
|
|
14
|
+
Can be overridden via PULSEMAKER_ROVER_DISTANCE=45.
|
|
15
|
+
"""
|
|
16
|
+
env_val = os.environ.get("PULSEMAKER_ROVER_DISTANCE")
|
|
17
|
+
if env_val:
|
|
18
|
+
try:
|
|
19
|
+
return int(env_val)
|
|
20
|
+
except ValueError:
|
|
21
|
+
pass
|
|
22
|
+
return self._distance
|
|
23
|
+
|
|
24
|
+
def set_distance(self, distance_cm: int) -> None:
|
|
25
|
+
self._distance = int(distance_cm)
|
|
26
|
+
|
|
27
|
+
class Rover:
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.v_left = 0
|
|
30
|
+
self.v_right = 0
|
|
31
|
+
self.ultrasonic = UltrasonicSensor(initial_dist=45)
|
|
32
|
+
self._left_line = False
|
|
33
|
+
self._right_line = False
|
|
34
|
+
|
|
35
|
+
def motors(self, v_left: int, v_right: int) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Set left and right motor velocities (-100 to 100).
|
|
38
|
+
"""
|
|
39
|
+
self.v_left = max(-100, min(100, int(v_left)))
|
|
40
|
+
self.v_right = max(-100, min(100, int(v_right)))
|
|
41
|
+
print(f"[Rover Motors: v_left={self.v_left}%, v_right={self.v_right}%]", flush=True)
|
|
42
|
+
|
|
43
|
+
def line_sensor_left(self) -> bool:
|
|
44
|
+
"""
|
|
45
|
+
Return True if left optical sensor detects line.
|
|
46
|
+
"""
|
|
47
|
+
return self._left_line
|
|
48
|
+
|
|
49
|
+
def line_sensor_right(self) -> bool:
|
|
50
|
+
"""
|
|
51
|
+
Return True if right optical sensor detects line.
|
|
52
|
+
"""
|
|
53
|
+
return self._right_line
|
|
54
|
+
|
|
55
|
+
def set_line_sensors(self, left: bool, right: bool) -> None:
|
|
56
|
+
self._left_line = bool(left)
|
|
57
|
+
self._right_line = bool(right)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PulseMaker Virtual Environment Sensors
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
class Sensors:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self._temp = 32.0 # Default 32°C so heatwave alarms trigger predictably in tests
|
|
9
|
+
self._light = 180 # Default 180 (0-255)
|
|
10
|
+
|
|
11
|
+
def temperature(self) -> float:
|
|
12
|
+
"""
|
|
13
|
+
Get live simulated ambient temperature in °C.
|
|
14
|
+
Can be overridden via environment variable PULSEMAKER_TEMP=35.
|
|
15
|
+
"""
|
|
16
|
+
env_val = os.environ.get("PULSEMAKER_TEMP")
|
|
17
|
+
if env_val:
|
|
18
|
+
try:
|
|
19
|
+
return float(env_val)
|
|
20
|
+
except ValueError:
|
|
21
|
+
pass
|
|
22
|
+
return self._temp
|
|
23
|
+
|
|
24
|
+
def light_level(self) -> int:
|
|
25
|
+
"""
|
|
26
|
+
Get live simulated light level (0-255).
|
|
27
|
+
Can be overridden via environment variable PULSEMAKER_LIGHT=200.
|
|
28
|
+
"""
|
|
29
|
+
env_val = os.environ.get("PULSEMAKER_LIGHT")
|
|
30
|
+
if env_val:
|
|
31
|
+
try:
|
|
32
|
+
return int(env_val)
|
|
33
|
+
except ValueError:
|
|
34
|
+
pass
|
|
35
|
+
return self._light
|
|
36
|
+
|
|
37
|
+
def set_temperature(self, degrees_celsius: float) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Set simulated ambient temperature.
|
|
40
|
+
"""
|
|
41
|
+
self._temp = float(degrees_celsius)
|
|
42
|
+
|
|
43
|
+
def set_light_level(self, level: int) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Set simulated light level (0 to 255).
|
|
46
|
+
"""
|
|
47
|
+
self._light = max(0, min(255, int(level)))
|
|
48
|
+
|
|
49
|
+
sensors = Sensors()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pulsemaker-simulator
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python and MicroPython runtime library for PulseMaker Simulator (DBE CAPS Coding & Robotics)
|
|
5
|
+
Author: PulseMaker Simulator Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: robotics,simulator,micropython,education,stem,caps
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Education
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# PulseMaker Python Library
|
|
17
|
+
|
|
18
|
+
The official Python / MicroPython companion library for the **PulseMaker Simulator** (DBE CAPS Coding & Robotics curriculum for South African schools, Grades R–7).
|
|
19
|
+
|
|
20
|
+
This library allows code generated or written in the PulseMaker browser simulator to run on any computer or MicroPython microcontroller without modifications.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install via pip:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install pulsemaker-simulator
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or install locally in editable mode:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# From the project root
|
|
36
|
+
pip install -e ./python
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Quick Example (Intermediate Phase 5x5 Board)
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from pulsemaker import display, music, pin0, pin1, pin2, button_a, button_b, sensors, Icon
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
# Check simulated environment sensors
|
|
48
|
+
if sensors.temperature() > 30:
|
|
49
|
+
display.show(Icon.SUN)
|
|
50
|
+
music.pitch(880, duration=300)
|
|
51
|
+
print("Heatwave Alert! Temp > 30C")
|
|
52
|
+
else:
|
|
53
|
+
display.show(Icon.SMILE)
|
|
54
|
+
print("Temperature Normal")
|
|
55
|
+
|
|
56
|
+
# Wait and clear
|
|
57
|
+
time.sleep_ms(1000)
|
|
58
|
+
display.clear()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Senior Phase Example (2D Autonomous Rover)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from pulsemaker_rover import Rover
|
|
67
|
+
import time
|
|
68
|
+
|
|
69
|
+
rover = Rover()
|
|
70
|
+
|
|
71
|
+
for _ in range(10):
|
|
72
|
+
dist = rover.ultrasonic.distance_cm()
|
|
73
|
+
print(f"Ultrasonic distance: {dist} cm")
|
|
74
|
+
|
|
75
|
+
if dist < 65:
|
|
76
|
+
# Avoid obstacle: spin right
|
|
77
|
+
rover.motors(v_left=65, v_right=-65)
|
|
78
|
+
else:
|
|
79
|
+
# Drive forward
|
|
80
|
+
rover.motors(v_left=70, v_right=70)
|
|
81
|
+
|
|
82
|
+
time.sleep_ms(200)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Features
|
|
88
|
+
|
|
89
|
+
- **5x5 LED Matrix Display**: Visual terminal rendering with ANSI color highlighting (`display.show`, `display.set_pixel`, `display.clear`).
|
|
90
|
+
- **Icons**: Pre-packaged icons including `Icon.HEART`, `Icon.SMILE`, `Icon.SAD`, `Icon.SUN`, `Icon.ARROW_N`, and `Icon.CHECK`.
|
|
91
|
+
- **Piezo Sound Synthesizer**: Native audio via PC speaker / `winsound` on Windows and terminal fallbacks on macOS/Linux.
|
|
92
|
+
- **Sensors & Pins**: Virtual inputs for Temperature, Light Level, Push Buttons A & B, and Edge Pins P0, P1, P2.
|
|
93
|
+
- **MicroPython Compatibility**: Automatically injects `time.sleep_ms()` into Python's built-in `time` module so MicroPython scripts run unmodified in standard CPython.
|
|
94
|
+
- **2D Rover Kinematics**: Complete differential drive robotics model with raycasting and line sensors (`pulsemaker_rover.Rover`).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pulsemaker/__init__.py
|
|
4
|
+
pulsemaker/buttons.py
|
|
5
|
+
pulsemaker/cli.py
|
|
6
|
+
pulsemaker/display.py
|
|
7
|
+
pulsemaker/icons.py
|
|
8
|
+
pulsemaker/music.py
|
|
9
|
+
pulsemaker/pins.py
|
|
10
|
+
pulsemaker/rover.py
|
|
11
|
+
pulsemaker/sensors.py
|
|
12
|
+
pulsemaker_rover/__init__.py
|
|
13
|
+
pulsemaker_simulator.egg-info/PKG-INFO
|
|
14
|
+
pulsemaker_simulator.egg-info/SOURCES.txt
|
|
15
|
+
pulsemaker_simulator.egg-info/dependency_links.txt
|
|
16
|
+
pulsemaker_simulator.egg-info/entry_points.txt
|
|
17
|
+
pulsemaker_simulator.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pulsemaker-simulator"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Python and MicroPython runtime library for PulseMaker Simulator (DBE CAPS Coding & Robotics)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{ name = "PulseMaker Simulator Team" }]
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
keywords = ["robotics", "simulator", "micropython", "education", "stem", "caps"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Education",
|
|
19
|
+
"Topic :: Scientific/Engineering",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
pulsemaker = "pulsemaker.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["."]
|
|
28
|
+
include = ["pulsemaker*", "pulsemaker_rover*"]
|