Skip to content

Commit 224680b

Browse files
Update the widget to display different views depending on the presence of already connected devices or not.
Add an identifier attribute to the connect method in movehub.py.
1 parent db1f956 commit 224680b

12 files changed

Lines changed: 214 additions & 94 deletions

File tree

bluetooth_manager/movehub.py

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,10 @@ async def __aexit__(self, exc_type, exc, tb):
146146

147147
class MoveHubLaneProxy(object):
148148

149-
def __init__(self, movehub, lane):
149+
def __init__(self, movehub, lane, identifier):
150150
self.movehub = movehub
151151
self.lane = lane
152+
self.identifier = identifier
152153

153154
async def get_distance_async(self):
154155
await self._poll()
@@ -420,7 +421,7 @@ def __init__(self, *args, **kwargs):
420421
self._run_lock = asyncio.Lock()
421422
self._lane_locks = [asyncio.Lock() for i in range(self.n_lanes)]
422423

423-
def run_async_program(self, program, lane=0, output=None):
424+
def run_async_program(self, program, lane=0, output=None, identifier=identifier):
424425

425426
if lane < 0 or lane >= self.n_lanes :
426427
raise RuntimeError(f"lane must be >=0 and < {self.n_lanes} but is {lane}")
@@ -429,11 +430,11 @@ def run_async_program(self, program, lane=0, output=None):
429430
output = Output()
430431
display(output)
431432
return asyncio.ensure_future(
432-
self._run_async_program(lane=lane, program=program, output=output)
433+
self._run_async_program(lane=lane, program=program, output=output, identifier=identifier)
433434
)
434435

435436

436-
def run_async_programs_concurrently(self, programs, output=None):
437+
def run_async_programs_concurrently(self, programs, output=None, identifier=None):
437438
if output is None:
438439
output = Output()
439440
display(output)
@@ -443,8 +444,10 @@ def run_async_programs_concurrently(self, programs, output=None):
443444

444445
futures = []
445446
for lane, program in enumerate(programs):
447+
if identifier is None:
448+
identifier = self.identifier
446449
f = asyncio.ensure_future(
447-
self._run_async_program(lane=lane, program=program, output=output)
450+
self._run_async_program(lane=lane, program=program, output=output, identifier=identifier)
448451
)
449452
futures.append(f)
450453
return futures
@@ -460,13 +463,20 @@ def run_program(self, program, output=None):
460463
lane_proxy = MoveHubLaneProxy(movehub=self, lane=0)
461464
program(lane_proxy, output)
462465

463-
def connect(self, output=None):
464-
async def main(lane, log):
466+
def connect(self, output=None, identifier=None):
467+
async def main(lane, log, identifier):
465468
pass
469+
if output is None:
470+
output = Output()
471+
display(output)
472+
473+
if identifier is None:
474+
identifier = self.identifier
466475

467-
self.run_async_program(main, output=output)
476+
self.run_async_program(main, output=output, identifier=identifier)
477+
468478

469-
async def _run_async_program(self, lane, program, output):
479+
async def _run_async_program(self, lane, program, output, identifier):
470480
def log(*args, **kwargs):
471481
old_stdout = sys.stdout
472482
sys.stdout = mystdout = StringIO()
@@ -481,10 +491,10 @@ def log(*args, **kwargs):
481491
self._log = log
482492

