From ca1ec0a64ee65f76ae9c2ae09dd626005ec81417 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 1 Apr 2021 13:26:46 -0500 Subject: [PATCH 1/7] Add Device Information Service support The Pybricks firmware now includes the BLE Device Information Service to provide firmware and Pybricks protocol versions. For now, checking that the service is present is enough to know that the firmware is up to date. But we will need to add additional checks as soon as the protocol is changed. --- src/ble-device-info-service/protocol.ts | 15 +++++ src/ble-uart/sagas.ts | 90 ++++++++++++++++++++++++- src/ble/actions.ts | 10 ++- src/notifications/i18n.en.json | 2 +- src/notifications/sagas.test.ts | 5 +- src/notifications/sagas.ts | 8 ++- 6 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 src/ble-device-info-service/protocol.ts diff --git a/src/ble-device-info-service/protocol.ts b/src/ble-device-info-service/protocol.ts new file mode 100644 index 00000000..f1517a86 --- /dev/null +++ b/src/ble-device-info-service/protocol.ts @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors +// +// Pybricks uses the standard Device Info service. +// Refer to Device Information Service (DIS) at https://www.bluetooth.com/specifications/specs/ +// and assigned numbers at https://www.bluetooth.com/specifications/assigned-numbers/ + +/** Device Information service UUID. */ +export const serviceUUID = 0x180a; + +/** Firmware Revision String characteristic UUID. */ +export const firmwareRevisionStringUUID = 0x2a26; + +/** Software Revision String characteristic UUID. */ +export const softwareRevisionStringUUID = 0x2a28; diff --git a/src/ble-uart/sagas.ts b/src/ble-uart/sagas.ts index ce85d20d..a16e0d2c 100644 --- a/src/ble-uart/sagas.ts +++ b/src/ble-uart/sagas.ts @@ -15,6 +15,11 @@ import { takeEvery, takeMaybe, } from 'typed-redux-saga/macro'; +import { + serviceUUID as deviceInfoServiceUUID, + firmwareRevisionStringUUID, + softwareRevisionStringUUID, +} from '../ble-device-info-service/protocol'; import { BlePybricksServiceActionType, didFailToWriteCommand, @@ -53,6 +58,8 @@ import { TxCharUUID as uartTxCharUUID, } from './protocol'; +const decoder = new TextDecoder(); + function disconnect( server: BluetoothRemoteGATTServer, _action: BleDeviceDisconnectAction, @@ -109,7 +116,11 @@ function* connect(_action: BleDeviceConnectAction): Generator { device = yield* call(() => navigator.bluetooth.requestDevice({ filters: [{ services: [pybricksServiceUUID] }], - optionalServices: [pybricksServiceUUID, uartServiceUUID], + optionalServices: [ + pybricksServiceUUID, + deviceInfoServiceUUID, + uartServiceUUID, + ], }), ); } catch (err) { @@ -145,6 +156,79 @@ function* connect(_action: BleDeviceConnectAction): Generator { yield* takeEvery(BLEDeviceActionType.Disconnect, disconnect, server); + let deviceInfoService: BluetoothRemoteGATTService; + try { + deviceInfoService = yield* call( + [server, 'getPrimaryService'], + deviceInfoServiceUUID, + ); + } catch (err) { + server.disconnect(); + yield* takeMaybe(disconnectChannel); + if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { + yield* put(didFailToConnect({ reason: Reason.NoDeviceInfoService })); + } else { + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + } + return; + } + + let firmwareVersionChar: BluetoothRemoteGATTCharacteristic; + try { + firmwareVersionChar = yield* call( + [deviceInfoService, 'getCharacteristic'], + firmwareRevisionStringUUID, + ); + } catch (err) { + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + let firmwareVersion: string; + try { + firmwareVersion = decoder.decode( + yield* call([firmwareVersionChar, 'readValue']), + ); + } catch (err) { + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + // TODO: save firmware version for later use + console.log(`Hub firmware version: ${firmwareVersion}`); + + let softwareVersionChar: BluetoothRemoteGATTCharacteristic; + try { + softwareVersionChar = yield* call( + [deviceInfoService, 'getCharacteristic'], + softwareRevisionStringUUID, + ); + } catch (err) { + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + let protocolVersion: string; + try { + protocolVersion = decoder.decode( + yield* call([softwareVersionChar, 'readValue']), + ); + } catch (err) { + server.disconnect(); + yield* takeMaybe(disconnectChannel); + yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + return; + } + + // TODO: verify that minimum protocol version is met + console.log(`Pybricks protocol version: ${protocolVersion}`); + let pybricksService: BluetoothRemoteGATTService; try { pybricksService = yield* call( @@ -155,7 +239,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { server.disconnect(); yield* takeMaybe(disconnectChannel); if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { - yield* put(didFailToConnect({ reason: Reason.NoService })); + yield* put(didFailToConnect({ reason: Reason.NoPybricksService })); } else { yield* put(didFailToConnect({ reason: Reason.Unknown, err })); } @@ -231,7 +315,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { server.disconnect(); yield* takeMaybe(disconnectChannel); if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { - yield* put(didFailToConnect({ reason: Reason.NoService })); + yield* put(didFailToConnect({ reason: Reason.NoPybricksService })); } else { yield* put(didFailToConnect({ reason: Reason.Unknown, err })); } diff --git a/src/ble/actions.ts b/src/ble/actions.ts index 0847fd7a..aa1a5ce6 100644 --- a/src/ble/actions.ts +++ b/src/ble/actions.ts @@ -58,7 +58,8 @@ export enum BleDeviceFailToConnectReasonType { NoBluetooth = 'ble.device.didFailToConnect.noBluetooth', Canceled = 'ble.device.didFailToConnect.canceled', NoGatt = 'ble.device.didFailToConnect.noGatt', - NoService = 'ble.device.didFailToConnect.noService', + NoDeviceInfoService = 'ble.device.didFailToConnect.noDeviceInfoService', + NoPybricksService = 'ble.device.didFailToConnect.noPybricksService', Unknown = 'ble.device.didFailToConnect.unknown', } @@ -74,7 +75,9 @@ export type BleDeviceFailToConnectCanceledReason = Reason; -export type BleDeviceFailToConnectNoServiceReason = Reason; +export type BleDeviceFailToConnectNoDeviceInfoServiceReason = Reason; + +export type BleDeviceFailToConnectNoPybricksServiceReason = Reason; export type BleDeviceFailToConnectUnknownReason = Reason & { err: Error; @@ -85,7 +88,8 @@ export type BleDeviceDidFailToConnectReason = | BleDeviceFailToConnectNoBluetoothReason | BleDeviceFailToConnectCanceledReason | BleDeviceFailToConnectNoGattReason - | BleDeviceFailToConnectNoServiceReason + | BleDeviceFailToConnectNoDeviceInfoServiceReason + | BleDeviceFailToConnectNoPybricksServiceReason | BleDeviceFailToConnectUnknownReason; export type BleDeviceDidFailToConnectAction = Action & diff --git a/src/notifications/i18n.en.json b/src/notifications/i18n.en.json index c1ab03e6..c7264b45 100644 --- a/src/notifications/i18n.en.json +++ b/src/notifications/i18n.en.json @@ -6,7 +6,7 @@ }, "ble": { "gattPermission": "The web browser did not give permission to use Bluetooth Low Energy", - "gattServiceNotFound": "Connected to hub but failed to get {serviceName} service. Try removing the \"{hubName}\" device in your OS Bluetooth settings, then try again.", + "gattServiceNotFound": "Connected to hub but failed to get {serviceName} service. Ensure that you are using the most recent firmware. If the problem persists, try removing the \"{hubName}\" device in your OS Bluetooth settings, then try connecting again.", "noWebBluetooth": "This web browser does not support Web Bluetooth or it is not enabled.", "noBluetooth": "No Bluetooth adapter could be found. Bluetooth won't work.", "unexpectedError": "Unexpected error while trying to connect: {errorMessage}" diff --git a/src/notifications/sagas.test.ts b/src/notifications/sagas.test.ts index 125bc967..bf6157ea 100644 --- a/src/notifications/sagas.test.ts +++ b/src/notifications/sagas.test.ts @@ -31,7 +31,10 @@ test.each([ bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoWebBluetooth }), bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoBluetooth }), bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoGatt }), - bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoService }), + bleDidFailToConnect({ + reason: BleDeviceFailToConnectReasonType.NoDeviceInfoService, + }), + bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.NoPybricksService }), bleDidFailToConnect({ reason: BleDeviceFailToConnectReasonType.Unknown, err: { name: 'test', message: 'unknown' }, diff --git a/src/notifications/sagas.ts b/src/notifications/sagas.ts index acfce26a..74aa93fb 100644 --- a/src/notifications/sagas.ts +++ b/src/notifications/sagas.ts @@ -168,12 +168,18 @@ function* showBleDeviceDidFailToConnectError( yield* showSingleton(Level.Error, MessageId.BleGattPermission); break; - case BleDeviceFailToConnectReasonType.NoService: + case BleDeviceFailToConnectReasonType.NoPybricksService: yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, { serviceName: 'Pybricks', hubName: 'Pybricks Hub', }); break; + case BleDeviceFailToConnectReasonType.NoDeviceInfoService: + yield* showSingleton(Level.Error, MessageId.BleGattServiceNotFound, { + serviceName: 'Device Information', + hubName: 'Pybricks Hub', + }); + break; case BleDeviceFailToConnectReasonType.NoBluetooth: yield* showSingleton(Level.Error, MessageId.BleNoBluetooth); break; From 55e724e8d84bc676987fa1f85effdf0194f84ef9 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 1 Apr 2021 13:58:01 -0500 Subject: [PATCH 2/7] Allow splitting long notifications This adds the ability to split notification messages in to multiple paragraphs. --- src/notifications/Notification.tsx | 17 ++++++++++++++++- src/notifications/i18n.en.json | 4 ++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/notifications/Notification.tsx b/src/notifications/Notification.tsx index 88ec7c6f..fc49da35 100644 --- a/src/notifications/Notification.tsx +++ b/src/notifications/Notification.tsx @@ -20,5 +20,20 @@ export default function Notification(props: OwnProps): JSX.Element { fallback: en, }); const { messageId, replacements } = props; - return <>{i18n.translate(messageId, replacements)}; + let message = i18n.translate(messageId, replacements) as + | React.ReactElement + | string; + + // Use newline characters to create paragraphs + if (typeof message === 'string') { + message = ( + <> + {message.split('\n').map((x, i) => ( +

