diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a6c40e6..59842dbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ ## [Unreleased] +### Fixed +- Fix flashing firmware on Android [support#403]. + +### Changed +- Checksum is now validated as firmware flash progresses instead of just at the + end [support#433]. ## [1.1.0-beta.3] - 2021-07-20 @@ -46,13 +52,15 @@ Prerelease changes are documented at [support#48]. - + [issue#470]: https://github.com/pybricks/pybricks-code/issues/470 [issue#471]: https://github.com/pybricks/pybricks-code/issues/471 [issue#472]: https://github.com/pybricks/pybricks-code/issues/472 [support#48]: https://github.com/pybricks/support/issues/48 [support#375]: https://github.com/pybricks/support/issues/375 [support#378]: https://github.com/pybricks/support/issues/378 +[support#403]: https://github.com/pybricks/support/issues/403 +[support#433]: https://github.com/pybricks/support/issues/433 [v3.0.0]: https://github.com/pybricks/pybricks-micropython/blob/master/CHANGELOG.md#300---2021-06-08 [v3.1.0a1]: https://github.com/pybricks/pybricks-micropython/blob/master/CHANGELOG.md#310a1---2021-06-23 [v3.1.0a2]: https://github.com/pybricks/pybricks-micropython/blob/master/CHANGELOG.md#310a2---2021-07-06 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 cba8ca3d..dda1f939 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 { isAndroid } from '../utils/os'; import { @@ -381,8 +381,16 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { const maxDataSize = (!isAndroid() && 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(), @@ -407,13 +415,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(); + } } } @@ -432,14 +460,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( 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-----