openswarm-master 0.1.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.
openswarm/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """
2
+ OpenSwarm Master SDK
3
+ ====================
4
+ Zero-Control-Logic Distributed Hardware Abstraction Layer (Remote HAL) Master.
5
+ All robot intelligence, sensor reading, and actuator control runs here on the Master.
6
+ """
7
+
8
+ from .master import SwarmMaster
9
+ from .node import RemoteSwarmNode
10
+
11
+ __version__ = "0.1.0"
12
+ __all__ = ["SwarmMaster", "RemoteSwarmNode"]
openswarm/cli.py ADDED
@@ -0,0 +1,76 @@
1
+ """
2
+ OpenSwarm Command-Line Interface (`openswarm`)
3
+ Allows scanning for Slave nodes, monitoring live telemetry, and testing LED/pins directly from terminal.
4
+ """
5
+
6
+ import argparse
7
+ import time
8
+ from .master import SwarmMaster
9
+
10
+
11
+ def cmd_scan(args: argparse.Namespace) -> None:
12
+ master = SwarmMaster()
13
+ print(f"Scanning local network for OpenSwarm Slave nodes ({args.wait:.1f}s)...")
14
+ nodes = master.discover_all(wait_seconds=args.wait)
15
+ if not nodes:
16
+ print("No Slave nodes discovered via UDP broadcast.")
17
+ print("Tip: If your Wi-Fi router blocks UDP broadcast, use `--ip <SLAVE_IP>` in commands.")
18
+ else:
19
+ print(f"\nDiscovered {len(nodes)} Slave Node(s):")
20
+ print("-" * 78)
21
+ for node in nodes.values():
22
+ print(f" Node ID : {node.node_id} ({node.board})")
23
+ print(f" Address : {node.ip}:{node.port}")
24
+ print(f" Actuators : {node.declared_actuators}")
25
+ print(f" Sensors : {node.sensors}")
26
+ print("-" * 78)
27
+ master.close()
28
+
29
+
30
+ def cmd_blink(args: argparse.Namespace) -> None:
31
+ master = SwarmMaster()
32
+ node = master.connect(args.node, ip=args.ip, timeout=args.timeout)
33
+ print(f"\nConnected to '{node.node_id}' at {node.ip}:{node.port}!")
34
+ print(f"Blinking '{args.actuator}' (Ctrl+C to stop)...\n")
35
+
36
+ state = False
37
+ try:
38
+ while True:
39
+ state = not state
40
+ node.digital_write(args.actuator, state)
41
+ print(
42
+ f" -> LED={'ON ' if state else 'OFF'} | "
43
+ f"Uptime={node.uptime_ms / 1000.0:.1f}s | "
44
+ f"Sensors={node.sensors}"
45
+ )
46
+ time.sleep(args.interval)
47
+ except KeyboardInterrupt:
48
+ print("\nTurning LED OFF and closing Master connection...")
49
+ node.digital_write(args.actuator, 0)
50
+ master.close()
51
+
52
+
53
+ def main() -> None:
54
+ parser = argparse.ArgumentParser(description="OpenSwarm Linux Master CLI")
55
+ sub = parser.add_subparsers(dest="command", required=True)
56
+
57
+ # openswarm scan
58
+ p_scan = sub.add_parser("scan", help="Auto-discover all OpenSwarm Slave nodes on Wi-Fi")
59
+ p_scan.add_argument("--wait", type=float, default=2.5, help="Scan duration in seconds")
60
+ p_scan.set_defaults(func=cmd_scan)
61
+
62
+ # openswarm blink <node_id>
63
+ p_blink = sub.add_parser("blink", help="Blink a registered LED/Digital output on a Slave node")
64
+ p_blink.add_argument("node", nargs="?", default="esp32_node", help="Slave Node ID (default: esp32_node)")
65
+ p_blink.add_argument("--ip", type=str, default=None, help="Optional Slave IP address (e.g. 192.168.0.118)")
66
+ p_blink.add_argument("--actuator", type=str, default="led", help="Actuator name (default: led)")
67
+ p_blink.add_argument("--interval", type=float, default=0.5, help="Blink interval in seconds")
68
+ p_blink.add_argument("--timeout", type=float, default=8.0, help="Discovery timeout in seconds")
69
+ p_blink.set_defaults(func=cmd_blink)
70
+
71
+ args = parser.parse_args()
72
+ args.func(args)
73
+
74
+
75
+ if __name__ == "__main__":
76
+ main()
openswarm/master.py ADDED
@@ -0,0 +1,223 @@
1
+ """
2
+ SwarmMaster — Central Linux/Cross-Platform Master Engine for OpenSwarm.
3
+ - Listens on UDP Port 4211 for Slave auto-discovery broadcasts ("HELLO").
4
+ - Sends low-latency actuator commands and 5 Hz watchdog heartbeats ("PING") over UDP Port 4210.
5
+ - Receives 20 Hz sensor telemetry streams ("STATE") from all connected Slaves in a background thread.
6
+ """
7
+
8
+ import json
9
+ import socket
10
+ import threading
11
+ import time
12
+ from typing import Any, Dict, Optional
13
+
14
+ from .node import RemoteSwarmNode
15
+
16
+ SWARM_DEFAULT_SLAVE_PORT = 4210
17
+ SWARM_DISCOVERY_PORT = 4211
18
+ SWARM_HEARTBEAT_SEC = 0.20 # 200ms keepalive (well below the 500ms Slave hardware watchdog)
19
+
20
+
21
+ class SwarmMaster:
22
+ def __init__(self, discovery_port: int = SWARM_DISCOVERY_PORT, heartbeat_sec: float = SWARM_HEARTBEAT_SEC):
23
+ self.discovery_port = discovery_port
24
+ self.heartbeat_sec = heartbeat_sec
25
+ self._nodes: Dict[str, RemoteSwarmNode] = {}
26
+ self._lock = threading.Lock()
27
+ self._running = True
28
+
29
+ # 1. Command & Telemetry Socket (ephemeral port; Slaves reply with STATE to this socket)
30
+ self._cmd_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
31
+ self._cmd_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
32
+ self._cmd_sock.bind(("0.0.0.0", 0))
33
+ self._cmd_sock.settimeout(0.05)
34
+
35
+ # 2. Discovery Socket (listens on UDP 4211 for Slave HELLO broadcasts)
36
+ self._disc_sock: Optional[socket.socket] = None
37
+ try:
38
+ self._disc_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
39
+ self._disc_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
40
+ if hasattr(socket, "SO_BROADCAST"):
41
+ self._disc_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
42
+ self._disc_sock.bind(("0.0.0.0", self.discovery_port))
43
+ self._disc_sock.settimeout(0.05)
44
+ except OSError:
45
+ # If another process is already bound to 4211, direct IP connect still works
46
+ self._disc_sock = None
47
+
48
+ # Start background network & heartbeat daemon threads
49
+ self._rx_thread = threading.Thread(target=self._network_loop, daemon=True, name="OpenSwarmRx")
50
+ self._hb_thread = threading.Thread(target=self._heartbeat_loop, daemon=True, name="OpenSwarmHeartbeat")
51
+ self._rx_thread.start()
52
+ self._hb_thread.start()
53
+
54
+ # -------------------------------------------------------------------------
55
+ # Public Discovery & Connection API
56
+ # -------------------------------------------------------------------------
57
+ def connect(
58
+ self,
59
+ node_id: str,
60
+ ip: Optional[str] = None,
61
+ port: int = SWARM_DEFAULT_SLAVE_PORT,
62
+ timeout: float = 10.0,
63
+ ) -> RemoteSwarmNode:
64
+ """
65
+ Connects to a Slave node by its Node ID (e.g. 'esp32_node').
66
+ - If `ip` is provided (e.g. ip='192.168.0.118'), connects immediately and verifies telemetry.
67
+ - Otherwise, waits up to `timeout` seconds for the Slave's UDP auto-discovery broadcast.
68
+ """
69
+ if ip is not None:
70
+ with self._lock:
71
+ if node_id not in self._nodes:
72
+ self._nodes[node_id] = RemoteSwarmNode(
73
+ node_id=node_id,
74
+ ip=ip,
75
+ port=port,
76
+ send_fn=self._send_json,
77
+ )
78
+ else:
79
+ self._nodes[node_id].ip = ip
80
+ self._nodes[node_id].port = port
81
+ node = self._nodes[node_id]
82
+
83
+ # Immediately send a PING so the Slave starts streaming 20Hz telemetry to us
84
+ node.ping()
85
+ time.sleep(0.1)
86
+ return node
87
+
88
+ print(f"[OpenSwarm Master] Waiting for Slave '{node_id}' on UDP port {self.discovery_port}...")
89
+ start = time.time()
90
+ while (time.time() - start) < timeout:
91
+ with self._lock:
92
+ if node_id in self._nodes:
93
+ node = self._nodes[node_id]
94
+ node.ping()
95
+ print(
96
+ f"[OpenSwarm Master] Discovered '{node.node_id}' ({node.board}) "
97
+ f"at {node.ip}:{node.port} | Actuators: {node.declared_actuators} | Sensors: {node.declared_sensors}"
98
+ )
99
+ return node
100
+ time.sleep(0.05)
101
+
102
+ raise TimeoutError(
103
+ f"Could not auto-discover Slave '{node_id}' within {timeout}s. "
104
+ f"Make sure your laptop and ESP32 are on the same Wi-Fi network, or pass ip='192.168.x.x' directly: "
105
+ f"master.connect('{node_id}', ip='192.168.0.118')"
106
+ )
107
+
108
+ def discover_all(self, wait_seconds: float = 2.5) -> Dict[str, RemoteSwarmNode]:
109
+ """Listens for `wait_seconds` and returns all discovered Slave nodes on the network."""
110
+ time.sleep(wait_seconds)
111
+ with self._lock:
112
+ return dict(self._nodes)
113
+
114
+ @property
115
+ def nodes(self) -> Dict[str, RemoteSwarmNode]:
116
+ with self._lock:
117
+ return dict(self._nodes)
118
+
119
+ def close(self) -> None:
120
+ """Stops all connected Slave motors and closes Master sockets."""
121
+ self._running = False
122
+ with self._lock:
123
+ for node in self._nodes.values():
124
+ try:
125
+ node.stop()
126
+ except Exception:
127
+ pass
128
+ try:
129
+ self._cmd_sock.close()
130
+ except Exception:
131
+ pass
132
+ if self._disc_sock:
133
+ try:
134
+ self._disc_sock.close()
135
+ except Exception:
136
+ pass
137
+
138
+ # -------------------------------------------------------------------------
139
+ # Internal UDP Transport & Background Threads
140
+ # -------------------------------------------------------------------------
141
+ def _send_json(self, ip: str, port: int, payload: Dict[str, Any]) -> None:
142
+ raw = json.dumps(payload, separators=(",", ":")).encode("utf-8")
143
+ try:
144
+ self._cmd_sock.sendto(raw, (ip, port))
145
+ except OSError:
146
+ pass
147
+
148
+ def _network_loop(self) -> None:
149
+ while self._running:
150
+ # 1. Check Discovery Socket (HELLO broadcasts on port 4211)
151
+ if self._disc_sock is not None:
152
+ try:
153
+ data, addr = self._disc_sock.recvfrom(2048)
154
+ self._handle_packet(data, addr)
155
+ except socket.timeout:
156
+ pass
157
+ except OSError:
158
+ pass
159
+
160
+ # 2. Check Command/Telemetry Socket (STATE telemetry packets)
161
+ try:
162
+ data, addr = self._cmd_sock.recvfrom(2048)
163
+ self._handle_packet(data, addr)
164
+ except socket.timeout:
165
+ pass
166
+ except OSError:
167
+ pass
168
+
169
+ def _handle_packet(self, data: bytes, addr: tuple) -> None:
170
+ try:
171
+ msg = json.loads(data.decode("utf-8", errors="ignore"))
172
+ except Exception:
173
+ return
174
+
175
+ pkt_type = msg.get("type")
176
+ node_id = msg.get("node")
177
+ if not node_id:
178
+ return
179
+
180
+ sender_ip = msg.get("ip") or addr[0]
181
+ sender_port = int(msg.get("port") or SWARM_DEFAULT_SLAVE_PORT)
182
+
183
+ with self._lock:
184
+ if pkt_type == "HELLO":
185
+ board = msg.get("board", "UNKNOWN")
186
+ actuators = msg.get("actuators", [])
187
+ sensors = msg.get("sensors", [])
188
+ if node_id not in self._nodes:
189
+ node = RemoteSwarmNode(
190
+ node_id=node_id,
191
+ ip=sender_ip,
192
+ port=sender_port,
193
+ send_fn=self._send_json,
194
+ board=board,
195
+ actuators=actuators,
196
+ sensors=sensors,
197
+ )
198
+ self._nodes[node_id] = node
199
+ # Trigger initial PING so Slave starts streaming STATE
200
+ node.ping()
201
+ else:
202
+ self._nodes[node_id]._update_hello(sender_ip, sender_port, board, actuators, sensors)
203
+
204
+ elif pkt_type == "STATE":
205
+ uptime_ms = int(msg.get("uptime_ms", 0))
206
+ sensors_dict = msg.get("sensors", {})
207
+ if node_id not in self._nodes:
208
+ self._nodes[node_id] = RemoteSwarmNode(
209
+ node_id=node_id,
210
+ ip=addr[0],
211
+ port=addr[1],
212
+ send_fn=self._send_json,
213
+ sensors=list(sensors_dict.keys()),
214
+ )
215
+ self._nodes[node_id]._update_state(uptime_ms, sensors_dict)
216
+
217
+ def _heartbeat_loop(self) -> None:
218
+ while self._running:
219
+ with self._lock:
220
+ nodes_list = list(self._nodes.values())
221
+ for node in nodes_list:
222
+ node.ping()
223
+ time.sleep(self.heartbeat_sec)
openswarm/node.py ADDED
@@ -0,0 +1,181 @@
1
+ """
2
+ RemoteSwarmNode — Represents a single connected Slave node (ESP32, UNO, Nano, RP2040, or Linux SBC).
3
+ Exposes clean Python methods to read live sensors and command any actuator, pin, or bus protocol.
4
+ """
5
+
6
+ import time
7
+ from typing import Any, Callable, Dict, List, Optional, Union
8
+
9
+
10
+ class RemoteSwarmNode:
11
+ def __init__(
12
+ self,
13
+ node_id: str,
14
+ ip: str,
15
+ port: int,
16
+ send_fn: Callable[[str, int, Dict[str, Any]], None],
17
+ board: str = "UNKNOWN",
18
+ actuators: Optional[List[str]] = None,
19
+ sensors: Optional[List[str]] = None,
20
+ ):
21
+ self.node_id: str = node_id
22
+ self.ip: str = ip
23
+ self.port: int = port
24
+ self.board: str = board
25
+ self.declared_actuators: List[str] = actuators or []
26
+ self.declared_sensors: List[str] = sensors or []
27
+
28
+ self._send_fn = send_fn
29
+ self._sensor_values: Dict[str, Union[float, List[float]]] = {}
30
+ self.uptime_ms: int = 0
31
+ self.last_seen: float = time.time()
32
+
33
+ # -------------------------------------------------------------------------
34
+ # Internal state updates (called by SwarmMaster background thread)
35
+ # -------------------------------------------------------------------------
36
+ def _update_hello(self, ip: str, port: int, board: str, actuators: List[str], sensors: List[str]) -> None:
37
+ self.ip = ip
38
+ self.port = port
39
+ self.board = board
40
+ self.declared_actuators = actuators
41
+ self.declared_sensors = sensors
42
+ self.last_seen = time.time()
43
+
44
+ def _update_state(self, uptime_ms: int, sensors_dict: Dict[str, Any]) -> None:
45
+ self.uptime_ms = uptime_ms
46
+ self._sensor_values.update(sensors_dict)
47
+ self.last_seen = time.time()
48
+
49
+ # -------------------------------------------------------------------------
50
+ # Sensor Reading API
51
+ # -------------------------------------------------------------------------
52
+ @property
53
+ def sensors(self) -> Dict[str, Union[float, List[float]]]:
54
+ """Returns a dictionary of the latest 20Hz streamed sensor values."""
55
+ return dict(self._sensor_values)
56
+
57
+ def read(self, sensor_name: str, default: Any = 0.0) -> Union[float, List[float]]:
58
+ """Reads the latest value of a named sensor (e.g., node.read('rssi') or node.read('front_sonar'))."""
59
+ return self._sensor_values.get(sensor_name, default)
60
+
61
+ @property
62
+ def is_online(self) -> bool:
63
+ """Returns True if we heard from this Slave within the last 2.0 seconds."""
64
+ return (time.time() - self.last_seen) < 2.0
65
+
66
+ # -------------------------------------------------------------------------
67
+ # Pin & GPIO Control API
68
+ # -------------------------------------------------------------------------
69
+ def digital_write(self, name: str, state: Union[int, bool]) -> None:
70
+ """
71
+ Writes HIGH (1/True) or LOW (0/False) to a registered digital output pin
72
+ (e.g., node.digital_write('led', 1)).
73
+ """
74
+ val = 1 if state else 0
75
+ self._send_fn(self.ip, self.port, {"cmd": "ACT", "name": name, "value": val})
76
+
77
+ def pwm_write(self, name_or_pin: Union[str, int], duty: int) -> None:
78
+ """
79
+ Writes a PWM duty cycle (0..255) either to a registered PWM output name
80
+ or directly to a raw hardware pin number.
81
+ """
82
+ duty_clamped = max(0, min(255, int(duty)))
83
+ if isinstance(name_or_pin, int):
84
+ self._send_fn(self.ip, self.port, {"cmd": "PIN", "pin": name_or_pin, "value": duty_clamped, "pwm": 1})
85
+ else:
86
+ self._send_fn(self.ip, self.port, {"cmd": "ACT", "name": name_or_pin, "value": duty_clamped})
87
+
88
+ def pin_write(self, pin: int, value: Union[int, bool], pwm: bool = False) -> None:
89
+ """
90
+ Direct raw GPIO pin control by pin number (even if not registered in setup()).
91
+ Example: node.pin_write(2, 1) -> sets GPIO 2 HIGH.
92
+ """
93
+ val = int(value)
94
+ self._send_fn(
95
+ self.ip,
96
+ self.port,
97
+ {"cmd": "PIN", "pin": int(pin), "value": val, "pwm": 1 if pwm else 0},
98
+ )
99
+
100
+ # -------------------------------------------------------------------------
101
+ # Motor, Servo & Stepper Control API
102
+ # -------------------------------------------------------------------------
103
+ def drive(self, name: str, left: int, right: int = 0) -> None:
104
+ """
105
+ Drives a dual H-Bridge (L298N / DRV8833) or single 2-pin DC motor (-255 to +255).
106
+ Example: node.drive('drive', left=180, right=180)
107
+ """
108
+ self._send_fn(
109
+ self.ip,
110
+ self.port,
111
+ {"cmd": "ACT", "name": name, "left": int(left), "right": int(right)},
112
+ )
113
+
114
+ def servo(self, name: str, angle: int) -> None:
115
+ """
116
+ Sets a hobby servo angle (0 to 180 degrees).
117
+ Example: node.servo('gripper_servo', 90)
118
+ """
119
+ self._send_fn(
120
+ self.ip,
121
+ self.port,
122
+ {"cmd": "ACT", "name": name, "angle": int(angle)},
123
+ )
124
+
125
+ def stepper(self, name: str, steps: int, step_delay_us: int = 800) -> None:
126
+ """
127
+ Commands a Step/Dir stepper driver (positive = forward, negative = reverse).
128
+ Example: node.stepper('lifter_step', steps=200, step_delay_us=800)
129
+ """
130
+ self._send_fn(
131
+ self.ip,
132
+ self.port,
133
+ {"cmd": "ACT", "name": name, "steps": int(steps), "v2": int(step_delay_us)},
134
+ )
135
+
136
+ def stop(self) -> None:
137
+ """Immediately stops all motors on this Slave node."""
138
+ self._send_fn(self.ip, self.port, {"cmd": "STOP"})
139
+
140
+ # -------------------------------------------------------------------------
141
+ # Universal Bus & Custom Actuator API (LoRa, SPI, I2C, UART, Custom)
142
+ # -------------------------------------------------------------------------
143
+ def send_bus(self, name: str, data: str = "", v1: int = 0, v2: int = 0) -> None:
144
+ """
145
+ Transmits a string payload or numeric values over a registered LoRa, UART, or Custom actuator.
146
+ Example: node.send_bus('lora_radio', data='HELLO_SWARM')
147
+ """
148
+ self._send_fn(
149
+ self.ip,
150
+ self.port,
151
+ {"cmd": "ACT", "name": name, "v1": int(v1), "v2": int(v2), "data": str(data)},
152
+ )
153
+
154
+ def write_reg(self, name: str, reg: int, val: int) -> None:
155
+ """
156
+ Writes a register byte [reg, val] to a registered I2C or SPI actuator device.
157
+ Example: node.write_reg('i2c_pwm_chip', reg=0x06, val=128)
158
+ """
159
+ self._send_fn(
160
+ self.ip,
161
+ self.port,
162
+ {"cmd": "ACT", "name": name, "reg": int(reg), "val": int(val)},
163
+ )
164
+
165
+ def set_actuator(self, name: str, v1: int = 0, v2: int = 0, data: str = "") -> None:
166
+ """Generic low-level actuator command."""
167
+ payload: Dict[str, Any] = {"cmd": "ACT", "name": name, "v1": int(v1), "v2": int(v2)}
168
+ if data:
169
+ payload["data"] = data
170
+ self._send_fn(self.ip, self.port, payload)
171
+
172
+ def ping(self) -> None:
173
+ """Sends a heartbeat PING to request immediate telemetry and reset the Slave watchdog."""
174
+ self._send_fn(self.ip, self.port, {"cmd": "PING"})
175
+
176
+ def __repr__(self) -> str:
177
+ return (
178
+ f"<RemoteSwarmNode id='{self.node_id}' board='{self.board}' "
179
+ f"ip='{self.ip}:{self.port}' actuators={self.declared_actuators} "
180
+ f"sensors={self.declared_sensors}>"
181
+ )
@@ -0,0 +1,82 @@
1
+ Metadata-Version: 2.4
2
+ Name: openswarm-master
3
+ Version: 0.1.0
4
+ Summary: Central Linux Master SDK & Runtime for the OpenSwarm Distributed Robotics Framework
5
+ Author-email: Mohammed Raihan Soniwala <raihann.eu@gmail.com>
6
+ License: MIT
7
+ Keywords: robotics,swarm-robotics,esp32,arduino,hal,distributed-systems
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+
11
+ # OpenSwarm Master (`openswarm-master`)
12
+
13
+ The Central Linux Master SDK & CLI for the [**OpenSwarm**](https://github.com/raihhann/OpenSwarm) Distributed Hardware-Agnostic Robotics Framework.
14
+
15
+ * **Zero Slave Control Logic:** All sensor processing, state machines, kinematics, and actuator commands run here on the Master.
16
+ * **Zero External Dependencies:** Pure Python 3 standard library (`socket`, `threading`, `json`) for instant installation on any Linux laptop, Ubuntu PC, Raspberry Pi, or Windows machine.
17
+ * **Auto-Discovery + Direct IP:** Automatically discovers Slaves via UDP port `4211` (`HELLO` broadcast) or connects directly by IP (`192.168.x.x:4210`).
18
+ * **Built-In Keepalive:** Sends a 200 ms background heartbeat (`PING`) to keep the Slave's 500 ms hardware safety watchdog active and stream 20 Hz sensor telemetry (`STATE`).
19
+
20
+ ---
21
+
22
+ ## 1. Installation
23
+
24
+ ### Option A: Install Directly from GitHub
25
+ ```bash
26
+ pip install "git+https://github.com/raihhann/OpenSwarm.git#subdirectory=master"
27
+ ```
28
+
29
+ ### Option B: Clone & Install in Editable Mode (Recommended for Development)
30
+ ```bash
31
+ git clone https://github.com/raihhann/OpenSwarm.git
32
+ cd OpenSwarm/master
33
+ pip install -e .
34
+ ```
35
+
36
+ ---
37
+
38
+ ## 2. Terminal CLI (`openswarm`)
39
+
40
+ ```bash
41
+ # Scan the local Wi-Fi network for active Slave nodes:
42
+ openswarm scan
43
+
44
+ # Blink the built-in LED ("led" on GPIO 2) on esp32_node and print live 20 Hz RSSI:
45
+ openswarm blink esp32_node --ip 192.168.0.118
46
+ ```
47
+
48
+ ---
49
+
50
+ ## 3. Python API Reference
51
+
52
+ ```python
53
+ import time
54
+ from openswarm import SwarmMaster
55
+
56
+ master = SwarmMaster()
57
+
58
+ # Connect by Node ID (auto-discovers on LAN, or pass ip="192.168.0.118" directly)
59
+ bot = master.connect("esp32_node", ip="192.168.0.118")
60
+
61
+ # 1. Digital & PWM Pin Control
62
+ bot.digital_write("led", 1) # Named digital output HIGH
63
+ bot.digital_write("led", 0) # Named digital output LOW
64
+ bot.pwm_write("led", 128) # Named PWM output (0..255)
65
+ bot.pin_write(2, 1) # Direct GPIO pin number write
66
+
67
+ # 2. Motor, Servo & Stepper Control
68
+ bot.drive("drive", left=180, right=180) # Dual H-Bridge (L298N / DRV8833)
69
+ bot.servo("gripper", angle=90) # Hobby Servo (0..180 deg)
70
+ bot.stepper("lifter", steps=200) # Stepper driver
71
+ bot.stop() # Immediate emergency stop
72
+
73
+ # 3. Universal Bus & Custom Actuator Control (LoRa, SPI, I2C, UART)
74
+ bot.send_bus("lora_radio", data="PING_SWARM")
75
+ bot.write_reg("i2c_pwm_chip", reg=0x06, val=200)
76
+
77
+ # 4. Live 20 Hz Sensor Telemetry
78
+ print("RSSI:", bot.read("rssi"))
79
+ print("All Sensors:", bot.sensors)
80
+
81
+ master.close()
82
+ ```
@@ -0,0 +1,9 @@
1
+ openswarm/__init__.py,sha256=Ti9psc5Gf_fcJjynmMzEygc-bSmZB6jHrEadeji01ZM,350
2
+ openswarm/cli.py,sha256=TY7fcBT2aKvUCwVqoOACfqRbwidMWADONFDOcG75mnY,3042
3
+ openswarm/master.py,sha256=5S44Bh03s7BByY2eWmDFIswHZopOcLhus1Cav47KtJs,8994
4
+ openswarm/node.py,sha256=hjSLwsYKvslRVXGWZf49JmkdZbjeRWgQR6R-9dsPboI,7468
5
+ openswarm_master-0.1.0.dist-info/METADATA,sha256=dKQmlRu_oeRnR-9UW4Jveqv3CHQ1SlbapFiW5nqEy0c,3061
6
+ openswarm_master-0.1.0.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
7
+ openswarm_master-0.1.0.dist-info/entry_points.txt,sha256=j_tjlaID1-3rpK5TC3PfBf8jvCE5ow9sirBoHfKM47M,49
8
+ openswarm_master-0.1.0.dist-info/top_level.txt,sha256=xw4v2UTMTsXuf200nzQ8OiYwa3do7gDpiIiTtW92tFE,10
9
+ openswarm_master-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.3.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openswarm = openswarm.cli:main
@@ -0,0 +1 @@
1
+ openswarm