{x}

+ ))} + + ); + } + + return message; } diff --git a/src/notifications/i18n.en.json b/src/notifications/i18n.en.json index c7264b45..d20fb9f5 100644 --- a/src/notifications/i18n.en.json +++ b/src/notifications/i18n.en.json @@ -6,14 +6,14 @@ }, "ble": { "gattPermission": "The web browser did not give permission to use Bluetooth Low Energy", - "gattServiceNotFound": "Connected to hub but failed to get {serviceName} service. Ensure that you are using the most recent firmware. If the problem persists, try removing the \"{hubName}\" device in your OS Bluetooth settings, then try connecting again.", + "gattServiceNotFound": "Connected to hub but failed to get {serviceName} service.\nEnsure that you are using the most recent firmware.\nIf the problem persists, try removing the \"{hubName}\" device in your OS Bluetooth settings, then try connecting again.", "noWebBluetooth": "This web browser does not support Web Bluetooth or it is not enabled.", "noBluetooth": "No Bluetooth adapter could be found. Bluetooth won't work.", "unexpectedError": "Unexpected error while trying to connect: {errorMessage}" }, "editor": { "programChanged": { - "message": "The program was changed in another window. Do you want to delete this program and replace it with the new program?", + "message": "The program was changed in another window.\nDo you want to delete this program and replace it with the new program?", "action": "Reload" } }, From 09ba8bb035b60a6c65336e42ab03c326ab3bf269 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 1 Apr 2021 14:13:25 -0500 Subject: [PATCH 3/7] Increase toast max-width This makes it big enough to fit the compiler error message IndentationError: unindent doesn't match any outer indent level If we still have problems with overflow we should probably look at ways to wrap
 text instead of making this wider.
