brickpipe 0.1.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.
- brickpipe-0.1.0/PKG-INFO +95 -0
- brickpipe-0.1.0/README.md +81 -0
- brickpipe-0.1.0/brickpipe.egg-info/PKG-INFO +95 -0
- brickpipe-0.1.0/brickpipe.egg-info/SOURCES.txt +8 -0
- brickpipe-0.1.0/brickpipe.egg-info/dependency_links.txt +1 -0
- brickpipe-0.1.0/brickpipe.egg-info/requires.txt +5 -0
- brickpipe-0.1.0/brickpipe.egg-info/top_level.txt +1 -0
- brickpipe-0.1.0/main.py +441 -0
- brickpipe-0.1.0/pyproject.toml +17 -0
- brickpipe-0.1.0/setup.cfg +4 -0
brickpipe-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brickpipe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A json IPC interface for interacting with pybricks hubs
|
|
5
|
+
Project-URL: Source Code, https://github.com/shaggysa/brickpipe
|
|
6
|
+
Project-URL: Issues, https://github.com/shaggysa/brickpipe/issues
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: packaging>=26.2
|
|
10
|
+
Requires-Dist: pybricksdev==2.3.2
|
|
11
|
+
Requires-Dist: pyusb>=1.3.1
|
|
12
|
+
Requires-Dist: reactivex>=4.1.0
|
|
13
|
+
Requires-Dist: bleak>=3.0.1
|
|
14
|
+
|
|
15
|
+
# BrickPipe
|
|
16
|
+
|
|
17
|
+
Brickpipe provides a JSON-based Inter-Process Communication (IPC) interface to interact with Pybricks hubs. It
|
|
18
|
+
communicates via standard input (`stdin`) for commands and standard output (`stdout`) for events.
|
|
19
|
+
|
|
20
|
+
Each message (incoming or outgoing) must be a single-line JSON object.
|
|
21
|
+
|
|
22
|
+
## Message Format
|
|
23
|
+
|
|
24
|
+
All messages are JSON objects that contain an `event_type` field.
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"event_type": "event_name",
|
|
29
|
+
"other_field": "value"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Incoming Events (Commands)
|
|
36
|
+
|
|
37
|
+
These commands are sent to the script via `stdin`.
|
|
38
|
+
|
|
39
|
+
| `event_type` | Parameters | Description |
|
|
40
|
+
|:-------------------------|:--------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------|
|
|
41
|
+
| `start_ble_scanning` | `timout` : int \| float (required) | Begins bluetooth scanning with a specified timeout. |
|
|
42
|
+
| `connect_to_hub` | `conn_type`: `"ble"` or `"usb"` (required)<br>`ble_address`: string (optional)<br>`ble_hub_name`: string (optional) | Connects to a hub. `ble_address` and `ble_hub_name` are used for filtering bluetooth hubs. |
|
|
43
|
+
| `disconnect_from_hub` | None | Disconnects the currently connected hub. |
|
|
44
|
+
| `recompile_download` | `program_path`: string (required) | Stops any running program, recompiles the specified Python file, and downloads it to the hub. |
|
|
45
|
+
| `recompile_run` | `program_path`: string (required) | Stops any running program, recompiles, downloads, and then starts the program. |
|
|
46
|
+
| `run_stored` | None | Stops any running program and starts the program already stored in the hub. |
|
|
47
|
+
| `send_string` | `string`: string (required) | Sends a string to the hub's `stdin`. |
|
|
48
|
+
| `cancel_running_program` | None | Stops the user program currently running on the hub. This has no effect if a program is not running. |
|
|
49
|
+
| `exit` | None | Disconnects any active hub and terminates the process. |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Outgoing Events
|
|
54
|
+
|
|
55
|
+
These events are sent from the script via `stdout`.
|
|
56
|
+
|
|
57
|
+
| `event_type` | Payload Fields | Description |
|
|
58
|
+
|:---------------------------|:------------------------------------------------------------|:---------------------------------------------------------------------------------------|
|
|
59
|
+
| `ble_device_found` | `device_name`: string<br/>`address`: string<br/>`rssi`: int | Found a hub from scanning. Also triggered by an `rssi` update when scanning. |
|
|
60
|
+
| `hub_connected` | None | Successfully established a connection with a hub. |
|
|
61
|
+
| `connection_timeout` | None | Failed to find or connect to a hub. |
|
|
62
|
+
| `download_progress_update` | `percentage`: float (0.00 - 100.00) | Indicates the current progress when downloading a program onto the hub. |
|
|
63
|
+
| `program_started` | None | A user program has started running on the hub. |
|
|
64
|
+
| `program_complete` | None | The user program has finished running or was stopped. |
|
|
65
|
+
| `hub_printed_line` | `line`: string | A line of text output from the hub (stdout). |
|
|
66
|
+
| `compile_error` | `traceback`: string | The Python script failed to compile. Contains the error details. |
|
|
67
|
+
| `precondition_violated` | `explanation`: string | The command could not be executed (e.g., missing arguments, or hub not connected). |
|
|
68
|
+
| `hub_firmware_outdated` | `explanation`: string | The hub's firmware version is not supported. Can only appear when connecting to a hub. |
|
|
69
|
+
| `hub_disconnected` | None | The hub has been disconnected (either manually or due to an error). |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Example Usage
|
|
74
|
+
|
|
75
|
+
### Connecting and Running a Script
|
|
76
|
+
|
|
77
|
+
**1. Connect via bluetooth:**
|
|
78
|
+
|
|
79
|
+
- **Input:** `{"event_type": "connect_to_hub", "conn_type": "ble", "hub_name": "Robot"}`
|
|
80
|
+
- **Output:** `{"event_type": "hub_connected"}`
|
|
81
|
+
|
|
82
|
+
**2. Download and Run a Program:**
|
|
83
|
+
|
|
84
|
+
- **Input:** `{"event_type": "recompile_run", "program_path": "main.py"}`
|
|
85
|
+
- **Output:**
|
|
86
|
+
- `{"event_type": "download_progress_update", "percentage": 50.0}`
|
|
87
|
+
- `{"event_type": "download_progress_update", "percentage": 100.0}`
|
|
88
|
+
- `{"event_type": "program_started"}`
|
|
89
|
+
- `{"event_type": "hub_printed_line", "line": "Hello, World!"}`
|
|
90
|
+
- `{"event_type": "program_complete"}`
|
|
91
|
+
|
|
92
|
+
**3. Handling a Compile Error:**
|
|
93
|
+
|
|
94
|
+
- **Input:** `{"event_type": "recompile_run", "program_path": "broken.py"}`
|
|
95
|
+
- **Output:** `{"event_type": "compile_error", "traceback": "..."}`
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# BrickPipe
|
|
2
|
+
|
|
3
|
+
Brickpipe provides a JSON-based Inter-Process Communication (IPC) interface to interact with Pybricks hubs. It
|
|
4
|
+
communicates via standard input (`stdin`) for commands and standard output (`stdout`) for events.
|
|
5
|
+
|
|
6
|
+
Each message (incoming or outgoing) must be a single-line JSON object.
|
|
7
|
+
|
|
8
|
+
## Message Format
|
|
9
|
+
|
|
10
|
+
All messages are JSON objects that contain an `event_type` field.
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"event_type": "event_name",
|
|
15
|
+
"other_field": "value"
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Incoming Events (Commands)
|
|
22
|
+
|
|
23
|
+
These commands are sent to the script via `stdin`.
|
|
24
|
+
|
|
25
|
+
| `event_type` | Parameters | Description |
|
|
26
|
+
|:-------------------------|:--------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------|
|
|
27
|
+
| `start_ble_scanning` | `timout` : int \| float (required) | Begins bluetooth scanning with a specified timeout. |
|
|
28
|
+
| `connect_to_hub` | `conn_type`: `"ble"` or `"usb"` (required)<br>`ble_address`: string (optional)<br>`ble_hub_name`: string (optional) | Connects to a hub. `ble_address` and `ble_hub_name` are used for filtering bluetooth hubs. |
|
|
29
|
+
| `disconnect_from_hub` | None | Disconnects the currently connected hub. |
|
|
30
|
+
| `recompile_download` | `program_path`: string (required) | Stops any running program, recompiles the specified Python file, and downloads it to the hub. |
|
|
31
|
+
| `recompile_run` | `program_path`: string (required) | Stops any running program, recompiles, downloads, and then starts the program. |
|
|
32
|
+
| `run_stored` | None | Stops any running program and starts the program already stored in the hub. |
|
|
33
|
+
| `send_string` | `string`: string (required) | Sends a string to the hub's `stdin`. |
|
|
34
|
+
| `cancel_running_program` | None | Stops the user program currently running on the hub. This has no effect if a program is not running. |
|
|
35
|
+
| `exit` | None | Disconnects any active hub and terminates the process. |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Outgoing Events
|
|
40
|
+
|
|
41
|
+
These events are sent from the script via `stdout`.
|
|
42
|
+
|
|
43
|
+
| `event_type` | Payload Fields | Description |
|
|
44
|
+
|:---------------------------|:------------------------------------------------------------|:---------------------------------------------------------------------------------------|
|
|
45
|
+
| `ble_device_found` | `device_name`: string<br/>`address`: string<br/>`rssi`: int | Found a hub from scanning. Also triggered by an `rssi` update when scanning. |
|
|
46
|
+
| `hub_connected` | None | Successfully established a connection with a hub. |
|
|
47
|
+
| `connection_timeout` | None | Failed to find or connect to a hub. |
|
|
48
|
+
| `download_progress_update` | `percentage`: float (0.00 - 100.00) | Indicates the current progress when downloading a program onto the hub. |
|
|
49
|
+
| `program_started` | None | A user program has started running on the hub. |
|
|
50
|
+
| `program_complete` | None | The user program has finished running or was stopped. |
|
|
51
|
+
| `hub_printed_line` | `line`: string | A line of text output from the hub (stdout). |
|
|
52
|
+
| `compile_error` | `traceback`: string | The Python script failed to compile. Contains the error details. |
|
|
53
|
+
| `precondition_violated` | `explanation`: string | The command could not be executed (e.g., missing arguments, or hub not connected). |
|
|
54
|
+
| `hub_firmware_outdated` | `explanation`: string | The hub's firmware version is not supported. Can only appear when connecting to a hub. |
|
|
55
|
+
| `hub_disconnected` | None | The hub has been disconnected (either manually or due to an error). |
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Example Usage
|
|
60
|
+
|
|
61
|
+
### Connecting and Running a Script
|
|
62
|
+
|
|
63
|
+
**1. Connect via bluetooth:**
|
|
64
|
+
|
|
65
|
+
- **Input:** `{"event_type": "connect_to_hub", "conn_type": "ble", "hub_name": "Robot"}`
|
|
66
|
+
- **Output:** `{"event_type": "hub_connected"}`
|
|
67
|
+
|
|
68
|
+
**2. Download and Run a Program:**
|
|
69
|
+
|
|
70
|
+
- **Input:** `{"event_type": "recompile_run", "program_path": "main.py"}`
|
|
71
|
+
- **Output:**
|
|
72
|
+
- `{"event_type": "download_progress_update", "percentage": 50.0}`
|
|
73
|
+
- `{"event_type": "download_progress_update", "percentage": 100.0}`
|
|
74
|
+
- `{"event_type": "program_started"}`
|
|
75
|
+
- `{"event_type": "hub_printed_line", "line": "Hello, World!"}`
|
|
76
|
+
- `{"event_type": "program_complete"}`
|
|
77
|
+
|
|
78
|
+
**3. Handling a Compile Error:**
|
|
79
|
+
|
|
80
|
+
- **Input:** `{"event_type": "recompile_run", "program_path": "broken.py"}`
|
|
81
|
+
- **Output:** `{"event_type": "compile_error", "traceback": "..."}`
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: brickpipe
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A json IPC interface for interacting with pybricks hubs
|
|
5
|
+
Project-URL: Source Code, https://github.com/shaggysa/brickpipe
|
|
6
|
+
Project-URL: Issues, https://github.com/shaggysa/brickpipe/issues
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: packaging>=26.2
|
|
10
|
+
Requires-Dist: pybricksdev==2.3.2
|
|
11
|
+
Requires-Dist: pyusb>=1.3.1
|
|
12
|
+
Requires-Dist: reactivex>=4.1.0
|
|
13
|
+
Requires-Dist: bleak>=3.0.1
|
|
14
|
+
|
|
15
|
+
# BrickPipe
|
|
16
|
+
|
|
17
|
+
Brickpipe provides a JSON-based Inter-Process Communication (IPC) interface to interact with Pybricks hubs. It
|
|
18
|
+
communicates via standard input (`stdin`) for commands and standard output (`stdout`) for events.
|
|
19
|
+
|
|
20
|
+
Each message (incoming or outgoing) must be a single-line JSON object.
|
|
21
|
+
|
|
22
|
+
## Message Format
|
|
23
|
+
|
|
24
|
+
All messages are JSON objects that contain an `event_type` field.
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{
|
|
28
|
+
"event_type": "event_name",
|
|
29
|
+
"other_field": "value"
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Incoming Events (Commands)
|
|
36
|
+
|
|
37
|
+
These commands are sent to the script via `stdin`.
|
|
38
|
+
|
|
39
|
+
| `event_type` | Parameters | Description |
|
|
40
|
+
|:-------------------------|:--------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------|
|
|
41
|
+
| `start_ble_scanning` | `timout` : int \| float (required) | Begins bluetooth scanning with a specified timeout. |
|
|
42
|
+
| `connect_to_hub` | `conn_type`: `"ble"` or `"usb"` (required)<br>`ble_address`: string (optional)<br>`ble_hub_name`: string (optional) | Connects to a hub. `ble_address` and `ble_hub_name` are used for filtering bluetooth hubs. |
|
|
43
|
+
| `disconnect_from_hub` | None | Disconnects the currently connected hub. |
|
|
44
|
+
| `recompile_download` | `program_path`: string (required) | Stops any running program, recompiles the specified Python file, and downloads it to the hub. |
|
|
45
|
+
| `recompile_run` | `program_path`: string (required) | Stops any running program, recompiles, downloads, and then starts the program. |
|
|
46
|
+
| `run_stored` | None | Stops any running program and starts the program already stored in the hub. |
|
|
47
|
+
| `send_string` | `string`: string (required) | Sends a string to the hub's `stdin`. |
|
|
48
|
+
| `cancel_running_program` | None | Stops the user program currently running on the hub. This has no effect if a program is not running. |
|
|
49
|
+
| `exit` | None | Disconnects any active hub and terminates the process. |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Outgoing Events
|
|
54
|
+
|
|
55
|
+
These events are sent from the script via `stdout`.
|
|
56
|
+
|
|
57
|
+
| `event_type` | Payload Fields | Description |
|
|
58
|
+
|:---------------------------|:------------------------------------------------------------|:---------------------------------------------------------------------------------------|
|
|
59
|
+
| `ble_device_found` | `device_name`: string<br/>`address`: string<br/>`rssi`: int | Found a hub from scanning. Also triggered by an `rssi` update when scanning. |
|
|
60
|
+
| `hub_connected` | None | Successfully established a connection with a hub. |
|
|
61
|
+
| `connection_timeout` | None | Failed to find or connect to a hub. |
|
|
62
|
+
| `download_progress_update` | `percentage`: float (0.00 - 100.00) | Indicates the current progress when downloading a program onto the hub. |
|
|
63
|
+
| `program_started` | None | A user program has started running on the hub. |
|
|
64
|
+
| `program_complete` | None | The user program has finished running or was stopped. |
|
|
65
|
+
| `hub_printed_line` | `line`: string | A line of text output from the hub (stdout). |
|
|
66
|
+
| `compile_error` | `traceback`: string | The Python script failed to compile. Contains the error details. |
|
|
67
|
+
| `precondition_violated` | `explanation`: string | The command could not be executed (e.g., missing arguments, or hub not connected). |
|
|
68
|
+
| `hub_firmware_outdated` | `explanation`: string | The hub's firmware version is not supported. Can only appear when connecting to a hub. |
|
|
69
|
+
| `hub_disconnected` | None | The hub has been disconnected (either manually or due to an error). |
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Example Usage
|
|
74
|
+
|
|
75
|
+
### Connecting and Running a Script
|
|
76
|
+
|
|
77
|
+
**1. Connect via bluetooth:**
|
|
78
|
+
|
|
79
|
+
- **Input:** `{"event_type": "connect_to_hub", "conn_type": "ble", "hub_name": "Robot"}`
|
|
80
|
+
- **Output:** `{"event_type": "hub_connected"}`
|
|
81
|
+
|
|
82
|
+
**2. Download and Run a Program:**
|
|
83
|
+
|
|
84
|
+
- **Input:** `{"event_type": "recompile_run", "program_path": "main.py"}`
|
|
85
|
+
- **Output:**
|
|
86
|
+
- `{"event_type": "download_progress_update", "percentage": 50.0}`
|
|
87
|
+
- `{"event_type": "download_progress_update", "percentage": 100.0}`
|
|
88
|
+
- `{"event_type": "program_started"}`
|
|
89
|
+
- `{"event_type": "hub_printed_line", "line": "Hello, World!"}`
|
|
90
|
+
- `{"event_type": "program_complete"}`
|
|
91
|
+
|
|
92
|
+
**3. Handling a Compile Error:**
|
|
93
|
+
|
|
94
|
+
- **Input:** `{"event_type": "recompile_run", "program_path": "broken.py"}`
|
|
95
|
+
- **Output:** `{"event_type": "compile_error", "traceback": "..."}`
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
main
|
brickpipe-0.1.0/main.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import struct
|
|
4
|
+
import sys
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from subprocess import CalledProcessError
|
|
7
|
+
|
|
8
|
+
import reactivex.operators as op
|
|
9
|
+
from bleak import BLEDevice, AdvertisementData, BleakScanner
|
|
10
|
+
from packaging.version import Version
|
|
11
|
+
from pybricksdev.ble import find_device as find_ble
|
|
12
|
+
from pybricksdev.ble.pybricks import (
|
|
13
|
+
PYBRICKS_COMMAND_EVENT_UUID,
|
|
14
|
+
Command,
|
|
15
|
+
StatusFlag,
|
|
16
|
+
)
|
|
17
|
+
from pybricksdev.ble.pybricks import PYBRICKS_SERVICE_UUID
|
|
18
|
+
from pybricksdev.cli import _get_script_path
|
|
19
|
+
from pybricksdev.connections.pybricks import PybricksHubBLE, HubDisconnectError, PybricksHub
|
|
20
|
+
from pybricksdev.connections.pybricks import PybricksHubUSB
|
|
21
|
+
from pybricksdev.tools import chunk
|
|
22
|
+
from pybricksdev.usb import (
|
|
23
|
+
EV3_USB_PID,
|
|
24
|
+
LEGO_USB_VID,
|
|
25
|
+
MINDSTORMS_INVENTOR_USB_PID,
|
|
26
|
+
NXT_USB_PID,
|
|
27
|
+
SPIKE_ESSENTIAL_USB_PID,
|
|
28
|
+
SPIKE_PRIME_USB_PID,
|
|
29
|
+
)
|
|
30
|
+
from usb.core import find as find_usb
|
|
31
|
+
|
|
32
|
+
incoming_messages = asyncio.Queue()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IncomingEventType(str, Enum):
|
|
36
|
+
# `timeout (int | float, seconds)`
|
|
37
|
+
start_ble_scanning = 'start_ble_scanning'
|
|
38
|
+
|
|
39
|
+
# `conn_type: "ble" | "usb"`
|
|
40
|
+
# `ble_address` (optional)
|
|
41
|
+
# `ble_hub_name` (optional)
|
|
42
|
+
connect_to_hub = 'connect_to_hub'
|
|
43
|
+
|
|
44
|
+
disconnect_from_hub = 'disconnect_from_hub'
|
|
45
|
+
|
|
46
|
+
# `program_path`
|
|
47
|
+
recompile_download = 'recompile_download'
|
|
48
|
+
|
|
49
|
+
# `program_path`
|
|
50
|
+
recompile_run = 'recompile_run'
|
|
51
|
+
|
|
52
|
+
run_stored = 'run_stored'
|
|
53
|
+
|
|
54
|
+
# `string`
|
|
55
|
+
send_string = 'send_string'
|
|
56
|
+
|
|
57
|
+
cancel_running_program = 'cancel_running_program'
|
|
58
|
+
|
|
59
|
+
exit = 'exit'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class OutgoingEventType(str, Enum):
|
|
63
|
+
# `device_name`
|
|
64
|
+
# `address`
|
|
65
|
+
# `rssi: int`
|
|
66
|
+
ble_device_found = 'ble_device_found'
|
|
67
|
+
|
|
68
|
+
hub_connected = 'hub_connected'
|
|
69
|
+
|
|
70
|
+
connection_timeout = 'connection_timeout'
|
|
71
|
+
|
|
72
|
+
# `percentage (0.00 - 100.00)`
|
|
73
|
+
download_progress_update = 'download_progress_update'
|
|
74
|
+
|
|
75
|
+
program_started = 'program_started'
|
|
76
|
+
|
|
77
|
+
program_complete = 'program_complete'
|
|
78
|
+
|
|
79
|
+
# `line`
|
|
80
|
+
hub_printed_line = 'hub_printed_line'
|
|
81
|
+
|
|
82
|
+
# `traceback`
|
|
83
|
+
compile_error = 'compile_error'
|
|
84
|
+
|
|
85
|
+
# `explanation`
|
|
86
|
+
precondition_violated = 'precondition_violated'
|
|
87
|
+
|
|
88
|
+
# `explanation`
|
|
89
|
+
hub_firmware_outdated = 'hub_firmware_outdated'
|
|
90
|
+
|
|
91
|
+
hub_disconnected = 'hub_disconnected'
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
async def send_event(event_type: OutgoingEventType, payload: dict | None = None):
|
|
95
|
+
if payload is None:
|
|
96
|
+
payload = {"event_type": event_type.value}
|
|
97
|
+
else:
|
|
98
|
+
payload.update({"event_type": event_type.value})
|
|
99
|
+
print(json.dumps(payload), flush=True)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def watch_incoming_events():
|
|
103
|
+
while True:
|
|
104
|
+
line = await asyncio.to_thread(sys.stdin.readline)
|
|
105
|
+
if not line:
|
|
106
|
+
await incoming_messages.put({"event_type": IncomingEventType.exit.value})
|
|
107
|
+
break
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(line.strip())
|
|
110
|
+
await incoming_messages.put(data)
|
|
111
|
+
except json.decoder.JSONDecodeError:
|
|
112
|
+
await send_event(OutgoingEventType.precondition_violated, {"explanation": "received invalid json"})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_pybricks_usb(dev):
|
|
116
|
+
return (
|
|
117
|
+
(dev.idVendor == LEGO_USB_VID)
|
|
118
|
+
and (
|
|
119
|
+
dev.idProduct
|
|
120
|
+
in [
|
|
121
|
+
NXT_USB_PID,
|
|
122
|
+
EV3_USB_PID,
|
|
123
|
+
SPIKE_PRIME_USB_PID,
|
|
124
|
+
SPIKE_ESSENTIAL_USB_PID,
|
|
125
|
+
MINDSTORMS_INVENTOR_USB_PID,
|
|
126
|
+
]
|
|
127
|
+
)
|
|
128
|
+
and dev.product.endswith("Pybricks")
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def observe_running_status(hub: PybricksHub):
|
|
133
|
+
user_program_running: asyncio.Queue[bool] = asyncio.Queue()
|
|
134
|
+
|
|
135
|
+
with hub.status_observable.pipe(
|
|
136
|
+
op.map(lambda s: bool(s & StatusFlag.USER_PROGRAM_RUNNING)),
|
|
137
|
+
op.distinct_until_changed(),
|
|
138
|
+
).subscribe(lambda s: user_program_running.put_nowait(s)):
|
|
139
|
+
is_running = await hub.race_disconnect(user_program_running.get())
|
|
140
|
+
if is_running:
|
|
141
|
+
await send_event(OutgoingEventType.program_started)
|
|
142
|
+
# don't do anything if a program isn't running, the hub was just connected
|
|
143
|
+
try:
|
|
144
|
+
while True:
|
|
145
|
+
is_running = await hub.race_disconnect(user_program_running.get())
|
|
146
|
+
if is_running:
|
|
147
|
+
await send_event(OutgoingEventType.program_started)
|
|
148
|
+
else:
|
|
149
|
+
await send_event(OutgoingEventType.program_complete)
|
|
150
|
+
except asyncio.CancelledError:
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
async def observe_stdout(hub: PybricksHub):
|
|
155
|
+
try:
|
|
156
|
+
while True:
|
|
157
|
+
line = await hub.read_line()
|
|
158
|
+
if line:
|
|
159
|
+
await send_event(OutgoingEventType.hub_printed_line, {"line": line})
|
|
160
|
+
except asyncio.CancelledError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def observe_hub(hub: PybricksHub):
|
|
165
|
+
hub._enable_line_handler = True
|
|
166
|
+
hub.print_output = False
|
|
167
|
+
|
|
168
|
+
stdout_task = asyncio.create_task(observe_stdout(hub))
|
|
169
|
+
running_task = asyncio.create_task(observe_running_status(hub))
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
await hub.race_disconnect(asyncio.Future())
|
|
173
|
+
finally:
|
|
174
|
+
stdout_task.cancel()
|
|
175
|
+
running_task.cancel()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# hub.download calls a download_user_program method internally,
|
|
179
|
+
# which prints progress to the terminal. This function
|
|
180
|
+
# overrides it to send progress events instead
|
|
181
|
+
async def download_user_program_override(self: PybricksHub, program: bytes):
|
|
182
|
+
# the hub tells us the max size of program that is allowed, so we can fail early
|
|
183
|
+
if len(program) > self._max_user_program_size:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"program is too big ({len(program)} bytes). Hub has limit of {self._max_user_program_size} bytes."
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# clear user program meta so hub doesn't try to run invalid program
|
|
189
|
+
await self.write_gatt_char(
|
|
190
|
+
PYBRICKS_COMMAND_EVENT_UUID,
|
|
191
|
+
struct.pack("<BI", Command.WRITE_USER_PROGRAM_META, 0),
|
|
192
|
+
response=True,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# payload is max size minus header size
|
|
196
|
+
payload_size = self._max_write_size - 5
|
|
197
|
+
|
|
198
|
+
bytes_sent = 0
|
|
199
|
+
|
|
200
|
+
# write program data while sending progress events
|
|
201
|
+
for i, c in enumerate(chunk(program, payload_size)):
|
|
202
|
+
await self.write_gatt_char(
|
|
203
|
+
PYBRICKS_COMMAND_EVENT_UUID,
|
|
204
|
+
struct.pack(
|
|
205
|
+
f"<BI{len(c)}s",
|
|
206
|
+
Command.COMMAND_WRITE_USER_RAM,
|
|
207
|
+
i * payload_size,
|
|
208
|
+
c,
|
|
209
|
+
),
|
|
210
|
+
response=True,
|
|
211
|
+
)
|
|
212
|
+
bytes_sent += (len(c))
|
|
213
|
+
percentage = round((bytes_sent / len(program)) * 100, 2)
|
|
214
|
+
|
|
215
|
+
await send_event(OutgoingEventType.download_progress_update, {"percentage": percentage})
|
|
216
|
+
|
|
217
|
+
# set the metadata to notify that writing was successful
|
|
218
|
+
await self.write_gatt_char(
|
|
219
|
+
PYBRICKS_COMMAND_EVENT_UUID,
|
|
220
|
+
struct.pack("<BI", Command.WRITE_USER_PROGRAM_META, len(program)),
|
|
221
|
+
response=True,
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
async def ble_scanner_callback(device: BLEDevice, adv: AdvertisementData):
|
|
226
|
+
if PYBRICKS_SERVICE_UUID in adv.service_uuids and adv.local_name:
|
|
227
|
+
await send_event(OutgoingEventType.ble_device_found,
|
|
228
|
+
{"address": device.address, "device_name": device.name, "rssi": adv.rssi})
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
async def main_loop():
|
|
232
|
+
ble_scanner = BleakScanner(ble_scanner_callback)
|
|
233
|
+
asyncio.create_task(watch_incoming_events())
|
|
234
|
+
|
|
235
|
+
hub = None
|
|
236
|
+
hub_monitor_tasks = None
|
|
237
|
+
ble_scan_stop_event = asyncio.Event()
|
|
238
|
+
|
|
239
|
+
while True:
|
|
240
|
+
try:
|
|
241
|
+
if hub:
|
|
242
|
+
command = await hub.race_disconnect(incoming_messages.get())
|
|
243
|
+
else:
|
|
244
|
+
command = await incoming_messages.get()
|
|
245
|
+
|
|
246
|
+
if 'event_type' not in command:
|
|
247
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
248
|
+
{'explanation': 'all events must have an "event_type" tag'})
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
ble_scan_stop_event.set()
|
|
252
|
+
|
|
253
|
+
match command.get('event_type'):
|
|
254
|
+
case IncomingEventType.start_ble_scanning:
|
|
255
|
+
if 'timeout' not in command:
|
|
256
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
257
|
+
{
|
|
258
|
+
'explanation': 'a "start_ble_scanning" command must have a "timeout argument"'})
|
|
259
|
+
|
|
260
|
+
timeout = command.get('timeout')
|
|
261
|
+
|
|
262
|
+
if type(timeout) is not float and type(timeout) is not int:
|
|
263
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
264
|
+
{
|
|
265
|
+
'explanation': 'the "timeout" argument must be an integer or floating-point number'})
|
|
266
|
+
|
|
267
|
+
ble_scan_stop_event.clear()
|
|
268
|
+
|
|
269
|
+
async with BleakScanner(ble_scanner_callback) as scanner:
|
|
270
|
+
try:
|
|
271
|
+
await asyncio.wait_for(ble_scan_stop_event.wait(), timeout)
|
|
272
|
+
except TimeoutError:
|
|
273
|
+
pass
|
|
274
|
+
|
|
275
|
+
case IncomingEventType.connect_to_hub:
|
|
276
|
+
if hub:
|
|
277
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
278
|
+
payload={'explanation': 'a hub is already connected'})
|
|
279
|
+
else:
|
|
280
|
+
if 'ble_hub_name' in command:
|
|
281
|
+
name = command.get('ble_hub_name')
|
|
282
|
+
else:
|
|
283
|
+
name = None
|
|
284
|
+
|
|
285
|
+
if 'conn_type' not in command:
|
|
286
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
287
|
+
{
|
|
288
|
+
'explanation': 'a "connect_to_hub" command must have an "conn_type" argument"'})
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
conntype = command.get('conn_type')
|
|
292
|
+
|
|
293
|
+
if conntype == 'ble':
|
|
294
|
+
try:
|
|
295
|
+
if 'ble_address' in command:
|
|
296
|
+
device_or_address = await BleakScanner.find_device_by_address(
|
|
297
|
+
command.get('ble_address'))
|
|
298
|
+
else:
|
|
299
|
+
device_or_address = await find_ble(name)
|
|
300
|
+
|
|
301
|
+
hub = PybricksHubBLE(device_or_address)
|
|
302
|
+
except TimeoutError:
|
|
303
|
+
await send_event(OutgoingEventType.connection_timeout)
|
|
304
|
+
elif conntype == 'usb':
|
|
305
|
+
device_or_address = find_usb(custom_match=is_pybricks_usb)
|
|
306
|
+
if device_or_address is None:
|
|
307
|
+
await send_event(OutgoingEventType.connection_timeout)
|
|
308
|
+
continue
|
|
309
|
+
hub = PybricksHubUSB(device_or_address)
|
|
310
|
+
else:
|
|
311
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
312
|
+
payload={
|
|
313
|
+
'explanation': 'usb and ble are the only valid connection types'})
|
|
314
|
+
|
|
315
|
+
if hub:
|
|
316
|
+
await hub.connect()
|
|
317
|
+
if hub.fw_version < Version("3.2.0-beta.4"):
|
|
318
|
+
await hub.disconnect()
|
|
319
|
+
await send_event(OutgoingEventType.hub_firmware_outdated,
|
|
320
|
+
{"explanation": "this tool requires hub firmware version >= 3.2.0"})
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
hub_monitor_tasks = asyncio.create_task(observe_hub(hub))
|
|
324
|
+
await send_event(OutgoingEventType.hub_connected)
|
|
325
|
+
|
|
326
|
+
case IncomingEventType.disconnect_from_hub:
|
|
327
|
+
if hub:
|
|
328
|
+
await hub.disconnect()
|
|
329
|
+
hub = None
|
|
330
|
+
if hub_monitor_tasks:
|
|
331
|
+
# the tasks should already be canceled when the hub is disconnected,
|
|
332
|
+
# but double-cancelling has no negative side effects
|
|
333
|
+
hub_monitor_tasks.cancel()
|
|
334
|
+
hub_monitor_tasks = None
|
|
335
|
+
|
|
336
|
+
await send_event(OutgoingEventType.hub_disconnected)
|
|
337
|
+
|
|
338
|
+
case IncomingEventType.recompile_download:
|
|
339
|
+
if hub:
|
|
340
|
+
# the hub doesn't like data being sent while a program is running
|
|
341
|
+
# calling stop does nothing if a program isn't running
|
|
342
|
+
await hub.stop_user_program()
|
|
343
|
+
|
|
344
|
+
if 'program_path' not in command:
|
|
345
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
346
|
+
payload={
|
|
347
|
+
'explanation': '"recompile_download" events must have a "program_path" argument'})
|
|
348
|
+
continue
|
|
349
|
+
|
|
350
|
+
program_path = command.get('program_path')
|
|
351
|
+
|
|
352
|
+
try:
|
|
353
|
+
with _get_script_path(open(program_path)) as script_path:
|
|
354
|
+
await hub.download(script_path)
|
|
355
|
+
except FileNotFoundError:
|
|
356
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
357
|
+
payload={'explanation': 'received file path is not valid'})
|
|
358
|
+
else:
|
|
359
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
360
|
+
payload={'explanation': 'a hub must be connected to download a program'})
|
|
361
|
+
|
|
362
|
+
case IncomingEventType.recompile_run:
|
|
363
|
+
if hub:
|
|
364
|
+
# the hub doesn't like data being sent while a program is running
|
|
365
|
+
# calling stop does nothing if a program isn't running
|
|
366
|
+
await hub.stop_user_program()
|
|
367
|
+
|
|
368
|
+
if 'program_path' not in command:
|
|
369
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
370
|
+
payload={
|
|
371
|
+
'explanation': '"recompile_run" events must have a "program_path" argument'})
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
program_path = command.get('program_path')
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
with _get_script_path(open(program_path)) as script_path:
|
|
378
|
+
await hub.download(script_path)
|
|
379
|
+
await hub.start_user_program()
|
|
380
|
+
except FileNotFoundError:
|
|
381
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
382
|
+
payload={'explanation': 'received file path is not valid'})
|
|
383
|
+
else:
|
|
384
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
385
|
+
payload={'explanation': 'a hub must be connected to run a program'})
|
|
386
|
+
|
|
387
|
+
case IncomingEventType.run_stored:
|
|
388
|
+
if hub:
|
|
389
|
+
await hub.stop_user_program()
|
|
390
|
+
await hub.start_user_program()
|
|
391
|
+
else:
|
|
392
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
393
|
+
payload={'explanation': 'a hub must be connected to run a program'})
|
|
394
|
+
|
|
395
|
+
case IncomingEventType.send_string:
|
|
396
|
+
if 'string' not in command:
|
|
397
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
398
|
+
payload={
|
|
399
|
+
'explanation': '"send_string" events must have a "string" argument'})
|
|
400
|
+
continue
|
|
401
|
+
|
|
402
|
+
if hub:
|
|
403
|
+
hub.write_string(command.get('string'))
|
|
404
|
+
|
|
405
|
+
else:
|
|
406
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
407
|
+
payload={'explanation': 'a hub must be connected to send text to stdin'})
|
|
408
|
+
|
|
409
|
+
case IncomingEventType.cancel_running_program:
|
|
410
|
+
if hub:
|
|
411
|
+
await hub.stop_user_program()
|
|
412
|
+
else:
|
|
413
|
+
await send_event(OutgoingEventType.precondition_violated,
|
|
414
|
+
payload={'explanation': 'a hub must be connected to cancel a program'})
|
|
415
|
+
|
|
416
|
+
case IncomingEventType.exit:
|
|
417
|
+
if hub:
|
|
418
|
+
if hub_monitor_tasks:
|
|
419
|
+
hub_monitor_tasks.cancel()
|
|
420
|
+
await hub.disconnect()
|
|
421
|
+
exit(0)
|
|
422
|
+
|
|
423
|
+
case _:
|
|
424
|
+
print(f"Invalid command: {command}")
|
|
425
|
+
|
|
426
|
+
except HubDisconnectError:
|
|
427
|
+
await send_event(OutgoingEventType.hub_disconnected)
|
|
428
|
+
if hub_monitor_tasks:
|
|
429
|
+
hub_monitor_tasks.cancel()
|
|
430
|
+
hub_monitor_tasks = None
|
|
431
|
+
hub = None
|
|
432
|
+
# mpy-cross returned a compiler error
|
|
433
|
+
except CalledProcessError as e:
|
|
434
|
+
await send_event(OutgoingEventType.compile_error, {'traceback': e.stderr.decode()})
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
PybricksHubBLE.download_user_program = download_user_program_override
|
|
438
|
+
PybricksHubUSB.download_user_program = download_user_program_override
|
|
439
|
+
|
|
440
|
+
if __name__ == "__main__":
|
|
441
|
+
asyncio.run(main_loop())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "brickpipe"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
requires-python = ">=3.10"
|
|
5
|
+
description = "A json IPC interface for interacting with pybricks hubs"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"packaging>=26.2",
|
|
9
|
+
"pybricksdev==2.3.2",
|
|
10
|
+
"pyusb>=1.3.1",
|
|
11
|
+
"reactivex>=4.1.0",
|
|
12
|
+
"bleak>=3.0.1"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
"Source Code" = "https://github.com/shaggysa/brickpipe"
|
|
17
|
+
"Issues" = "https://github.com/shaggysa/brickpipe/issues"
|