brickremote 0.3.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.
@@ -0,0 +1,41 @@
1
+ name: release
2
+
3
+ # Push a tag like v0.3.0 to build the package and publish it on PyPI.
4
+ # Publishing uses PyPI trusted publishing, so no token is stored in the repository.
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ jobs:
10
+ build:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: actions/setup-python@v5
15
+ with:
16
+ python-version: "3.13"
17
+ - name: Check the tag matches the package version
18
+ run: |
19
+ version=$(grep -oP '__version__ = "\K[^"]+' src/brickremote/__init__.py)
20
+ [ "v$version" = "$GITHUB_REF_NAME" ] || { echo "tag $GITHUB_REF_NAME != version $version"; exit 1; }
21
+ - run: pip install build pytest aiohttp bleak && pip install -e . && pytest -q
22
+ - run: python -m build
23
+ - uses: actions/upload-artifact@v4
24
+ with:
25
+ name: dist
26
+ path: dist/
27
+
28
+ publish:
29
+ needs: build
30
+ runs-on: ubuntu-latest
31
+ environment:
32
+ name: pypi
33
+ url: https://pypi.org/p/brickremote
34
+ permissions:
35
+ id-token: write
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,19 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ strategy:
11
+ matrix:
12
+ python: ["3.10", "3.12", "3.13"]
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: ${{ matrix.python }}
18
+ - run: pip install -e ".[web,test]"
19
+ - run: pytest -q
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.egg-info/
3
+ .venv/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ - Renamed from `jmcble` to `brickremote` (package, command and config file; old settings are picked up).
6
+ - Remote control app: kid-friendly look, big steering wheel, mouse support, searching screen with
7
+ automatic reconnect, instant quit with `q`.
8
+ - Steering springs back as soon as the key is released; hold (`h`), pair (`p`) and reverse (`r`).
9
+ - `scan` prints modules as soon as they are seen and stops early.
10
+ - `JMCModule.detach()` for exiting without waiting for BlueZ to disconnect.
11
+
12
+ ## 0.2.0
13
+
14
+ - Program and path scripts (`brickremote program`, `brickremote path`), port inversion and linking,
15
+ `JMCModule.run()`, first version of the terminal remote control.
16
+
17
+ ## 0.1.0
18
+
19
+ - First release: protocol, `JMCModule`, `Car`, `scan` and `run` commands.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexandre Strube
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.5
2
+ Name: brickremote
3
+ Version: 0.3.0
4
+ Summary: Drive Lego-compatible Bluetooth motor bricks (JG_JMC / XP Block "Smart creative module") from a computer
5
+ Project-URL: Homepage, https://surak.github.io/brickremote/
6
+ Project-URL: Source, https://github.com/surak/brickremote
7
+ Project-URL: Changelog, https://github.com/surak/brickremote/blob/main/CHANGELOG.md
8
+ Author-email: Alexandre Strube <surak@surak.eti.br>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ble,bluetooth,building blocks,jg_jmc,kids,motor,remote control,smart creative module,xp block
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AsyncIO
14
+ Classifier: Intended Audience :: Education
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Education
17
+ Classifier: Topic :: Home Automation
18
+ Classifier: Topic :: System :: Hardware
19
+ Requires-Python: >=3.10
20
+ Requires-Dist: bleak>=0.22
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest; extra == 'test'
23
+ Provides-Extra: web
24
+ Requires-Dist: aiohttp>=3.9; extra == 'web'
25
+ Description-Content-Type: text/markdown
26
+
27
+ # brickremote
28
+
29
+ **Website for kids and parents: https://surak.github.io/brickremote/**
30
+
31
+ Python library for the cheap Lego-compatible Bluetooth LE motor bricks that advertise as
32
+ `JG_JMC-xxxx` and are normally driven by the **XP Block** phone app ("Smart creative module",
33
+ four motor ports A–D). Works anywhere [bleak](https://github.com/hbldh/bleak) does: Linux
34
+ (incl. Raspberry Pi), macOS and Windows.
35
+
36
+ ```
37
+ pip install brickremote # library and terminal app
38
+ pip install "brickremote[web]" # plus the web remote control
39
+ ```
40
+
41
+ Or from a checkout, without installing anything yourself:
42
+
43
+ ```
44
+ ./run.sh # creates .venv on first run, then starts the remote control app
45
+ ./run.sh scan # any other command works the same way
46
+ ```
47
+
48
+ ## Remote control app
49
+
50
+ Run `brickremote` with no arguments for a terminal version of the phone app (everything except the
51
+ phone-only gyroscope and voice modes):
52
+
53
+ | Tab | Keys |
54
+ |-----|------|
55
+ | Car | arrows: forward/back, steer left/right (drive port A, steering port D; change with `--drive`/`--steer`) |
56
+ | Ports | like two game sticks: `W`/`S` port A, `A`/`D` port B, `I`/`K` port C, `J`/`L` port D |
57
+ | Program | `e` edit in `$EDITOR`, Enter run (see below) |
58
+ | Path | `e` edit, Enter run, `[` `]` / `{` `}` calibrate speed and turn rate |
59
+
60
+ Everywhere: `Tab` switch screen, `1`-`3` speed (60/120/255), `space` STOP, `q` quit, and
61
+
62
+ - `h` **hold**: motors keep running after you let go of the key (steering still springs back)
63
+ - `p` **pair**: one port copies another (off → A+B → A+C), for builds with two motors that
64
+ should always move together, like a tank
65
+ - `r` + port letter **reverse**: that port turns the other way round, for a motor that was
66
+ mounted backwards
67
+
68
+ Settings, programs and paths are saved in `~/.config/brickremote.json`.
69
+
70
+ Terminals report key presses but not releases, so a motor runs while its key auto-repeats and
71
+ stops a moment after you let go. Steering always springs back to centre, even with hold on.
72
+
73
+ ## Web remote control
74
+
75
+ ```
76
+ ./run.sh web # or: brickremote web (needs `pip install 'brickremote[web]'`)
77
+ ```
78
+
79
+ This starts a small web server and prints its address. Open it in any browser: on the same
80
+ computer, or on a tablet or laptop on the same Wi-Fi, which then becomes the remote control.
81
+ It has the same four screens as the terminal app, plus things a browser is good at:
82
+
83
+ - buttons you hold down, a steering wheel you can drag, and two joysticks for the ports
84
+ - program blocks you tap to build a program
85
+ - a grid where you draw the route for path mode
86
+
87
+ If the browser stops talking to the server (tab closed, Wi-Fi gone), the motors stop by themselves.
88
+
89
+ ## Command line
90
+
91
+ ```
92
+ brickremote scan # list modules nearby
93
+ brickremote run a=255 --time 2 # port A forward, full speed, 2 s
94
+ brickremote run a=-120 d=255 --time 1.5 # A reverse at medium speed, D forward
95
+ brickremote program dance.txt # run a program script
96
+ brickremote path route.txt --mps 0.6 # drive a path script
97
+ brickremote --invert d run d=255 # global options: --address, --slot, --invert, --drive, --steer
98
+ ```
99
+
100
+ ### Program scripts (the app's block programming)
101
+
102
+ ```
103
+ gear 2 # speed for + and -: 1 = 60, 2 = 120, 3 = 255
104
+ repeat 3
105
+ a + 1.5 # port A forward for 1.5 s
106
+ ac - 1 # ports A and C reverse for 1 s
107
+ wait 0.5
108
+ end
109
+ d 200 # port D at speed 200, keep running
110
+ forever # until Ctrl-C / space
111
+ b + 1
112
+ b - 1
113
+ end
114
+ ```
115
+
116
+ ### Path scripts (the app's path mode)
117
+
118
+ ```
119
+ forward 1.5 # metres
120
+ left 90 # degrees, driving forward with full steering
121
+ back 0.5
122
+ right 45
123
+ ```
124
+
125
+ Distances and angles become times through a calibration (`--mps`, `--dps`, or `[ ] { }` in the
126
+ app): measure how far the car goes in one second at full speed, and how many degrees it turns.
127
+
128
+ ## Library
129
+
130
+ ```python
131
+ import asyncio
132
+ from brickremote import JMCModule
133
+
134
+ async def main():
135
+ async with JMCModule() as m: # connects to the first module found
136
+ await m.drive(a=255, d=-120) # speeds -255..255, negative = reverse
137
+ await asyncio.sleep(2)
138
+ await m.stop()
139
+ print("battery:", m.battery, "%")
140
+
141
+ asyncio.run(main())
142
+ ```
143
+
144
+ `JMCModule(address)` targets a specific module, `discover()` lists all of them. While connected,
145
+ the current state is resent every 0.2 s, which keeps the module from going to sleep.
146
+
147
+ ```python
148
+ JMCModule(inverted="d", links={"a": "c"}) # reverse port D; C follows A
149
+ await m.run(2, a=255) # A forward for 2 s, then stop
150
+ ```
151
+
152
+ `brickremote.script` has the program and path engines (`parse_program`, `run_program`,
153
+ `parse_path`, `run_path`) if you want them from your own code.
154
+
155
+ For the stock car (drive motor on A, steering on D) there is a small wrapper:
156
+
157
+ ```python
158
+ from brickremote import Car, JMCModule
159
+
160
+ async with JMCModule() as m:
161
+ car = Car(m)
162
+ await car.left(speed=200) # forward while steering left
163
+ ```
164
+
165
+ ## Protocol
166
+
167
+ Reverse-engineered from the XP Block Android app. The module exposes service `FFF0`;
168
+ commands go to `FFF2` and **must be written with response** (write-without-response is
169
+ silently ignored):
170
+
171
+ ```
172
+ 5A 6B 02 00 05 dirAB speedAB dirCD speedCD slot SUM
173
+ ```
174
+
175
+ | Field | Meaning |
176
+ |---------|---------|
177
+ | dirAB | A: `1` forward, `2` reverse; B: `+4` forward, `+8` reverse |
178
+ | speedAB | 0–255, shared by A and B (the app's gears are 60 / 120 / 255) |
179
+ | dirCD, speedCD | same for C and D |
180
+ | slot | 1–3, module slot (the app can drive three modules at once) |
181
+ | SUM | sum of all previous bytes, modulo 256 |
182
+
183
+ Six-port modules use length `08` and append `dirEF speedE speedF`.
184
+
185
+ The module reports on `FFF1` with `5A 6B 01 00 01 <battery %> SUM`. It switches itself off
186
+ after about ten minutes without commands.
187
+
188
+ ## Limitations
189
+
190
+ - The two ports of a pair share one speed byte, so A and B (or C and D) cannot run at different
191
+ speeds at the same time; the faster one wins.
192
+ - Only tested with a four-port module. Six-port support follows the app's code but is unverified.
193
+
194
+ ## License
195
+
196
+ MIT
@@ -0,0 +1,170 @@
1
+ # brickremote
2
+
3
+ **Website for kids and parents: https://surak.github.io/brickremote/**
4
+
5
+ Python library for the cheap Lego-compatible Bluetooth LE motor bricks that advertise as
6
+ `JG_JMC-xxxx` and are normally driven by the **XP Block** phone app ("Smart creative module",
7
+ four motor ports A–D). Works anywhere [bleak](https://github.com/hbldh/bleak) does: Linux
8
+ (incl. Raspberry Pi), macOS and Windows.
9
+
10
+ ```
11
+ pip install brickremote # library and terminal app
12
+ pip install "brickremote[web]" # plus the web remote control
13
+ ```
14
+
15
+ Or from a checkout, without installing anything yourself:
16
+
17
+ ```
18
+ ./run.sh # creates .venv on first run, then starts the remote control app
19
+ ./run.sh scan # any other command works the same way
20
+ ```
21
+
22
+ ## Remote control app
23
+
24
+ Run `brickremote` with no arguments for a terminal version of the phone app (everything except the
25
+ phone-only gyroscope and voice modes):
26
+
27
+ | Tab | Keys |
28
+ |-----|------|
29
+ | Car | arrows: forward/back, steer left/right (drive port A, steering port D; change with `--drive`/`--steer`) |
30
+ | Ports | like two game sticks: `W`/`S` port A, `A`/`D` port B, `I`/`K` port C, `J`/`L` port D |
31
+ | Program | `e` edit in `$EDITOR`, Enter run (see below) |
32
+ | Path | `e` edit, Enter run, `[` `]` / `{` `}` calibrate speed and turn rate |
33
+
34
+ Everywhere: `Tab` switch screen, `1`-`3` speed (60/120/255), `space` STOP, `q` quit, and
35
+
36
+ - `h` **hold**: motors keep running after you let go of the key (steering still springs back)
37
+ - `p` **pair**: one port copies another (off → A+B → A+C), for builds with two motors that
38
+ should always move together, like a tank
39
+ - `r` + port letter **reverse**: that port turns the other way round, for a motor that was
40
+ mounted backwards
41
+
42
+ Settings, programs and paths are saved in `~/.config/brickremote.json`.
43
+
44
+ Terminals report key presses but not releases, so a motor runs while its key auto-repeats and
45
+ stops a moment after you let go. Steering always springs back to centre, even with hold on.
46
+
47
+ ## Web remote control
48
+
49
+ ```
50
+ ./run.sh web # or: brickremote web (needs `pip install 'brickremote[web]'`)
51
+ ```
52
+
53
+ This starts a small web server and prints its address. Open it in any browser: on the same
54
+ computer, or on a tablet or laptop on the same Wi-Fi, which then becomes the remote control.
55
+ It has the same four screens as the terminal app, plus things a browser is good at:
56
+
57
+ - buttons you hold down, a steering wheel you can drag, and two joysticks for the ports
58
+ - program blocks you tap to build a program
59
+ - a grid where you draw the route for path mode
60
+
61
+ If the browser stops talking to the server (tab closed, Wi-Fi gone), the motors stop by themselves.
62
+
63
+ ## Command line
64
+
65
+ ```
66
+ brickremote scan # list modules nearby
67
+ brickremote run a=255 --time 2 # port A forward, full speed, 2 s
68
+ brickremote run a=-120 d=255 --time 1.5 # A reverse at medium speed, D forward
69
+ brickremote program dance.txt # run a program script
70
+ brickremote path route.txt --mps 0.6 # drive a path script
71
+ brickremote --invert d run d=255 # global options: --address, --slot, --invert, --drive, --steer
72
+ ```
73
+
74
+ ### Program scripts (the app's block programming)
75
+
76
+ ```
77
+ gear 2 # speed for + and -: 1 = 60, 2 = 120, 3 = 255
78
+ repeat 3
79
+ a + 1.5 # port A forward for 1.5 s
80
+ ac - 1 # ports A and C reverse for 1 s
81
+ wait 0.5
82
+ end
83
+ d 200 # port D at speed 200, keep running
84
+ forever # until Ctrl-C / space
85
+ b + 1
86
+ b - 1
87
+ end
88
+ ```
89
+
90
+ ### Path scripts (the app's path mode)
91
+
92
+ ```
93
+ forward 1.5 # metres
94
+ left 90 # degrees, driving forward with full steering
95
+ back 0.5
96
+ right 45
97
+ ```
98
+
99
+ Distances and angles become times through a calibration (`--mps`, `--dps`, or `[ ] { }` in the
100
+ app): measure how far the car goes in one second at full speed, and how many degrees it turns.
101
+
102
+ ## Library
103
+
104
+ ```python
105
+ import asyncio
106
+ from brickremote import JMCModule
107
+
108
+ async def main():
109
+ async with JMCModule() as m: # connects to the first module found
110
+ await m.drive(a=255, d=-120) # speeds -255..255, negative = reverse
111
+ await asyncio.sleep(2)
112
+ await m.stop()
113
+ print("battery:", m.battery, "%")
114
+
115
+ asyncio.run(main())
116
+ ```
117
+
118
+ `JMCModule(address)` targets a specific module, `discover()` lists all of them. While connected,
119
+ the current state is resent every 0.2 s, which keeps the module from going to sleep.
120
+
121
+ ```python
122
+ JMCModule(inverted="d", links={"a": "c"}) # reverse port D; C follows A
123
+ await m.run(2, a=255) # A forward for 2 s, then stop
124
+ ```
125
+
126
+ `brickremote.script` has the program and path engines (`parse_program`, `run_program`,
127
+ `parse_path`, `run_path`) if you want them from your own code.
128
+
129
+ For the stock car (drive motor on A, steering on D) there is a small wrapper:
130
+
131
+ ```python
132
+ from brickremote import Car, JMCModule
133
+
134
+ async with JMCModule() as m:
135
+ car = Car(m)
136
+ await car.left(speed=200) # forward while steering left
137
+ ```
138
+
139
+ ## Protocol
140
+
141
+ Reverse-engineered from the XP Block Android app. The module exposes service `FFF0`;
142
+ commands go to `FFF2` and **must be written with response** (write-without-response is
143
+ silently ignored):
144
+
145
+ ```
146
+ 5A 6B 02 00 05 dirAB speedAB dirCD speedCD slot SUM
147
+ ```
148
+
149
+ | Field | Meaning |
150
+ |---------|---------|
151
+ | dirAB | A: `1` forward, `2` reverse; B: `+4` forward, `+8` reverse |
152
+ | speedAB | 0–255, shared by A and B (the app's gears are 60 / 120 / 255) |
153
+ | dirCD, speedCD | same for C and D |
154
+ | slot | 1–3, module slot (the app can drive three modules at once) |
155
+ | SUM | sum of all previous bytes, modulo 256 |
156
+
157
+ Six-port modules use length `08` and append `dirEF speedE speedF`.
158
+
159
+ The module reports on `FFF1` with `5A 6B 01 00 01 <battery %> SUM`. It switches itself off
160
+ after about ten minutes without commands.
161
+
162
+ ## Limitations
163
+
164
+ - The two ports of a pair share one speed byte, so A and B (or C and D) cannot run at different
165
+ speeds at the same time; the faster one wins.
166
+ - Only tested with a four-port module. Six-port support follows the app's code but is unverified.
167
+
168
+ ## License
169
+
170
+ MIT
Binary file
Binary file
Binary file
Binary file