---
 src/index.scss | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/index.scss b/src/index.scss
index 32230b79..f5c7cb43 100644
--- a/src/index.scss
+++ b/src/index.scss
@@ -37,6 +37,7 @@ body {
 
 .#{$ns}-toast {
     user-select: text;
+    max-width: 700px;
 }
 
 .#{$ns}-button {

From 829e47c75a0f047b7ec20b3f4ca2ea825e5ef7d3 Mon Sep 17 00:00:00 2001
From: David Lechner 
Date: Thu, 1 Apr 2021 14:24:31 -0500
Subject: [PATCH 4/7] Wrap long lines in compiler error

This way we don't overflow the toast.
---
 src/notifications/sagas.ts | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/notifications/sagas.ts b/src/notifications/sagas.ts
index 74aa93fb..5ecb6b8d 100644
--- a/src/notifications/sagas.ts
+++ b/src/notifications/sagas.ts
@@ -316,7 +316,11 @@ function* dismissCompilerError(): Generator {
 
 function* showCompilerError(action: MpyDidFailToCompileAction): Generator {
     yield* showSingleton(Level.Error, MessageId.MpyError, {
-        errorMessage: React.createElement('pre', undefined, action.err.join('\n')),
+        errorMessage: React.createElement(
+            'pre',
+            { style: { whiteSpace: 'pre-wrap', wordBreak: 'keep-all' } },
+            action.err.join('\n'),
+        ),
     });
 }
 

From bb9a89d7d951f8faf4e2be6cbdcd228b623c2ed8 Mon Sep 17 00:00:00 2001
From: David Lechner 
Date: Fri, 2 Apr 2021 12:48:06 -0500
Subject: [PATCH 5/7] prettier

---
 .eslintrc.js | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/.eslintrc.js b/.eslintrc.js
index fef7d592..c8eea72f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -21,15 +21,13 @@ module.exports = {
         'no-multi-spaces': 'error',
         'no-trailing-spaces': 'error',
         'no-multiple-empty-lines': 'error',
-        '@typescript-eslint/no-unused-vars': ["error", { argsIgnorePattern: "^_" }],
-        'no-unused-vars': ["error", { argsIgnorePattern: "^_" }],
+        '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
+        'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
         'sort-imports': ['error', { ignoreDeclarationSort: true }],
         'import/order': ['error', { alphabetize: { order: 'asc' } }],
     },
     settings: {
         react: { version: 'detect' },
     },
-    ignorePatterns: [
-        "test/env.js"
-    ],
+    ignorePatterns: ['test/env.js'],
 };