483493
async with self._lane_locks[lane]:
484-
lane_proxy = MoveHubLaneProxy(movehub=self, lane=lane)
494+
lane_proxy = MoveHubLaneProxy(movehub=self, lane=lane, identifier=identifier)
485495
try:
486496
await lane_proxy._connect()
487-
await program(lane_proxy, log)
497+
await program(lane_proxy, log, identifier)
488498
except Exception as ex:
489499
err_str = "".join(
490500
traceback.TracebackException.from_exception(ex).format()

examples/introduction.ipynb

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
{
1717
"cell_type": "code",
18-
"execution_count": 42,
18+
"execution_count": 1,
1919
"id": "762d552a-dafd-4b4b-9b0c-8b6612d2415b",
2020
"metadata": {
2121
"scrolled": true
@@ -53,60 +53,67 @@
5353
},
5454
{
5555
"cell_type": "code",
56-
"execution_count": 46,
56+
"execution_count": 2,
5757
"id": "aae633ad-bbb2-45c2-a1ff-5fab18146172",
5858
"metadata": {
5959
"scrolled": true
6060
},
61-
"outputs": [],
62-
"source": [
63-
"# number of concurrent \"lanes\"\n",
64-
"# Check the list of connected MoveHub in the Bluetooh Device section\n",
65-
"# If you want to use one of them in the widget, use its identifier \n",
66-
"# (in brackets)\n",
67-
"n_lanes = 4 \n",
68-
"identifier =\"V5AFFoX5OfQmQss/ap6fyw==\"\n",
69-
"movehub = MoveHubWidget(n_lanes=n_lanes, identifier=identifier)"
70-
]
71-
},
72-
{
73-
"cell_type": "code",
74-
"execution_count": 48,
75-
"id": "9b5f6086-3605-47ee-a170-57d3b97600e2",
76-
"metadata": {},
7761
"outputs": [
7862
{
7963
"data": {
8064
"application/vnd.jupyter.widget-view+json": {
81-
"model_id": "1c183206b68b4c5d993053ebbe9b86ae",
65+
"model_id": "b4963924a83143199f0756442e81826b",
8266
"version_major": 2,
8367
"version_minor": 0
8468
},
8569
"text/plain": [
86-
"Output()"
87-
]
88-
},
89-
"metadata": {},
90-
"output_type": "display_data"
91-
},
92-
{
93-
"data": {
94-
"application/vnd.jupyter.widget-view+json": {
95-
"model_id": "e2aa45575c9f44ceaa5bd88efdb12a5e",
96-
"version_major": 2,
97-
"version_minor": 0
98-
},
99-
"text/plain": [
100-
"MoveHubWidget(identifier='V5AFFoX5OfQmQss/ap6fyw==', n_lanes=4)"
70+
"MoveHubWidget(n_lanes=4)"
10171
]
10272
},
10373
"metadata": {},
10474
"output_type": "display_data"
10575
}
10676
],
10777
"source": [
108-
"movehub.connect()\n",
109-
"display(movehub) "
78+
"# number of concurrent \"lanes\"\n",
79+
"n_lanes = 4 \n",
80+
"widget = MoveHubWidget(n_lanes=n_lanes)\n",
81+
"output = Output()\n",
82+
"display(widget)"
83+
]
84+
},
85+
{
86+
"cell_type": "code",
87+
"execution_count": null,
88+
"id": "9b5f6086-3605-47ee-a170-57d3b97600e2",
89+
"metadata": {},
90+
"outputs": [],
91+
"source": [
92+
"# Case 1 : Execute this cell if there is no MoveHub connected\n",
93+
"widget.connect(output=output)"
94+
]
95+
},
96+
{
97+
"cell_type": "code",
98+
"execution_count": null,
99+
"id": "514c7c39-8b62-4caa-a506-4abb79891592",
100+
"metadata": {},
101+
"outputs": [],
102+
"source": [
103+
"# Case 2 : Execute this cell if there is/are one/some MoveHub(s) connected\n",
104+
"# Choose one of them and paste its identifier\n",
105+
"widget.identifier = \"dcRIlxx9l8bQ3T6UVofW8Q==\"\n",
106+
"widget.connect(output=output, identifier=widget.identifier)"
107+
]
108+
},
109+
{
110+
"cell_type": "code",
111+
"execution_count": null,
112+
"id": "e2b06dac-4056-435f-800a-60134c48c27e",
113+
"metadata": {},
114+
"outputs": [],
115+
"source": [
116+
"display(widget)"
110117
]
111118
},
112119
{
@@ -251,7 +258,7 @@
251258
"box = ipywidgets.HBox([button_disco, button_motors_ab, button_motor_c])\n",
252259
"output = Output()\n",
253260
"\n",
254-
"async def disco(lane, log):\n",
261+
"async def disco(lane, log):main\n",
255262
" button_disco.disabled = True\n",
256263
" for i in range(10):\n",
257264
" await lane.set_led_async(LedColor.pink)\n",
@@ -265,7 +272,7 @@
265272
" await lane.motor_time_multi_async(seconds=2, power_a=-10, power_b=10)\n",
266273
" await lane.motor_time_multi_async(seconds=2, power_a=10, power_b=-10)\n",
267274
" await lane.motor_time_multi_async(seconds=2, power_a=-10, power_b=-10)\n",
268-
" button_motors_ab.disabled = False\n",
275+
" button_motors_ab.disabled =main False\n",
269276
"\n",
270277
"async def motor_c(lane, log):\n",
271278
" button_motor_c.disabled = True\n",
@@ -401,7 +408,7 @@
401408
"name": "python",
402409
"nbconvert_exporter": "python",
403410
"pygments_lexer": "ipython3",
404-
"version": "3.13.1"
411+
"version": "3.13.2"
405412
}
406413
},
407414
"nbformat": 4,

src/bluetooth-extension/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ const BluetoothSidebarPlugin: JupyterFrontEndPlugin<void> = {
110110
}).then(async result => {
111111
if (result.button.accept) {
112112
bluetoothManager.registry.itemsList.forEach(async item => {
113-
if (item.identifier === result.value) {
113+
if (item.deviceType === result.value) {
114114
await bluetoothManager.connectDevice(item);
115115
} else {
116116
console.warn('There is no corresponding item in the registry!');
@@ -172,8 +172,8 @@ export class DropDownRegistry
172172
this.registry = registry;
173173
registry.itemsList.forEach(item => {
174174
const option = document.createElement('option');
175-
option.value = item.identifier;
176-
option.text = item.identifier;
175+
option.value = item.deviceType;
176+
option.text = item.deviceType;
177177
this._selectList.appendChild(option);
178178
});
179179
}

src/bluetooth/BluetoothManager.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { buildCompleteIdentifier } from '../bluetooth-extension';
55
import { IDisposable } from '@lumino/disposable';
66
import { Dialog, showDialog } from '@jupyterlab/apputils';
77

8+
89
/**
910
* A class used to update the list of connected device and the related signals used to rerender the connected devices section.
1011
*/
@@ -19,7 +20,7 @@ export class BluetoothManager implements IBluetoothManager {
1920
>(this);
2021
this._registry = new BluetoothManager.DeviceRegistry();
2122
this._deviceList = [];
22-
this.identifierRegistry = [];
23+
this._identifierRegistry = [];
2324
}
2425

2526
get deviceList(): Array<BluetoothManager.Device> {
@@ -30,6 +31,10 @@ export class BluetoothManager implements IBluetoothManager {
3031
return this._registry;
3132
}
3233

34+
get identifierRegistry(): Array<string> {
35+
return this._identifierRegistry;
36+
}
37+
3338
async connectDevice(
3439
registryItem: IDeviceRegistryItem
3540
): Promise<BluetoothManager.Device | undefined> {
@@ -88,7 +93,7 @@ export class BluetoothManager implements IBluetoothManager {
8893
this._registry.add(registryItem);
8994
this.registeredByAPlugin.emit(this._registry);
9095
console.warn(
91-
`New item from category ${registryItem.identifier} is added to the registry.`
96+
`New item from category ${registryItem.deviceType} is added to the registry.`
9297
);
9398
return this._registry;
9499
}
@@ -126,7 +131,6 @@ export class BluetoothManager implements IBluetoothManager {
126131
else {
127132
return;
128133
}
129-
130134
}
131135

132136
private _deviceList: Array<BluetoothManager.Device>;
@@ -136,7 +140,7 @@ export class BluetoothManager implements IBluetoothManager {
136140
BluetoothManager.DeviceRegistry
137141
>;
138142
private _registry: BluetoothManager.DeviceRegistry;
139-
public identifierRegistry: Array<string>;
143+
private _identifierRegistry: Array<string>;
140144
}
141145

142146
export namespace BluetoothManager {
@@ -277,10 +281,11 @@ export interface IBluetoothManager {
277281
>;
278282
get deviceList(): Array<BluetoothManager.Device>;
279283
get registry(): BluetoothManager.DeviceRegistry;
284+
get identifierRegistry(): Array<string>;
280285
}
281286

282287
export interface IDeviceRegistryItem {
283-
identifier: string;
288+
deviceType: string;
284289
factory: (
285290
native: BluetoothDevice
286291
) => Promise<BluetoothManager.Device | undefined>;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useState } from 'react';
2+
import copySVG from '../../../style/copy.svg';
3+
const copySVGUrl = `data:image/svg+xml;base64,${btoa(copySVG)}`;
4+
5+
interface ICopyToClipboardProps {
6+
textToCopy: string
7+
}
8+
export default function CopyToClipboard({ textToCopy }: ICopyToClipboardProps) {
9+
10+
const handleCopyClick = async () => {
11+
try {
12+
await navigator.clipboard.writeText(textToCopy);
13+
} catch (err) {
14+
console.error('Failed to copy text: ', err);
15+
}
16+
};
17+
return (
18+
<div style={{display:"flex", alignItems:"center", justifyContent:"center", width: "250px", gap:"10px", margin: "4px 0"}}>
19+
<div>ID: </div>
20+
<input
21+
className='input-movehub-id'
22+
type="text"
23+
value={textToCopy}
24+
placeholder={textToCopy}
25+
/>
26+
<button className='copy-button' onClick={handleCopyClick} title={"Copy ID"}>
27+
<img src={copySVGUrl} alt={"Button with copy icon"} />
28+
</button>
29+
</div>
30+
);
31+
}

src/movehub-extension/components/DeviceInfoTableComplete.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export function DeviceInfoTableComplete({ moveHub }: { moveHub: MoveHub }) {
3434
<tr className="custom-table-tr">
3535
<th className="custom-table-th"></th>
3636
<th className="custom-table-th">Status</th>
37+
<th className="custom-table-th">MAC address</th>
3738
<th className="custom-table-th">Identifier</th>
3839
<th className="custom-table-th">Led color</th>
3940
<th className="custom-table-th">Battery</th>
@@ -51,6 +52,13 @@ export function DeviceInfoTableComplete({ moveHub }: { moveHub: MoveHub }) {
5152
<ColoredCircleWithText color={'red'} text={'disconnected'} />
5253
)}
5354
</td>
55+
<td className="custom-table-td">
56+
{deviceState.connected ? (
57+
<div>{deviceState.primaryMACAddress}</div>
58+
) : (
59+
<div></div>
60+
)}
61+
</td>
5462
<td className="custom-table-td">
5563
{deviceState.connected ? (
5664
<div>{deviceState.identifier}</div>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { BluetoothManager, IBluetoothManager } from "../../bluetooth/BluetoothManager";
2+
import CopyToClipboard from "./CopyToClipboard";
3+
4+
interface IDeviceListProps {
5+
bluetoothManager: IBluetoothManager
6+
}
7+
8+
export function MoveHubList({ bluetoothManager }: IDeviceListProps) {
9+
const listItems = bluetoothManager.deviceList.map((item: BluetoothManager.Device, index) =>
10+
<>
11+
<div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: "10px" }}>
12+
<div>Move Hub n°{index + 1}</div> <CopyToClipboard textToCopy={item.native.id} />
13+
</div>
14+
</>
15+
);
16+
return (
17+
<ul>{listItems}</ul>
18+
)
19+
}

0 commit comments

Comments
 (0)