pulsemaker-simulator 1.0.0__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.
pulsemaker/__init__.py ADDED
@@ -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
+ ]
pulsemaker/buttons.py ADDED
@@ -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")
pulsemaker/cli.py ADDED
@@ -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()
pulsemaker/display.py ADDED
@@ -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()
pulsemaker/icons.py ADDED
@@ -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
+ ])
pulsemaker/music.py ADDED
@@ -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()
pulsemaker/pins.py ADDED
@@ -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")
pulsemaker/rover.py ADDED
@@ -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)
pulsemaker/sensors.py ADDED
@@ -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,6 @@
1
+ """
2
+ PulseMaker Rover Module
3
+ """
4
+ from pulsemaker.rover import Rover, UltrasonicSensor
5
+
6
+ __all__ = ["Rover", "UltrasonicSensor"]
@@ -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,15 @@
1
+ pulsemaker/__init__.py,sha256=deStSNhH0NGk5xUEqUEoYdtCctIhq4k4Xhdvz-zNLN8,1215
2
+ pulsemaker/buttons.py,sha256=Q6xCsttxec1h3uA3bVd4yZ8eX2PgdCHOvCedW-EMSN8,877
3
+ pulsemaker/cli.py,sha256=71AlRtojCGvRwGlbHxj9077swAQQ9N3xn4Er2wywQ7o,1116
4
+ pulsemaker/display.py,sha256=wLZPtSe6QMTlTgKorGt9YUMVEQ1A_0ytQC4ESrY4eIY,3617
5
+ pulsemaker/icons.py,sha256=CPR7FoyupvWChNM8PKuyT4_hQpQBV-64d6p4lpSocUE,1570
6
+ pulsemaker/music.py,sha256=zHO_-8E54cy1KNl2_CGrKmc3h7wjE_YsdlGJ2k3TJz8,1067
7
+ pulsemaker/pins.py,sha256=BGNgwLzwF5_ZNpjIq-Z9fKe5JvAvE48CwKFLHLTev4c,717
8
+ pulsemaker/rover.py,sha256=0TVwMAMlA-sWin3MThLFYxHbLOD1BE33DZHO1C6sRIE,1703
9
+ pulsemaker/sensors.py,sha256=PMkx1vtQlS58ACID2CT37hK7aaGQTFV57_Jh1w6v3Ng,1383
10
+ pulsemaker_rover/__init__.py,sha256=WIO-7YcV2vEHV8kLv7BXlMeVHgwyjbGLndSfDDoUF7w,126
11
+ pulsemaker_simulator-1.0.0.dist-info/METADATA,sha256=6wKAWGsujxa2d_vyaOGZ5WATVPjavxi2O2w4Ks_19ic,2829
12
+ pulsemaker_simulator-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
13
+ pulsemaker_simulator-1.0.0.dist-info/entry_points.txt,sha256=bjlgk01uUMQ32ahbEtZsNlHi4s2gwuFodM7GwZKVgmk,51
14
+ pulsemaker_simulator-1.0.0.dist-info/top_level.txt,sha256=GCr7j4RP4GDrbmAgEJG9ZjQJEhkqrv9cLNgrzP0tF1s,28
15
+ pulsemaker_simulator-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pulsemaker = pulsemaker.cli:main
@@ -0,0 +1,2 @@
1
+ pulsemaker
2
+ pulsemaker_rover