From e441c8b36d7a296db819ea0465a18a57d9f0345d Mon Sep 17 00:00:00 2001
From: David Lechner 
Date: Fri, 2 Apr 2021 16:08:38 -0500
Subject: [PATCH 6/7] Pybricks firmware v3.0.0b3 (2nd release)

This includes a fix for Move hub.
---
 package.json | 2 +-
 yarn.lock    | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/package.json b/package.json
index 1e5d9c8e..67c14cba 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "dependencies": {
         "@blueprintjs/core": "^3.41.0",
         "@craco/craco": "^6.1.1",
-        "@pybricks/firmware": "4.7.0",
+        "@pybricks/firmware": "4.7.1",
         "@pybricks/ide-docs": "1.1.1",
         "@pybricks/mpy-cross-v5": "^2.0.0",
         "@shopify/react-i18n": "^5.3.0",
diff --git a/yarn.lock b/yarn.lock
index 99427465..c314ad6a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1500,10 +1500,10 @@
     schema-utils "^2.6.5"
     source-map "^0.7.3"
 
-"@pybricks/firmware@4.7.0":
-  version "4.7.0"
-  resolved "https://registry.yarnpkg.com/@pybricks/firmware/-/firmware-4.7.0.tgz#5b3cdfa009dc65ac7d3e3ccd769ab74143a1bda6"
-  integrity sha512-Ln5puxxiEiWOv39AFja4Rr7OeAhxe/yJnWFI0IJXVnAPqFR9FIrME8rDZhPXyw5yIT76MMUuv6B21IJmA9WURw==
+"@pybricks/firmware@4.7.1":
+  version "4.7.1"
+  resolved "https://registry.yarnpkg.com/@pybricks/firmware/-/firmware-4.7.1.tgz#2d800bfc9ce2096e6790a43393a5d0f8a795c94f"
+  integrity sha512-nsYNhN2vOkclujsmeXyaziFz3xoCeLElUm81lQahxCCFl+bYaqQtXqhXqQ/pkYTWq+5bcd5cG8u/Y49jpeZ57A==
   dependencies:
     jszip "^3.5.0"
 

From 8be950036ee9a40b8dc7dbdf7b8ffee019bf1b53 Mon Sep 17 00:00:00 2001
From: David Lechner 
Date: Fri, 2 Apr 2021 16:09:38 -0500
Subject: [PATCH 7/7] beta 8

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 67c14cba..40105086 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "@pybricks/pybricks-code",
-    "version": "1.0.0-beta.7",
+    "version": "1.0.0-beta.8",
     "license": "MIT",
     "author": "The Pybricks Authors",
     "repository": {