From 59db5f40367ee3cb2b83df90b188c4e7752d869b Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 13 Aug 2021 10:07:52 -0500 Subject: [PATCH 1/2] tools: add https server script In order to test on Android, we must serve the local build directory with https, otherwise Web Bluetooth doesn't work. --- tools/serve.py | 34 ++++++++++++++++++++++++++++++++++ tools/test.pem | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100755 tools/serve.py create mode 100644 tools/test.pem diff --git a/tools/serve.py b/tools/serve.py new file mode 100755 index 00000000..7006d596 --- /dev/null +++ b/tools/serve.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +""" +Serve local build with https for testing. + +This is useful, e.g. for Android (WebBluetooth doesn't work without https). + +Usage: + + yarn build + ./tools/serve.py + +Browse to https://localhost:8443 on the local machine or +https://:8443 on a remote device. + +The browser will complain about the self-signed certificate, but this can be +bypassed (click "Advanced" in browser). +""" + +from http.server import HTTPServer, SimpleHTTPRequestHandler +from pathlib import Path +import os +import ssl + +THIS_DIR = Path(__file__).parent +TEST_CERT = (THIS_DIR / 'test.pem').absolute() +BUILD_DIR = (THIS_DIR / '..' / 'build').absolute() + +# HTTPServer serves the current directory. +os.chdir(BUILD_DIR) + +httpd = HTTPServer(('', 8443), SimpleHTTPRequestHandler) +httpd.socket = ssl.wrap_socket(httpd.socket, certfile=TEST_CERT, server_side=True) +httpd.serve_forever() diff --git a/tools/test.pem b/tools/test.pem new file mode 100644 index 00000000..85319120 --- /dev/null +++ b/tools/test.pem @@ -0,0 +1,48 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDLpTko58nNd2cE +q8MaB1Wv4xren/xiNomDwf44QK9jZGzvHqnXCKTQ0KsLxtZt7Ne6PU1WdIA3bf6/ +nVzVUhauP0+DF8t6bTH4GSlvLXKGFW6dkCkTMw8ginh73PkZL+OaQvaMVJd42CW9 +bPhFMF6tXsLq661ObXwTE+vBopSim4EEzy+VKXB8BHvMYvfMcWiJihKJhCPLZE9p +iHBO3yByMs0fLe6UQ/MAHXFVprxnXJ+P2mMe5s/c0BEcTK1deNy9ngKcUvKGCcfj +T19xtkS4IHS0csz0fkZ+93tGjTm9eCLAOmyvKRljhOMFZKV4J1w4We3Y2b+xHN9w +psukl2+JAgMBAAECggEBAKacuEZaVYYQQa3VpZs/EwEDavFnrYLhIeM1rXtymMxV +1rZQvR4ciVP61WaeOMPY+ar7CrJIQLqY+MrmwZQaREotrC0SI703Z6GXXIoEgeVn +1Yd98c0ughraj7avVPIhdzK3xKWNdBw/o6j2Za2d5Ggt1oWDath2xQOK0eEr1jWu +VNfZNMvjyvevbyzuBLlF2970ZbOKAi/nrqtmw8mlhqDK5hy+j0K37hAbxizVDmU6 +vZfGhgmgBqXCla59T4rN3StK6zYVfmZKuGHWQbBxbWuLpFA+m09WKF599rUgZG7e +l5RC1dOXCa4bOv8yVRULb5LSIyBHxIfPiqShw/gUm3ECgYEA5/QkAOw4M2RSpaGb +K0yaoAKkZ5whX3rzudOVocQkeMKETpiPcLRZpO/z3A/bRjDg5YG7FNhbBI2nqGHR +ShkZIZlx7OeZPsT4IpciKDGOjoWhfk2a3gaUj+24hUO13hK1DrudI0t+USb6wjfP +C+hWoonEOjGK9jzPj1fwKMwHbYUCgYEA4MHN8RkXP6AgdLleKB+/AE613Ozmc54F +I0uL5xXUny+udhYcflsF357ztj3vWZzWOhVvp84O7puJu9J84wQQtG/v6kD+pIA7 +TKJAcJ8eL/gZgbEAEbw1fBZLFkpis6sD06tDiCRx3qWez3pr/UiPLw4ldIsNnyZu +RfqL8LjspzUCgYEAqDfl+IlYBOVoDtkR+Kf4p7zi0IVpqp43rffl18ixDqc7XujD +dmBFdxnSiDEzvEbsg2W/cNGryLSW98XhC9UB5d0rwzz/ZYdh0Ww8CFxQX8pIIZpU +i1rIWU10+ajud5ynlIS55IP2mxIi38GrtZ4NXpl9JxsRBZ16hiGneV7msCUCgYEA +iZAVLGve/uJS+x45svU7QkTqqr2Uj084ddHXXyjNamEUttHxvqSXg7cWE4T06jRE +KmjUXjCQkxo/wuWh6pCiaM3aWdZOBQE3SkBEkKnTICkJ84cSM8iuM7SxYINGxhD1 +1qgMPVeRGmTkPrj/P5cZjANsiVhSjgZHmC0xWbTVo8ECgYBh6R7N4B/xYH+UgV1U +qyzFjLjsBQoIuhx5JpHCXmzZV3B/TV9mi3D1hcl4oS6nnww/j5WKggdUq6542975 +iVsnw8OFCSpU/R9P6crPwDEkdZt9miv1BOdh95AqxpmLEJrfXuVNYur0yur7Yv+E +AVMgdBof4DtucX+v+sLxuHAppA== +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDSzCCAjOgAwIBAgIUM9tef51ph3HVoJqF1h7ch5XmOQowDQYJKoZIhvcNAQEL +BQAwNTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxETAPBgNVBAoM +CFB5YnJpY2tzMB4XDTIxMDgxMzE0NDQyOFoXDTQ4MTIyOTE0NDQyOFowNTELMAkG +A1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxETAPBgNVBAoMCFB5YnJpY2tz +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy6U5KOfJzXdnBKvDGgdV +r+Ma3p/8YjaJg8H+OECvY2Rs7x6p1wik0NCrC8bWbezXuj1NVnSAN23+v51c1VIW +rj9PgxfLem0x+Bkpby1yhhVunZApEzMPIIp4e9z5GS/jmkL2jFSXeNglvWz4RTBe +rV7C6uutTm18ExPrwaKUopuBBM8vlSlwfAR7zGL3zHFoiYoSiYQjy2RPaYhwTt8g +cjLNHy3ulEPzAB1xVaa8Z1yfj9pjHubP3NARHEytXXjcvZ4CnFLyhgnH409fcbZE +uCB0tHLM9H5Gfvd7Ro05vXgiwDpsrykZY4TjBWSleCdcOFnt2Nm/sRzfcKbLpJdv +iQIDAQABo1MwUTAdBgNVHQ4EFgQUm+DZlQWXqhjtS6ATH3Ns0fzchs0wHwYDVR0j +BBgwFoAUm+DZlQWXqhjtS6ATH3Ns0fzchs0wDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAQEAlpu1OrX9wb8m2eaZOVumjNna4Xx4pYJG13AdfWx5D4qR +PqbxdUfxnlk3F6pD15ELK1NK94k6/XarScBxDwMexSxKVPPeU5aOG2JRUkWaN4wg +NIYo7xQeoorgcNqnV8pIZMh3IUKnL3i9BFrMlhBIZCb/y0XdmJbvb5P9yU3x+z9C +w1hPKjyhEIYIGnjTh1Rj5feciQSxcouDVOZSgi2iscGePK5734rAzO3zZzbi4lbs +R2rK2RVrX/s51njLgCHpwPonfEqy8TrMSJvCtH0OG7wQCNbnFOakfzvChpWI/1bI +Qlai98tuzS49ekXd/dL6lXZi8z7c9Necj21mMgePKQ== +-----END CERTIFICATE----- From 7ddeed58b72f5d40143d458fddf902263808bf20 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 13 Aug 2021 10:35:13 -0500 Subject: [PATCH 2/2] firmware: validate checksum while flashing Since we are using the checksum message for throttling flash speed anyway, we can validate the checksum during the flash process to catch errors earlier. Issue: https://github.com/pybricks/support/issues/433 --- src/firmware/sagas.test.ts | 3 ++- src/firmware/sagas.ts | 44 +++++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/firmware/sagas.test.ts b/src/firmware/sagas.test.ts index df3a3e94..5ca74b2b 100644 --- a/src/firmware/sagas.test.ts +++ b/src/firmware/sagas.test.ts @@ -1386,6 +1386,7 @@ describe('flashFirmware', () => { const dummyPayload = new ArrayBuffer(0); let id = 2; + for (let count = 1, offset = 0; ; count++, offset += 14) { action = await saga.take(); expect(action).toEqual( @@ -1413,7 +1414,7 @@ describe('flashFirmware', () => { expect(action).toEqual(checksumRequest(++id)); saga.put(didRequest(id)); - saga.put(checksumResponse(0)); + saga.put(checksumResponse(0x9b)); } } diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index b523bde0..792eb1be 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -53,7 +53,7 @@ import { compile, } from '../mpy/actions'; import { RootState } from '../reducers'; -import { defined, maybe } from '../utils'; +import { defined, hex, maybe } from '../utils'; import { fmod, sumComplement32 } from '../utils/math'; import { FailToFinishReasonType, @@ -379,8 +379,16 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { // 14 is "safe" size for all hubs const maxDataSize = MaxProgramFlashSize.get(info.hubType) || 14; + let runningChecksum = 0xff; + for (let count = 1, offset = 0; ; count++) { const payload = firmware.slice(offset, offset + maxDataSize); + + runningChecksum = payload.reduce( + (prev, curr) => prev ^ curr, + runningChecksum, + ); + const programAction = yield* put( programRequest( nextMessageId(), @@ -405,13 +413,33 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { // the hub is not known and could vary by device. if (count % 10 === 0) { const checksumAction = yield* put(checksumRequest(nextMessageId())); - yield* all({ + + const { response } = yield* all({ sent: waitForDidRequest(checksumAction.id), - checksum: waitForResponse( + response: waitForResponse( BootloaderResponseActionType.Checksum, 5000, ), }); + + if (response.checksum !== runningChecksum) { + // istanbul ignore next + if (process.env.NODE_ENV !== 'test') { + console.error( + `checksum: got ${hex(response.checksum, 2)} expected ${hex( + runningChecksum, + 2, + )}`, + ); + } + yield* put( + didFailToFinish( + FailToFinishReasonType.HubError, + HubError.ChecksumMismatch, + ), + ); + yield* disconnectAndCancel(); + } } } @@ -430,14 +458,14 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { yield* disconnectAndCancel(); } - const checksum = firmware.reduce((prev, curr) => prev ^ curr, 0xff); - if (flash.checksum !== checksum) { + if (flash.checksum !== runningChecksum) { // istanbul ignore next if (process.env.NODE_ENV !== 'test') { console.error( - 'checksum:', - flash.checksum.toString(16).padStart(2, '0').padStart(4, '0x'), - checksum.toString(16).padStart(2, '0').padStart(4, '0x'), + `final checksum: got ${hex(flash.checksum, 2)} expected ${hex( + runningChecksum, + 2, + )}`, ); } yield* put(