├── .github └── workflows │ ├── build.yml │ └── pr-url.yml ├── .gitignore ├── .gitmodules ├── .prettierignore ├── .prettierrc.json ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── bin └── print-ci-env.js ├── ci-build.sh ├── deployment.js ├── package-lock.json ├── package.json ├── src ├── Makefile ├── board │ ├── accelerometer.ts │ ├── audio │ │ ├── built-in-sounds.ts │ │ ├── index.ts │ │ ├── musical-progressions.ts │ │ ├── sound-emoji-synthesizer.ts │ │ ├── sound-expressions.ts │ │ ├── sound-synthesizer-effects.ts │ │ └── sound-synthesizer.ts │ ├── buttons.ts │ ├── compass.ts │ ├── constants.ts │ ├── conversions.ts │ ├── data-logging.test.ts │ ├── data-logging.ts │ ├── display.ts │ ├── fs.ts │ ├── index.ts │ ├── microphone.ts │ ├── pins.ts │ ├── radio.test.ts │ ├── radio.ts │ ├── state.ts │ ├── svg.d.ts │ ├── util.ts │ └── wasm.ts ├── demo.html ├── drv_radio.c ├── environment.ts ├── examples │ ├── accelerometer.py │ ├── audio.py │ ├── background.py │ ├── buttons.py │ ├── compass.py │ ├── data_logging.py │ ├── display.py │ ├── inline_assembler.py │ ├── microphone.py │ ├── music.py │ ├── pin_logo.py │ ├── radio.py │ ├── random.py │ ├── sensors.py │ ├── sound_effects_builtin.py │ ├── sound_effects_user.py │ ├── speech.py │ ├── stack_size.py │ └── volume.py ├── flags.ts ├── index.html ├── jshal.d.ts ├── jshal.h ├── jshal.js ├── main.c ├── microbit-drawing.svg ├── microbitfs.c ├── microbithal_js.c ├── microbithal_js.h ├── modmachine.c ├── mpconfigport.h ├── mphalport.c ├── mphalport.h ├── simulator.html ├── simulator.ts ├── sw.ts └── term.js └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | release: 5 | types: [created] 6 | push: 7 | branches: 8 | - "**" 9 | 10 | # This is conservative: ideally we'd include branch and stage in this key 11 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#concurrency 12 | concurrency: deploy-python-simulator 13 | 14 | jobs: 15 | build: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: read 20 | env: 21 | AWS_DEFAULT_REGION: eu-west-1 22 | PRODUCTION_CLOUDFRONT_DISTRIBUTION_ID: E15FPP46STH15O 23 | STAGING_CLOUDFRONT_DISTRIBUTION_ID: E15FPP46STH15O 24 | REVIEW_CLOUDFRONT_DISTRIBUTION_ID: E2DW5F7PA9W7JD 25 | 26 | steps: 27 | # Note: This workflow will not run on forks without modification; we're open to making steps 28 | # that rely on our deployment infrastructure conditional. Please open an issue. 29 | - uses: actions/checkout@v4 30 | - name: Configure node 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 20.x 34 | cache: "npm" 35 | registry-url: "https://npm.pkg.github.com" 36 | - run: npm ci 37 | env: 38 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | - run: npm install --no-save @microbit-foundation/website-deploy-aws@0.3.0 @microbit-foundation/website-deploy-aws-config@0.7.1 @microbit-foundation/circleci-npm-package-versioner@1 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | - run: node ./bin/print-ci-env.js >> $GITHUB_ENV 43 | - run: npm run ci:update-version 44 | - run: ./ci-build.sh 45 | - run: npm run deploy 46 | env: 47 | AWS_ACCESS_KEY_ID: ${{ secrets.WEB_DEPLOY_AWS_ACCESS_KEY_ID }} 48 | AWS_SECRET_ACCESS_KEY: ${{ secrets.WEB_DEPLOY_AWS_SECRET_ACCESS_KEY }} 49 | - run: npm run invalidate 50 | env: 51 | AWS_ACCESS_KEY_ID: ${{ secrets.WEB_DEPLOY_AWS_ACCESS_KEY_ID }} 52 | AWS_SECRET_ACCESS_KEY: ${{ secrets.WEB_DEPLOY_AWS_SECRET_ACCESS_KEY }} 53 | -------------------------------------------------------------------------------- /.github/workflows/pr-url.yml: -------------------------------------------------------------------------------- 1 | name: "pr-url" 2 | on: 3 | pull_request: 4 | types: [opened] 5 | jobs: 6 | pr-url: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: microbit-foundation/action-pr-url-template@v0.1.2 10 | with: 11 | uri-template: "https://review-python-simulator.usermbit.org/{branch}/" 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/build 2 | build/ 3 | node_modules 4 | /.vscode 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/micropython-microbit-v2"] 2 | path = lib/micropython-microbit-v2 3 | url = https://github.com/microbit-foundation/micropython-microbit-v2.git 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | term.js 3 | src/build 4 | lib 5 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for looking here! We'd love your help. The micro:bit project is only 2 | possible through contributions of companies and individuals around the world. 3 | 4 | This project is managed on GitHub, and the best way to contribute is to jump in 5 | and fix/file issues. 6 | 7 | https://github.com/microbit-foundation/micropython-microbit-v2-simulator 8 | 9 | Significant features are best discussed first to make sure everyone is agreed 10 | on the best direction. 11 | 12 | The project uses [Prettier](https://prettier.io/) to format TypeScript code so 13 | please check out their documentation and set it up with your code editor. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Damien P. George, Micro:bit Educational Foundation 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC = src 2 | BUILD = build 3 | 4 | all: build dist 5 | 6 | build: 7 | $(MAKE) -C src 8 | 9 | dist: build 10 | mkdir -p $(BUILD)/build 11 | cp -r $(SRC)/*.html $(SRC)/term.js src/examples $(SRC)/build/sw.js $(BUILD) 12 | cp $(SRC)/build/firmware.js $(SRC)/build/simulator.js $(SRC)/build/firmware.wasm $(BUILD)/build/ 13 | 14 | watch: dist 15 | fswatch -o -e src/build src | while read _; do $(MAKE) dist; done 16 | 17 | clean: 18 | $(MAKE) -C src clean 19 | rm -rf $(BUILD) 20 | 21 | .PHONY: build dist watch clean all 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython-micro:bit simulator 2 | 3 | ## Try it out 4 | 5 | To try the simulator use the [micro:bit Python Editor](https://python.microbit.org). 6 | 7 | The rest of this page explains how to embed the simulator in an application 8 | and develop the simulator software. 9 | 10 | ## License 11 | 12 | This software is under the MIT open source license. 13 | 14 | [SPDX-License-Identifier: MIT](LICENSE) 15 | 16 | ## Embedding 17 | 18 | The simulator is designed to be embedded into other web applications 19 | as an iframe. The embedding API and URLs are not yet stable and are 20 | subject to change. If you are interested in embedding the simulator 21 | in your application please [get in touch](mailto:support@microbit.org). 22 | 23 | The URL to embed is https://python-simulator.usermbit.org/v/0.1/simulator.html. 24 | You can embed this in your application directly. We would love to hear about 25 | your use of the simulator so please open an issue to get in touch. 26 | 27 | The iframe provides the micro:bit board user interface and some limited 28 | interactions. It does not provide a terminal for serial/the REPL or any UI to change the board sensor state. 29 | 30 | A value for a brand color can be passed to the simulator via a query 31 | string and is used to style the play button. E.g., https://python-simulator.usermbit.org/v/0.1/simulator.html?color=blue 32 | 33 | [demo.html](./src/demo.html) is an example of embedding the simulator. 34 | It connects the iframe to a terminal and provides a simple interface for 35 | sensors. 36 | 37 | The following sections document the messages supported by the iframe embed 38 | via postMessage. 39 | 40 | ### Messages sent to parent window from iframe 41 | 42 | These messages are sent as the result of user interactions or actions taken 43 | by the running program. The simulator starts stopped with no program flashed. 44 | 45 | 46 | 47 | 48 | 52 | 53 | 83 | 108 | 120 | 133 | 147 |
Kind 49 | Example 50 | Description 51 |
ready 54 | 55 | 56 | ```javascript 57 | { 58 | "kind": "ready", 59 | "state": { 60 | "lightLevel": { 61 | "id": "lightLevel", 62 | "type": "range", 63 | "min": 0, 64 | "max": 255 65 | }, 66 | "soundLevel": { 67 | "id": "soundLevel", 68 | "type": "range", 69 | "min": 0, 70 | "max": 255 71 | // Microphone sensor only: 72 | "lowThreshold": 50, 73 | "highThreshold": 150 74 | } 75 | // Full state continues here. 76 | } 77 | } 78 | ``` 79 | 80 | Sent when the simulator is ready for input. Includes a description of the available sensors. 81 | 82 |
sensor_change 84 | 85 | 86 | ```javascript 87 | { 88 | "kind": "state_change", 89 | "change": { 90 | "soundLevel": { 91 | "id": "soundLevel", 92 | "type": "range", 93 | "min": 0, 94 | "max": 255 95 | // Microphone sensor only: 96 | "lowThreshold": 50, 97 | "highThreshold": 150 98 | } 99 | // Optionally, further keys here. 100 | } 101 | ] 102 | } 103 | ``` 104 | 105 | Sent when the simulator state changes. The keys are a subset of the original state. The values are always sent in full. 106 | 107 |
request_flash 109 | 110 | 111 | ```javascript 112 | { 113 | "kind": "request_flash", 114 | } 115 | ``` 116 | 117 | Sent when the user requests the simulator starts. The embedder should flash the latest code via the flash message. 118 | 119 |
serial_output 121 | 122 | 123 | ```javascript 124 | { 125 | "kind": "serial_output", 126 | "data": "text" 127 | } 128 | ``` 129 | 130 | Serial output suitable for a terminal or other use. 131 | 132 |
radio_output 134 | 135 | 136 | ```javascript 137 | { 138 | "kind": "radio_output", 139 | "data": new Uint8Array([]) 140 | } 141 | ``` 142 | 143 | Radio output (sent from the user's program) as bytes. 144 | If you send string data from the program then it will be prepended with the three bytes 0x01, 0x00, 0x01. 145 | 146 |
internal_error 148 | 149 | 150 | ```javascript 151 | { 152 | "kind": "internal_error", 153 | "error": new Error() 154 | } 155 | ``` 156 | 157 | A debug message sent for internal (unexpected) errors thrown by the simulator. Suitable for application-level logging. Please raise issues in this project as these indicate a bug in the simulator. 158 | 159 |
160 | 161 | ### Messages you can send to the iframe from the embedding app 162 | 163 | 164 | 165 | 166 | 170 | 171 | 188 | 198 | 199 | 200 | 210 | 211 | 212 | 222 | 223 | 224 | 234 | 235 | 236 | 248 | 262 |
Kind 167 | Example 168 | Description 169 |
flash 172 | 173 | 174 | ```javascript 175 | { 176 | "kind": "flash", 177 | "filesystem": { 178 | "main.py": 179 | new TextEncoder() 180 | .encode("# your program here") 181 | } 182 | } 183 | ``` 184 | 185 | Update the micro:bit filesystem and restart the program. You must send this in response to the request_flash message. 186 | 187 |
stop 189 | 190 | 191 | ```javascript 192 | { 193 | "kind": "stop" 194 | } 195 | ``` 196 | 197 | Stop the program.
reset 201 | 202 | 203 | ```javascript 204 | { 205 | "kind": "reset" 206 | } 207 | ``` 208 | 209 | Reset the program.
mute 213 | 214 | 215 | ```javascript 216 | { 217 | "kind": "mute" 218 | } 219 | ``` 220 | 221 | Mute the simulator.
unmute 225 | 226 | 227 | ```javascript 228 | { 229 | "kind": "unmute" 230 | } 231 | ``` 232 | 233 | Unmute the simulator.
serial_input 237 | 238 | 239 | ```javascript 240 | { 241 | "kind": "serial_input", 242 | "data": "text" 243 | } 244 | ``` 245 | 246 | Serial input. If the REPL is active it will echo this text via serial_write. 247 |
sensor_set 249 | 250 | 251 | ```javascript 252 | { 253 | "kind": "set_value", 254 | "id": "lightLevel", 255 | "value": 255 256 | } 257 | ``` 258 | 259 | Set a sensor, button or pin value. The sensor, button or pin is identified by the top-level key in the state. Buttons and pins (touch state) have 0 and 1 values. In future, analog values will be supported for pins. 260 | 261 |
radio_input 263 | 264 | 265 | ```javascript 266 | { 267 | "kind": "radio_input", 268 | "data": new Uint8Array([]) 269 | } 270 | ``` 271 | 272 | Radio input (received by the user's program as if sent from another micro:bit) as bytes. 273 | If you want to send string data then prepend the byte array with the three bytes 0x01, 0x00, 0x01. 274 | Otherwise, the user will need to use radio.receive_bytes or radio.receive_full. The input is assumed to be sent to the currently configured radio group. 275 | 276 |
277 | 278 | ## Developing the simulator 279 | 280 | ### Build steps 281 | 282 | The simulator is a variant of the MicroPython codal_port which is compiled with 283 | Emscripten. It provides a simulated micro:bit (including REPL) in the browser. 284 | 285 | To build, first fetch the submodules (don't use recursive fetch): 286 | 287 | $ git submodule update --init lib/micropython-microbit-v2 288 | $ git -C lib/micropython-microbit-v2 submodule update --init lib/micropython 289 | 290 | Then run (from this top-level directory): 291 | 292 | $ make 293 | 294 | Once it is built the pages in build/ need to be served, e.g. via: 295 | 296 | $ npx serve build 297 | 298 | View at http://localhost:3000/demo.html 299 | 300 | $ (cd build && python -m http.server) 301 | 302 | View at http://localhost:8000/demo.html 303 | 304 | ### Branch deployments 305 | 306 | There is a CloudFlare pages based build for development purposes only. Do not 307 | embed the simulator via this URL as it may be removed at any time. CloudFlare's 308 | GitHub integration will comment on PRs with deployment details. 309 | 310 | Branches in this repository are also deployed via CircleCI to https://review-python-simulator.usermbit.org/{branchName}/. This requires the user pushing code to have 311 | permissions for the relevant Micro:bit Educational Foundation infrastructure. 312 | 313 | Similarly, the main branch is deployed to https://python-simulator.usermbit.org/staging/. 314 | 315 | Tagged releases with a `v` prefix are deployed to https://python-simulator.usermbit.org/v/{number}/ 316 | 317 | ### Upgrading micropython-microbit-v2 318 | 319 | 1. Update the lib/micropython-microbit-v2 to the relevant hash. Make sure that its lib/micropython submodule is updated (see checkout instructions above). 320 | 2. Review the full diff for micropython-microbit-v2. In particular, note changes to: 321 | 1. main.c, src/Makefile and mpconfigport.h all which have simulator versions that may need updates 322 | 2. the HAL, which may require implementing in the simulator 323 | 3. the filesystem, which has a JavaScript implementation. 324 | 325 | ### Web Assembly debugging 326 | 327 | Steps for WASM debugging in Chrome: 328 | 329 | - Add the source folder in dev tools 330 | - Install the C/C++ debug extension: https://helpgoo.gle/wasm-debugging-extension 331 | - Enable "WebAssembly Debugging: Enable DWARF support" in DevTools Experiments 332 | - DEBUG=1 make 333 | 334 | ## Code of Conduct 335 | 336 | Trust, partnership, simplicity and passion are our core values we live and 337 | breathe in our daily work life and within our projects. Our open-source 338 | projects are no exception. We have an active community which spans the globe 339 | and we welcome and encourage participation and contributions to our projects 340 | by everyone. We work to foster a positive, open, inclusive and supportive 341 | environment and trust that our community respects the micro:bit code of 342 | conduct. Please see our [code of conduct](https://microbit.org/safeguarding/) 343 | which outlines our expectations for all those that participate in our 344 | community and details on how to report any concerns and what would happen 345 | should breaches occur. 346 | -------------------------------------------------------------------------------- /bin/print-ci-env.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const ref = process.env.GITHUB_REF; 3 | let stage; 4 | if (ref === "refs/heads/main") { 5 | stage = "STAGING"; 6 | } else if (ref.startsWith("refs/tags/v")) { 7 | stage = "PRODUCTION"; 8 | } else { 9 | stage = "REVIEW"; 10 | } 11 | 12 | console.log(`STAGE=${stage}`); 13 | -------------------------------------------------------------------------------- /ci-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euxo pipefail 3 | 4 | # Submodules 5 | git submodule update --init lib/micropython-microbit-v2 6 | (cd lib/micropython-microbit-v2 && git submodule update --init lib/micropython) 7 | 8 | # Emscripten 9 | VERSION="3.1.25" 10 | export PYTHON=python3.7 # Needed by Emscripten in Netlify's build image. 11 | git clone https://github.com/emscripten-core/emsdk.git -b $VERSION ~/.emsdk 12 | ~/.emsdk/emsdk install $VERSION 13 | ~/.emsdk/emsdk activate $VERSION 14 | source ~/.emsdk/emsdk_env.sh 15 | 16 | npm run test && npm run build 17 | -------------------------------------------------------------------------------- /deployment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2021, Micro:bit Educational Foundation and contributors 3 | * 4 | * SPDX-License-Identifier: MIT 5 | */ 6 | const { 7 | createDeploymentDetailsFromOptions, 8 | } = require("@microbit-foundation/website-deploy-aws-config"); 9 | 10 | const { s3Config } = createDeploymentDetailsFromOptions({ 11 | production: { 12 | bucket: "python-simulator.usermbit.org", 13 | mode: "patch", 14 | }, 15 | staging: { 16 | bucket: "python-simulator.usermbit.org", 17 | prefix: "staging", 18 | }, 19 | review: { 20 | bucket: "review-python-simulator.usermbit.org", 21 | mode: "branch-prefix", 22 | }, 23 | }); 24 | module.exports = { 25 | ...s3Config, 26 | region: "eu-west-1", 27 | removeNonexistentObjects: true, 28 | enableS3StaticWebsiteHosting: true, 29 | errorDocumentKey: "index.html", 30 | redirects: [], 31 | params: { 32 | "**/*": { 33 | CacheControl: "public, max-age=0, must-revalidate", 34 | }, 35 | // We need hashes in the filenames to enable these 36 | // "**/**/!(sw).js": { CacheControl: "public, max-age=31536000, immutable" }, 37 | // "**/**.wasm": { CacheControl: "public, max-age=31536000, immutable" }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@microbit-foundation/microbit-micropython-v2-simulator", 3 | "version": "0.1.0", 4 | "description": "Experimental Wasm-based MicroPython micro:bit v2 simulator", 5 | "main": "index.js", 6 | "directories": { 7 | "lib": "lib" 8 | }, 9 | "scripts": { 10 | "build": "make", 11 | "test": "vitest", 12 | "ci:update-version": "update-ci-version", 13 | "deploy": "website-deploy-aws", 14 | "invalidate": "aws cloudfront create-invalidation --distribution-id $(printenv ${STAGE}_CLOUDFRONT_DISTRIBUTION_ID) --paths \"/*\"" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/microbit-foundation/micropython-microbit-v2-simulator.git" 19 | }, 20 | "author": "", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/microbit-foundation/micropython-microbit-v2-simulator/issues" 24 | }, 25 | "homepage": "https://github.com/microbit-foundation/micropython-microbit-v2-simulator#readme", 26 | "devDependencies": { 27 | "esbuild": "^0.14.49", 28 | "prettier": "2.6.0", 29 | "vitest": "^0.22.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile to build WASM and simulator JS. 2 | 3 | # Build upon the codal_port code. 4 | CODAL_PORT = $(abspath ../lib/micropython-microbit-v2/src/codal_port) 5 | CODAL_APP = $(abspath ../lib/micropython-microbit-v2/src/codal_app) 6 | 7 | MICROPY_ROM_TEXT_COMPRESSION ?= 1 8 | FROZEN_MANIFEST ?= $(CODAL_PORT)/manifest.py 9 | 10 | include ../lib/micropython-microbit-v2/lib/micropython/py/mkenv.mk 11 | -include mpconfigport.mk 12 | 13 | # QSTR definitions (must come before including py.mk). 14 | QSTR_DEFS = $(CODAL_PORT)/qstrdefsport.h 15 | 16 | # Include py core make definitions. 17 | include $(TOP)/py/py.mk 18 | 19 | CC = emcc 20 | LD = emcc 21 | 22 | MP_VER_FILE = $(HEADER_BUILD)/mpversion.h 23 | MBIT_VER_FILE = $(HEADER_BUILD)/microbitversion.h 24 | 25 | LOCAL_LIB_DIR = ../lib/micropython-microbit-v2/lib 26 | 27 | INC += -I. 28 | INC += -I$(CODAL_PORT) 29 | INC += -I$(CODAL_APP) 30 | INC += -I$(LOCAL_LIB_DIR) 31 | INC += -I$(TOP) 32 | INC += -I$(BUILD) 33 | 34 | # Compiler settings. 35 | CWARN += -Wall -Wpointer-arith -Wuninitialized -Wno-array-bounds 36 | CFLAGS += $(INC) $(CWARN) -std=c99 -funsigned-char $(CFLAGS_MOD) $(CFLAGS_ARCH) $(COPT) $(CFLAGS_EXTRA) 37 | 38 | # Debugging/Optimization 39 | ifdef DEBUG 40 | COPT += -O3 41 | CFLAGS += -g 42 | else 43 | COPT += -O3 -DNDEBUG 44 | endif 45 | 46 | JSFLAGS += -s ASYNCIFY 47 | # We can hit lower values due to user stack use. See stack_size.py example. 48 | JSFLAGS += -s ASYNCIFY_STACK_SIZE=262144 49 | JSFLAGS += -s EXIT_RUNTIME 50 | JSFLAGS += -s MODULARIZE=1 51 | JSFLAGS += -s EXPORT_NAME=createModule 52 | JSFLAGS += -s EXPORTED_FUNCTIONS="['_mp_js_main','_microbit_hal_audio_ready_callback','_microbit_hal_audio_speech_ready_callback','_microbit_hal_gesture_callback','_microbit_hal_level_detector_callback','_microbit_radio_rx_buffer','_mp_js_force_stop','_mp_js_request_stop']" 53 | JSFLAGS += -s EXPORTED_RUNTIME_METHODS="['ccall', 'cwrap']" --js-library jshal.js 54 | 55 | ifdef DEBUG 56 | JSFLAGS += -g 57 | endif 58 | 59 | SRC_C += \ 60 | drv_radio.c \ 61 | microbitfs.c \ 62 | microbithal_js.c \ 63 | main.c \ 64 | mphalport.c \ 65 | modmachine.c \ 66 | 67 | SRC_C += $(addprefix $(CODAL_PORT)/, \ 68 | drv_display.c \ 69 | drv_image.c \ 70 | drv_softtimer.c \ 71 | drv_system.c \ 72 | help.c \ 73 | iters.c \ 74 | microbit_accelerometer.c \ 75 | microbit_button.c \ 76 | microbit_compass.c \ 77 | microbit_display.c \ 78 | microbit_i2c.c \ 79 | microbit_image.c \ 80 | microbit_constimage.c \ 81 | microbit_microphone.c \ 82 | microbit_pin.c \ 83 | microbit_pinaudio.c \ 84 | microbit_pinmode.c \ 85 | microbit_sound.c \ 86 | microbit_soundeffect.c \ 87 | microbit_soundevent.c \ 88 | microbit_speaker.c \ 89 | microbit_spi.c \ 90 | microbit_uart.c \ 91 | modantigravity.c \ 92 | modaudio.c \ 93 | modlog.c \ 94 | modlove.c \ 95 | modmachine.c \ 96 | modmicrobit.c \ 97 | modmusic.c \ 98 | modmusictunes.c \ 99 | modos.c \ 100 | modpower.c \ 101 | modradio.c \ 102 | modspeech.c \ 103 | modthis.c \ 104 | modutime.c \ 105 | mphalport.c \ 106 | ) 107 | 108 | SRC_C += \ 109 | shared/readline/readline.c \ 110 | shared/runtime/interrupt_char.c \ 111 | shared/runtime/pyexec.c \ 112 | shared/runtime/stdout_helpers.c \ 113 | $(abspath $(LOCAL_LIB_DIR)/sam/main.c) \ 114 | $(abspath $(LOCAL_LIB_DIR)/sam/reciter.c) \ 115 | $(abspath $(LOCAL_LIB_DIR)/sam/render.c) \ 116 | $(abspath $(LOCAL_LIB_DIR)/sam/sam.c) \ 117 | $(abspath $(LOCAL_LIB_DIR)/sam/debug.c) \ 118 | 119 | SRC_O += \ 120 | lib/utils/gchelper_m3.o \ 121 | 122 | OBJ = $(PY_O) 123 | OBJ += $(addprefix $(BUILD)/, $(SRC_C:.c=.o)) 124 | OBJ += $(addprefix $(BUILD)/, $(LIB_SRC_C:.c=.o)) 125 | 126 | # List of sources for qstr extraction. 127 | SRC_QSTR += $(SRC_C) $(LIB_SRC_C) 128 | # Append any auto-generated sources that are needed by sources listed in. 129 | # SRC_QSTR 130 | SRC_QSTR_AUTO_DEPS += 131 | QSTR_GLOBAL_REQUIREMENTS += $(MBIT_VER_FILE) 132 | 133 | # Top-level rule. 134 | all: $(MBIT_VER_FILE) $(BUILD)/micropython.js 135 | 136 | # Rule to build header with micro:bit specific version information. 137 | # Also rebuild MicroPython version header in correct directory to pick up git hash. 138 | $(MBIT_VER_FILE): FORCE 139 | $(Q)mkdir -p $(HEADER_BUILD) 140 | (cd $(TOP) && $(PYTHON) py/makeversionhdr.py $(abspath $(MP_VER_FILE))) 141 | $(PYTHON) $(TOP)/py/makeversionhdr.py $(MBIT_VER_FILE).pre 142 | $(CAT) $(MBIT_VER_FILE).pre | $(SED) s/MICROPY_/MICROBIT_/ > $(MBIT_VER_FILE) 143 | 144 | $(BUILD)/micropython.js: $(OBJ) jshal.js simulator-js 145 | $(ECHO) "LINK $(BUILD)/firmware.js" 146 | $(Q)emcc $(LDFLAGS) -o $(BUILD)/firmware.js $(OBJ) $(JSFLAGS) 147 | 148 | simulator-js: 149 | npx esbuild '--define:process.env.STAGE="$(STAGE)"' ./simulator.ts --bundle --outfile=$(BUILD)/simulator.js --loader:.svg=text 150 | npx esbuild --define:process.env.VERSION="$$(node -e 'process.stdout.write(`"` + require("../package.json").version + `"`)')" ./sw.ts --bundle --outfile=$(BUILD)/sw.js 151 | 152 | include $(TOP)/py/mkrules.mk 153 | 154 | .PHONY: simulator-js -------------------------------------------------------------------------------- /src/board/accelerometer.ts: -------------------------------------------------------------------------------- 1 | import { convertAccelerometerStringToNumber } from "./conversions"; 2 | import { EnumSensor, RangeSensor, State } from "./state"; 3 | import { clamp } from "./util"; 4 | 5 | type StateKeys = 6 | | "accelerometerX" 7 | | "accelerometerY" 8 | | "accelerometerZ" 9 | | "gesture"; 10 | 11 | type GestureCallback = (v: number) => void; 12 | 13 | export class Accelerometer { 14 | state: Pick< 15 | State, 16 | "accelerometerX" | "accelerometerY" | "accelerometerZ" | "gesture" 17 | >; 18 | 19 | private gestureCallback: GestureCallback | undefined; 20 | 21 | constructor(private onChange: (changes: Partial) => void) { 22 | const min = -2000; 23 | const max = 2000; 24 | this.state = { 25 | accelerometerX: new RangeSensor("accelerometerX", min, max, 0, "mg"), 26 | accelerometerY: new RangeSensor("accelerometerY", min, max, 0, "mg"), 27 | accelerometerZ: new RangeSensor("accelerometerZ", min, max, 0, "mg"), 28 | gesture: new EnumSensor( 29 | "gesture", 30 | [ 31 | "none", 32 | "up", 33 | "down", 34 | "left", 35 | "right", 36 | "face up", 37 | "face down", 38 | "freefall", 39 | "3g", 40 | "6g", 41 | "8g", 42 | "shake", 43 | ], 44 | "none" 45 | ), 46 | }; 47 | } 48 | 49 | setValue(id: StateKeys, value: any) { 50 | this.state[id].setValue(value); 51 | if (id === "gesture" && this.gestureCallback) { 52 | this.gestureCallback( 53 | convertAccelerometerStringToNumber(this.state.gesture.value) 54 | ); 55 | } 56 | } 57 | 58 | setRange(range: number) { 59 | const min = -1000 * range; 60 | const max = +1000 * range; 61 | const { accelerometerX, accelerometerY, accelerometerZ } = this.state; 62 | for (const sensor of [accelerometerX, accelerometerY, accelerometerZ]) { 63 | sensor.value = clamp(sensor.value, min, max); 64 | sensor.min = min; 65 | sensor.max = max; 66 | } 67 | this.onChange({ 68 | accelerometerX, 69 | accelerometerY, 70 | accelerometerZ, 71 | }); 72 | } 73 | 74 | initializeCallbacks(gestureCallback: GestureCallback) { 75 | this.gestureCallback = gestureCallback; 76 | } 77 | 78 | boardStopped() {} 79 | } 80 | -------------------------------------------------------------------------------- /src/board/audio/built-in-sounds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert named sounds (e.g. "giggle") to the corresponding expression. 3 | * 4 | * Other text is returned unchanged on the assumtion it's an expression. 5 | * 6 | * @param expression The expression name or expression. 7 | * @returns The expression. 8 | */ 9 | export function replaceBuiltinSound(expression: string) { 10 | switch (expression) { 11 | case "giggle": 12 | return "010230988019008440044008881023001601003300240000000000000000000000000000,110232570087411440044008880352005901003300010000000000000000010000000000,310232729021105440288908880091006300000000240700020000000000003000000000,310232729010205440288908880091006300000000240700020000000000003000000000,310232729011405440288908880091006300000000240700020000000000003000000000"; 13 | case "happy": 14 | return "010231992066911440044008880262002800001800020500000000000000010000000000,002322129029508440240408880000000400022400110000000000000000007500000000,000002129029509440240408880145000400022400110000000000000000007500000000"; 15 | case "hello": 16 | return "310230673019702440118708881023012800000000240000000000000000000000000000,300001064001602440098108880000012800000100040000000000000000000000000000,310231064029302440098108881023012800000100040000000000000000000000000000"; 17 | case "mysterious": 18 | return "400002390033100440240408880477000400022400110400000000000000008000000000,405512845385000440044008880000012803010500160000000000000000085000500015"; 19 | case "sad": 20 | return "310232226070801440162408881023012800000100240000000000000000000000000000,310231623093602440093908880000012800000100240000000000000000000000000000"; 21 | case "slide": 22 | return "105202325022302440240408881023012801020000110400000000000000010000000000,010232520091002440044008881023012801022400110400000000000000010000000000"; 23 | case "soaring": 24 | return "210234009530905440599908881023002202000400020250000000000000020000000000,402233727273014440044008880000003101024400030000000000000000000000000000"; 25 | case "spring": 26 | return "306590037116312440058708880807003400000000240000000000000000050000000000,010230037116313440058708881023003100000000240000000000000000050000000000"; 27 | case "twinkle": 28 | return "010180007672209440075608880855012800000000240000000000000000000000000000"; 29 | case "yawn": 30 | return "200002281133202440150008881023012801024100240400030000000000010000000000,005312520091002440044008880636012801022400110300000000000000010000000000,008220784019008440044008880681001600005500240000000000000000005000000000,004790784019008440044008880298001600000000240000000000000000005000000000,003210784019008440044008880108001600003300080000000000000000005000000000"; 31 | default: 32 | return expression; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/board/audio/index.ts: -------------------------------------------------------------------------------- 1 | import { replaceBuiltinSound } from "./built-in-sounds"; 2 | import { SoundEmojiSynthesizer } from "./sound-emoji-synthesizer"; 3 | import { parseSoundEffects } from "./sound-expressions"; 4 | 5 | declare global { 6 | interface Window { 7 | webkitAudioContext: typeof AudioContext; 8 | } 9 | } 10 | 11 | interface AudioOptions { 12 | defaultAudioCallback: () => void; 13 | speechAudioCallback: () => void; 14 | } 15 | 16 | export class Audio { 17 | private frequency: number = 440; 18 | // You can mute the sim before it's running so we can't immediately write to the muteNode. 19 | private muted: boolean = false; 20 | private context: AudioContext | undefined; 21 | private oscillator: OscillatorNode | undefined; 22 | private volumeNode: GainNode | undefined; 23 | private muteNode: GainNode | undefined; 24 | 25 | default: BufferedAudio | undefined; 26 | speech: BufferedAudio | undefined; 27 | soundExpression: BufferedAudio | undefined; 28 | currentSoundExpressionCallback: undefined | (() => void); 29 | 30 | constructor() {} 31 | 32 | initializeCallbacks({ 33 | defaultAudioCallback, 34 | speechAudioCallback, 35 | }: AudioOptions) { 36 | if (!this.context) { 37 | throw new Error("Context must be pre-created from a user event"); 38 | } 39 | this.muteNode = this.context.createGain(); 40 | this.muteNode.gain.setValueAtTime( 41 | this.muted ? 0 : 1, 42 | this.context.currentTime 43 | ); 44 | this.muteNode.connect(this.context.destination); 45 | this.volumeNode = this.context.createGain(); 46 | this.volumeNode.connect(this.muteNode); 47 | 48 | this.default = new BufferedAudio( 49 | this.context, 50 | this.volumeNode, 51 | defaultAudioCallback 52 | ); 53 | this.speech = new BufferedAudio( 54 | this.context, 55 | this.volumeNode, 56 | speechAudioCallback 57 | ); 58 | this.soundExpression = new BufferedAudio( 59 | this.context, 60 | this.volumeNode, 61 | () => { 62 | if (this.currentSoundExpressionCallback) { 63 | this.currentSoundExpressionCallback(); 64 | } 65 | } 66 | ); 67 | } 68 | 69 | async createAudioContextFromUserInteraction(): Promise { 70 | this.context = 71 | this.context ?? 72 | new (window.AudioContext || window.webkitAudioContext)({ 73 | // The highest rate is the sound expression synth. 74 | sampleRate: 44100, 75 | }); 76 | if (this.context.state === "suspended") { 77 | return this.context.resume(); 78 | } 79 | } 80 | 81 | playSoundExpression(expr: string) { 82 | const soundEffects = parseSoundEffects(replaceBuiltinSound(expr)); 83 | const onDone = () => { 84 | this.stopSoundExpression(); 85 | }; 86 | const synth = new SoundEmojiSynthesizer(0, onDone); 87 | synth.play(soundEffects); 88 | 89 | const callback = () => { 90 | const source = synth.pull(); 91 | if (this.context) { 92 | // Use createBuffer instead of new AudioBuffer to support Safari 14.0. 93 | const target = this.context.createBuffer( 94 | 1, 95 | source.length, 96 | synth.sampleRate 97 | ); 98 | const channel = target.getChannelData(0); 99 | for (let i = 0; i < source.length; i++) { 100 | // Buffer is (0, 1023) we need to map it to (-1, 1) 101 | channel[i] = (source[i] - 512) / 512; 102 | } 103 | this.soundExpression!.writeData(target); 104 | } 105 | }; 106 | this.currentSoundExpressionCallback = callback; 107 | callback(); 108 | } 109 | 110 | stopSoundExpression(): void { 111 | this.currentSoundExpressionCallback = undefined; 112 | } 113 | 114 | isSoundExpressionActive(): boolean { 115 | return !!this.currentSoundExpressionCallback; 116 | } 117 | 118 | mute() { 119 | this.muted = true; 120 | if (this.muteNode) { 121 | this.muteNode.gain.setValueAtTime(0, this.context!.currentTime); 122 | } 123 | } 124 | 125 | unmute() { 126 | this.muted = false; 127 | if (this.muteNode) { 128 | this.muteNode!.gain.setValueAtTime(1, this.context!.currentTime); 129 | } 130 | } 131 | 132 | setVolume(volume: number) { 133 | this.volumeNode!.gain.setValueAtTime( 134 | volume / 255, 135 | this.context!.currentTime 136 | ); 137 | } 138 | 139 | setPeriodUs(periodUs: number) { 140 | // CODAL defaults in this way: 141 | this.frequency = periodUs === 0 ? 6068 : 1000000 / periodUs; 142 | if (this.oscillator) { 143 | this.oscillator.frequency.value = this.frequency; 144 | } 145 | } 146 | 147 | setAmplitudeU10(amplitudeU10: number) { 148 | this.stopOscillator(); 149 | if (amplitudeU10) { 150 | this.oscillator = this.context!.createOscillator(); 151 | this.oscillator.type = "sine"; 152 | this.oscillator.connect(this.volumeNode!); 153 | this.oscillator.frequency.value = this.frequency; 154 | this.oscillator.start(); 155 | } 156 | } 157 | 158 | boardStopped() { 159 | this.stopOscillator(); 160 | this.speech?.dispose(); 161 | this.soundExpression?.dispose(); 162 | this.default?.dispose(); 163 | } 164 | 165 | private stopOscillator() { 166 | if (this.oscillator) { 167 | this.oscillator.stop(); 168 | this.oscillator = undefined; 169 | } 170 | } 171 | } 172 | 173 | class BufferedAudio { 174 | nextStartTime: number = -1; 175 | private sampleRate: number = -1; 176 | 177 | constructor( 178 | private context: AudioContext, 179 | private destination: AudioNode, 180 | private callback: () => void 181 | ) {} 182 | 183 | init(sampleRate: number) { 184 | this.sampleRate = sampleRate; 185 | this.nextStartTime = -1; 186 | } 187 | 188 | createBuffer(length: number) { 189 | // Use createBuffer instead of new AudioBuffer to support Safari 14.0. 190 | return this.context.createBuffer(1, length, this.sampleRate); 191 | } 192 | 193 | writeData(buffer: AudioBuffer) { 194 | // Use createBufferSource instead of new AudioBufferSourceNode to support Safari 14.0. 195 | const source = this.context.createBufferSource(); 196 | source.buffer = buffer; 197 | source.onended = this.callback; 198 | source.connect(this.destination); 199 | const currentTime = this.context.currentTime; 200 | const first = this.nextStartTime < currentTime; 201 | const startTime = first ? currentTime : this.nextStartTime; 202 | this.nextStartTime = startTime + buffer.length / buffer.sampleRate; 203 | // For audio frames, we're frequently out of data. Speech is smooth. 204 | if (first) { 205 | // We're just getting started so buffer another frame. 206 | this.callback(); 207 | } 208 | source.start(startTime); 209 | } 210 | 211 | dispose() { 212 | // Prevent calls into WASM when the buffer nodes finish. 213 | this.callback = () => {}; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/board/audio/musical-progressions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Microsoft MakeCode's conversion of the CODAL synthesizer. 3 | * Copyright (c) Microsoft Corporation 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * https://github.com/microsoft/pxt/blob/f2476687fe636ec7cbf47e96b22c7acec1978461/pxtsim/sound/musicalProgressions.ts 7 | */ 8 | 9 | export interface Progression { 10 | interval: number[]; 11 | length: number; 12 | } 13 | 14 | // #if CONFIG_ENABLED(JUST_SCALE) 15 | // const float MusicalIntervals.chromaticInterval[] = [1.000000, 1.059463, 1.122462, 1.189207, 1.259921, 1.334840, 1.414214, 1.498307, 1.587401, 1.681793, 1.781797, 1.887749]; 16 | // #else 17 | // const float MusicalIntervals.chromaticInterval[] = [1.000000, 1.0417, 1.1250, 1.2000, 1.2500, 1.3333, 1.4063, 1.5000, 1.6000, 1.6667, 1.8000, 1.8750]; 18 | // #endif 19 | 20 | export const chromaticInterval = [ 21 | 1.0, 1.0417, 1.125, 1.2, 1.25, 1.3333, 1.4063, 1.5, 1.6, 1.6667, 1.8, 1.875, 22 | ]; 23 | 24 | export const majorScaleInterval = [ 25 | chromaticInterval[0], 26 | chromaticInterval[2], 27 | chromaticInterval[4], 28 | chromaticInterval[5], 29 | chromaticInterval[7], 30 | chromaticInterval[9], 31 | chromaticInterval[11], 32 | ]; 33 | export const minorScaleInterval = [ 34 | chromaticInterval[0], 35 | chromaticInterval[2], 36 | chromaticInterval[3], 37 | chromaticInterval[5], 38 | chromaticInterval[7], 39 | chromaticInterval[8], 40 | chromaticInterval[10], 41 | ]; 42 | export const pentatonicScaleInterval = [ 43 | chromaticInterval[0], 44 | chromaticInterval[2], 45 | chromaticInterval[4], 46 | chromaticInterval[7], 47 | chromaticInterval[9], 48 | ]; 49 | export const majorTriadInterval = [ 50 | chromaticInterval[0], 51 | chromaticInterval[4], 52 | chromaticInterval[7], 53 | ]; 54 | export const minorTriadInterval = [ 55 | chromaticInterval[0], 56 | chromaticInterval[3], 57 | chromaticInterval[7], 58 | ]; 59 | export const diminishedInterval = [ 60 | chromaticInterval[0], 61 | chromaticInterval[3], 62 | chromaticInterval[6], 63 | chromaticInterval[9], 64 | ]; 65 | export const wholeToneInterval = [ 66 | chromaticInterval[0], 67 | chromaticInterval[2], 68 | chromaticInterval[4], 69 | chromaticInterval[6], 70 | chromaticInterval[8], 71 | chromaticInterval[10], 72 | ]; 73 | 74 | export const chromatic: Progression = { 75 | interval: chromaticInterval, 76 | length: 12, 77 | }; 78 | export const majorScale: Progression = { 79 | interval: majorScaleInterval, 80 | length: 7, 81 | }; 82 | export const minorScale: Progression = { 83 | interval: minorScaleInterval, 84 | length: 7, 85 | }; 86 | export const pentatonicScale: Progression = { 87 | interval: pentatonicScaleInterval, 88 | length: 5, 89 | }; 90 | export const majorTriad: Progression = { 91 | interval: majorTriadInterval, 92 | length: 3, 93 | }; 94 | export const minorTriad: Progression = { 95 | interval: minorTriadInterval, 96 | length: 3, 97 | }; 98 | export const diminished: Progression = { 99 | interval: diminishedInterval, 100 | length: 4, 101 | }; 102 | export const wholeTone: Progression = { 103 | interval: wholeToneInterval, 104 | length: 6, 105 | }; 106 | 107 | /** 108 | * Determine the frequency of a given note in a given progressions 109 | * 110 | * @param root The root frequency of the progression 111 | * @param progression The Progression to use 112 | * @param offset The offset (interval) of the note to generate 113 | * @return The frequency of the note requested in Hz. 114 | */ 115 | export function calculateFrequencyFromProgression( 116 | root: number, 117 | progression: Progression, 118 | offset: number 119 | ) { 120 | let octave = Math.floor(offset / progression.length); 121 | let index = offset % progression.length; 122 | 123 | return root * Math.pow(2, octave) * progression.interval[index]; 124 | } 125 | -------------------------------------------------------------------------------- /src/board/audio/sound-emoji-synthesizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Microsoft MakeCode's conversion of the CODAL synthesizer. 3 | * Copyright (c) Microsoft Corporation 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * https://github.com/microsoft/pxt/blob/f2476687fe636ec7cbf47e96b22c7acec1978461/pxtsim/sound/soundEmojiSynthesizer.ts 7 | */ 8 | 9 | import { SoundEffect } from "./sound-expressions"; 10 | 11 | // https://github.com/lancaster-university/codal-microbit-v2/blob/master/inc/SoundEmojiSynthesizer.h#L30 12 | export const EMOJI_SYNTHESIZER_SAMPLE_RATE = 44100; 13 | export const EMOJI_SYNTHESIZER_TONE_WIDTH_F = 1024; 14 | export const EMOJI_SYNTHESIZER_TONE_WIDTH = 1024; 15 | export const EMOJI_SYNTHESIZER_BUFFER_SIZE = 512; 16 | 17 | export const EMOJI_SYNTHESIZER_TONE_EFFECT_PARAMETERS = 2; 18 | export const EMOJI_SYNTHESIZER_TONE_EFFECTS = 3; 19 | 20 | export const EMOJI_SYNTHESIZER_STATUS_ACTIVE = 0x1; 21 | export const EMOJI_SYNTHESIZER_STATUS_OUTPUT_SILENCE_AS_EMPTY = 0x2; 22 | export const EMOJI_SYNTHESIZER_STATUS_STOPPING = 0x4; 23 | 24 | export class SoundEmojiSynthesizer { 25 | bufferSize: number; 26 | buffer: number[] = []; 27 | 28 | sampleRate: number; 29 | sampleRange: number; 30 | samplesPerStep: number[] = []; 31 | samplesToWrite: number; 32 | samplesWritten: number; 33 | 34 | orMask: number; 35 | frequency: number = -1; 36 | volume: number; 37 | 38 | position: number; 39 | status = 0; 40 | 41 | effectPointer: number = -1; 42 | effectBuffer: SoundEffect[] = []; 43 | 44 | get effect() { 45 | return this.effectBuffer[this.effectPointer]; 46 | } 47 | 48 | constructor( 49 | id: number, 50 | private onDone: () => void, 51 | sampleRate = EMOJI_SYNTHESIZER_SAMPLE_RATE 52 | ) { 53 | this.position = 0; 54 | this.bufferSize = EMOJI_SYNTHESIZER_BUFFER_SIZE; 55 | this.sampleRate = sampleRate; 56 | this.samplesToWrite = 0; 57 | this.samplesWritten = 0; 58 | this.sampleRange = 1023; 59 | this.orMask = 0; 60 | this.effectPointer = -1; 61 | this.volume = 1; 62 | } 63 | 64 | play(sound: SoundEffect[]) { 65 | this.effectBuffer = sound; 66 | this.effectPointer = -1; 67 | 68 | this.nextSoundEffect(); 69 | } 70 | 71 | nextSoundEffect() { 72 | const hadEffect = this.effect != null; 73 | if (this.status & EMOJI_SYNTHESIZER_STATUS_STOPPING) { 74 | this.effectPointer = -1; 75 | this.effectBuffer = []; 76 | } 77 | 78 | // If a sequence of SoundEffects are being played, attempt to move on to the next. 79 | // If not, select the first in the buffer. 80 | if (this.effect) this.effectPointer++; 81 | else this.effectPointer = 0; 82 | 83 | // Validate that we have a valid sound effect. If not, record that we have nothing to play. 84 | if (this.effectPointer >= this.effectBuffer.length) { 85 | // if we have an effect with a negative duration, reset the buffer (unless there is an update pending) 86 | this.effectPointer = 0; 87 | 88 | if (this.effect.duration >= 0) { 89 | this.effectPointer = -1; 90 | this.effectBuffer = []; 91 | this.samplesWritten = 0; 92 | this.samplesToWrite = 0; 93 | this.position = 0; 94 | return hadEffect; 95 | } 96 | } 97 | 98 | // We have a valid buffer. Set up our synthesizer to the requested parameters. 99 | this.samplesToWrite = this.determineSampleCount(this.effect.duration); 100 | this.frequency = this.effect.frequency; 101 | this.volume = this.effect.volume; 102 | this.samplesWritten = 0; 103 | 104 | // validate and initialise per effect rendering state. 105 | for (let i = 0; i < EMOJI_SYNTHESIZER_TONE_EFFECTS; i++) { 106 | this.effect.effects[i].step = 0; 107 | this.effect.effects[i].steps = Math.max(this.effect.effects[i].steps, 1); 108 | this.samplesPerStep[i] = Math.floor( 109 | this.samplesToWrite / this.effect.effects[i].steps 110 | ); 111 | } 112 | return false; 113 | } 114 | 115 | pull(): number[] { 116 | let done = false; 117 | let sample: number | null = null; 118 | let bufferEnd: number = -1; 119 | 120 | while (!done) { 121 | if ( 122 | this.samplesWritten == this.samplesToWrite || 123 | this.status & EMOJI_SYNTHESIZER_STATUS_STOPPING 124 | ) { 125 | let renderComplete = this.nextSoundEffect(); 126 | 127 | // If we have just completed active playout of an effect, and there are no more effects scheduled, 128 | // unblock any fibers that may be waiting to play a sound effect. 129 | if ( 130 | this.samplesToWrite == 0 || 131 | this.status & EMOJI_SYNTHESIZER_STATUS_STOPPING 132 | ) { 133 | done = true; 134 | if ( 135 | renderComplete || 136 | this.status & EMOJI_SYNTHESIZER_STATUS_STOPPING 137 | ) { 138 | this.status &= ~EMOJI_SYNTHESIZER_STATUS_STOPPING; 139 | 140 | this.onDone(); 141 | // Event(id, DEVICE_SOUND_EMOJI_SYNTHESIZER_EVT_DONE); 142 | // lock.notify(); 143 | } 144 | } 145 | } 146 | 147 | // If we have something to do, ensure our buffers are created. 148 | // We defer creation to avoid unnecessary heap allocation when generating silence. 149 | if ( 150 | (this.samplesWritten < this.samplesToWrite || 151 | !(this.status & EMOJI_SYNTHESIZER_STATUS_OUTPUT_SILENCE_AS_EMPTY)) && 152 | sample == null 153 | ) { 154 | this.buffer = new Array(this.bufferSize); 155 | sample = 0; 156 | bufferEnd = this.buffer.length; 157 | } 158 | 159 | // Generate some samples with the current this.effect parameters. 160 | while (this.samplesWritten < this.samplesToWrite) { 161 | let skip = 162 | (EMOJI_SYNTHESIZER_TONE_WIDTH_F * this.frequency) / this.sampleRate; 163 | let gain = (this.sampleRange * this.volume) / 1024; 164 | let offset = 512 - 512 * gain; 165 | 166 | let effectStepEnd: number[] = []; 167 | 168 | for (let i = 0; i < EMOJI_SYNTHESIZER_TONE_EFFECTS; i++) { 169 | effectStepEnd[i] = 170 | this.samplesPerStep[i] * this.effect.effects[i].step; 171 | if (this.effect.effects[i].step == this.effect.effects[i].steps - 1) 172 | effectStepEnd[i] = this.samplesToWrite; 173 | } 174 | 175 | let stepEndPosition = effectStepEnd[0]; 176 | for (let i = 1; i < EMOJI_SYNTHESIZER_TONE_EFFECTS; i++) 177 | stepEndPosition = Math.min(stepEndPosition, effectStepEnd[i]); 178 | 179 | // Write samples until the end of the next this.effect-step 180 | while (this.samplesWritten < stepEndPosition) { 181 | // Stop processing when we've filled the requested this.buffer 182 | if (sample == bufferEnd) { 183 | // downStream.pullRequest(); 184 | return this.buffer; 185 | } 186 | 187 | // Synthesize a sample 188 | let s = this.effect.tone.tonePrint( 189 | this.effect.tone.parameter, 190 | Math.max(this.position, 0) 191 | ); 192 | 193 | // Apply volume scaling and OR mask (if specified). 194 | this.buffer[sample as number] = s * gain + offset; // | this.orMask; 195 | 196 | // Move on our pointers. 197 | (sample as number)++; 198 | this.samplesWritten++; 199 | this.position += skip; 200 | 201 | // Keep our toneprint pointer in range 202 | while (this.position > EMOJI_SYNTHESIZER_TONE_WIDTH_F) 203 | this.position -= EMOJI_SYNTHESIZER_TONE_WIDTH_F; 204 | } 205 | 206 | // Invoke the this.effect function for any effects that are due. 207 | for (let i = 0; i < EMOJI_SYNTHESIZER_TONE_EFFECTS; i++) { 208 | if (this.samplesWritten == effectStepEnd[i]) { 209 | if (this.effect.effects[i].step < this.effect.effects[i].steps) { 210 | if (!this.effect.effects[i].effect) { 211 | throw new Error("Can this really be null/undefined?"); 212 | } 213 | this.effect.effects[i].effect(this, this.effect.effects[i]); 214 | 215 | this.effect.effects[i].step++; 216 | } 217 | } 218 | } 219 | } 220 | } 221 | 222 | // if we have no data to send, return an empty this.buffer (if requested) 223 | if (sample == null) { 224 | this.buffer = []; 225 | } else { 226 | // Pad the output this.buffer with silence if necessary. 227 | const silence = this.sampleRange * 0.5; // | this.orMask; 228 | while (sample < bufferEnd) { 229 | this.buffer[sample] = silence; 230 | sample++; 231 | } 232 | } 233 | 234 | // Issue a Pull Request so that we are always receiver driven, and we're done. 235 | // downStream.pullRequest(); 236 | return this.buffer; 237 | } 238 | 239 | determineSampleCount(playoutTime: number) { 240 | if (playoutTime < 0) playoutTime = -playoutTime; 241 | 242 | const seconds = playoutTime / 1000; 243 | return Math.floor(this.sampleRate * seconds); 244 | } 245 | 246 | totalDuration() { 247 | let duration = 0; 248 | 249 | for (const effect of this.effectBuffer) duration += effect.duration; 250 | 251 | return duration; 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/board/audio/sound-expressions.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Microsoft MakeCode's conversion of the CODAL synthesizer. 3 | * Copyright (c) Microsoft Corporation 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * https://github.com/microsoft/pxt/blob/41530725a3d70f67ee4e501066c701e7a0c20ff6/pxtsim/sound/soundexpression.ts 7 | */ 8 | 9 | import { Progression } from "./musical-progressions"; 10 | import { 11 | EMOJI_SYNTHESIZER_TONE_EFFECTS, 12 | SoundEmojiSynthesizer, 13 | } from "./sound-emoji-synthesizer"; 14 | import * as SoundSynthesizerEffects from "./sound-synthesizer-effects"; 15 | import * as Synthesizer from "./sound-synthesizer"; 16 | import * as MusicalProgressions from "./musical-progressions"; 17 | 18 | /** 19 | * Adapted from lancaster-university/codal-microbit-v2 20 | * https://github.com/lancaster-university/codal-microbit-v2/blob/master/source/SoundExpressions.cpp 21 | */ 22 | export function parseSoundEffects(notes: string) { 23 | // https://github.com/lancaster-university/codal-microbit-v2/blob/master/source/SoundExpressions.cpp#L57 24 | 25 | // 72 characters of sound data comma separated 26 | const charsPerEffect = 72; 27 | const effectCount = Math.floor((notes.length + 1) / (charsPerEffect + 1)); 28 | const expectedLength = effectCount * (charsPerEffect + 1) - 1; 29 | if (notes.length != expectedLength) { 30 | return []; 31 | } 32 | 33 | const soundEffects: SoundEffect[] = []; 34 | 35 | for (let i = 0; i < effectCount; ++i) { 36 | const start = i * charsPerEffect + i; 37 | if (start > 0 && notes[start - 1] != ",") { 38 | return []; 39 | } 40 | const effect = blankSoundEffect(); 41 | if (!parseSoundExpression(notes.substr(start), effect)) { 42 | return []; 43 | } 44 | soundEffects.push(effect); 45 | } 46 | 47 | return soundEffects; 48 | } 49 | 50 | export interface TonePrint { 51 | tonePrint: (arg: number[], position: number) => number; 52 | parameter: number[]; 53 | } 54 | 55 | export interface ToneEffect { 56 | effect: (synth: SoundEmojiSynthesizer, context: ToneEffect) => void; 57 | step: number; 58 | steps: number; 59 | parameter: number[]; 60 | parameter_p: Progression[]; 61 | } 62 | 63 | export interface SoundEffect { 64 | frequency: number; 65 | volume: number; 66 | duration: number; 67 | tone: TonePrint; 68 | effects: ToneEffect[]; 69 | } 70 | 71 | export function parseSoundExpression(soundChars: string, fx: SoundEffect) { 72 | // https://github.com/lancaster-university/codal-microbit-v2/blob/master/source/SoundExpressions.cpp#L115 73 | 74 | // Encoded as a sequence of zero padded decimal strings. 75 | // This encoding is worth reconsidering if we can! 76 | // The ADSR effect (and perhaps others in future) has two parameters which cannot be expressed. 77 | 78 | // 72 chars total 79 | // [0] 0-4 wave 80 | let wave = parseInt(soundChars.substr(0, 1)); 81 | // [1] 0000-1023 volume 82 | let effectVolume = parseInt(soundChars.substr(1, 4)); 83 | // [5] 0000-9999 frequency 84 | let frequency = parseInt(soundChars.substr(5, 4)); 85 | // [9] 0000-9999 duration 86 | let duration = parseInt(soundChars.substr(9, 4)); 87 | // [13] 00 shape (specific known values) 88 | let shape = parseInt(soundChars.substr(13, 2)); 89 | // [15] XXX unused/bug. This was startFrequency but we use frequency above. 90 | // [18] 0000-9999 end frequency 91 | let endFrequency = parseInt(soundChars.substr(18, 4)); 92 | // [22] XXXX unused. This was start volume but we use volume above. 93 | // [26] 0000-1023 end volume 94 | let endVolume = parseInt(soundChars.substr(26, 4)); 95 | // [30] 0000-9999 steps 96 | let steps = parseInt(soundChars.substr(30, 4)); 97 | // [34] 00-03 fx choice 98 | let fxChoice = parseInt(soundChars.substr(34, 2)); 99 | // [36] 0000-9999 fxParam 100 | let fxParam = parseInt(soundChars.substr(36, 4)); 101 | // [40] 0000-9999 fxnSteps 102 | let fxnSteps = parseInt(soundChars.substr(40, 4)); 103 | 104 | // Details that encoded randomness to be applied when frame is used: 105 | // Can the randomness cause any parameters to go out of range? 106 | // [44] 0000-9999 frequency random 107 | frequency = applyRandom(frequency, parseInt(soundChars.substr(44, 4))); 108 | // [48] 0000-9999 end frequency random 109 | endFrequency = applyRandom(endFrequency, parseInt(soundChars.substr(48, 4))); 110 | // [52] 0000-9999 volume random 111 | effectVolume = applyRandom(effectVolume, parseInt(soundChars.substr(52, 4))); 112 | // [56] 0000-9999 end volume random 113 | endVolume = applyRandom(endVolume, parseInt(soundChars.substr(56, 4))); 114 | // [60] 0000-9999 duration random 115 | duration = applyRandom(duration, parseInt(soundChars.substr(60, 4))); 116 | // [64] 0000-9999 fxParamRandom 117 | fxParam = applyRandom(fxParam, parseInt(soundChars.substr(64, 4))); 118 | // [68] 0000-9999 fxnStepsRandom 119 | fxnSteps = applyRandom(fxnSteps, parseInt(soundChars.substr(68, 4))); 120 | 121 | if ( 122 | frequency == -1 || 123 | endFrequency == -1 || 124 | effectVolume == -1 || 125 | endVolume == -1 || 126 | duration == -1 || 127 | fxParam == -1 || 128 | fxnSteps == -1 129 | ) { 130 | return false; 131 | } 132 | 133 | let volumeScaleFactor = 1; 134 | 135 | switch (wave) { 136 | case 0: 137 | fx.tone.tonePrint = Synthesizer.SineTone; 138 | break; 139 | case 1: 140 | fx.tone.tonePrint = Synthesizer.SawtoothTone; 141 | break; 142 | case 2: 143 | fx.tone.tonePrint = Synthesizer.TriangleTone; 144 | break; 145 | case 3: 146 | fx.tone.tonePrint = Synthesizer.SquareWaveTone; 147 | break; 148 | case 4: 149 | fx.tone.tonePrint = Synthesizer.NoiseTone; 150 | break; 151 | } 152 | 153 | fx.frequency = frequency; 154 | fx.duration = duration; 155 | 156 | fx.effects[0].steps = steps; 157 | switch (shape) { 158 | case 0: 159 | fx.effects[0].effect = SoundSynthesizerEffects.noInterpolation; 160 | break; 161 | case 1: 162 | fx.effects[0].effect = SoundSynthesizerEffects.linearInterpolation; 163 | fx.effects[0].parameter[0] = endFrequency; 164 | break; 165 | case 2: 166 | fx.effects[0].effect = SoundSynthesizerEffects.curveInterpolation; 167 | fx.effects[0].parameter[0] = endFrequency; 168 | break; 169 | case 5: 170 | fx.effects[0].effect = 171 | SoundSynthesizerEffects.exponentialRisingInterpolation; 172 | fx.effects[0].parameter[0] = endFrequency; 173 | break; 174 | case 6: 175 | fx.effects[0].effect = 176 | SoundSynthesizerEffects.exponentialFallingInterpolation; 177 | fx.effects[0].parameter[0] = endFrequency; 178 | break; 179 | case 8: // various ascending scales - see next switch 180 | case 10: 181 | case 12: 182 | case 14: 183 | case 16: 184 | fx.effects[0].effect = SoundSynthesizerEffects.appregrioAscending; 185 | break; 186 | case 9: // various descending scales - see next switch 187 | case 11: 188 | case 13: 189 | case 15: 190 | case 17: 191 | fx.effects[0].effect = SoundSynthesizerEffects.appregrioDescending; 192 | break; 193 | case 18: 194 | fx.effects[0].effect = SoundSynthesizerEffects.logarithmicInterpolation; 195 | fx.effects[0].parameter[0] = endFrequency; 196 | break; 197 | } 198 | 199 | // Scale 200 | switch (shape) { 201 | case 8: 202 | case 9: 203 | fx.effects[0].parameter_p[0] = MusicalProgressions.majorScale; 204 | break; 205 | case 10: 206 | case 11: 207 | fx.effects[0].parameter_p[0] = MusicalProgressions.minorScale; 208 | break; 209 | case 12: 210 | case 13: 211 | fx.effects[0].parameter_p[0] = MusicalProgressions.diminished; 212 | break; 213 | case 14: 214 | case 15: 215 | fx.effects[0].parameter_p[0] = MusicalProgressions.chromatic; 216 | break; 217 | case 16: 218 | case 17: 219 | fx.effects[0].parameter_p[0] = MusicalProgressions.wholeTone; 220 | break; 221 | } 222 | 223 | // Volume envelope 224 | let effectVolumeFloat = CLAMP(0, effectVolume, 1023) / 1023.0; 225 | let endVolumeFloat = CLAMP(0, endVolume, 1023) / 1023.0; 226 | fx.volume = volumeScaleFactor * effectVolumeFloat; 227 | fx.effects[1].effect = SoundSynthesizerEffects.volumeRampEffect; 228 | fx.effects[1].steps = 36; 229 | fx.effects[1].parameter[0] = volumeScaleFactor * endVolumeFloat; 230 | 231 | // Vibrato effect 232 | // Steps need to be spread across duration evenly. 233 | let normalizedFxnSteps = Math.round((fx.duration / 10000) * fxnSteps); 234 | switch (fxChoice) { 235 | case 1: 236 | fx.effects[2].steps = normalizedFxnSteps; 237 | fx.effects[2].effect = SoundSynthesizerEffects.frequencyVibratoEffect; 238 | fx.effects[2].parameter[0] = fxParam; 239 | break; 240 | case 2: 241 | fx.effects[2].steps = normalizedFxnSteps; 242 | fx.effects[2].effect = SoundSynthesizerEffects.volumeVibratoEffect; 243 | fx.effects[2].parameter[0] = fxParam; 244 | break; 245 | case 3: 246 | fx.effects[2].steps = normalizedFxnSteps; 247 | fx.effects[2].effect = SoundSynthesizerEffects.warbleInterpolation; 248 | fx.effects[2].parameter[0] = fxParam; 249 | break; 250 | } 251 | return true; 252 | } 253 | 254 | function random(max: number) { 255 | return Math.floor(Math.random() * max); 256 | } 257 | 258 | function CLAMP(min: number, value: number, max: number) { 259 | return Math.min(max, Math.max(min, value)); 260 | } 261 | 262 | function applyRandom(value: number, rand: number) { 263 | if (value < 0 || rand < 0) { 264 | return -1; 265 | } 266 | const delta = random(rand * 2 + 1) - rand; 267 | return Math.abs(value + delta); 268 | } 269 | 270 | function blankSoundEffect() { 271 | const res: SoundEffect = { 272 | frequency: 0, 273 | volume: 1, 274 | duration: 0, 275 | tone: { 276 | tonePrint: Synthesizer.SineTone, 277 | parameter: [0], 278 | }, 279 | effects: [], 280 | }; 281 | 282 | for (let i = 0; i < EMOJI_SYNTHESIZER_TONE_EFFECTS; i++) { 283 | res.effects.push({ 284 | effect: SoundSynthesizerEffects.noInterpolation, 285 | step: 0, 286 | steps: 0, 287 | parameter: [], 288 | parameter_p: [], 289 | }); 290 | } 291 | 292 | return res; 293 | } 294 | -------------------------------------------------------------------------------- /src/board/audio/sound-synthesizer-effects.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Microsoft MakeCode's conversion of the CODAL synthesizer. 3 | * Copyright (c) Microsoft Corporation 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * https://github.com/microsoft/pxt/blob/676b0eeaf419386fc50b251ec60a73d940941d80/pxtsim/sound/soundSynthesizerEffects.ts 7 | */ 8 | 9 | import { calculateFrequencyFromProgression } from "./musical-progressions"; 10 | import { SoundEmojiSynthesizer } from "./sound-emoji-synthesizer"; 11 | import { ToneEffect } from "./sound-expressions"; 12 | 13 | /* 14 | * Definitions of standard progressions. 15 | */ 16 | 17 | /** 18 | * Root Frequency Interpolation Effect Functions 19 | */ 20 | 21 | export function noInterpolation( 22 | synth: SoundEmojiSynthesizer, 23 | context: ToneEffect 24 | ) {} 25 | 26 | // Linear interpolate function. 27 | // parameter[0]: end frequency 28 | export function linearInterpolation( 29 | synth: SoundEmojiSynthesizer, 30 | context: ToneEffect 31 | ) { 32 | let interval = 33 | (context.parameter[0] - synth.effect.frequency) / context.steps; 34 | synth.frequency = synth.effect.frequency + interval * context.step; 35 | } 36 | 37 | // Linear interpolate function. 38 | // parameter[0]: end frequency 39 | export function logarithmicInterpolation( 40 | synth: SoundEmojiSynthesizer, 41 | context: ToneEffect 42 | ) { 43 | synth.frequency = 44 | synth.effect.frequency + 45 | (Math.log10(Math.max(context.step, 0.1)) * 46 | (context.parameter[0] - synth.effect.frequency)) / 47 | 1.95; 48 | } 49 | 50 | // Curve interpolate function 51 | // parameter[0]: end frequency 52 | export function curveInterpolation( 53 | synth: SoundEmojiSynthesizer, 54 | context: ToneEffect 55 | ) { 56 | synth.frequency = 57 | Math.sin((context.step * 3.12159) / 180.0) * 58 | (context.parameter[0] - synth.effect.frequency) + 59 | synth.effect.frequency; 60 | } 61 | 62 | // Cosine interpolate function 63 | // parameter[0]: end frequency 64 | export function slowVibratoInterpolation( 65 | synth: SoundEmojiSynthesizer, 66 | context: ToneEffect 67 | ) { 68 | synth.frequency = 69 | Math.sin(context.step / 10) * context.parameter[0] + synth.effect.frequency; 70 | } 71 | 72 | //warble function 73 | // parameter[0]: end frequency 74 | export function warbleInterpolation( 75 | synth: SoundEmojiSynthesizer, 76 | context: ToneEffect 77 | ) { 78 | synth.frequency = 79 | Math.sin(context.step) * (context.parameter[0] - synth.effect.frequency) + 80 | synth.effect.frequency; 81 | } 82 | 83 | // Vibrato function 84 | // parameter[0]: end frequency 85 | export function vibratoInterpolation( 86 | synth: SoundEmojiSynthesizer, 87 | context: ToneEffect 88 | ) { 89 | synth.frequency = 90 | synth.effect.frequency + Math.sin(context.step) * context.parameter[0]; 91 | } 92 | 93 | // Exponential rising function 94 | // parameter[0]: end frequency 95 | export function exponentialRisingInterpolation( 96 | synth: SoundEmojiSynthesizer, 97 | context: ToneEffect 98 | ) { 99 | synth.frequency = 100 | synth.effect.frequency + 101 | Math.sin(0.01745329 * context.step) * context.parameter[0]; 102 | } 103 | 104 | // Exponential falling function 105 | export function exponentialFallingInterpolation( 106 | synth: SoundEmojiSynthesizer, 107 | context: ToneEffect 108 | ) { 109 | synth.frequency = 110 | synth.effect.frequency + 111 | Math.cos(0.01745329 * context.step) * context.parameter[0]; 112 | } 113 | 114 | // Argeppio functions 115 | export function appregrioAscending( 116 | synth: SoundEmojiSynthesizer, 117 | context: ToneEffect 118 | ) { 119 | synth.frequency = calculateFrequencyFromProgression( 120 | synth.effect.frequency, 121 | context.parameter_p[0], 122 | context.step 123 | ); 124 | } 125 | 126 | export function appregrioDescending( 127 | synth: SoundEmojiSynthesizer, 128 | context: ToneEffect 129 | ) { 130 | synth.frequency = calculateFrequencyFromProgression( 131 | synth.effect.frequency, 132 | context.parameter_p[0], 133 | context.steps - context.step - 1 134 | ); 135 | } 136 | 137 | /** 138 | * Frequency Delta effects 139 | */ 140 | 141 | // Frequency vibrato function 142 | // parameter[0]: vibrato frequency multiplier 143 | export function frequencyVibratoEffect( 144 | synth: SoundEmojiSynthesizer, 145 | context: ToneEffect 146 | ) { 147 | if (context.step == 0) return; 148 | 149 | if (context.step % 2 == 0) synth.frequency /= context.parameter[0]; 150 | else synth.frequency *= context.parameter[0]; 151 | } 152 | 153 | // Volume vibrato function 154 | // parameter[0]: vibrato volume multiplier 155 | export function volumeVibratoEffect( 156 | synth: SoundEmojiSynthesizer, 157 | context: ToneEffect 158 | ) { 159 | if (context.step == 0) return; 160 | 161 | if (context.step % 2 == 0) synth.volume /= context.parameter[0]; 162 | else synth.volume *= context.parameter[0]; 163 | } 164 | 165 | /** 166 | * Volume Delta effects 167 | */ 168 | 169 | /** Simple ADSR enveleope effect. 170 | * parameter[0]: Centre volume 171 | * parameter[1]: End volume 172 | * effect.volume: start volume 173 | */ 174 | export function adsrVolumeEffect( 175 | synth: SoundEmojiSynthesizer, 176 | context: ToneEffect 177 | ) { 178 | let halfSteps = context.steps * 0.5; 179 | 180 | if (context.step <= halfSteps) { 181 | let delta = (context.parameter[0] - synth.effect.volume) / halfSteps; 182 | synth.volume = synth.effect.volume + context.step * delta; 183 | } else { 184 | let delta = (context.parameter[1] - context.parameter[0]) / halfSteps; 185 | synth.volume = context.parameter[0] + (context.step - halfSteps) * delta; 186 | } 187 | } 188 | 189 | /** 190 | * Simple volume ramp effect 191 | * parameter[0]: End volume 192 | * effect.volume: start volume 193 | */ 194 | export function volumeRampEffect( 195 | synth: SoundEmojiSynthesizer, 196 | context: ToneEffect 197 | ) { 198 | let delta = (context.parameter[0] - synth.effect.volume) / context.steps; 199 | synth.volume = synth.effect.volume + context.step * delta; 200 | } 201 | -------------------------------------------------------------------------------- /src/board/audio/sound-synthesizer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adapted from Microsoft MakeCode's conversion of the CODAL synthesizer. 3 | * Copyright (c) Microsoft Corporation 4 | * SPDX-License-Identifier: MIT 5 | * 6 | * https://github.com/microsoft/pxt/blob/676b0eeaf419386fc50b251ec60a73d940941d80/pxtsim/sound/soundSynthesizer.ts 7 | */ 8 | 9 | /** 10 | * Adapted from lancaster-university/codal-core 11 | * https://github.com/lancaster-university/codal-core/blob/master/source/streams/Synthesizer.cpp#L54 12 | */ 13 | const sineTone = [ 14 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 6, 15 | 6, 7, 7, 8, 8, 9, 9, 10, 11, 11, 12, 13, 13, 14, 15, 16, 16, 17, 18, 19, 20, 16 | 21, 22, 22, 23, 24, 25, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 37, 38, 40, 17 | 41, 42, 43, 45, 46, 47, 49, 50, 51, 53, 54, 56, 57, 58, 60, 61, 63, 64, 66, 18 | 68, 69, 71, 72, 74, 76, 77, 79, 81, 82, 84, 86, 87, 89, 91, 93, 95, 96, 98, 19 | 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 20 | 130, 132, 134, 136, 138, 141, 143, 145, 147, 149, 152, 154, 156, 158, 161, 21 | 163, 165, 167, 170, 172, 175, 177, 179, 182, 184, 187, 189, 191, 194, 196, 22 | 199, 201, 204, 206, 209, 211, 214, 216, 219, 222, 224, 227, 229, 232, 235, 23 | 237, 240, 243, 245, 248, 251, 253, 256, 259, 262, 264, 267, 270, 273, 275, 24 | 278, 281, 284, 287, 289, 292, 295, 298, 301, 304, 307, 309, 312, 315, 318, 25 | 321, 324, 327, 330, 333, 336, 339, 342, 345, 348, 351, 354, 357, 360, 363, 26 | 366, 369, 372, 375, 378, 381, 384, 387, 390, 393, 396, 399, 402, 405, 408, 27 | 411, 414, 417, 420, 424, 427, 430, 433, 436, 439, 442, 445, 448, 452, 455, 28 | 458, 461, 464, 467, 470, 473, 477, 480, 483, 486, 489, 492, 495, 498, 502, 29 | 505, 508, 511, 514, 517, 520, 524, 527, 530, 533, 536, 539, 542, 545, 549, 30 | 552, 555, 558, 561, 564, 567, 570, 574, 577, 580, 583, 586, 589, 592, 595, 31 | 598, 602, 605, 608, 611, 614, 617, 620, 623, 626, 629, 632, 635, 638, 641, 32 | 644, 647, 650, 653, 656, 659, 662, 665, 668, 671, 674, 677, 680, 683, 686, 33 | 689, 692, 695, 698, 701, 704, 707, 710, 713, 715, 718, 721, 724, 727, 730, 34 | 733, 735, 738, 741, 744, 747, 749, 752, 755, 758, 760, 763, 766, 769, 771, 35 | 774, 777, 779, 782, 785, 787, 790, 793, 795, 798, 800, 803, 806, 808, 811, 36 | 813, 816, 818, 821, 823, 826, 828, 831, 833, 835, 838, 840, 843, 845, 847, 37 | 850, 852, 855, 857, 859, 861, 864, 866, 868, 870, 873, 875, 877, 879, 881, 38 | 884, 886, 888, 890, 892, 894, 896, 898, 900, 902, 904, 906, 908, 910, 912, 39 | 914, 916, 918, 920, 922, 924, 926, 927, 929, 931, 933, 935, 936, 938, 940, 40 | 941, 943, 945, 946, 948, 950, 951, 953, 954, 956, 958, 959, 961, 962, 964, 41 | 965, 966, 968, 969, 971, 972, 973, 975, 976, 977, 979, 980, 981, 982, 984, 42 | 985, 986, 987, 988, 989, 990, 992, 993, 994, 995, 996, 997, 998, 999, 1000, 43 | 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1006, 1007, 1008, 1009, 1009, 1010, 44 | 1011, 1011, 1012, 1013, 1013, 1014, 1014, 1015, 1015, 1016, 1016, 1017, 1017, 45 | 1018, 1018, 1019, 1019, 1019, 1020, 1020, 1020, 1021, 1021, 1021, 1021, 1022, 46 | 1022, 1022, 1022, 1022, 1022, 1022, 1022, 1022, 1022, 1023, 1022, 47 | ]; 48 | const TONE_WIDTH = 1024; 49 | 50 | export function SineTone(arg: number[], position: number) { 51 | position |= 0; 52 | let off = TONE_WIDTH - position; 53 | if (off < TONE_WIDTH / 2) position = off; 54 | return sineTone[position]; 55 | } 56 | 57 | export function SawtoothTone(arg: number[], position: number) { 58 | return position; 59 | } 60 | 61 | export function TriangleTone(arg: number[], position: number) { 62 | return position < 512 ? position * 2 : (1023 - position) * 2; 63 | } 64 | 65 | export function NoiseTone(arg: number[], position: number) { 66 | // deterministic, semi-random noise 67 | let mult = arg[0]; 68 | if (mult == 0) mult = 7919; 69 | return (position * mult) & 1023; 70 | } 71 | 72 | export function SquareWaveTone(arg: number[], position: number) { 73 | return position < 512 ? 1023 : 0; 74 | } 75 | -------------------------------------------------------------------------------- /src/board/buttons.ts: -------------------------------------------------------------------------------- 1 | import { RangeSensor, State } from "./state"; 2 | 3 | export class Button { 4 | public state: RangeSensor; 5 | 6 | private _presses: number = 0; 7 | private _mouseDown: boolean = false; 8 | 9 | private keyListener: (e: KeyboardEvent) => void; 10 | private mouseDownListener: (e: MouseEvent) => void; 11 | private touchStartListener: (e: TouchEvent) => void; 12 | private mouseUpTouchEndListener: (e: MouseEvent | TouchEvent) => void; 13 | private mouseLeaveListener: (e: MouseEvent) => void; 14 | 15 | constructor( 16 | private id: "buttonA" | "buttonB", 17 | private element: SVGElement, 18 | private label: () => string, 19 | private onChange: (change: Partial) => void 20 | ) { 21 | this._presses = 0; 22 | this.state = new RangeSensor(id, 0, 1, 0, undefined); 23 | 24 | this.element.setAttribute("role", "button"); 25 | this.element.setAttribute("tabindex", "0"); 26 | this.element.style.cursor = "pointer"; 27 | 28 | this.keyListener = (e) => { 29 | switch (e.key) { 30 | case "Enter": 31 | case " ": 32 | e.preventDefault(); 33 | if (e.type === "keydown") { 34 | this.press(); 35 | } else { 36 | this.release(); 37 | } 38 | } 39 | }; 40 | 41 | this.mouseDownListener = (e) => { 42 | e.preventDefault(); 43 | this.mouseDownTouchStartAction(); 44 | }; 45 | this.touchStartListener = (e) => { 46 | this.mouseDownTouchStartAction(); 47 | }; 48 | this.mouseUpTouchEndListener = (e) => { 49 | e.preventDefault(); 50 | if (this._mouseDown) { 51 | this._mouseDown = false; 52 | this.release(); 53 | } 54 | }; 55 | this.mouseLeaveListener = (e) => { 56 | if (this._mouseDown) { 57 | this._mouseDown = false; 58 | this.release(); 59 | } 60 | }; 61 | 62 | this.element.addEventListener("mousedown", this.mouseDownListener); 63 | this.element.addEventListener("touchstart", this.touchStartListener); 64 | this.element.addEventListener("mouseup", this.mouseUpTouchEndListener); 65 | this.element.addEventListener("touchend", this.mouseUpTouchEndListener); 66 | this.element.addEventListener("keydown", this.keyListener); 67 | this.element.addEventListener("keyup", this.keyListener); 68 | this.element.addEventListener("mouseleave", this.mouseLeaveListener); 69 | } 70 | 71 | updateTranslations() { 72 | this.element.ariaLabel = this.label(); 73 | } 74 | 75 | setValue(value: any) { 76 | this.setValueInternal(value, false); 77 | } 78 | 79 | private setValueInternal(value: any, internalChange: boolean) { 80 | this.state.setValue(value); 81 | if (value) { 82 | this._presses++; 83 | } 84 | if (internalChange) { 85 | this.onChange({ 86 | [this.id]: this.state, 87 | }); 88 | } 89 | this.render(); 90 | } 91 | 92 | private mouseDownTouchStartAction() { 93 | this._mouseDown = true; 94 | this.press(); 95 | } 96 | 97 | press() { 98 | this.setValueInternal( 99 | this.state.value === this.state.min ? this.state.max : this.state.min, 100 | true 101 | ); 102 | } 103 | 104 | release() { 105 | this.setValueInternal( 106 | this.state.value === this.state.max ? this.state.min : this.state.max, 107 | true 108 | ); 109 | } 110 | 111 | isPressed() { 112 | return !!this.state.value; 113 | } 114 | 115 | render() { 116 | const fill = !!this.state.value ? "#00c800" : "#000000"; 117 | this.element.querySelectorAll("circle").forEach((c) => { 118 | c.style.fill = fill; 119 | }); 120 | } 121 | 122 | getAndClearPresses() { 123 | const result = this._presses; 124 | this._presses = 0; 125 | return result; 126 | } 127 | 128 | boardStopped() { 129 | this._presses = 0; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/board/compass.ts: -------------------------------------------------------------------------------- 1 | import { RangeSensor, State } from "./state"; 2 | 3 | type StateKeys = "compassX" | "compassY" | "compassZ" | "compassHeading"; 4 | 5 | export class Compass { 6 | state: Pick; 7 | 8 | constructor() { 9 | const min = -2_000_000; 10 | const max = 2_000_000; 11 | this.state = { 12 | compassX: new RangeSensor("compassX", min, max, 0, "nT"), 13 | compassY: new RangeSensor("compassY", min, max, 0, "nT"), 14 | compassZ: new RangeSensor("compassZ", min, max, 0, "nT"), 15 | compassHeading: new RangeSensor("compassHeading", 0, 360, 0, "deg"), 16 | }; 17 | } 18 | 19 | setValue(id: StateKeys, value: any) { 20 | this.state[id].setValue(value); 21 | } 22 | 23 | getFieldStrength() { 24 | const x = this.state.compassX.value; 25 | const y = this.state.compassY.value; 26 | const z = this.state.compassZ.value; 27 | return Math.sqrt(x * x + y * y + z * z); 28 | } 29 | 30 | boardStopped() {} 31 | } 32 | -------------------------------------------------------------------------------- /src/board/constants.ts: -------------------------------------------------------------------------------- 1 | // Matches microbithal.h 2 | 3 | // General error codes, only define the ones needed by this HAL. 4 | export const MICROBIT_HAL_DEVICE_OK = 0; 5 | export const MICROBIT_HAL_DEVICE_NO_RESOURCES = -1; 6 | export const MICROBIT_HAL_DEVICE_ERROR = -2; 7 | 8 | // These numbers refer to indices in the (private) pin_obj table. 9 | export const MICROBIT_HAL_PIN_P0 = 0; 10 | export const MICROBIT_HAL_PIN_P1 = 1; 11 | export const MICROBIT_HAL_PIN_P2 = 2; 12 | export const MICROBIT_HAL_PIN_P3 = 3; 13 | export const MICROBIT_HAL_PIN_P4 = 4; 14 | export const MICROBIT_HAL_PIN_P5 = 5; 15 | export const MICROBIT_HAL_PIN_P6 = 6; 16 | export const MICROBIT_HAL_PIN_P7 = 7; 17 | export const MICROBIT_HAL_PIN_P8 = 8; 18 | export const MICROBIT_HAL_PIN_P9 = 9; 19 | export const MICROBIT_HAL_PIN_P10 = 10; 20 | export const MICROBIT_HAL_PIN_P11 = 11 21 | export const MICROBIT_HAL_PIN_P12 = 12; 22 | export const MICROBIT_HAL_PIN_P13 = 13; 23 | export const MICROBIT_HAL_PIN_P14 = 14; 24 | export const MICROBIT_HAL_PIN_P15 = 15; 25 | export const MICROBIT_HAL_PIN_P16 = 16; 26 | export const MICROBIT_HAL_PIN_P19 = 17; 27 | export const MICROBIT_HAL_PIN_P20 = 18; 28 | export const MICROBIT_HAL_PIN_FACE = 19; 29 | export const MICROBIT_HAL_PIN_SPEAKER = 20; 30 | export const MICROBIT_HAL_PIN_USB_TX = 30; 31 | export const MICROBIT_HAL_PIN_USB_RX = 31; 32 | export const MICROBIT_HAL_PIN_MIXER = 33; 33 | 34 | // These match the micro:bit v1 constants. 35 | export const MICROBIT_HAL_PIN_PULL_UP = 0; 36 | export const MICROBIT_HAL_PIN_PULL_DOWN = 1; 37 | export const MICROBIT_HAL_PIN_PULL_NONE = 2; 38 | 39 | export const MICROBIT_HAL_PIN_TOUCH_RESISTIVE = 0; 40 | export const MICROBIT_HAL_PIN_TOUCH_CAPACITIVE = 1; 41 | 42 | export const MICROBIT_HAL_ACCELEROMETER_EVT_NONE = 0; 43 | export const MICROBIT_HAL_ACCELEROMETER_EVT_TILT_UP = 1; 44 | export const MICROBIT_HAL_ACCELEROMETER_EVT_TILT_DOWN = 2; 45 | export const MICROBIT_HAL_ACCELEROMETER_EVT_TILT_LEFT = 3; 46 | export const MICROBIT_HAL_ACCELEROMETER_EVT_TILT_RIGHT = 4; 47 | export const MICROBIT_HAL_ACCELEROMETER_EVT_FACE_UP = 5; 48 | export const MICROBIT_HAL_ACCELEROMETER_EVT_FACE_DOWN = 6; 49 | export const MICROBIT_HAL_ACCELEROMETER_EVT_FREEFALL = 7; 50 | export const MICROBIT_HAL_ACCELEROMETER_EVT_3G = 8; 51 | export const MICROBIT_HAL_ACCELEROMETER_EVT_6G = 9; 52 | export const MICROBIT_HAL_ACCELEROMETER_EVT_8G = 10; 53 | export const MICROBIT_HAL_ACCELEROMETER_EVT_SHAKE = 11; 54 | export const MICROBIT_HAL_ACCELEROMETER_EVT_2G = 12; 55 | 56 | // Microphone events, passed to microbit_hal_level_detector_callback(). 57 | export const MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_LOW = 1; 58 | export const MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_HIGH = 2; 59 | 60 | // Threshold kind, passed to microbit_hal_microphone_set_threshold(). 61 | export const MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_LOW = 0; 62 | export const MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_HIGH = 1; 63 | 64 | export const MICROBIT_HAL_LOG_TIMESTAMP_NONE = 0; 65 | export const MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS = 1; 66 | export const MICROBIT_HAL_LOG_TIMESTAMP_SECONDS = 10; 67 | export const MICROBIT_HAL_LOG_TIMESTAMP_MINUTES = 600; 68 | export const MICROBIT_HAL_LOG_TIMESTAMP_HOURS = 36000; 69 | export const MICROBIT_HAL_LOG_TIMESTAMP_DAYS = 864000; 70 | -------------------------------------------------------------------------------- /src/board/conversions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MICROBIT_HAL_ACCELEROMETER_EVT_2G, 3 | MICROBIT_HAL_ACCELEROMETER_EVT_3G, 4 | MICROBIT_HAL_ACCELEROMETER_EVT_6G, 5 | MICROBIT_HAL_ACCELEROMETER_EVT_8G, 6 | MICROBIT_HAL_ACCELEROMETER_EVT_FACE_DOWN, 7 | MICROBIT_HAL_ACCELEROMETER_EVT_FACE_UP, 8 | MICROBIT_HAL_ACCELEROMETER_EVT_FREEFALL, 9 | MICROBIT_HAL_ACCELEROMETER_EVT_NONE, 10 | MICROBIT_HAL_ACCELEROMETER_EVT_SHAKE, 11 | MICROBIT_HAL_ACCELEROMETER_EVT_TILT_DOWN, 12 | MICROBIT_HAL_ACCELEROMETER_EVT_TILT_LEFT, 13 | MICROBIT_HAL_ACCELEROMETER_EVT_TILT_RIGHT, 14 | MICROBIT_HAL_ACCELEROMETER_EVT_TILT_UP, 15 | MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_HIGH, 16 | MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_LOW, 17 | } from "./constants"; 18 | 19 | export function convertSoundThresholdNumberToString( 20 | value: number 21 | ): "low" | "high" { 22 | switch (value) { 23 | case MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_LOW: 24 | return "low"; 25 | case MICROBIT_HAL_MICROPHONE_SET_THRESHOLD_HIGH: 26 | return "high"; 27 | default: 28 | throw new Error(`Invalid value ${value}`); 29 | } 30 | } 31 | 32 | export function convertAccelerometerStringToNumber(value: string): number { 33 | switch (value) { 34 | case "none": 35 | return MICROBIT_HAL_ACCELEROMETER_EVT_NONE; 36 | case "up": 37 | return MICROBIT_HAL_ACCELEROMETER_EVT_TILT_UP; 38 | case "down": 39 | return MICROBIT_HAL_ACCELEROMETER_EVT_TILT_DOWN; 40 | case "left": 41 | return MICROBIT_HAL_ACCELEROMETER_EVT_TILT_LEFT; 42 | case "right": 43 | return MICROBIT_HAL_ACCELEROMETER_EVT_TILT_RIGHT; 44 | case "face up": 45 | return MICROBIT_HAL_ACCELEROMETER_EVT_FACE_UP; 46 | case "face down": 47 | return MICROBIT_HAL_ACCELEROMETER_EVT_FACE_DOWN; 48 | case "freefall": 49 | return MICROBIT_HAL_ACCELEROMETER_EVT_FREEFALL; 50 | case "2g": 51 | return MICROBIT_HAL_ACCELEROMETER_EVT_2G; 52 | case "3g": 53 | return MICROBIT_HAL_ACCELEROMETER_EVT_3G; 54 | case "6g": 55 | return MICROBIT_HAL_ACCELEROMETER_EVT_6G; 56 | case "8g": 57 | return MICROBIT_HAL_ACCELEROMETER_EVT_8G; 58 | case "shake": 59 | return MICROBIT_HAL_ACCELEROMETER_EVT_SHAKE; 60 | default: 61 | throw new Error(`Invalid value ${value}`); 62 | } 63 | } 64 | 65 | export function convertAccelerometerNumberToString(value: number): string { 66 | switch (value) { 67 | case MICROBIT_HAL_ACCELEROMETER_EVT_NONE: 68 | return "none"; 69 | case MICROBIT_HAL_ACCELEROMETER_EVT_TILT_UP: 70 | return "up"; 71 | case MICROBIT_HAL_ACCELEROMETER_EVT_TILT_DOWN: 72 | return "down"; 73 | case MICROBIT_HAL_ACCELEROMETER_EVT_TILT_LEFT: 74 | return "left"; 75 | case MICROBIT_HAL_ACCELEROMETER_EVT_TILT_RIGHT: 76 | return "right"; 77 | case MICROBIT_HAL_ACCELEROMETER_EVT_FACE_UP: 78 | return "face up"; 79 | case MICROBIT_HAL_ACCELEROMETER_EVT_FACE_DOWN: 80 | return "face down"; 81 | case MICROBIT_HAL_ACCELEROMETER_EVT_FREEFALL: 82 | return "freefall"; 83 | case MICROBIT_HAL_ACCELEROMETER_EVT_2G: 84 | return "2g"; 85 | case MICROBIT_HAL_ACCELEROMETER_EVT_3G: 86 | return "3g"; 87 | case MICROBIT_HAL_ACCELEROMETER_EVT_6G: 88 | return "6g"; 89 | case MICROBIT_HAL_ACCELEROMETER_EVT_8G: 90 | return "8g"; 91 | case MICROBIT_HAL_ACCELEROMETER_EVT_SHAKE: 92 | return "shake"; 93 | default: 94 | throw new Error(`Invalid value ${value}`); 95 | } 96 | } 97 | 98 | export const convertAudioBuffer = ( 99 | heap: Uint8Array, 100 | source: number, 101 | target: AudioBuffer 102 | ) => { 103 | const channel = target.getChannelData(0); 104 | for (let i = 0; i < channel.length; ++i) { 105 | // Convert from uint8 to -1..+1 float. 106 | channel[i] = (heap[source + i] / 255) * 2 - 1; 107 | } 108 | return target; 109 | }; 110 | -------------------------------------------------------------------------------- /src/board/data-logging.test.ts: -------------------------------------------------------------------------------- 1 | import { DataLogging } from "./data-logging"; 2 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 3 | import { LogEntry } from "."; 4 | import { 5 | MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS, 6 | MICROBIT_HAL_LOG_TIMESTAMP_NONE, 7 | MICROBIT_HAL_LOG_TIMESTAMP_SECONDS, 8 | } from "./constants"; 9 | 10 | describe("DataLogging", () => { 11 | let time = 0; 12 | let log: LogEntry[] = []; 13 | let serial: string[] = []; 14 | const currentTime = () => time++; 15 | const onLogOutput = (entry: LogEntry) => log.push(entry); 16 | const onLogDelete = () => (log.length = 0); 17 | const onSerialOutput = (text: string) => serial.push(text); 18 | let onChange = vi.fn(); 19 | let logging = new DataLogging( 20 | currentTime, 21 | onLogOutput, 22 | onSerialOutput, 23 | onLogDelete, 24 | onChange 25 | ); 26 | 27 | afterEach(() => { 28 | time = 0; 29 | log = []; 30 | serial = []; 31 | onChange = vi.fn(); 32 | logging = new DataLogging( 33 | currentTime, 34 | onLogOutput, 35 | onSerialOutput, 36 | onLogDelete, 37 | onChange 38 | ); 39 | }); 40 | 41 | it("works in a basic scenario", () => { 42 | logging.setMirroring(true); 43 | logging.beginRow(); 44 | logging.logData("a", "1"); 45 | logging.logData("b", "2"); 46 | logging.endRow(); 47 | logging.beginRow(); 48 | logging.logData("a", "3"); 49 | logging.logData("b", "4"); 50 | logging.endRow(); 51 | 52 | expect(log).toEqual([ 53 | { 54 | headings: ["a", "b"], 55 | data: ["1", "2"], 56 | }, 57 | { 58 | data: ["3", "4"], 59 | }, 60 | ]); 61 | // Mirroring is enabled: 62 | expect(serial).toEqual(["a,b\r\n", "1,2\r\n", "3,4\r\n"]); 63 | }); 64 | 65 | it("can add headings on the fly", () => { 66 | logging.beginRow(); 67 | logging.logData("a", "1"); 68 | logging.endRow(); 69 | logging.beginRow(); 70 | logging.logData("b", "4"); 71 | logging.endRow(); 72 | 73 | expect(log).toEqual([ 74 | { 75 | headings: ["a"], 76 | data: ["1"], 77 | }, 78 | { 79 | headings: ["a", "b"], 80 | data: ["", "4"], 81 | }, 82 | ]); 83 | 84 | expect(serial).toEqual([]); 85 | }); 86 | 87 | it("uses timestamp", () => { 88 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS); 89 | logging.beginRow(); 90 | logging.logData("a", "1"); 91 | logging.endRow(); 92 | logging.beginRow(); 93 | logging.logData("a", "2"); 94 | logging.endRow(); 95 | 96 | expect(log).toEqual([ 97 | { 98 | headings: ["Time (milliseconds)", "a"], 99 | data: ["0", "1"], 100 | }, 101 | { 102 | data: ["1", "2"], 103 | }, 104 | ]); 105 | }); 106 | 107 | it("allows change of timestamp before logged output", () => { 108 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS); 109 | logging.beginRow(); 110 | logging.logData("a", "1"); 111 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_SECONDS); 112 | logging.endRow(); 113 | 114 | expect(log).toEqual([ 115 | { 116 | headings: ["Time (seconds)", "a"], 117 | data: ["0.00", "1"], 118 | }, 119 | ]); 120 | }); 121 | 122 | it("allows change of timestamp after logged output (but appends)", () => { 123 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS); 124 | logging.beginRow(); 125 | logging.logData("a", "1"); 126 | logging.endRow(); 127 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_SECONDS); 128 | logging.beginRow(); 129 | logging.logData("a", "2"); 130 | logging.endRow(); 131 | 132 | expect(log).toEqual([ 133 | { 134 | headings: ["Time (milliseconds)", "a"], 135 | data: ["0", "1"], 136 | }, 137 | { 138 | headings: ["Time (milliseconds)", "a", "Time (seconds)"], 139 | data: ["", "2", "0.00"], 140 | }, 141 | ]); 142 | }); 143 | 144 | it("allows change of timestamp to none after logged output", () => { 145 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS); 146 | logging.beginRow(); 147 | logging.logData("a", "1"); 148 | logging.endRow(); 149 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_NONE); 150 | logging.beginRow(); 151 | logging.logData("a", "2"); 152 | logging.endRow(); 153 | 154 | expect(log).toEqual([ 155 | { 156 | headings: ["Time (milliseconds)", "a"], 157 | data: ["0", "1"], 158 | }, 159 | { 160 | data: ["", "2"], 161 | }, 162 | ]); 163 | }); 164 | 165 | it("allows header only change", () => { 166 | // MicroPython uses this to implement log.set_labels. 167 | logging.beginRow(); 168 | logging.logData("a", ""); 169 | logging.endRow(); 170 | logging.beginRow(); 171 | logging.logData("b", ""); 172 | logging.endRow(); 173 | 174 | expect(log).toEqual([ 175 | { 176 | headings: ["a"], 177 | }, 178 | { 179 | headings: ["a", "b"], 180 | }, 181 | ]); 182 | }); 183 | 184 | it("fills up the log", () => { 185 | const big = "1".repeat(1024); 186 | let limit = 0; 187 | for (; limit < 1000; ++limit) { 188 | logging.beginRow(); 189 | logging.logData("a", big); 190 | if (logging.endRow()) { 191 | break; 192 | } 193 | } 194 | expect(limit).toEqual(115); 195 | expect(onChange.mock.lastCall![0]).toEqual({ 196 | dataLogging: { 197 | type: "dataLogging", 198 | logFull: true, 199 | }, 200 | }); 201 | logging.delete(); 202 | expect(onChange.mock.lastCall![0]).toEqual({ 203 | dataLogging: { 204 | type: "dataLogging", 205 | logFull: false, 206 | }, 207 | }); 208 | }); 209 | 210 | it("deletes the log resetting mirroring but remembering timestamp", () => { 211 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_SECONDS); 212 | logging.setMirroring(true); 213 | logging.beginRow(); 214 | logging.logData("a", "1"); 215 | logging.endRow(); 216 | 217 | logging.delete(); 218 | 219 | expect(log).toEqual([]); 220 | 221 | serial.length = 0; 222 | logging.beginRow(); 223 | logging.logData("b", "2"); 224 | logging.endRow(); 225 | expect(log).toEqual([ 226 | { 227 | headings: ["Time (seconds)", "b"], // No "a" remembered 228 | data: ["0.00", "2"], 229 | }, 230 | ]); 231 | expect(serial).toEqual([]); 232 | }); 233 | 234 | it("dispose resets timestamp if nothing written", () => { 235 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_SECONDS); 236 | logging.boardStopped(); 237 | logging.initialize(); 238 | 239 | logging.beginRow(); 240 | logging.logData("b", "2"); 241 | logging.endRow(); 242 | expect(log).toEqual([ 243 | { 244 | headings: ["b"], 245 | data: ["2"], 246 | }, 247 | ]); 248 | }); 249 | 250 | it("dispose keeps timestamp if header written", () => { 251 | logging.setTimestamp(MICROBIT_HAL_LOG_TIMESTAMP_SECONDS); 252 | logging.beginRow(); 253 | logging.logData("b", ""); 254 | logging.endRow(); 255 | 256 | logging.boardStopped(); 257 | logging.initialize(); 258 | 259 | logging.beginRow(); 260 | logging.logData("b", "2"); 261 | logging.endRow(); 262 | expect(log).toEqual([ 263 | { 264 | headings: ["Time (seconds)", "b"], 265 | }, 266 | { 267 | data: ["0.00", "2"], 268 | }, 269 | ]); 270 | }); 271 | }); 272 | -------------------------------------------------------------------------------- /src/board/data-logging.ts: -------------------------------------------------------------------------------- 1 | import { LogEntry } from "."; 2 | import { 3 | MICROBIT_HAL_DEVICE_NO_RESOURCES, 4 | MICROBIT_HAL_DEVICE_OK, 5 | MICROBIT_HAL_LOG_TIMESTAMP_DAYS, 6 | MICROBIT_HAL_LOG_TIMESTAMP_HOURS, 7 | MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS, 8 | MICROBIT_HAL_LOG_TIMESTAMP_MINUTES, 9 | MICROBIT_HAL_LOG_TIMESTAMP_NONE, 10 | MICROBIT_HAL_LOG_TIMESTAMP_SECONDS, 11 | } from "./constants"; 12 | import { DataLoggingState, State } from "./state"; 13 | 14 | // Determined via a CODAL program dumping logEnd - dataStart in MicroBitLog.cpp. 15 | // This is only approximate as we don't serialize our state in the same way but 16 | // it's important for the user to see that the log can fill up. 17 | const maxSizeBytes = 118780; 18 | 19 | export class DataLogging { 20 | private mirroring: boolean = false; 21 | private size: number = 0; 22 | private timestamp = MICROBIT_HAL_LOG_TIMESTAMP_NONE; 23 | private timestampOnLastEndRow: number | undefined; 24 | private headingsChanged: boolean = false; 25 | private headings: string[] = []; 26 | private row: string[] | undefined; 27 | state: DataLoggingState = { type: "dataLogging", logFull: false }; 28 | 29 | constructor( 30 | private currentTimeMillis: () => number, 31 | private onLogOutput: (data: LogEntry) => void, 32 | private onSerialOutput: (data: string) => void, 33 | private onLogDelete: () => void, 34 | private onChange: (state: Partial) => void 35 | ) {} 36 | 37 | setMirroring(mirroring: boolean) { 38 | this.mirroring = mirroring; 39 | } 40 | 41 | setTimestamp(timestamp: number) { 42 | this.timestamp = timestamp; 43 | } 44 | 45 | beginRow() { 46 | this.row = new Array(this.headings.length); 47 | this.row.fill(""); 48 | return MICROBIT_HAL_DEVICE_OK; 49 | } 50 | 51 | endRow() { 52 | if (!this.row) { 53 | throw noRowError(); 54 | } 55 | if ( 56 | this.timestamp !== this.timestampOnLastEndRow && 57 | this.timestamp !== MICROBIT_HAL_LOG_TIMESTAMP_NONE 58 | ) { 59 | // New timestamp column required. Put it first if there's been no output. 60 | if (this.size === 0) { 61 | this.headings = [timestampToHeading(this.timestamp), ...this.headings]; 62 | this.row = ["", ...this.row]; 63 | } else { 64 | this.logData(timestampToHeading(this.timestamp), ""); 65 | } 66 | } 67 | 68 | const entry: LogEntry = {}; 69 | if (this.headingsChanged) { 70 | this.headingsChanged = false; 71 | entry.headings = [...this.headings]; 72 | } 73 | const validData = this.row.some((x) => x?.length > 0); 74 | if (validData) { 75 | if (this.timestamp !== MICROBIT_HAL_LOG_TIMESTAMP_NONE) { 76 | this.logData( 77 | timestampToHeading(this.timestamp), 78 | formatTimestamp(this.timestamp, this.currentTimeMillis()) 79 | ); 80 | } 81 | entry.data = this.row; 82 | } 83 | 84 | if (entry.data || entry.headings) { 85 | const entrySize = calculateEntrySize(entry); 86 | if (this.size + entrySize > maxSizeBytes) { 87 | if (!this.state.logFull) { 88 | this.state = { 89 | ...this.state, 90 | logFull: true, 91 | }; 92 | this.onChange({ 93 | dataLogging: this.state, 94 | }); 95 | } 96 | return MICROBIT_HAL_DEVICE_NO_RESOURCES; 97 | } 98 | this.size += entrySize; 99 | this.output(entry); 100 | } 101 | this.timestampOnLastEndRow = this.timestamp; 102 | this.row = undefined; 103 | return MICROBIT_HAL_DEVICE_OK; 104 | } 105 | 106 | logData(key: string, value: string) { 107 | if (!this.row) { 108 | throw noRowError(); 109 | } 110 | const index = this.headings.indexOf(key); 111 | if (index === -1) { 112 | this.headings.push(key); 113 | this.row.push(value); 114 | this.headingsChanged = true; 115 | } else { 116 | this.row[index] = value; 117 | } 118 | 119 | return MICROBIT_HAL_DEVICE_OK; 120 | } 121 | 122 | private output(entry: LogEntry) { 123 | this.onLogOutput(entry); 124 | if (this.mirroring) { 125 | if (entry.headings) { 126 | this.onSerialOutput(entry.headings.join(",") + "\r\n"); 127 | } 128 | if (entry.data) { 129 | this.onSerialOutput(entry.data.join(",") + "\r\n"); 130 | } 131 | } 132 | } 133 | 134 | initialize() {} 135 | 136 | boardStopped() { 137 | // We don't delete the log here as it's on flash, but we do reset in-memory state. 138 | this.resetNonFlashStateExceptTimestamp(); 139 | // Keep the timestamp if we could restore it from a persisted log. 140 | if (this.size === 0) { 141 | this.timestamp = MICROBIT_HAL_LOG_TIMESTAMP_NONE; 142 | } 143 | } 144 | 145 | delete() { 146 | this.resetNonFlashStateExceptTimestamp(); 147 | this.headings = []; 148 | this.timestampOnLastEndRow = undefined; 149 | 150 | this.size = 0; 151 | if (this.state.logFull) { 152 | this.state = { 153 | ...this.state, 154 | logFull: false, 155 | }; 156 | this.onChange({ 157 | dataLogging: this.state, 158 | }); 159 | } 160 | 161 | this.onLogDelete(); 162 | } 163 | 164 | private resetNonFlashStateExceptTimestamp() { 165 | // headings are considered flash state as MicroBitLog reparses them from flash 166 | this.mirroring = false; 167 | this.headingsChanged = false; 168 | this.row = undefined; 169 | } 170 | } 171 | 172 | function noRowError() { 173 | return new Error("HAL clients should always start a row"); 174 | } 175 | 176 | function calculateEntrySize(entry: LogEntry): number { 177 | // +1s for commas and a newline, approximating the CSV in the flash. 178 | const headings = entry.headings 179 | ? entry.headings.reduce((prev, curr) => prev + curr.length + 1, 0) + 1 180 | : 0; 181 | const data = entry.data 182 | ? entry.data.reduce((prev, curr) => prev + curr.length + 1, 0) + 1 183 | : 0; 184 | return headings + data; 185 | } 186 | 187 | function timestampToHeading(timestamp: number): string { 188 | return `Time (${timestampToUnitString(timestamp)})`; 189 | } 190 | 191 | function timestampToUnitString(timestamp: number): string { 192 | switch (timestamp) { 193 | case MICROBIT_HAL_LOG_TIMESTAMP_DAYS: 194 | return "days"; 195 | case MICROBIT_HAL_LOG_TIMESTAMP_HOURS: 196 | return "hours"; 197 | case MICROBIT_HAL_LOG_TIMESTAMP_MINUTES: 198 | return "minutes"; 199 | case MICROBIT_HAL_LOG_TIMESTAMP_SECONDS: 200 | return "seconds"; 201 | case MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS: 202 | return "milliseconds"; 203 | case MICROBIT_HAL_LOG_TIMESTAMP_NONE: 204 | // Fall through 205 | default: 206 | throw new Error("Not valid"); 207 | } 208 | } 209 | 210 | function formatTimestamp(format: number, currentTime: number): string { 211 | if (format === MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS) { 212 | return currentTime.toString(); // No fractional part. 213 | } 214 | return (currentTime / millisInFormat(format)).toFixed(2); 215 | } 216 | 217 | function millisInFormat(format: number) { 218 | switch (format) { 219 | case MICROBIT_HAL_LOG_TIMESTAMP_MILLISECONDS: 220 | return 1; 221 | case MICROBIT_HAL_LOG_TIMESTAMP_SECONDS: 222 | return 1000; 223 | case MICROBIT_HAL_LOG_TIMESTAMP_MINUTES: 224 | return 1000 * 60; 225 | case MICROBIT_HAL_LOG_TIMESTAMP_HOURS: 226 | return 1000 * 60 * 60; 227 | case MICROBIT_HAL_LOG_TIMESTAMP_DAYS: 228 | return 1000 * 60 * 60 * 24; 229 | default: 230 | throw new Error(); 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/board/display.ts: -------------------------------------------------------------------------------- 1 | // This mapping is designed to give a set of 10 visually distinct levels. 2 | 3 | import { RangeSensor } from "./state"; 4 | import { clamp } from "./util"; 5 | 6 | // Carried across from microbit_hal_display_set_pixel. 7 | const brightMap = [0, 20, 40, 60, 80, 120, 160, 190, 220, 255]; 8 | 9 | export class Display { 10 | lightLevel: RangeSensor = new RangeSensor( 11 | "lightLevel", 12 | 0, 13 | 255, 14 | 127, 15 | undefined 16 | ); 17 | private state: Array>; 18 | constructor(private leds: SVGElement[]) { 19 | this.leds = leds; 20 | this.state = this.initialState(); 21 | } 22 | 23 | initialState() { 24 | return [ 25 | [0, 0, 0, 0, 0], 26 | [0, 0, 0, 0, 0], 27 | [0, 0, 0, 0, 0], 28 | [0, 0, 0, 0, 0], 29 | [0, 0, 0, 0, 0], 30 | ]; 31 | } 32 | 33 | /** 34 | * This is only used for panic. HAL interactions are via setPixel. 35 | */ 36 | show(image: Array>) { 37 | for (let y = 0; y < 5; ++y) { 38 | for (let x = 0; x < 5; ++x) { 39 | this.state[x][y] = clamp(image[y][x], 0, 9); 40 | } 41 | } 42 | this.render(); 43 | } 44 | 45 | clear() { 46 | this.state = this.initialState(); 47 | this.render(); 48 | } 49 | 50 | setPixel(x: number, y: number, value: number) { 51 | value = clamp(value, 0, 9); 52 | this.state[x][y] = value; 53 | this.render(); 54 | } 55 | 56 | getPixel(x: number, y: number) { 57 | return this.state[x][y]; 58 | } 59 | 60 | render() { 61 | for (let x = 0; x < 5; ++x) { 62 | for (let y = 0; y < 5; ++y) { 63 | const on = this.state[x][y]; 64 | const led = this.leds[x * 5 + y]; 65 | if (on) { 66 | const bright = brightMap[this.state[x][y]]; 67 | led.style.display = "inline"; 68 | led.style.opacity = (bright / 255).toString(); 69 | } else { 70 | led.style.display = "none"; 71 | } 72 | } 73 | } 74 | } 75 | 76 | boardStopped() { 77 | this.clear(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/board/fs.ts: -------------------------------------------------------------------------------- 1 | // Size as per C implementation. 2 | const maxSize = 31.5 * 1024; 3 | 4 | export class FileSystem { 5 | // Each entry is an FsFile object. The indexes are used as identifiers. 6 | // When a file is deleted the entry becomes ['', null] and can be reused. 7 | private _content: Array = []; 8 | private _size = 0; 9 | 10 | create(name: string) { 11 | let free_idx = -1; 12 | for (let idx = 0; idx < this._content.length; ++idx) { 13 | const entry = this._content[idx]; 14 | if (entry === null) { 15 | free_idx = idx; 16 | } else if (entry.name === name) { 17 | // Truncate existing file and return it. 18 | entry.truncate(); 19 | return idx; 20 | } 21 | } 22 | if (free_idx < 0) { 23 | // Add a new file and return it. 24 | this._content.push(new FsFile(name)); 25 | return this._content.length - 1; 26 | } else { 27 | // Reuse existing slot for the new file. 28 | this._content[free_idx] = new FsFile(name); 29 | return free_idx; 30 | } 31 | } 32 | 33 | find(name: string) { 34 | for (let idx = 0; idx < this._content.length; ++idx) { 35 | if (this._content[idx]?.name === name) { 36 | return idx; 37 | } 38 | } 39 | return -1; 40 | } 41 | 42 | name(idx: number) { 43 | const file = this._content[idx]; 44 | return file ? file.name : undefined; 45 | } 46 | 47 | size(idx: number) { 48 | const file = this._content[idx]; 49 | if (!file) { 50 | throw new Error("File must exist"); 51 | } 52 | return file.size(); 53 | } 54 | 55 | remove(idx: number) { 56 | const file = this._content[idx]; 57 | if (file) { 58 | this._size -= file.size(); 59 | this._content[idx] = null; 60 | } 61 | } 62 | 63 | readbyte(idx: number, offset: number) { 64 | const file = this._content[idx]; 65 | return file ? file.readbyte(offset) : -1; 66 | } 67 | 68 | write(idx: number, data: Uint8Array, force: boolean = false): boolean { 69 | const file = this._content[idx]; 70 | if (!file) { 71 | throw new Error("File must exist"); 72 | } 73 | if (!force && this._size + data.length > maxSize) { 74 | return false; 75 | } 76 | this._size += data.length; 77 | file.append(data); 78 | return true; 79 | } 80 | 81 | clear() { 82 | for (let idx = 0; idx < this._content.length; ++idx) { 83 | this.remove(idx); 84 | } 85 | } 86 | 87 | toString() { 88 | return this._content.toString(); 89 | } 90 | } 91 | 92 | const EMPTY_ARRAY = new Uint8Array(0); 93 | 94 | class FsFile { 95 | constructor(public name: string, private buffer: Uint8Array = EMPTY_ARRAY) {} 96 | readbyte(offset: number) { 97 | if (offset < this.buffer.length) { 98 | return this.buffer[offset]; 99 | } 100 | return -1; 101 | } 102 | append(data: Uint8Array) { 103 | const updated = new Uint8Array(this.buffer.length + data.length); 104 | updated.set(this.buffer); 105 | updated.set(data, this.buffer.length); 106 | this.buffer = updated; 107 | } 108 | truncate() { 109 | this.buffer = EMPTY_ARRAY; 110 | } 111 | size() { 112 | return this.buffer.length; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/board/microphone.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_HIGH, 3 | MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_LOW, 4 | } from "./constants"; 5 | import { RangeSensor, State } from "./state"; 6 | 7 | type SoundLevelCallback = (v: number) => void; 8 | 9 | export class Microphone { 10 | public soundLevel: RangeSensor = new RangeSensor( 11 | "soundLevel", 12 | 0, 13 | 255, 14 | 0, 15 | undefined, 16 | 75, 17 | 150 18 | ); 19 | private soundLevelCallback: SoundLevelCallback | undefined; 20 | 21 | constructor( 22 | private element: SVGElement, 23 | private onChange: (changes: Partial) => void 24 | ) {} 25 | 26 | microphoneOn() { 27 | this.element.style.display = "unset"; 28 | } 29 | 30 | private microphoneOff() { 31 | this.element.style.display = "none"; 32 | } 33 | 34 | setThreshold(threshold: "low" | "high", value: number) { 35 | const proposed = value > 255 ? 255 : value < 0 ? 0 : value; 36 | if (threshold === "low") { 37 | this.soundLevel.lowThreshold = proposed; 38 | } else { 39 | this.soundLevel.highThreshold = proposed; 40 | } 41 | this.onChange({ 42 | soundLevel: this.soundLevel, 43 | }); 44 | } 45 | 46 | setValue(value: number) { 47 | const prev = this.soundLevel.value; 48 | const curr = value; 49 | this.soundLevel.value = value; 50 | 51 | const low = this.soundLevel.lowThreshold!; 52 | const high = this.soundLevel.highThreshold!; 53 | if (this.soundLevelCallback) { 54 | if (prev > low && curr <= low) { 55 | this.soundLevelCallback(MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_LOW); 56 | } else if (prev < high && curr >= high!) { 57 | this.soundLevelCallback(MICROBIT_HAL_MICROPHONE_EVT_THRESHOLD_HIGH); 58 | } 59 | } 60 | } 61 | 62 | initializeCallbacks(soundLevelCallback: (v: number) => void) { 63 | this.soundLevelCallback = soundLevelCallback; 64 | } 65 | 66 | boardStopped() { 67 | this.microphoneOff(); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/board/pins.ts: -------------------------------------------------------------------------------- 1 | import { RangeSensor, State } from "./state"; 2 | 3 | export interface Pin { 4 | state: RangeSensor; 5 | 6 | updateTranslations(): void; 7 | 8 | setValue(value: any): void; 9 | 10 | isTouched(): boolean; 11 | 12 | boardStopped(): void; 13 | 14 | setAnalogPeriodUs(period: number): number; 15 | 16 | getAnalogPeriodUs(): number; 17 | } 18 | 19 | const initialAnalogPeriodUs = -1; 20 | 21 | abstract class BasePin implements Pin { 22 | state: RangeSensor; 23 | 24 | // For now we just allow get/set of this value 25 | // but don't do anything with it. 26 | private analogPeriodUs: number = initialAnalogPeriodUs; 27 | 28 | constructor(id: string) { 29 | this.state = new RangeSensor(id, 0, 1, 0, undefined); 30 | } 31 | 32 | updateTranslations() {} 33 | 34 | setValue(value: any): void { 35 | this.state.setValue(value); 36 | } 37 | 38 | setAnalogPeriodUs(period: number) { 39 | this.analogPeriodUs = period; 40 | return 0; 41 | } 42 | 43 | getAnalogPeriodUs() { 44 | return this.analogPeriodUs; 45 | } 46 | 47 | isTouched(): boolean { 48 | return false; 49 | } 50 | 51 | boardStopped() { 52 | this.analogPeriodUs = initialAnalogPeriodUs; 53 | } 54 | } 55 | 56 | export class StubPin extends BasePin {} 57 | 58 | export class TouchPin extends BasePin { 59 | private _mouseDown: boolean = false; 60 | 61 | private keyListener: (e: KeyboardEvent) => void; 62 | private mouseDownListener: (e: MouseEvent) => void; 63 | private touchStartListener: (e: TouchEvent) => void; 64 | private mouseUpTouchEndListener: (e: MouseEvent | TouchEvent) => void; 65 | private mouseLeaveListener: (e: MouseEvent) => void; 66 | 67 | constructor( 68 | private id: "pin0" | "pin1" | "pin2" | "pinLogo", 69 | private ui: { element: SVGElement; label: () => string } | null, 70 | private onChange: (changes: Partial) => void 71 | ) { 72 | super(id); 73 | 74 | if (this.ui) { 75 | const { element, label } = this.ui; 76 | element.setAttribute("role", "button"); 77 | element.setAttribute("tabindex", "0"); 78 | element.style.cursor = "pointer"; 79 | } 80 | 81 | this.keyListener = (e) => { 82 | switch (e.key) { 83 | case "Enter": 84 | case " ": 85 | e.preventDefault(); 86 | if (e.type === "keydown") { 87 | this.press(); 88 | } else { 89 | this.release(); 90 | } 91 | } 92 | }; 93 | 94 | this.mouseDownListener = (e) => { 95 | e.preventDefault(); 96 | this.mouseDownTouchStartAction(); 97 | }; 98 | this.touchStartListener = (e) => { 99 | this.mouseDownTouchStartAction(); 100 | }; 101 | this.mouseUpTouchEndListener = (e) => { 102 | e.preventDefault(); 103 | if (this._mouseDown) { 104 | this._mouseDown = false; 105 | this.release(); 106 | } 107 | }; 108 | this.mouseLeaveListener = (e) => { 109 | if (this._mouseDown) { 110 | this._mouseDown = false; 111 | this.release(); 112 | } 113 | }; 114 | 115 | if (this.ui) { 116 | const { element } = this.ui; 117 | element.addEventListener("mousedown", this.mouseDownListener); 118 | element.addEventListener("touchstart", this.touchStartListener); 119 | element.addEventListener("mouseup", this.mouseUpTouchEndListener); 120 | element.addEventListener("touchend", this.mouseUpTouchEndListener); 121 | element.addEventListener("keydown", this.keyListener); 122 | element.addEventListener("keyup", this.keyListener); 123 | element.addEventListener("mouseleave", this.mouseLeaveListener); 124 | } 125 | } 126 | 127 | setValue(value: any) { 128 | this.setValueInternal(value, false); 129 | } 130 | 131 | private setValueInternal(value: any, internalChange: boolean) { 132 | super.setValue(value); 133 | 134 | if (internalChange) { 135 | this.onChange({ 136 | [this.id]: this.state, 137 | }); 138 | } 139 | this.render(); 140 | } 141 | 142 | private mouseDownTouchStartAction() { 143 | this._mouseDown = true; 144 | this.press(); 145 | } 146 | 147 | press() { 148 | this.setValueInternal( 149 | this.state.value === this.state.min ? this.state.max : this.state.min, 150 | true 151 | ); 152 | } 153 | 154 | release() { 155 | this.setValueInternal( 156 | this.state.value === this.state.max ? this.state.min : this.state.max, 157 | true 158 | ); 159 | } 160 | 161 | isTouched() { 162 | return !!this.state.value; 163 | } 164 | 165 | updateTranslations() { 166 | if (this.ui) { 167 | this.ui.element.ariaLabel = this.ui.label(); 168 | } 169 | } 170 | 171 | render() { 172 | if (this.ui) { 173 | const fill = !!this.state.value ? "#00c800" : "url(#an)"; 174 | this.ui.element.querySelectorAll("path").forEach((p) => { 175 | if (!p.classList.contains("no-edit")) { 176 | p.style.fill = fill; 177 | } 178 | }); 179 | } 180 | } 181 | 182 | boardStopped() {} 183 | } 184 | -------------------------------------------------------------------------------- /src/board/radio.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { Radio } from "./radio"; 3 | 4 | const encoder = new TextEncoder(); 5 | const msg = encoder.encode("hello"); 6 | const altMsg = encoder.encode("goodbye"); 7 | const longMsg = encoder.encode( 8 | "This is a message that is longer than 32 bytes" 9 | ); 10 | // Truncated to 32 bytes - 3 bytes for header info. 11 | const longMsgTruncated = encoder.encode("This is a message that is lon"); 12 | 13 | describe("Radio", () => { 14 | let time = 0; 15 | let sentMessages: Uint8Array[] = []; 16 | const currentTime = () => time++; 17 | const onSend = (data: Uint8Array) => sentMessages.push(data); 18 | let onChange = vi.fn(); 19 | let radio = new Radio(onSend, onChange, currentTime); 20 | 21 | afterEach(() => { 22 | time = 0; 23 | sentMessages = []; 24 | onChange = vi.fn(); 25 | radio = new Radio(onSend, onChange, currentTime); 26 | }); 27 | 28 | beforeEach(() => { 29 | radio.enable({ 30 | maxPayload: 32, 31 | queue: 3, 32 | group: 1, 33 | }); 34 | }); 35 | 36 | it("sends messages", () => { 37 | expect(sentMessages.length).toEqual(0); 38 | radio.send(msg); 39 | expect(sentMessages.length).toEqual(1); 40 | }); 41 | 42 | it("handles receiving user messages", () => { 43 | radio.receive(msg); 44 | expect(radio.peek()!.join("")).toContain(msg.join("")); 45 | radio.pop(); 46 | expect(radio.peek()).toBeUndefined(); 47 | }); 48 | 49 | it("enables the radio with the correct config", () => { 50 | expect(radio.state).toEqual({ 51 | type: "radio", 52 | enabled: true, 53 | group: 1, 54 | }); 55 | }); 56 | 57 | it("disables the radio", () => { 58 | radio.boardStopped(); 59 | expect(radio.state).toEqual({ 60 | type: "radio", 61 | enabled: false, 62 | group: 0, 63 | }); 64 | }); 65 | 66 | it("receives a message that is too big and truncates it appropriately", () => { 67 | radio.receive(longMsg); 68 | expect(radio.peek()!.join("")).not.toContain(longMsg.join("")); 69 | expect(radio.peek()!.join("")).toContain(longMsgTruncated.join("")); 70 | }); 71 | 72 | it("handles the message queue correctly", () => { 73 | radio.receive(msg); 74 | radio.receive(msg); 75 | radio.receive(altMsg); 76 | // No more messages can be received based on the queue length set in config. 77 | radio.receive(msg); 78 | expect(radio.peek()!.join("")).toContain(msg.join("")); 79 | radio.pop(); 80 | expect(radio.peek()!.join("")).toContain(msg.join("")); 81 | radio.pop(); 82 | expect(radio.peek()!.join("")).toContain(altMsg.join("")); 83 | radio.pop(); 84 | // Confirm that fourth message was not added to the queue. 85 | expect(radio.peek()).toBeUndefined(); 86 | }); 87 | 88 | it("updates the config group without clearing receive queue", () => { 89 | radio.receive(msg); 90 | radio.receive(msg); 91 | radio.receive(altMsg); 92 | radio.updateConfig({ 93 | maxPayload: 32, 94 | queue: 3, 95 | group: 2, 96 | }); 97 | expect(radio.state).toEqual({ 98 | type: "radio", 99 | enabled: true, 100 | group: 2, 101 | }); 102 | expect(radio.peek()!.join("")).toContain(msg.join("")); 103 | radio.pop(); 104 | expect(radio.peek()!.join("")).toContain(msg.join("")); 105 | radio.pop(); 106 | expect(radio.peek()!.join("")).toContain(altMsg.join("")); 107 | }); 108 | 109 | it("throws an error if maxPayload or queue are updated without disabling the radio first", () => { 110 | expect(() => { 111 | radio.updateConfig({ 112 | maxPayload: 64, 113 | queue: 6, 114 | group: 2, 115 | }); 116 | }).toThrowError( 117 | new Error("If queue or payload change then should call disable/enable.") 118 | ); 119 | }); 120 | 121 | it("updates all config fields successfully", () => { 122 | radio.disable(); 123 | radio.enable({ 124 | maxPayload: 64, 125 | queue: 6, 126 | group: 2, 127 | }); 128 | radio.receive(longMsg); 129 | // Long message over 32 bytes, but under 64 bytes can now be received in its entirety. 130 | expect(radio.peek()!.join("")).toContain(longMsg.join("")); 131 | }); 132 | }); 133 | -------------------------------------------------------------------------------- /src/board/radio.ts: -------------------------------------------------------------------------------- 1 | import { RadioState, State } from "./state"; 2 | 3 | export interface RadioConfig { 4 | maxPayload: number; 5 | queue: number; 6 | group: number; 7 | } 8 | 9 | export class Radio { 10 | private rxQueue: Uint8Array[] | undefined; 11 | private config: RadioConfig | undefined; 12 | state: RadioState = { type: "radio", enabled: false, group: 0 }; 13 | 14 | constructor( 15 | private onSend: (data: Uint8Array) => void, 16 | private onChange: (changes: Partial) => void, 17 | private ticksMilliseconds: () => number 18 | ) {} 19 | 20 | peek(): Uint8Array | undefined { 21 | return this.rxQueue![0]; 22 | } 23 | 24 | pop() { 25 | this.rxQueue!.shift(); 26 | } 27 | 28 | send(data: Uint8Array) { 29 | this.onSend(data); 30 | } 31 | 32 | receive(data: Uint8Array) { 33 | if (this.rxQueue!.length === this.config!.queue) { 34 | // Drop the message as the queue is full. 35 | } else { 36 | // Truncate at the payload size as we're writing to a fixed size buffer. 37 | const len = Math.min(data.length, this.config!.maxPayload); 38 | data = data.slice(0, len); 39 | // Add extra information to make a radio packet in the expected format 40 | // rather than just data. Clients must prepend \x01\x00\x01 if desired. 41 | const size = 42 | 1 + // len 43 | len + 44 | 1 + // RSSI 45 | 4; // time 46 | const rssi = 127; // This is inverted by modradio. 47 | const time = this.ticksMilliseconds(); 48 | 49 | const packet = new Uint8Array(size); 50 | packet[0] = len; 51 | packet.set(data, 1); 52 | packet[1 + len] = rssi; 53 | packet[1 + len + 1] = time & 0xff; 54 | packet[1 + len + 2] = (time >> 8) & 0xff; 55 | packet[1 + len + 3] = (time >> 16) & 0xff; 56 | packet[1 + len + 4] = (time >> 24) & 0xff; 57 | 58 | this.rxQueue!.push(packet); 59 | } 60 | } 61 | 62 | updateConfig(config: RadioConfig) { 63 | // This needs to just change the config, not trash the receive queue. 64 | // This is somewhat odd as you can have a message in the queue from 65 | // a different radio group. 66 | if ( 67 | !this.config || 68 | config.queue !== this.config.queue || 69 | config.maxPayload !== this.config.maxPayload 70 | ) { 71 | throw new Error( 72 | "If queue or payload change then should call disable/enable." 73 | ); 74 | } 75 | 76 | if (this.state.group !== config.group) { 77 | this.state.group = config.group; 78 | this.onChange({ 79 | radio: this.state, 80 | }); 81 | } 82 | } 83 | 84 | enable(config: RadioConfig) { 85 | this.config = config; 86 | this.rxQueue = []; 87 | if (!this.state.enabled) { 88 | this.state = { 89 | ...this.state, 90 | enabled: true, 91 | group: config.group, 92 | }; 93 | this.onChange({ 94 | radio: this.state, 95 | }); 96 | } 97 | } 98 | 99 | disable() { 100 | this.rxQueue = undefined; 101 | 102 | if (this.state.enabled) { 103 | this.state.enabled = false; 104 | this.onChange({ 105 | radio: this.state, 106 | }); 107 | } 108 | } 109 | 110 | boardStopped() { 111 | this.rxQueue = undefined; 112 | this.config = undefined; 113 | this.state = { 114 | type: "radio", 115 | enabled: false, 116 | group: 0, 117 | }; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/board/state.ts: -------------------------------------------------------------------------------- 1 | export interface RadioState { 2 | type: "radio"; 3 | enabled: boolean; 4 | group: number; 5 | } 6 | 7 | export interface DataLoggingState { 8 | type: "dataLogging"; 9 | logFull: boolean; 10 | } 11 | 12 | export interface State { 13 | radio: RadioState; 14 | 15 | dataLogging: DataLoggingState; 16 | 17 | accelerometerX: RangeSensor; 18 | accelerometerY: RangeSensor; 19 | accelerometerZ: RangeSensor; 20 | gesture: EnumSensor; 21 | 22 | compassX: RangeSensor; 23 | compassY: RangeSensor; 24 | compassZ: RangeSensor; 25 | compassHeading: RangeSensor; 26 | 27 | pin0: RangeSensor; 28 | pin1: RangeSensor; 29 | pin2: RangeSensor; 30 | pinLogo: RangeSensor; 31 | 32 | temperature: RangeSensor; 33 | lightLevel: RangeSensor; 34 | soundLevel: RangeSensor; 35 | 36 | buttonA: RangeSensor; 37 | buttonB: RangeSensor; 38 | } 39 | 40 | export abstract class Sensor { 41 | constructor(public type: string, public id: string) {} 42 | 43 | abstract setValue(value: any): void; 44 | 45 | protected valueError(value: any) { 46 | return new Error( 47 | `${this.id} given invalid value: ${JSON.stringify(value)}` 48 | ); 49 | } 50 | } 51 | 52 | export class RangeSensor extends Sensor { 53 | public value: number; 54 | 55 | constructor( 56 | id: string, 57 | public min: number, 58 | public max: number, 59 | initial: number, 60 | public unit: string | undefined, 61 | public lowThreshold?: number, 62 | public highThreshold?: number 63 | ) { 64 | super("range", id); 65 | this.value = initial; 66 | } 67 | 68 | setValue(value: any): void { 69 | let proposed: number; 70 | if (typeof value === "number") { 71 | proposed = value; 72 | } else if (typeof value === "string") { 73 | try { 74 | proposed = parseInt(value, 10); 75 | } catch (e) { 76 | throw this.valueError(value); 77 | } 78 | } else { 79 | throw this.valueError(value); 80 | } 81 | if (proposed > this.max || proposed < this.min) { 82 | throw this.valueError(value); 83 | } 84 | this.value = proposed; 85 | } 86 | } 87 | 88 | export class EnumSensor extends Sensor { 89 | public value: string; 90 | 91 | constructor(id: string, public choices: string[], initial: string) { 92 | super("enum", id); 93 | this.id = id; 94 | this.choices = choices; 95 | this.value = initial; 96 | } 97 | 98 | setValue(value: any): void { 99 | if (typeof value !== "string" || !this.choices.includes(value)) { 100 | throw this.valueError(value); 101 | } 102 | this.value = value; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/board/svg.d.ts: -------------------------------------------------------------------------------- 1 | // We use esbuild's text loader to allow us to bundle the svg. 2 | declare module "*.svg"; 3 | -------------------------------------------------------------------------------- /src/board/util.ts: -------------------------------------------------------------------------------- 1 | export function clamp(value: number, min: number, max: number) { 2 | if (value < min) { 3 | return min; 4 | } 5 | if (value > max) { 6 | return max; 7 | } 8 | return value; 9 | } 10 | -------------------------------------------------------------------------------- /src/board/wasm.ts: -------------------------------------------------------------------------------- 1 | import { Board } from "."; 2 | import * as conversions from "./conversions"; 3 | import { FileSystem } from "./fs"; 4 | 5 | export interface EmscriptenModule { 6 | cwrap: any; 7 | ExitStatus: Error; 8 | 9 | // See EXPORTED_FUNCTIONS in the Makefile. 10 | _mp_js_request_stop(): void; 11 | _mp_js_force_stop(): void; 12 | _microbit_hal_audio_ready_callback(): void; 13 | _microbit_hal_audio_speech_ready_callback(): void; 14 | _microbit_hal_gesture_callback(gesture: number): void; 15 | _microbit_hal_level_detector_callback(level: number): void; 16 | _microbit_radio_rx_buffer(): number; 17 | 18 | HEAPU8: Uint8Array; 19 | 20 | // Added by us at module creation time for jshal to access. 21 | board: Board; 22 | fs: FileSystem; 23 | conversions: typeof conversions; 24 | } 25 | 26 | export class ModuleWrapper { 27 | private main: () => Promise; 28 | 29 | constructor(private module: EmscriptenModule) { 30 | const main = module.cwrap("mp_js_main", "null", ["number"], { 31 | async: true, 32 | }); 33 | this.main = () => main(64 * 1024); 34 | } 35 | 36 | /** 37 | * Throws PanicError if MicroPython panics. 38 | */ 39 | async start(): Promise { 40 | return this.main!(); 41 | } 42 | 43 | requestStop(): void { 44 | this.module._mp_js_request_stop(); 45 | } 46 | 47 | forceStop(): void { 48 | this.module._mp_js_force_stop(); 49 | } 50 | 51 | writeRadioRxBuffer(packet: Uint8Array) { 52 | const buf = this.module._microbit_radio_rx_buffer!(); 53 | this.module.HEAPU8.set(packet, buf); 54 | return buf; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MicroPython-micro:bit simulator example embedding 5 | 63 | 64 | 65 |
66 |

MicroPython-micro:bit simulator example embedding

67 |
68 |
69 | 77 |
78 |
79 |
80 |
81 |
82 | 106 | 114 |
115 | 116 | 117 | 118 | 119 |
120 |
121 |
122 |
123 |
124 | 125 | 345 | 346 | 347 | -------------------------------------------------------------------------------- /src/drv_radio.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | #include "py/runtime.h" 28 | #include "drv_radio.h" 29 | #include "jshal.h" 30 | 31 | // Format: 32 | // 1 byte for len 33 | // N data bytes as per len 34 | // 1 byte RSSI 35 | // 4 bytes time 36 | #define RADIO_PACKET_OVERHEAD (1 + 1 + 4) 37 | 38 | static uint8_t rx_buf_size = 0; 39 | 40 | void microbit_radio_enable(microbit_radio_config_t *config) { 41 | microbit_radio_disable(); 42 | 43 | uint8_t group = config->prefix0; 44 | mp_js_radio_enable(group, config->max_payload, config->queue_len); 45 | 46 | // We have an rx buffer of size 1, the queue itself is in the JavaScript. 47 | rx_buf_size = config->max_payload + RADIO_PACKET_OVERHEAD; 48 | MP_STATE_PORT(radio_buf) = m_new(uint8_t, rx_buf_size); 49 | } 50 | 51 | void microbit_radio_disable(void) { 52 | mp_js_radio_disable(); 53 | 54 | // free any old buffers 55 | if (MP_STATE_PORT(radio_buf) != NULL) { 56 | m_del(uint8_t, MP_STATE_PORT(radio_buf), rx_buf_size); 57 | MP_STATE_PORT(radio_buf) = NULL; 58 | rx_buf_size = 0; 59 | } 60 | } 61 | 62 | // Exposed so JavaScript can write directly into the max_payload sized buffer. 63 | uint8_t *microbit_radio_rx_buffer() { 64 | return MP_STATE_PORT(radio_buf); 65 | } 66 | 67 | void microbit_radio_update_config(microbit_radio_config_t *config) { 68 | // This is not called if the max_payload or queue length change. 69 | // Instead we are disabled then enabled. 70 | uint8_t group = config->prefix0; 71 | mp_js_radio_update_config(group, config->max_payload, config->queue_len); 72 | } 73 | 74 | // This assumes the radio is enabled. 75 | void microbit_radio_send(const void *buf, size_t len, const void *buf2, size_t len2) { 76 | mp_js_radio_send(buf, len, buf2, len2); 77 | } 78 | 79 | const uint8_t *microbit_radio_peek(void) { 80 | // This call writes to the rx buffer. 81 | return mp_js_radio_peek(); 82 | } 83 | 84 | void microbit_radio_pop(void) { 85 | mp_js_radio_pop(); 86 | } 87 | -------------------------------------------------------------------------------- /src/environment.ts: -------------------------------------------------------------------------------- 1 | export type Stage = "local" | "REVIEW" | "STAGING" | "PRODUCTION"; 2 | 3 | export const stage = (process.env.STAGE || "local") as Stage; 4 | -------------------------------------------------------------------------------- /src/examples/accelerometer.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | last = None 4 | while True: 5 | current = accelerometer.current_gesture() 6 | if current != last: 7 | last = current 8 | print(current) 9 | -------------------------------------------------------------------------------- /src/examples/audio.py: -------------------------------------------------------------------------------- 1 | # micro:bit demo, playing a sine wave using AudioFrame's 2 | # This doesn't sound great in the sim or on V2. 3 | # The time per frame is significantly higher in the sim. 4 | 5 | import math 6 | import audio 7 | from microbit import pin0, running_time 8 | 9 | print("frames should take {} ms to play".format(32 / 7812.5 * 1000)) 10 | 11 | 12 | def repeated_frame(frame, count): 13 | for i in range(count): 14 | yield frame 15 | 16 | 17 | frame = audio.AudioFrame() 18 | nframes = 100 19 | 20 | for freq in range(2, 17, 2): 21 | l = len(frame) 22 | for i in range(l): 23 | frame[i] = int(127 * (math.sin(math.pi * i / l * freq) + 1)) 24 | print("play tone at frequency {}".format(freq * 7812.5 / 32)) 25 | t0 = running_time() 26 | audio.play(repeated_frame(frame, nframes), wait=True, pin=pin0) 27 | dt_ms = running_time() - t0 28 | print("{} frames took {} ms = {} ms/frame".format(nframes, dt_ms, dt_ms / nframes)) 29 | -------------------------------------------------------------------------------- /src/examples/background.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | import music 3 | 4 | music.play(['f', 'a', 'c', 'e'], wait=False) 5 | display.scroll("Music and scrolling in the background", wait=False) 6 | print("This should be printed before the scroll and play complete") -------------------------------------------------------------------------------- /src/examples/buttons.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | while True: 4 | if button_a.is_pressed(): 5 | print("A") 6 | if button_b.was_pressed(): 7 | print("B") 8 | print(button_b.get_presses()) 9 | break -------------------------------------------------------------------------------- /src/examples/compass.py: -------------------------------------------------------------------------------- 1 | # Imports go at the top 2 | from microbit import * 3 | 4 | # Code in a 'while True:' loop repeats forever 5 | while True: 6 | print("x: ", compass.get_x()) 7 | print("y: ", compass.get_y()) 8 | print("z: ", compass.get_z()) 9 | print("heading: ", compass.heading()) 10 | print("field strength: ", compass.get_field_strength()) 11 | sleep(1000) 12 | -------------------------------------------------------------------------------- /src/examples/data_logging.py: -------------------------------------------------------------------------------- 1 | # Imports go at the top 2 | from microbit import * 3 | import log 4 | 5 | 6 | # Code in a 'while True:' loop repeats forever 7 | while True: 8 | log.set_labels('temperature', 'sound', 'light') 9 | log.add({ 10 | 'temperature': temperature(), 11 | 'sound': microphone.sound_level(), 12 | 'light': display.read_light_level() 13 | }) 14 | sleep(1000) 15 | -------------------------------------------------------------------------------- /src/examples/display.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | display.scroll("Hello") 4 | display.scroll("World", wait=False) 5 | for _ in range(0, 50): 6 | print(display.get_pixel(2, 2)) 7 | sleep(100) 8 | display.show(Image.HEART) 9 | sleep(1000) 10 | display.clear() -------------------------------------------------------------------------------- /src/examples/inline_assembler.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | # Unsupported in the simulator 4 | @micropython.asm_thumb 5 | def asm_add(r0, r1): 6 | add(r0, r0, r1) 7 | 8 | while True: 9 | if button_a.was_pressed(): 10 | print(1 + 2) 11 | elif button_b.was_pressed(): 12 | print(asm_add(1, 2)) -------------------------------------------------------------------------------- /src/examples/microphone.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | while True: 4 | print(microphone.sound_level()) 5 | if microphone.was_event(SoundEvent.LOUD): 6 | display.show(Image.HAPPY) 7 | elif microphone.current_event() == SoundEvent.QUIET: 8 | display.show(Image.ASLEEP) 9 | else: 10 | display.clear() 11 | sleep(500) 12 | -------------------------------------------------------------------------------- /src/examples/music.py: -------------------------------------------------------------------------------- 1 | import music 2 | 3 | music.play(['f', 'a', 'c', 'e']) -------------------------------------------------------------------------------- /src/examples/pin_logo.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | while True: 4 | while pin_logo.is_touched(): 5 | display.show(Image.HAPPY) 6 | display.clear() 7 | -------------------------------------------------------------------------------- /src/examples/radio.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | import radio 3 | 4 | 5 | radio.on(); 6 | while True: 7 | message = radio.receive_bytes() 8 | if message: 9 | print(message) 10 | -------------------------------------------------------------------------------- /src/examples/random.py: -------------------------------------------------------------------------------- 1 | from microbit import display, sleep 2 | import random 3 | import machine 4 | 5 | 6 | n = random.randrange(0, 100) 7 | print("Default seed ", n) 8 | random.seed(1) 9 | n = random.randrange(0, 100) 10 | print("Fixed seed ", n) 11 | print() 12 | sleep(2000) 13 | machine.reset() 14 | -------------------------------------------------------------------------------- /src/examples/sensors.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | while True: 4 | print(display.read_light_level()) 5 | sleep(1000); -------------------------------------------------------------------------------- /src/examples/sound_effects_builtin.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | # You don't hear this one because we don't wait then play another which stops this one. 4 | display.show(Image.HAPPY) 5 | audio.play(Sound.GIGGLE, wait=False) 6 | audio.play(Sound.GIGGLE) 7 | display.show(Image.SAD) # This doesn't happen until the giggling is over. 8 | audio.play(Sound.SAD) 9 | -------------------------------------------------------------------------------- /src/examples/sound_effects_user.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | 3 | # Create a Sound Effect and immediately play it 4 | audio.play( 5 | audio.SoundEffect( 6 | freq_start=400, 7 | freq_end=2000, 8 | duration=500, 9 | vol_start=100, 10 | vol_end=255, 11 | waveform=audio.SoundEffect.WAVEFORM_TRIANGLE, 12 | fx=audio.SoundEffect.FX_VIBRATO, 13 | shape=audio.SoundEffect.SHAPE_LOG, 14 | ) 15 | ) 16 | -------------------------------------------------------------------------------- /src/examples/speech.py: -------------------------------------------------------------------------------- 1 | import speech 2 | import time 3 | 4 | t = time.ticks_ms() 5 | speech.pronounce(' /HEHLOW WERLD') 6 | print(time.ticks_diff(time.ticks_ms(), t)) -------------------------------------------------------------------------------- /src/examples/stack_size.py: -------------------------------------------------------------------------------- 1 | def g(): 2 | global depth 3 | depth += 1 4 | g() 5 | depth = 0 6 | try: 7 | g() 8 | except RuntimeError: 9 | pass 10 | print('maximum recursion depth g():', depth) 11 | -------------------------------------------------------------------------------- /src/examples/volume.py: -------------------------------------------------------------------------------- 1 | from microbit import * 2 | import music 3 | 4 | 5 | music.pitch(440, wait=False) 6 | while True: 7 | # Conveniently both 0..255. 8 | set_volume(display.read_light_level()) -------------------------------------------------------------------------------- /src/flags.ts: -------------------------------------------------------------------------------- 1 | import { Stage, stage as stageFromEnvironment } from "./environment"; 2 | 3 | /** 4 | * A union of the flag names (alphabetical order). 5 | */ 6 | export type Flag = 7 | /** 8 | * Enables service worker registration. 9 | * 10 | * Registers the service worker and enables offline use. 11 | */ 12 | "sw"; 13 | 14 | interface FlagMetadata { 15 | defaultOnStages: Stage[]; 16 | name: Flag; 17 | } 18 | 19 | const allFlags: FlagMetadata[] = [{ name: "sw", defaultOnStages: [] }]; 20 | 21 | type Flags = Record; 22 | 23 | const flagsForParams = (stage: Stage, params: URLSearchParams) => { 24 | const enableFlags = new Set(params.getAll("flag")); 25 | const allFlagsDefault = enableFlags.has("none") 26 | ? false 27 | : enableFlags.has("*") 28 | ? true 29 | : undefined; 30 | return Object.fromEntries( 31 | allFlags.map((f) => [ 32 | f.name, 33 | isEnabled(f, stage, allFlagsDefault, enableFlags.has(f.name)), 34 | ]) 35 | ) as Flags; 36 | }; 37 | 38 | const isEnabled = ( 39 | f: FlagMetadata, 40 | stage: Stage, 41 | allFlagsDefault: boolean | undefined, 42 | thisFlagOn: boolean 43 | ): boolean => { 44 | if (thisFlagOn) { 45 | return true; 46 | } 47 | if (allFlagsDefault !== undefined) { 48 | return allFlagsDefault; 49 | } 50 | return f.defaultOnStages.includes(stage); 51 | }; 52 | 53 | export const flags: Flags = (() => { 54 | const params = new URLSearchParams(window.location.search); 55 | return flagsForParams(stageFromEnvironment, params); 56 | })(); 57 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MicroPython-micro:bit simulator 5 | 6 | 17 | 18 | 19 |
20 |

MicroPython-micro:bit simulator

21 |

22 | This site hosts a simulator designed to be embedded in other 23 | application. Please see the 24 | developer documentation on GitHub 28 |

29 |

Try it out the example embedding.

30 |
31 | 32 | 33 | -------------------------------------------------------------------------------- /src/jshal.d.ts: -------------------------------------------------------------------------------- 1 | import { EmscriptenModule } from "./board/wasm"; 2 | 3 | global { 4 | // In reality this is a local variable as jshal.js is splatted into the generated code. 5 | declare const Module: EmscriptenModule; 6 | declare const LibraryManager: { 7 | library: any; 8 | }; 9 | 10 | // Just what we need. There are lots more Emscripten helpers available. 11 | declare function UTF8ToString(ptr: number, len?: number); 12 | declare function stringToUTF8(s: string, buf: number, len: number); 13 | declare function lengthBytesUTF8(s: string); 14 | declare function mergeInto(library: any, functions: Record); 15 | } 16 | -------------------------------------------------------------------------------- /src/jshal.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | void mp_js_hal_init(void); 28 | void mp_js_hal_deinit(void); 29 | 30 | uint32_t mp_js_rng_generate_random_word(); 31 | 32 | uint32_t mp_js_hal_ticks_ms(void); 33 | void mp_js_hal_stdout_tx_strn(const char *ptr, size_t len); 34 | int mp_js_hal_stdin_pop_char(void); 35 | 36 | int mp_js_hal_filesystem_find(const char *name, size_t len); 37 | int mp_js_hal_filesystem_create(const char *name, size_t len); 38 | int mp_js_hal_filesystem_name(int idx, char *buf); 39 | int mp_js_hal_filesystem_size(int idx); 40 | void mp_js_hal_filesystem_remove(int idx); 41 | int mp_js_hal_filesystem_readbyte(int idx, size_t offset); 42 | bool mp_js_hal_filesystem_write(int idx, const char *buf, size_t len); 43 | 44 | void mp_js_hal_panic(int code); 45 | void mp_js_hal_reset(void); 46 | 47 | int mp_js_hal_temperature(void); 48 | 49 | int mp_js_hal_button_get_presses(int button); 50 | bool mp_js_hal_button_is_pressed(int button); 51 | 52 | bool mp_js_hal_pin_is_touched(int pin); 53 | int mp_js_hal_pin_get_analog_period_us(int pin); 54 | int mp_js_hal_pin_set_analog_period_us(int pin, int period); 55 | 56 | int mp_js_hal_display_get_pixel(int x, int y); 57 | void mp_js_hal_display_set_pixel(int x, int y, int value); 58 | void mp_js_hal_display_clear(void); 59 | int mp_js_hal_display_read_light_level(void); 60 | 61 | int mp_js_hal_accelerometer_get_x(void); 62 | int mp_js_hal_accelerometer_get_y(void); 63 | int mp_js_hal_accelerometer_get_z(void); 64 | int mp_js_hal_accelerometer_get_gesture(void); 65 | void mp_js_hal_accelerometer_set_range(int r); 66 | 67 | int mp_js_hal_compass_get_x(void); 68 | int mp_js_hal_compass_get_y(void); 69 | int mp_js_hal_compass_get_z(void); 70 | int mp_js_hal_compass_get_field_strength(void); 71 | int mp_js_hal_compass_get_heading(void); 72 | 73 | void mp_js_hal_audio_set_volume(int value); 74 | void mp_js_hal_audio_init(uint32_t sample_rate); 75 | void mp_js_hal_audio_write_data(const uint8_t *buf, size_t num_samples); 76 | void mp_js_hal_audio_speech_init(uint32_t sample_rate); 77 | void mp_js_hal_audio_speech_write_data(const uint8_t *buf, size_t num_samples); 78 | void mp_js_hal_audio_period_us(int period); 79 | void mp_js_hal_audio_amplitude_u10(int amplitude); 80 | void mp_js_hal_audio_play_expression(const char *name); 81 | void mp_js_hal_audio_stop_expression(void); 82 | bool mp_js_hal_audio_is_expression_active(void); 83 | 84 | void mp_js_hal_microphone_init(void); 85 | void mp_js_hal_microphone_set_threshold(int kind, int value); 86 | int mp_js_hal_microphone_get_level(void); 87 | 88 | void mp_js_radio_enable(uint8_t group, uint8_t max_payload, uint8_t queue); 89 | void mp_js_radio_disable(void); 90 | void mp_js_radio_update_config(uint8_t group, uint8_t max_payload, uint8_t queue); 91 | void mp_js_radio_send(const void *buf, size_t len, const void *buf2, size_t len2); 92 | uint8_t *mp_js_radio_peek(void); 93 | void mp_js_radio_pop(void); 94 | 95 | void mp_js_hal_log_delete(bool full_erase); 96 | void mp_js_hal_log_set_mirroring(bool serial); 97 | void mp_js_hal_log_set_timestamp(int period); 98 | int mp_js_hal_log_begin_row(void); 99 | int mp_js_hal_log_end_row(void); 100 | int mp_js_hal_log_data(const char *key, const char *value); 101 | -------------------------------------------------------------------------------- /src/jshal.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // @ts-check 28 | /// 29 | 30 | mergeInto(LibraryManager.library, { 31 | mp_js_hal_init: async function () { 32 | Module.board.initialize(); 33 | }, 34 | 35 | mp_js_hal_deinit: function () { 36 | Module.board.stopComponents(); 37 | }, 38 | 39 | mp_js_rng_generate_random_word: function () { 40 | return (Math.random() * 0x100000000) >>> 0; 41 | }, 42 | 43 | mp_js_hal_ticks_ms: function () { 44 | return Module.board.ticksMilliseconds(); 45 | }, 46 | 47 | mp_js_hal_stdin_pop_char: function () { 48 | return Module.board.readSerialInput(); 49 | }, 50 | 51 | mp_js_hal_stdout_tx_strn: function ( 52 | /** @type {number} */ ptr, 53 | /** @type {number} */ len 54 | ) { 55 | Module.board.writeSerialOutput(UTF8ToString(ptr, len)); 56 | }, 57 | 58 | mp_js_hal_filesystem_find: function ( 59 | /** @type {number} */ name, 60 | /** @type {number} */ len 61 | ) { 62 | return Module.fs.find(UTF8ToString(name, len)); 63 | }, 64 | 65 | mp_js_hal_filesystem_create: function ( 66 | /** @type {number} */ name, 67 | /** @type {number} */ len 68 | ) { 69 | const filename = UTF8ToString(name, len); 70 | return Module.fs.create(filename); 71 | }, 72 | 73 | mp_js_hal_filesystem_name: function ( 74 | /** @type {number} */ idx, 75 | /** @type {number} */ buf 76 | ) { 77 | const name = Module.fs.name(idx); 78 | if (name === undefined) { 79 | return -1; 80 | } 81 | const len = lengthBytesUTF8(name); 82 | stringToUTF8(name, buf, len + 1); 83 | return len; 84 | }, 85 | 86 | mp_js_hal_filesystem_size: function (/** @type {number} */ idx) { 87 | return Module.fs.size(idx); 88 | }, 89 | 90 | mp_js_hal_filesystem_remove: function (/** @type {number} */ idx) { 91 | return Module.fs.remove(idx); 92 | }, 93 | 94 | mp_js_hal_filesystem_readbyte: function ( 95 | /** @type {number} */ idx, 96 | /** @type {number} */ offset 97 | ) { 98 | return Module.fs.readbyte(idx, offset); 99 | }, 100 | 101 | mp_js_hal_filesystem_write: function ( 102 | /** @type {number} */ idx, 103 | /** @type {number} */ buf, 104 | /** @type {number} */ len 105 | ) { 106 | const data = new Uint8Array(Module.HEAPU8.buffer, buf, len); 107 | return Module.fs.write(idx, data); 108 | }, 109 | 110 | mp_js_hal_reset: function () { 111 | Module.board.throwReset(); 112 | }, 113 | 114 | mp_js_hal_panic: function (/** @type {number} */ code) { 115 | Module.board.throwPanic(code); 116 | }, 117 | 118 | mp_js_hal_temperature: function () { 119 | return Module.board.temperature.value; 120 | }, 121 | 122 | mp_js_hal_button_get_presses: function (/** @type {number} */ button) { 123 | return Module.board.buttons[button].getAndClearPresses(); 124 | }, 125 | 126 | mp_js_hal_button_is_pressed: function (/** @type {number} */ button) { 127 | return Module.board.buttons[button].isPressed(); 128 | }, 129 | 130 | mp_js_hal_pin_is_touched: function (/** @type {number} */ pin) { 131 | return Module.board.pins[pin].isTouched(); 132 | }, 133 | 134 | mp_js_hal_pin_get_analog_period_us: function (/** @type {number} */ pin) { 135 | return Module.board.pins[pin].getAnalogPeriodUs(); 136 | }, 137 | 138 | mp_js_hal_pin_set_analog_period_us: function ( 139 | /** @type {number} */ pin, 140 | /** @type {number} */ period 141 | ) { 142 | return Module.board.pins[pin].setAnalogPeriodUs(period); 143 | }, 144 | 145 | mp_js_hal_display_get_pixel: function ( 146 | /** @type {number} */ x, 147 | /** @type {number} */ y 148 | ) { 149 | return Module.board.display.getPixel(x, y); 150 | }, 151 | 152 | mp_js_hal_display_set_pixel: function ( 153 | /** @type {number} */ x, 154 | /** @type {number} */ y, 155 | /** @type {number} */ value 156 | ) { 157 | Module.board.display.setPixel(x, y, value); 158 | }, 159 | 160 | mp_js_hal_display_clear: function () { 161 | Module.board.display.clear(); 162 | }, 163 | 164 | mp_js_hal_display_read_light_level: function () { 165 | return Module.board.display.lightLevel.value; 166 | }, 167 | 168 | mp_js_hal_accelerometer_get_x: function () { 169 | return Module.board.accelerometer.state.accelerometerX.value; 170 | }, 171 | 172 | mp_js_hal_accelerometer_get_y: function () { 173 | return Module.board.accelerometer.state.accelerometerY.value; 174 | }, 175 | 176 | mp_js_hal_accelerometer_get_z: function () { 177 | return Module.board.accelerometer.state.accelerometerZ.value; 178 | }, 179 | 180 | mp_js_hal_accelerometer_get_gesture: function () { 181 | return Module.conversions.convertAccelerometerStringToNumber( 182 | Module.board.accelerometer.state.gesture.value 183 | ); 184 | }, 185 | 186 | mp_js_hal_accelerometer_set_range: function (/** @type {number} */ r) { 187 | Module.board.accelerometer.setRange(r); 188 | }, 189 | 190 | mp_js_hal_compass_get_x: function () { 191 | return Module.board.compass.state.compassX.value; 192 | }, 193 | 194 | mp_js_hal_compass_get_y: function () { 195 | return Module.board.compass.state.compassY.value; 196 | }, 197 | 198 | mp_js_hal_compass_get_z: function () { 199 | return Module.board.compass.state.compassZ.value; 200 | }, 201 | 202 | mp_js_hal_compass_get_field_strength: function () { 203 | return Module.board.compass.getFieldStrength(); 204 | }, 205 | 206 | mp_js_hal_compass_get_heading: function () { 207 | return Module.board.compass.state.compassHeading.value; 208 | }, 209 | 210 | mp_js_hal_audio_set_volume: function (/** @type {number} */ value) { 211 | Module.board.audio.setVolume(value); 212 | }, 213 | 214 | mp_js_hal_audio_init: function (/** @type {number} */ sample_rate) { 215 | // @ts-expect-error 216 | Module.board.audio.default.init(sample_rate); 217 | }, 218 | 219 | mp_js_hal_audio_write_data: function ( 220 | /** @type {number} */ buf, 221 | /** @type {number} */ num_samples 222 | ) { 223 | // @ts-expect-error 224 | Module.board.audio.default.writeData( 225 | Module.conversions.convertAudioBuffer( 226 | Module.HEAPU8, 227 | buf, 228 | // @ts-expect-error 229 | Module.board.audio.default.createBuffer(num_samples) 230 | ) 231 | ); 232 | }, 233 | 234 | mp_js_hal_audio_speech_init: function (/** @type {number} */ sample_rate) { 235 | // @ts-expect-error 236 | Module.board.audio.speech.init(sample_rate); 237 | }, 238 | 239 | mp_js_hal_audio_speech_write_data: function ( 240 | /** @type {number} */ buf, 241 | /** @type {number} */ num_samples 242 | ) { 243 | /** @type {AudioBuffer | undefined} */ let webAudioBuffer; 244 | try { 245 | // @ts-expect-error 246 | webAudioBuffer = Module.board.audio.speech.createBuffer(num_samples); 247 | } catch (e) { 248 | // Swallow error on older Safari to keep the sim in a good state. 249 | // @ts-expect-error 250 | if (e.name === "NotSupportedError") { 251 | return; 252 | } else { 253 | throw e; 254 | } 255 | } 256 | // @ts-expect-error 257 | Module.board.audio.speech.writeData( 258 | Module.conversions.convertAudioBuffer(Module.HEAPU8, buf, webAudioBuffer) 259 | ); 260 | }, 261 | 262 | mp_js_hal_audio_period_us: function (/** @type {number} */ period_us) { 263 | Module.board.audio.setPeriodUs(period_us); 264 | }, 265 | 266 | mp_js_hal_audio_amplitude_u10: function ( 267 | /** @type {number} */ amplitude_u10 268 | ) { 269 | Module.board.audio.setAmplitudeU10(amplitude_u10); 270 | }, 271 | 272 | mp_js_hal_microphone_init: function () { 273 | Module.board.microphone.microphoneOn(); 274 | }, 275 | 276 | mp_js_hal_microphone_set_threshold: function ( 277 | /** @type {number} */ kind, 278 | /** @type {number} */ value 279 | ) { 280 | Module.board.microphone.setThreshold( 281 | Module.conversions.convertSoundThresholdNumberToString(kind), 282 | value 283 | ); 284 | }, 285 | 286 | mp_js_hal_microphone_get_level: function () { 287 | return Module.board.microphone.soundLevel.value; 288 | }, 289 | 290 | mp_js_hal_audio_play_expression: function (/** @type {any} */ expr) { 291 | return Module.board.audio.playSoundExpression(UTF8ToString(expr)); 292 | }, 293 | 294 | mp_js_hal_audio_stop_expression: function () { 295 | return Module.board.audio.stopSoundExpression(); 296 | }, 297 | 298 | mp_js_hal_audio_is_expression_active: function () { 299 | return Module.board.audio.isSoundExpressionActive(); 300 | }, 301 | 302 | mp_js_radio_enable: function ( 303 | /** @type {number} */ group, 304 | /** @type {number} */ max_payload, 305 | /** @type {number} */ queue 306 | ) { 307 | Module.board.radio.enable({ group, maxPayload: max_payload, queue }); 308 | }, 309 | 310 | mp_js_radio_disable: function () { 311 | Module.board.radio.disable(); 312 | }, 313 | 314 | mp_js_radio_update_config: function ( 315 | /** @type {number} */ group, 316 | /** @type {number} */ max_payload, 317 | /** @type {number} */ queue 318 | ) { 319 | Module.board.radio.updateConfig({ group, maxPayload: max_payload, queue }); 320 | }, 321 | 322 | mp_js_radio_send: function ( 323 | /** @type {number} */ buf, 324 | /** @type {number} */ len, 325 | /** @type {number} */ buf2, 326 | /** @type {number} */ len2 327 | ) { 328 | const data = new Uint8Array(len + len2); 329 | data.set(Module.HEAPU8.slice(buf, buf + len)); 330 | data.set(Module.HEAPU8.slice(buf2, buf2 + len2), len); 331 | Module.board.radio.send(data); 332 | }, 333 | 334 | mp_js_radio_peek: function () { 335 | const packet = Module.board.radio.peek(); 336 | if (packet) { 337 | return Module.board.writeRadioRxBuffer(packet); 338 | } 339 | return null; 340 | }, 341 | 342 | mp_js_radio_pop: function () { 343 | Module.board.radio.pop(); 344 | }, 345 | 346 | mp_js_hal_log_delete: function (/** @type {boolean} */ full_erase) { 347 | // We don't have a notion of non-full erase. 348 | Module.board.dataLogging.delete(); 349 | }, 350 | 351 | mp_js_hal_log_set_mirroring: function (/** @type {boolean} */ serial) { 352 | Module.board.dataLogging.setMirroring(serial); 353 | }, 354 | 355 | mp_js_hal_log_set_timestamp: function (/** @type {number} */ period) { 356 | Module.board.dataLogging.setTimestamp(period); 357 | }, 358 | 359 | mp_js_hal_log_begin_row: function () { 360 | return Module.board.dataLogging.beginRow(); 361 | }, 362 | 363 | mp_js_hal_log_end_row: function () { 364 | return Module.board.dataLogging.endRow(); 365 | }, 366 | 367 | mp_js_hal_log_data: function ( 368 | /** @type {number} */ key, 369 | /** @type {number} */ value 370 | ) { 371 | return Module.board.dataLogging.logData( 372 | UTF8ToString(key), 373 | UTF8ToString(value) 374 | ); 375 | }, 376 | }); 377 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | #include 28 | #include 29 | #include 30 | 31 | #include "py/gc.h" 32 | #include "py/compile.h" 33 | #include "py/mperrno.h" 34 | #include "py/mphal.h" 35 | #include "py/runtime.h" 36 | #include "shared/readline/readline.h" 37 | #include "shared/runtime/gchelper.h" 38 | #include "shared/runtime/pyexec.h" 39 | #include "drv_system.h" 40 | #include "drv_display.h" 41 | #include "modmicrobit.h" 42 | #include "microbithal_js.h" 43 | 44 | // Set to true if a soft-timer callback can use mp_sched_exception to propagate out an exception. 45 | bool microbit_outer_nlr_will_handle_soft_timer_exceptions; 46 | 47 | void microbit_pyexec_file(const char *filename); 48 | 49 | bool stop_requested = 0; 50 | 51 | void mp_js_request_stop(void) { 52 | stop_requested = 1; 53 | } 54 | 55 | void mp_js_force_stop(void) { 56 | emscripten_force_exit(0); 57 | } 58 | 59 | // Main entrypoint called from JavaScript. 60 | // Calling mp_js_request_stop allows Ctrl-D to exit, otherwise Ctrl-D does a soft reset. 61 | // As we use asyncify you can await this call. 62 | void mp_js_main(int heap_size) { 63 | while (!stop_requested) { 64 | microbit_hal_init(); 65 | microbit_system_init(); 66 | microbit_display_init(); 67 | 68 | #if MICROPY_ENABLE_GC 69 | char *heap = (char *)malloc(heap_size * sizeof(char)); 70 | gc_init(heap, heap + heap_size); 71 | #endif 72 | 73 | #if MICROPY_ENABLE_PYSTACK 74 | static mp_obj_t pystack[1024]; 75 | mp_pystack_init(pystack, &pystack[MP_ARRAY_SIZE(pystack)]); 76 | #endif 77 | 78 | mp_init(); 79 | 80 | if (pyexec_mode_kind == PYEXEC_MODE_FRIENDLY_REPL) { 81 | const char *main_py = "main.py"; 82 | if (mp_import_stat(main_py) == MP_IMPORT_STAT_FILE) { 83 | // exec("main.py") 84 | microbit_pyexec_file(main_py); 85 | } else { 86 | // from microbit import * 87 | mp_import_all(mp_import_name(MP_QSTR_microbit, mp_const_empty_tuple, MP_OBJ_NEW_SMALL_INT(0))); 88 | } 89 | } 90 | 91 | for (;;) { 92 | if (pyexec_mode_kind == PYEXEC_MODE_RAW_REPL) { 93 | if (pyexec_raw_repl() != 0) { 94 | break; 95 | } 96 | } else { 97 | if (pyexec_friendly_repl() != 0) { 98 | break; 99 | } 100 | } 101 | } 102 | 103 | mp_printf(MP_PYTHON_PRINTER, "MPY: soft reboot\n"); 104 | //microbit_soft_timer_deinit(); 105 | microbit_hal_deinit(); 106 | gc_sweep_all(); 107 | mp_deinit(); 108 | free(heap); 109 | } 110 | } 111 | 112 | STATIC void microbit_display_exception(mp_obj_t exc_in) { 113 | // Construct the message string ready for display. 114 | mp_uint_t n, *values; 115 | mp_obj_exception_get_traceback(exc_in, &n, &values); 116 | vstr_t vstr; 117 | mp_print_t print; 118 | vstr_init_print(&vstr, 50, &print); 119 | #if MICROPY_ENABLE_SOURCE_LINE 120 | if (n >= 3) { 121 | mp_printf(&print, "line %u ", values[1]); 122 | } 123 | #endif 124 | if (mp_obj_is_native_exception_instance(exc_in)) { 125 | mp_obj_exception_t *exc = MP_OBJ_TO_PTR(exc_in); 126 | mp_printf(&print, "%q ", exc->base.type->name); 127 | if (exc->args != NULL && exc->args->len != 0) { 128 | mp_obj_print_helper(&print, exc->args->items[0], PRINT_STR); 129 | } 130 | } 131 | 132 | // Show the message, and allow ctrl-C to stop it. 133 | nlr_buf_t nlr; 134 | if (nlr_push(&nlr) == 0) { 135 | mp_hal_set_interrupt_char(CHAR_CTRL_C); 136 | microbit_display_show((void *)µbit_const_image_sad_obj); 137 | mp_hal_delay_ms(1000); 138 | microbit_display_scroll(vstr_null_terminated_str(&vstr)); 139 | nlr_pop(); 140 | } else { 141 | // Uncaught exception, just ignore it. 142 | } 143 | mp_hal_set_interrupt_char(-1); // disable interrupt 144 | mp_handle_pending(false); // clear any pending exceptions (and run any callbacks) 145 | vstr_clear(&vstr); 146 | } 147 | 148 | void microbit_pyexec_file(const char *filename) { 149 | nlr_buf_t nlr; 150 | if (nlr_push(&nlr) == 0) { 151 | // Parse and comple the file. 152 | mp_lexer_t *lex = mp_lexer_new_from_file(filename); 153 | qstr source_name = lex->source_name; 154 | mp_parse_tree_t parse_tree = mp_parse(lex, MP_PARSE_FILE_INPUT); 155 | mp_obj_t module_fun = mp_compile(&parse_tree, source_name, false); 156 | 157 | // Execute the code. 158 | mp_hal_set_interrupt_char(CHAR_CTRL_C); // allow ctrl-C to interrupt us 159 | mp_call_function_0(module_fun); 160 | mp_hal_set_interrupt_char(-1); // disable interrupt 161 | mp_handle_pending(true); // handle any pending exceptions (and any callbacks) 162 | nlr_pop(); 163 | } else { 164 | // Handle uncaught exception. 165 | mp_hal_set_interrupt_char(-1); // disable interrupt 166 | mp_handle_pending(false); // clear any pending exceptions (and run any callbacks) 167 | 168 | mp_obj_t exc_type = MP_OBJ_FROM_PTR(((mp_obj_base_t *)nlr.ret_val)->type); 169 | if (!mp_obj_is_subclass_fast(exc_type, MP_OBJ_FROM_PTR(&mp_type_SystemExit))) { 170 | // Print exception to stdout. 171 | mp_obj_print_exception(&mp_plat_print, MP_OBJ_FROM_PTR(nlr.ret_val)); 172 | 173 | // Print exception to the display, but not if it's KeyboardInterrupt. 174 | if (!mp_obj_is_subclass_fast(exc_type, MP_OBJ_FROM_PTR(&mp_type_KeyboardInterrupt))) { 175 | microbit_display_exception(MP_OBJ_FROM_PTR(nlr.ret_val)); 176 | } 177 | } 178 | } 179 | } 180 | 181 | void nlr_jump_fail(void *val) { 182 | printf("FATAL: uncaught NLR %p\n", val); 183 | exit(1); 184 | } 185 | 186 | STATIC void gc_scan_func(void *begin, void *end) { 187 | gc_collect_root((void **)begin, (void **)end - (void **)begin + 1); 188 | } 189 | 190 | void gc_collect(void) { 191 | gc_collect_start(); 192 | emscripten_scan_stack(gc_scan_func); 193 | emscripten_scan_registers(gc_scan_func); 194 | gc_collect_end(); 195 | } 196 | -------------------------------------------------------------------------------- /src/microbitfs.c: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the Micro Python project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2016 Mark Shannon 7 | * Copyright (c) 2017 Ayke van Laethem 8 | * Copyright (c) 2022 Damien P. George 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining a copy 11 | * of this software and associated documentation files (the "Software"), to deal 12 | * in the Software without restriction, including without limitation the rights 13 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | * copies of the Software, and to permit persons to whom the Software is 15 | * furnished to do so, subject to the following conditions: 16 | * 17 | * The above copyright notice and this permission notice shall be included in 18 | * all copies or substantial portions of the Software. 19 | * 20 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 | * THE SOFTWARE. 27 | */ 28 | 29 | #include "py/stream.h" 30 | #include "py/runtime.h" 31 | #include "extmod/vfs.h" 32 | #include "ports/nrf/modules/uos/microbitfs.h" 33 | #include "jshal.h" 34 | 35 | #if MICROPY_MBFS 36 | 37 | // This is a version of microbitfs for the simulator. 38 | // File data is stored externally in JavaScripti and access via mp_js_hal_filesystem_xxx() functions. 39 | 40 | #define MAX_FILENAME_LENGTH (120) 41 | 42 | /******************************************************************************/ 43 | // os-level functions 44 | 45 | STATIC mp_obj_t uos_mbfs_listdir(void) { 46 | mp_obj_t res = mp_obj_new_list(0, NULL); 47 | char buf[MAX_FILENAME_LENGTH]; 48 | for (size_t i = 0;; ++i) { 49 | int len = mp_js_hal_filesystem_name(i, buf); 50 | if (len < 0) { 51 | // End of listing. 52 | break; 53 | } 54 | if (len > 0) { 55 | mp_obj_list_append(res, mp_obj_new_str(buf, len)); 56 | } 57 | } 58 | return res; 59 | } 60 | MP_DEFINE_CONST_FUN_OBJ_0(uos_mbfs_listdir_obj, uos_mbfs_listdir); 61 | 62 | typedef struct { 63 | mp_obj_base_t base; 64 | mp_fun_1_t iternext; 65 | uint8_t idx; 66 | } uos_mbfs_ilistdir_it_t; 67 | 68 | STATIC mp_obj_t uos_mbfs_ilistdir_it_iternext(mp_obj_t self_in) { 69 | uos_mbfs_ilistdir_it_t *self = MP_OBJ_TO_PTR(self_in); 70 | for (;;) { 71 | char buf[MAX_FILENAME_LENGTH]; 72 | int len = mp_js_hal_filesystem_name(self->idx, buf); 73 | if (len < 0) { 74 | return MP_OBJ_STOP_ITERATION; 75 | } 76 | self->idx += 1; 77 | if (len > 0) { 78 | mp_obj_t name = mp_obj_new_str(buf, len); 79 | mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(3, NULL)); 80 | t->items[0] = name; 81 | t->items[1] = MP_OBJ_NEW_SMALL_INT(MP_S_IFREG); // all entries are files 82 | t->items[2] = MP_OBJ_NEW_SMALL_INT(0); // no inode number 83 | return MP_OBJ_FROM_PTR(t); 84 | } 85 | } 86 | } 87 | 88 | STATIC mp_obj_t uos_mbfs_ilistdir(void) { 89 | uos_mbfs_ilistdir_it_t *iter = m_new_obj(uos_mbfs_ilistdir_it_t); 90 | iter->base.type = &mp_type_polymorph_iter; 91 | iter->iternext = uos_mbfs_ilistdir_it_iternext; 92 | iter->idx = 0; 93 | return MP_OBJ_FROM_PTR(iter); 94 | } 95 | MP_DEFINE_CONST_FUN_OBJ_0(uos_mbfs_ilistdir_obj, uos_mbfs_ilistdir); 96 | 97 | STATIC mp_obj_t uos_mbfs_remove(mp_obj_t filename_in) { 98 | size_t name_len; 99 | const char *name = mp_obj_str_get_data(filename_in, &name_len); 100 | int idx = mp_js_hal_filesystem_find(name, name_len); 101 | if (idx < 0) { 102 | mp_raise_OSError(MP_ENOENT); 103 | } 104 | mp_js_hal_filesystem_remove(idx); 105 | return mp_const_none; 106 | } 107 | MP_DEFINE_CONST_FUN_OBJ_1(uos_mbfs_remove_obj, uos_mbfs_remove); 108 | 109 | STATIC mp_obj_t uos_mbfs_stat(mp_obj_t filename_in) { 110 | size_t name_len; 111 | const char *name = mp_obj_str_get_data(filename_in, &name_len); 112 | int idx = mp_js_hal_filesystem_find(name, name_len); 113 | if (idx < 0) { 114 | mp_raise_OSError(MP_ENOENT); 115 | } 116 | mp_obj_t file_size = mp_obj_new_int(mp_js_hal_filesystem_size(idx)); 117 | 118 | mp_obj_tuple_t *t = MP_OBJ_TO_PTR(mp_obj_new_tuple(10, NULL)); 119 | t->items[0] = MP_OBJ_NEW_SMALL_INT(MP_S_IFREG); // st_mode 120 | t->items[1] = MP_OBJ_NEW_SMALL_INT(0); // st_ino 121 | t->items[2] = MP_OBJ_NEW_SMALL_INT(0); // st_dev 122 | t->items[3] = MP_OBJ_NEW_SMALL_INT(0); // st_nlink 123 | t->items[4] = MP_OBJ_NEW_SMALL_INT(0); // st_uid 124 | t->items[5] = MP_OBJ_NEW_SMALL_INT(0); // st_gid 125 | t->items[6] = file_size; // st_size 126 | t->items[7] = MP_OBJ_NEW_SMALL_INT(0); // st_atime 127 | t->items[8] = MP_OBJ_NEW_SMALL_INT(0); // st_mtime 128 | t->items[9] = MP_OBJ_NEW_SMALL_INT(0); // st_ctime 129 | return MP_OBJ_FROM_PTR(t); 130 | } 131 | MP_DEFINE_CONST_FUN_OBJ_1(uos_mbfs_stat_obj, uos_mbfs_stat); 132 | 133 | /******************************************************************************/ 134 | // File object 135 | 136 | typedef struct _mbfs_file_obj_t { 137 | mp_obj_base_t base; 138 | int idx; 139 | size_t offset; 140 | bool writable; 141 | bool open; 142 | bool binary; 143 | } mbfs_file_obj_t; 144 | 145 | STATIC mp_obj_t uos_mbfs_file___exit__(size_t n_args, const mp_obj_t *args) { 146 | (void)n_args; 147 | return mp_stream_close(args[0]); 148 | } 149 | STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(uos_mbfs_file___exit___obj, 4, 4, uos_mbfs_file___exit__); 150 | 151 | STATIC mp_obj_t uos_mbfs_file_name(mp_obj_t self_in) { 152 | mbfs_file_obj_t *self = MP_OBJ_TO_PTR(self_in); 153 | char buf[MAX_FILENAME_LENGTH]; 154 | int len = mp_js_hal_filesystem_name(self->idx, buf); 155 | return mp_obj_new_str(buf, len); 156 | } 157 | STATIC MP_DEFINE_CONST_FUN_OBJ_1(uos_mbfs_file_name_obj, uos_mbfs_file_name); 158 | 159 | STATIC mp_obj_t microbit_file_writable(mp_obj_t self) { 160 | return mp_obj_new_bool(((mbfs_file_obj_t *)MP_OBJ_TO_PTR(self))->writable); 161 | } 162 | STATIC MP_DEFINE_CONST_FUN_OBJ_1(microbit_file_writable_obj, microbit_file_writable); 163 | 164 | STATIC const mp_rom_map_elem_t uos_mbfs_file_locals_dict_table[] = { 165 | { MP_ROM_QSTR(MP_QSTR___enter__), MP_ROM_PTR(&mp_identity_obj) }, 166 | { MP_ROM_QSTR(MP_QSTR___exit__), MP_ROM_PTR(&uos_mbfs_file___exit___obj) }, 167 | { MP_ROM_QSTR(MP_QSTR_name), MP_ROM_PTR(&uos_mbfs_file_name_obj) }, 168 | { MP_ROM_QSTR(MP_QSTR_writable), MP_ROM_PTR(µbit_file_writable_obj) }, 169 | 170 | { MP_ROM_QSTR(MP_QSTR_close), MP_ROM_PTR(&mp_stream_close_obj) }, 171 | { MP_ROM_QSTR(MP_QSTR_read), MP_ROM_PTR(&mp_stream_read_obj) }, 172 | { MP_ROM_QSTR(MP_QSTR_readinto), MP_ROM_PTR(&mp_stream_readinto_obj) }, 173 | { MP_ROM_QSTR(MP_QSTR_readline), MP_ROM_PTR(&mp_stream_unbuffered_readline_obj) }, 174 | { MP_ROM_QSTR(MP_QSTR_write), MP_ROM_PTR(&mp_stream_write_obj) }, 175 | }; 176 | STATIC MP_DEFINE_CONST_DICT(uos_mbfs_file_locals_dict, uos_mbfs_file_locals_dict_table); 177 | 178 | STATIC void check_file_open(mbfs_file_obj_t *self) { 179 | if (!self->open) { 180 | mp_raise_ValueError(MP_ERROR_TEXT("I/O operation on closed file")); 181 | } 182 | } 183 | 184 | STATIC mp_uint_t microbit_file_read(mp_obj_t self_in, void *buf_in, mp_uint_t size, int *errcode) { 185 | mbfs_file_obj_t *self = MP_OBJ_TO_PTR(self_in); 186 | check_file_open(self); 187 | if (self->writable) { 188 | *errcode = MP_EBADF; 189 | return MP_STREAM_ERROR; 190 | } 191 | uint32_t bytes_read = 0; 192 | uint8_t *buf = buf_in; 193 | while (size--) { 194 | int chr = mp_js_hal_filesystem_readbyte(self->idx, self->offset); 195 | if (chr < 0) { 196 | break; 197 | } 198 | *buf++ = chr; 199 | self->offset += 1; 200 | bytes_read += 1; 201 | } 202 | return bytes_read; 203 | } 204 | 205 | STATIC mp_uint_t microbit_file_write(mp_obj_t self_in, const void *buf, mp_uint_t size, int *errcode) { 206 | mbfs_file_obj_t *self = MP_OBJ_TO_PTR(self_in); 207 | check_file_open(self); 208 | if (!self->writable) { 209 | *errcode = MP_EBADF; 210 | return MP_STREAM_ERROR; 211 | } 212 | bool success = mp_js_hal_filesystem_write(self->idx, buf, size); 213 | if (!success) { 214 | *errcode = MP_ENOSPC; 215 | return MP_STREAM_ERROR; 216 | } 217 | return size; 218 | } 219 | 220 | STATIC mp_uint_t microbit_file_ioctl(mp_obj_t self_in, mp_uint_t request, uintptr_t arg, int *errcode) { 221 | mbfs_file_obj_t *self = MP_OBJ_TO_PTR(self_in); 222 | 223 | if (request == MP_STREAM_CLOSE) { 224 | self->open = false; 225 | return 0; 226 | } else { 227 | *errcode = MP_EINVAL; 228 | return MP_STREAM_ERROR; 229 | } 230 | } 231 | 232 | STATIC const mp_stream_p_t textio_stream_p = { 233 | .read = microbit_file_read, 234 | .write = microbit_file_write, 235 | .ioctl = microbit_file_ioctl, 236 | .is_text = true, 237 | }; 238 | 239 | const mp_obj_type_t uos_mbfs_textio_type = { 240 | { &mp_type_type }, 241 | .name = MP_QSTR_TextIO, 242 | .protocol = &textio_stream_p, 243 | .locals_dict = (mp_obj_dict_t *)&uos_mbfs_file_locals_dict, 244 | }; 245 | 246 | 247 | STATIC const mp_stream_p_t fileio_stream_p = { 248 | .read = microbit_file_read, 249 | .write = microbit_file_write, 250 | .ioctl = microbit_file_ioctl, 251 | }; 252 | 253 | const mp_obj_type_t uos_mbfs_fileio_type = { 254 | { &mp_type_type }, 255 | .name = MP_QSTR_FileIO, 256 | .protocol = &fileio_stream_p, 257 | .locals_dict = (mp_obj_dict_t *)&uos_mbfs_file_locals_dict, 258 | }; 259 | 260 | STATIC mbfs_file_obj_t *microbit_file_open(const char *name, size_t name_len, bool write, bool binary) { 261 | if (name_len > MAX_FILENAME_LENGTH) { 262 | return NULL; 263 | } 264 | int idx; 265 | if (write) { 266 | idx = mp_js_hal_filesystem_create(name, name_len); 267 | } else { 268 | idx = mp_js_hal_filesystem_find(name, name_len); 269 | if (idx < 0) { 270 | // File not found. 271 | return NULL; 272 | } 273 | } 274 | 275 | mbfs_file_obj_t *res = m_new_obj(mbfs_file_obj_t); 276 | if (binary) { 277 | res->base.type = &uos_mbfs_fileio_type; 278 | } else { 279 | res->base.type = &uos_mbfs_textio_type; 280 | } 281 | res->idx = idx; 282 | res->writable = write; 283 | res->open = true; 284 | res->binary = binary; 285 | return res; 286 | } 287 | 288 | /******************************************************************************/ 289 | // Import and reader interface 290 | 291 | mp_import_stat_t mp_import_stat(const char *path) { 292 | int idx = mp_js_hal_filesystem_find(path, strlen(path)); 293 | if (idx < 0) { 294 | return MP_IMPORT_STAT_NO_EXIST; 295 | } else { 296 | return MP_IMPORT_STAT_FILE; 297 | } 298 | } 299 | 300 | STATIC mp_uint_t file_readbyte(void *self_in) { 301 | mbfs_file_obj_t *self = self_in; 302 | int chr = mp_js_hal_filesystem_readbyte(self->idx, self->offset); 303 | if (chr < 0) { 304 | return MP_READER_EOF; 305 | } 306 | self->offset += 1; 307 | return chr; 308 | } 309 | 310 | STATIC void file_close(void *self_in) { 311 | mbfs_file_obj_t *self = self_in; 312 | self->open = false; 313 | } 314 | 315 | mp_lexer_t *mp_lexer_new_from_file(const char *filename) { 316 | mbfs_file_obj_t *file = microbit_file_open(filename, strlen(filename), false, false); 317 | if (file == NULL) { 318 | mp_raise_OSError(MP_ENOENT); 319 | } 320 | mp_reader_t reader; 321 | reader.data = file; 322 | reader.readbyte = file_readbyte; 323 | reader.close = file_close; 324 | return mp_lexer_new(qstr_from_str(filename), reader); 325 | } 326 | 327 | /******************************************************************************/ 328 | // Built-in open function 329 | 330 | mp_obj_t mp_builtin_open(size_t n_args, const mp_obj_t *args, mp_map_t *kwargs) { 331 | (void)kwargs; 332 | 333 | /// -1 means default; 0 explicitly false; 1 explicitly true. 334 | int read = -1; 335 | int text = -1; 336 | if (n_args == 2) { 337 | size_t len; 338 | const char *mode = mp_obj_str_get_data(args[1], &len); 339 | for (mp_uint_t i = 0; i < len; i++) { 340 | if (mode[i] == 'r' || mode[i] == 'w') { 341 | if (read >= 0) { 342 | goto mode_error; 343 | } 344 | read = (mode[i] == 'r'); 345 | } else if (mode[i] == 'b' || mode[i] == 't') { 346 | if (text >= 0) { 347 | goto mode_error; 348 | } 349 | text = (mode[i] == 't'); 350 | } else { 351 | goto mode_error; 352 | } 353 | } 354 | } 355 | size_t name_len; 356 | const char *filename = mp_obj_str_get_data(args[0], &name_len); 357 | mbfs_file_obj_t *res = microbit_file_open(filename, name_len, read == 0, text == 0); 358 | if (res == NULL) { 359 | mp_raise_OSError(MP_ENOENT); 360 | } 361 | return res; 362 | 363 | mode_error: 364 | mp_raise_ValueError(MP_ERROR_TEXT("illegal mode")); 365 | } 366 | MP_DEFINE_CONST_FUN_OBJ_KW(mp_builtin_open_obj, 1, mp_builtin_open); 367 | 368 | #endif // MICROPY_MBFS 369 | -------------------------------------------------------------------------------- /src/microbithal_js.c: -------------------------------------------------------------------------------- 1 | // Implementation of the microbit HAL for a JavaScript/browser environment. 2 | 3 | #include 4 | #include "py/runtime.h" 5 | #include "py/mphal.h" 6 | #include "shared/runtime/interrupt_char.h" 7 | #include "microbithal.h" 8 | #include "microbithal_js.h" 9 | #include "jshal.h" 10 | 11 | #define BITMAP_FONT_ASCII_START 32 12 | #define BITMAP_FONT_ASCII_END 126 13 | #define BITMAP_FONT_WIDTH 5 14 | #define BITMAP_FONT_HEIGHT 5 15 | 16 | // This font data is taken from the CODAL source. 17 | const unsigned char pendolino3[475] = { 18 | 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x8, 0x8, 0x0, 0x8, 0xa, 0x4a, 0x40, 0x0, 0x0, 0xa, 0x5f, 0xea, 0x5f, 0xea, 0xe, 0xd9, 0x2e, 0xd3, 0x6e, 0x19, 0x32, 0x44, 0x89, 0x33, 0xc, 0x92, 0x4c, 0x92, 0x4d, 0x8, 0x8, 0x0, 0x0, 0x0, 0x4, 0x88, 0x8, 0x8, 0x4, 0x8, 0x4, 0x84, 0x84, 0x88, 0x0, 0xa, 0x44, 0x8a, 0x40, 0x0, 0x4, 0x8e, 0xc4, 0x80, 0x0, 0x0, 0x0, 0x4, 0x88, 0x0, 0x0, 0xe, 0xc0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x0, 0x1, 0x22, 0x44, 0x88, 0x10, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x4, 0x8c, 0x84, 0x84, 0x8e, 0x1c, 0x82, 0x4c, 0x90, 0x1e, 0x1e, 0xc2, 0x44, 0x92, 0x4c, 0x6, 0xca, 0x52, 0x5f, 0xe2, 0x1f, 0xf0, 0x1e, 0xc1, 0x3e, 0x2, 0x44, 0x8e, 0xd1, 0x2e, 0x1f, 0xe2, 0x44, 0x88, 0x10, 0xe, 0xd1, 0x2e, 0xd1, 0x2e, 0xe, 0xd1, 0x2e, 0xc4, 0x88, 0x0, 0x8, 0x0, 0x8, 0x0, 0x0, 0x4, 0x80, 0x4, 0x88, 0x2, 0x44, 0x88, 0x4, 0x82, 0x0, 0xe, 0xc0, 0xe, 0xc0, 0x8, 0x4, 0x82, 0x44, 0x88, 0xe, 0xd1, 0x26, 0xc0, 0x4, 0xe, 0xd1, 0x35, 0xb3, 0x6c, 0xc, 0x92, 0x5e, 0xd2, 0x52, 0x1c, 0x92, 0x5c, 0x92, 0x5c, 0xe, 0xd0, 0x10, 0x10, 0xe, 0x1c, 0x92, 0x52, 0x52, 0x5c, 0x1e, 0xd0, 0x1c, 0x90, 0x1e, 0x1e, 0xd0, 0x1c, 0x90, 0x10, 0xe, 0xd0, 0x13, 0x71, 0x2e, 0x12, 0x52, 0x5e, 0xd2, 0x52, 0x1c, 0x88, 0x8, 0x8, 0x1c, 0x1f, 0xe2, 0x42, 0x52, 0x4c, 0x12, 0x54, 0x98, 0x14, 0x92, 0x10, 0x10, 0x10, 0x10, 0x1e, 0x11, 0x3b, 0x75, 0xb1, 0x31, 0x11, 0x39, 0x35, 0xb3, 0x71, 0xc, 0x92, 0x52, 0x52, 0x4c, 0x1c, 0x92, 0x5c, 0x90, 0x10, 0xc, 0x92, 0x52, 0x4c, 0x86, 0x1c, 0x92, 0x5c, 0x92, 0x51, 0xe, 0xd0, 0xc, 0x82, 0x5c, 0x1f, 0xe4, 0x84, 0x84, 0x84, 0x12, 0x52, 0x52, 0x52, 0x4c, 0x11, 0x31, 0x31, 0x2a, 0x44, 0x11, 0x31, 0x35, 0xbb, 0x71, 0x12, 0x52, 0x4c, 0x92, 0x52, 0x11, 0x2a, 0x44, 0x84, 0x84, 0x1e, 0xc4, 0x88, 0x10, 0x1e, 0xe, 0xc8, 0x8, 0x8, 0xe, 0x10, 0x8, 0x4, 0x82, 0x41, 0xe, 0xc2, 0x42, 0x42, 0x4e, 0x4, 0x8a, 0x40, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1f, 0x8, 0x4, 0x80, 0x0, 0x0, 0x0, 0xe, 0xd2, 0x52, 0x4f, 0x10, 0x10, 0x1c, 0x92, 0x5c, 0x0, 0xe, 0xd0, 0x10, 0xe, 0x2, 0x42, 0x4e, 0xd2, 0x4e, 0xc, 0x92, 0x5c, 0x90, 0xe, 0x6, 0xc8, 0x1c, 0x88, 0x8, 0xe, 0xd2, 0x4e, 0xc2, 0x4c, 0x10, 0x10, 0x1c, 0x92, 0x52, 0x8, 0x0, 0x8, 0x8, 0x8, 0x2, 0x40, 0x2, 0x42, 0x4c, 0x10, 0x14, 0x98, 0x14, 0x92, 0x8, 0x8, 0x8, 0x8, 0x6, 0x0, 0x1b, 0x75, 0xb1, 0x31, 0x0, 0x1c, 0x92, 0x52, 0x52, 0x0, 0xc, 0x92, 0x52, 0x4c, 0x0, 0x1c, 0x92, 0x5c, 0x90, 0x0, 0xe, 0xd2, 0x4e, 0xc2, 0x0, 0xe, 0xd0, 0x10, 0x10, 0x0, 0x6, 0xc8, 0x4, 0x98, 0x8, 0x8, 0xe, 0xc8, 0x7, 0x0, 0x12, 0x52, 0x52, 0x4f, 0x0, 0x11, 0x31, 0x2a, 0x44, 0x0, 0x11, 0x31, 0x35, 0xbb, 0x0, 0x12, 0x4c, 0x8c, 0x92, 0x0, 0x11, 0x2a, 0x44, 0x98, 0x0, 0x1e, 0xc4, 0x88, 0x1e, 0x6, 0xc4, 0x8c, 0x84, 0x86, 0x8, 0x8, 0x8, 0x8, 0x8, 0x18, 0x8, 0xc, 0x88, 0x18, 0x0, 0x0, 0xc, 0x83, 0x60}; 19 | 20 | static uint16_t button_state[2]; 21 | 22 | void microbit_hal_init(void) { 23 | mp_js_hal_init(); 24 | } 25 | 26 | // Sim only deinit. 27 | void microbit_hal_deinit(void) { 28 | // If we don't do this then the radio has a reference to the previous heap. 29 | // Can be revisited if we stop/restart in way that resets WASM state. 30 | extern void microbit_radio_disable(void); 31 | microbit_radio_disable(); 32 | 33 | mp_js_hal_deinit(); 34 | } 35 | 36 | static void microbit_hal_process_events(void) { 37 | // Call microbit_hal_timer_callback() every 6ms. 38 | static uint32_t last_ms = 0; 39 | uint32_t ms = mp_hal_ticks_ms(); 40 | if (ms - last_ms >= 6) { 41 | last_ms = ms; 42 | extern void microbit_hal_timer_callback(void); 43 | microbit_hal_timer_callback(); 44 | } 45 | 46 | // Process stdin. 47 | int c = mp_js_hal_stdin_pop_char(); 48 | if (c >= 0) { 49 | if (c == mp_interrupt_char) { 50 | mp_sched_keyboard_interrupt(); 51 | } else { 52 | ringbuf_put(&stdin_ringbuf, c); 53 | } 54 | } 55 | } 56 | 57 | void microbit_hal_background_processing(void) { 58 | microbit_hal_process_events(); 59 | emscripten_sleep(0); 60 | } 61 | 62 | void microbit_hal_idle(void) { 63 | microbit_hal_process_events(); 64 | emscripten_sleep(5); 65 | } 66 | 67 | void microbit_hal_reset(void) { 68 | mp_js_hal_reset(); 69 | } 70 | 71 | void microbit_hal_panic(int code) { 72 | mp_js_hal_panic(code); 73 | } 74 | 75 | int microbit_hal_temperature(void) { 76 | return mp_js_hal_temperature(); 77 | } 78 | 79 | void microbit_hal_power_clear_wake_sources(void) { 80 | // Stub, unsupported. 81 | } 82 | 83 | void microbit_hal_power_wake_on_button(int button, bool wake_on_active) { 84 | // Stub, unsupported. 85 | } 86 | 87 | void microbit_hal_power_wake_on_pin(int pin, bool wake_on_active) { 88 | // Stub, unsupported. 89 | } 90 | 91 | void microbit_hal_power_off(void) { 92 | // Stub, unsupported. 93 | } 94 | 95 | bool microbit_hal_power_deep_sleep(bool wake_on_ms, uint32_t ms) { 96 | // Stub, unsupported, always claim we were interrupted. 97 | return true; 98 | } 99 | 100 | void microbit_hal_pin_set_pull(int pin, int pull) { 101 | //pin_obj[pin]->setPull(pin_pull_mode_mapping[pull]); 102 | //pin_pull_state[pin] = pull; 103 | } 104 | 105 | int microbit_hal_pin_get_pull(int pin) { 106 | //return pin_pull_state[pin]; 107 | return 0; 108 | } 109 | 110 | int microbit_hal_pin_set_analog_period_us(int pin, int period) { 111 | // Change the audio virtual-pin period if the pin is the special mixer pin. 112 | if (pin == MICROBIT_HAL_PIN_MIXER) { 113 | mp_js_hal_audio_period_us(period); 114 | return 0; 115 | } 116 | return mp_js_hal_pin_set_analog_period_us(pin, period); 117 | } 118 | 119 | int microbit_hal_pin_get_analog_period_us(int pin) { 120 | return mp_js_hal_pin_get_analog_period_us(pin); 121 | } 122 | 123 | void microbit_hal_pin_set_touch_mode(int pin, int mode) { 124 | //pin_obj[pin]->isTouched((TouchMode)mode); 125 | } 126 | 127 | int microbit_hal_pin_read(int pin) { 128 | //return pin_obj[pin]->getDigitalValue(); 129 | return 0; 130 | } 131 | 132 | void microbit_hal_pin_write(int pin, int value) { 133 | //pin_obj[pin]->setDigitalValue(value); 134 | } 135 | 136 | int microbit_hal_pin_read_analog_u10(int pin) { 137 | //return pin_obj[pin]->getAnalogValue(); 138 | return 0; 139 | } 140 | 141 | void microbit_hal_pin_write_analog_u10(int pin, int value) { 142 | if (pin == MICROBIT_HAL_PIN_MIXER) { 143 | mp_js_hal_audio_amplitude_u10(value); 144 | return; 145 | } 146 | /* 147 | pin_obj[pin]->setAnalogValue(value); 148 | */ 149 | } 150 | 151 | int microbit_hal_pin_is_touched(int pin) { 152 | if (pin == MICROBIT_HAL_PIN_FACE || pin == MICROBIT_HAL_PIN_P0 || pin == MICROBIT_HAL_PIN_P1 || pin == MICROBIT_HAL_PIN_P2) { 153 | return mp_js_hal_pin_is_touched(pin); 154 | } 155 | /* 156 | if (pin == MICROBIT_HAL_PIN_FACE) { 157 | // For touch on the face/logo, delegate to the TouchButton instance. 158 | return uBit.logo.buttonActive(); 159 | } 160 | return pin_obj[pin]->isTouched(); 161 | */ 162 | return 0; 163 | } 164 | 165 | void microbit_hal_pin_write_ws2812(int pin, const uint8_t *buf, size_t len) { 166 | //neopixel_send_buffer(*pin_obj[pin], buf, len); 167 | } 168 | 169 | int microbit_hal_i2c_init(int scl, int sda, int freq) { 170 | /* 171 | // TODO set pins 172 | int ret = uBit.i2c.setFrequency(freq); 173 | if (ret != DEVICE_OK) { 174 | return ret;; 175 | } 176 | */ 177 | return 0; 178 | } 179 | 180 | int microbit_hal_i2c_readfrom(uint8_t addr, uint8_t *buf, size_t len, int stop) { 181 | /* 182 | int ret = uBit.i2c.read(addr << 1, (uint8_t *)buf, len, !stop); 183 | if (ret != DEVICE_OK) { 184 | return ret; 185 | } 186 | */ 187 | return 0; 188 | } 189 | 190 | int microbit_hal_i2c_writeto(uint8_t addr, const uint8_t *buf, size_t len, int stop) { 191 | /* 192 | int ret = uBit.i2c.write(addr << 1, (uint8_t *)buf, len, !stop); 193 | if (ret != DEVICE_OK) { 194 | return ret; 195 | } 196 | */ 197 | return 0; 198 | } 199 | 200 | int microbit_hal_uart_init(int tx, int rx, int baudrate, int bits, int parity, int stop) { 201 | /* 202 | // TODO set bits, parity stop 203 | int ret = uBit.serial.redirect(*pin_obj[tx], *pin_obj[rx]); 204 | if (ret != DEVICE_OK) { 205 | return ret; 206 | } 207 | ret = uBit.serial.setBaud(baudrate); 208 | if (ret != DEVICE_OK) { 209 | return ret; 210 | } 211 | */ 212 | return 0; 213 | } 214 | 215 | //static NRF52SPI *spi = NULL; 216 | 217 | int microbit_hal_spi_init(int sclk, int mosi, int miso, int frequency, int bits, int mode) { 218 | /* 219 | if (spi != NULL) { 220 | delete spi; 221 | } 222 | spi = new NRF52SPI(*pin_obj[mosi], *pin_obj[miso], *pin_obj[sclk], NRF_SPIM2); 223 | int ret = spi->setFrequency(frequency); 224 | if (ret != DEVICE_OK) { 225 | return ret; 226 | } 227 | ret = spi->setMode(mode, bits); 228 | if (ret != DEVICE_OK) { 229 | return ret; 230 | } 231 | */ 232 | return 0; 233 | } 234 | 235 | int microbit_hal_spi_transfer(size_t len, const uint8_t *src, uint8_t *dest) { 236 | /* 237 | int ret; 238 | if (dest == NULL) { 239 | ret = spi->transfer(src, len, NULL, 0); 240 | } else { 241 | ret = spi->transfer(src, len, dest, len); 242 | } 243 | return ret; 244 | */ 245 | return 0; 246 | } 247 | 248 | int microbit_hal_button_state(int button, int *was_pressed, int *num_presses) { 249 | /* 250 | Button *b = button_obj[button]; 251 | if (was_pressed != NULL || num_presses != NULL) { 252 | uint16_t state = button_state[button]; 253 | int p = b->wasPressed(); 254 | if (p) { 255 | // Update state based on number of presses since last call. 256 | // Low bit is "was pressed at least once", upper bits are "number of presses". 257 | state = (state + (p << 1)) | 1; 258 | } 259 | if (was_pressed != NULL) { 260 | *was_pressed = state & 1; 261 | state &= ~1; 262 | } 263 | if (num_presses != NULL) { 264 | *num_presses = state >> 1; 265 | state &= 1; 266 | } 267 | button_state[button] = state; 268 | } 269 | return b->isPressed(); 270 | */ 271 | // Unlike CODAL, MicroPython clears the state for num_presses count 272 | // and was_pressed independently, so we keep the state here in the same way. 273 | if (was_pressed != NULL || num_presses != NULL) { 274 | uint16_t state = button_state[button]; 275 | int p = mp_js_hal_button_get_presses(button); 276 | if (p) { 277 | // Update state based on number of presses since last call. 278 | // Low bit is "was pressed at least once", upper bits are "number of presses". 279 | state = (state + (p << 1)) | 1; 280 | } 281 | if (was_pressed != NULL) { 282 | *was_pressed = state & 1; 283 | state &= ~1; 284 | } 285 | if (num_presses != NULL) { 286 | *num_presses = state >> 1; 287 | state &= 1; 288 | } 289 | button_state[button] = state; 290 | } 291 | return mp_js_hal_button_is_pressed(button); 292 | } 293 | 294 | void microbit_hal_display_enable(int value) { 295 | /* 296 | if (value) { 297 | uBit.display.enable(); 298 | } else { 299 | uBit.display.disable(); 300 | } 301 | */ 302 | } 303 | 304 | void microbit_hal_display_clear(void) { 305 | mp_js_hal_display_clear(); 306 | } 307 | 308 | int microbit_hal_display_get_pixel(int x, int y) { 309 | return mp_js_hal_display_get_pixel(x, y); 310 | } 311 | 312 | void microbit_hal_display_set_pixel(int x, int y, int bright) { 313 | mp_js_hal_display_set_pixel(x, y, bright); 314 | } 315 | 316 | int microbit_hal_display_read_light_level(void) { 317 | return mp_js_hal_display_read_light_level(); 318 | } 319 | 320 | void microbit_hal_accelerometer_get_sample(int axis[3]) { 321 | axis[0] = mp_js_hal_accelerometer_get_x(); 322 | axis[1] = mp_js_hal_accelerometer_get_y(); 323 | axis[2] = mp_js_hal_accelerometer_get_z(); 324 | } 325 | 326 | int microbit_hal_accelerometer_get_gesture(void) { 327 | return mp_js_hal_accelerometer_get_gesture(); 328 | } 329 | 330 | void microbit_hal_accelerometer_set_range(int r) { 331 | return mp_js_hal_accelerometer_set_range(r); 332 | } 333 | 334 | int microbit_hal_compass_is_calibrated(void) { 335 | // Always calibrated in the simulator. 336 | return 1; 337 | } 338 | 339 | void microbit_hal_compass_clear_calibration(void) { 340 | // No calibration to clear. 341 | } 342 | 343 | void microbit_hal_compass_calibrate(void) { 344 | // No calibration to set. 345 | } 346 | 347 | void microbit_hal_compass_get_sample(int axis[3]) { 348 | axis[0] = mp_js_hal_compass_get_x(); 349 | axis[1] = mp_js_hal_compass_get_y(); 350 | axis[2] = mp_js_hal_compass_get_z(); 351 | } 352 | 353 | int microbit_hal_compass_get_field_strength(void) { 354 | return mp_js_hal_compass_get_field_strength(); 355 | } 356 | 357 | int microbit_hal_compass_get_heading(void) { 358 | return mp_js_hal_compass_get_heading(); 359 | } 360 | 361 | const uint8_t *microbit_hal_get_font_data(char c) { 362 | if (c < BITMAP_FONT_ASCII_START || c > BITMAP_FONT_ASCII_END) 363 | return NULL; 364 | 365 | return pendolino3 + (c-BITMAP_FONT_ASCII_START) * ((1 + (BITMAP_FONT_WIDTH / 8)) * BITMAP_FONT_HEIGHT); 366 | } 367 | 368 | void microbit_hal_log_delete(bool full_erase) { 369 | mp_js_hal_log_delete(full_erase); 370 | } 371 | 372 | void microbit_hal_log_set_mirroring(bool serial) { 373 | mp_js_hal_log_set_mirroring(serial); 374 | } 375 | 376 | void microbit_hal_log_set_timestamp(int period) { 377 | mp_js_hal_log_set_timestamp(period); 378 | } 379 | 380 | int microbit_hal_log_begin_row(void) { 381 | return mp_js_hal_log_begin_row(); 382 | } 383 | 384 | int microbit_hal_log_end_row(void) { 385 | return mp_js_hal_log_end_row(); 386 | } 387 | 388 | int microbit_hal_log_data(const char *key, const char *value) { 389 | return mp_js_hal_log_data(key, value); 390 | } 391 | 392 | // This is used to seed the random number generator. 393 | uint32_t rng_generate_random_word(void) { 394 | return mp_js_rng_generate_random_word(); 395 | } 396 | 397 | void microbit_hal_audio_select_pin(int pin) { 398 | /* 399 | if (pin < 0) { 400 | uBit.audio.setPinEnabled(false); 401 | } else { 402 | uBit.audio.setPinEnabled(true); 403 | uBit.audio.setPin(*pin_obj[pin]); 404 | } 405 | */ 406 | } 407 | 408 | void microbit_hal_audio_select_speaker(bool enable) { 409 | /* 410 | uBit.audio.setSpeakerEnabled(enable); 411 | */ 412 | } 413 | 414 | // Input value has range 0-255 inclusive. 415 | void microbit_hal_audio_set_volume(int value) { 416 | mp_js_hal_audio_set_volume(value); 417 | } 418 | 419 | void microbit_hal_sound_synth_callback(int event) { 420 | // We don't use this callback. Instead microbit_hal_audio_is_expression_active 421 | // calls through to JS which has this state. 422 | } 423 | 424 | bool microbit_hal_audio_is_expression_active(void) { 425 | return mp_js_hal_audio_is_expression_active(); 426 | } 427 | 428 | void microbit_hal_audio_play_expression(const char *expr) { 429 | mp_js_hal_audio_play_expression(expr); 430 | } 431 | 432 | void microbit_hal_audio_stop_expression(void) { 433 | mp_js_hal_audio_stop_expression(); 434 | } 435 | 436 | void microbit_hal_audio_init(uint32_t sample_rate) { 437 | mp_js_hal_audio_init(sample_rate); 438 | } 439 | 440 | void microbit_hal_audio_write_data(const uint8_t *buf, size_t num_samples) { 441 | mp_js_hal_audio_write_data(buf, num_samples); 442 | } 443 | 444 | void microbit_hal_audio_speech_init(uint32_t sample_rate) { 445 | mp_js_hal_audio_speech_init(sample_rate); 446 | } 447 | 448 | void microbit_hal_audio_speech_write_data(const uint8_t *buf, size_t num_samples) { 449 | mp_js_hal_audio_speech_write_data(buf, num_samples); 450 | } 451 | 452 | void microbit_hal_microphone_init(void) { 453 | // This does not implement the use of an external microphone. 454 | // It turns on the microphone indicator light on the sim board. 455 | mp_js_hal_microphone_init(); 456 | /* 457 | if (mic == NULL) { 458 | mic = uBit.adc.getChannel(uBit.io.microphone); 459 | mic->setGain(7, 0); 460 | 461 | processor = new StreamNormalizer(mic->output, 0.05, true, DATASTREAM_FORMAT_8BIT_SIGNED); 462 | level = new LevelDetector(processor->output, 600, 200); 463 | 464 | uBit.io.runmic.setDigitalValue(1); 465 | uBit.io.runmic.setHighDrive(true); 466 | } 467 | */ 468 | } 469 | 470 | void microbit_hal_microphone_set_threshold(int kind, int value) { 471 | mp_js_hal_microphone_set_threshold(kind, value); 472 | /* 473 | value = value * SOUND_LEVEL_MAXIMUM / 255; 474 | if (kind == 0) { 475 | level->setLowThreshold(value); 476 | } else { 477 | level->setHighThreshold(value); 478 | } 479 | */ 480 | } 481 | 482 | int microbit_hal_microphone_get_level(void) { 483 | return mp_js_hal_microphone_get_level(); 484 | /* 485 | if (level == NULL) { 486 | return -1; 487 | } else { 488 | int l = level->getValue(); 489 | l = min(255, l * 255 / SOUND_LEVEL_MAXIMUM); 490 | return l; 491 | } 492 | */ 493 | } 494 | -------------------------------------------------------------------------------- /src/microbithal_js.h: -------------------------------------------------------------------------------- 1 | void microbit_hal_init(void); 2 | void microbit_hal_deinit(void); 3 | void microbit_hal_background_processing(void); 4 | -------------------------------------------------------------------------------- /src/modmachine.c: -------------------------------------------------------------------------------- 1 | #include "py/runtime.h" 2 | 3 | // Just defines the memory access functions, otherwise we use codal_port's 4 | // implementation. 5 | 6 | uintptr_t machine_mem_get_read_addr(mp_obj_t addr_o, uint align) { 7 | uintptr_t addr = mp_obj_get_int_truncated(addr_o); 8 | if ((addr & (align - 1)) != 0) { 9 | mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("address %08x is not aligned to %d bytes"), addr, align); 10 | } 11 | 12 | static const uint32_t FICR = 0x10000000; 13 | static const uint32_t FICR_DEVICEID_0 = FICR + 0x060; 14 | static const uint32_t FICR_DEVICEID_1 = FICR + 0x064; 15 | 16 | static uint32_t mem; 17 | switch (addr) { 18 | case FICR_DEVICEID_0: 19 | case FICR_DEVICEID_1: { 20 | // There's machine.unique_id backed by hal for this 21 | // but existing code reads via FICR. 22 | mem = 0; 23 | break; 24 | } 25 | default: { 26 | mp_raise_NotImplementedError(MP_ERROR_TEXT("simulator limitation: memory read")); 27 | } 28 | } 29 | return (uintptr_t)&mem; 30 | } 31 | 32 | uintptr_t machine_mem_get_write_addr(mp_obj_t addr_o, uint align) { 33 | mp_raise_NotImplementedError(MP_ERROR_TEXT("simulator limitation: memory write")); 34 | } 35 | -------------------------------------------------------------------------------- /src/mpconfigport.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the MicroPython project, http://micropython.org/ 3 | * 4 | * The MIT License (MIT) 5 | * 6 | * Copyright (c) 2022 Damien P. George 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | 27 | // Options to control how MicroPython is built. 28 | 29 | #ifndef MICROPY_INCLUDED_CODAL_PORT_MPCONFIGPORT_H 30 | #define MICROPY_INCLUDED_CODAL_PORT_MPCONFIGPORT_H 31 | 32 | #include 33 | 34 | // Memory allocation policy 35 | #define MICROPY_ALLOC_PATH_MAX (128) 36 | 37 | // MicroPython emitters 38 | #define MICROPY_EMIT_INLINE_THUMB (1) 39 | 40 | // Python internal features 41 | #define MICROPY_VM_HOOK_COUNT (256) 42 | #define MICROPY_VM_HOOK_INIT \ 43 | static unsigned int vm_hook_divisor = MICROPY_VM_HOOK_COUNT; 44 | #define MICROPY_VM_HOOK_POLL \ 45 | if (--vm_hook_divisor == 0) { \ 46 | vm_hook_divisor = MICROPY_VM_HOOK_COUNT; \ 47 | extern void microbit_hal_background_processing(void); \ 48 | microbit_hal_background_processing(); \ 49 | } 50 | #define MICROPY_VM_HOOK_LOOP MICROPY_VM_HOOK_POLL 51 | #define MICROPY_VM_HOOK_RETURN MICROPY_VM_HOOK_POLL 52 | #define MICROPY_ENABLE_GC (1) 53 | #define MICROPY_STACK_CHECK (0) 54 | #define MICROPY_KBD_EXCEPTION (1) 55 | #define MICROPY_HELPER_REPL (1) 56 | #define MICROPY_REPL_AUTO_INDENT (1) 57 | #define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ) 58 | #define MICROPY_ENABLE_SOURCE_LINE (1) 59 | #define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_FLOAT) 60 | #define MICROPY_STREAMS_NON_BLOCK (1) 61 | #define MICROPY_MODULE_BUILTIN_INIT (1) 62 | #define MICROPY_MODULE_WEAK_LINKS (1) 63 | #define MICROPY_MODULE_FROZEN_MPY (1) 64 | #define MICROPY_QSTR_EXTRA_POOL mp_qstr_frozen_const_pool 65 | #define MICROPY_USE_INTERNAL_ERRNO (1) 66 | #define MICROPY_USE_INTERNAL_PRINTF (0) 67 | #define MICROPY_ENABLE_PYSTACK (1) 68 | #define MICROPY_ENABLE_SCHEDULER (1) 69 | 70 | // Fine control over Python builtins, classes, modules, etc 71 | #define MICROPY_PY_BUILTINS_STR_UNICODE (1) 72 | #define MICROPY_PY_BUILTINS_MEMORYVIEW (1) 73 | #define MICROPY_PY_BUILTINS_FROZENSET (1) 74 | #define MICROPY_PY_BUILTINS_INPUT (1) 75 | #define MICROPY_PY_BUILTINS_HELP (1) 76 | #define MICROPY_PY_BUILTINS_HELP_TEXT microbit_help_text 77 | #define MICROPY_PY_BUILTINS_HELP_MODULES (1) 78 | #define MICROPY_PY___FILE__ (0) 79 | #define MICROPY_PY_MICROPYTHON_MEM_INFO (1) 80 | #define MICROPY_PY_COLLECTIONS_ORDEREDDICT (1) 81 | #define MICROPY_PY_IO (0) 82 | #define MICROPY_PY_SYS_MAXSIZE (1) 83 | #define MICROPY_PY_SYS_PLATFORM "microbit" 84 | 85 | // Extended modules 86 | #define MICROPY_PY_UERRNO (1) 87 | #define MICROPY_PY_UTIME_MP_HAL (1) 88 | #define MICROPY_PY_URANDOM (1) 89 | #define MICROPY_PY_URANDOM_SEED_INIT_FUNC (rng_generate_random_word()) 90 | #define MICROPY_PY_URANDOM_EXTRA_FUNCS (1) 91 | #define MICROPY_PY_MACHINE (1) 92 | #define MICROPY_PY_MACHINE_PULSE (1) 93 | 94 | #define MICROPY_HW_ENABLE_RNG (1) 95 | 96 | // The simulator provides its own version of the relevant mbfs methods. 97 | #define MICROPY_MBFS (1) 98 | 99 | // extra built in names to add to the global namespace 100 | #if MICROPY_MBFS 101 | #define MICROPY_PORT_BUILTINS \ 102 | { MP_ROM_QSTR(MP_QSTR_open), MP_ROM_PTR(&mp_builtin_open_obj) }, 103 | #endif 104 | 105 | #define MICROBIT_RELEASE "2.1.1" 106 | #define MICROBIT_BOARD_NAME "micro:bit" 107 | #define MICROPY_HW_BOARD_NAME MICROBIT_BOARD_NAME " v" MICROBIT_RELEASE 108 | #define MICROPY_HW_MCU_NAME "nRF52833" 109 | 110 | #define MP_STATE_PORT MP_STATE_VM 111 | 112 | extern const struct _mp_obj_module_t antigravity_module; 113 | extern const struct _mp_obj_module_t audio_module; 114 | extern const struct _mp_obj_module_t log_module; 115 | extern const struct _mp_obj_module_t love_module; 116 | extern const struct _mp_obj_module_t machine_module; 117 | extern const struct _mp_obj_module_t microbit_module; 118 | extern const struct _mp_obj_module_t music_module; 119 | extern const struct _mp_obj_module_t os_module; 120 | extern const struct _mp_obj_module_t power_module; 121 | extern const struct _mp_obj_module_t radio_module; 122 | extern const struct _mp_obj_module_t speech_module; 123 | extern const struct _mp_obj_module_t this_module; 124 | extern const struct _mp_obj_module_t utime_module; 125 | 126 | #define MICROPY_PORT_BUILTIN_MODULES \ 127 | { MP_ROM_QSTR(MP_QSTR_antigravity), MP_ROM_PTR(&antigravity_module) }, \ 128 | { MP_ROM_QSTR(MP_QSTR_audio), MP_ROM_PTR(&audio_module) }, \ 129 | { MP_ROM_QSTR(MP_QSTR_log), MP_ROM_PTR(&log_module) }, \ 130 | { MP_ROM_QSTR(MP_QSTR_love), MP_ROM_PTR(&love_module) }, \ 131 | { MP_ROM_QSTR(MP_QSTR_machine), MP_ROM_PTR(&machine_module) }, \ 132 | { MP_ROM_QSTR(MP_QSTR_microbit), MP_ROM_PTR(µbit_module) }, \ 133 | { MP_ROM_QSTR(MP_QSTR_music), MP_ROM_PTR(&music_module) }, \ 134 | { MP_ROM_QSTR(MP_QSTR_os), MP_ROM_PTR(&os_module) }, \ 135 | { MP_ROM_QSTR(MP_QSTR_power), MP_ROM_PTR(&power_module) }, \ 136 | { MP_ROM_QSTR(MP_QSTR_radio), MP_ROM_PTR(&radio_module) }, \ 137 | { MP_ROM_QSTR(MP_QSTR_speech), MP_ROM_PTR(&speech_module) }, \ 138 | { MP_ROM_QSTR(MP_QSTR_this), MP_ROM_PTR(&this_module) }, \ 139 | { MP_ROM_QSTR(MP_QSTR_utime), MP_ROM_PTR(&utime_module) }, \ 140 | 141 | #define MICROPY_PORT_ROOT_POINTERS \ 142 | const char *readline_hist[8]; \ 143 | void *display_data; \ 144 | uint8_t *radio_buf; \ 145 | void *audio_source; \ 146 | void *speech_data; \ 147 | struct _music_data_t *music_data; \ 148 | struct _microbit_soft_timer_entry_t *soft_timer_heap; \ 149 | 150 | #define MP_SSIZE_MAX (0x7fffffff) 151 | 152 | // Type definitions for the specific machine 153 | typedef intptr_t mp_int_t; // must be pointer size 154 | typedef uintptr_t mp_uint_t; // must be pointer size 155 | typedef long mp_off_t; 156 | 157 | // We need to provide a declaration/definition of alloca() 158 | #include 159 | 160 | // Needed for MICROPY_PY_URANDOM_SEED_INIT_FUNC. 161 | extern uint32_t rng_generate_random_word(void); 162 | 163 | // Intercept modmachine memory access. 164 | #define MICROPY_MACHINE_MEM_GET_READ_ADDR machine_mem_get_read_addr 165 | #define MICROPY_MACHINE_MEM_GET_WRITE_ADDR machine_mem_get_write_addr 166 | 167 | #define MICROPY_MAKE_POINTER_CALLABLE(p) \ 168 | ((mp_raise_NotImplementedError(MP_ERROR_TEXT("simulator limitation: asm_thumb code"))), p) 169 | 170 | #endif 171 | -------------------------------------------------------------------------------- /src/mphalport.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include "py/mphal.h" 3 | #include "py/stream.h" 4 | #include "microbithal_js.h" 5 | #include "jshal.h" 6 | 7 | static uint8_t stdin_ringbuf_array[260]; 8 | ringbuf_t stdin_ringbuf = {stdin_ringbuf_array, sizeof(stdin_ringbuf_array), 0, 0}; 9 | 10 | uintptr_t mp_hal_stdio_poll(uintptr_t poll_flags) { 11 | uintptr_t ret = 0; 12 | if ((poll_flags & MP_STREAM_POLL_RD) && stdin_ringbuf.iget != stdin_ringbuf.iput) { 13 | ret |= MP_STREAM_POLL_RD; 14 | } 15 | return ret; 16 | } 17 | 18 | void mp_hal_stdout_tx_strn(const char *str, size_t len) { 19 | mp_js_hal_stdout_tx_strn(str, len); 20 | } 21 | 22 | int mp_hal_stdin_rx_chr(void) { 23 | for (;;) { 24 | int c = ringbuf_get(&stdin_ringbuf); 25 | if (c != -1) { 26 | return c; 27 | } 28 | mp_handle_pending(true); 29 | microbit_hal_idle(); 30 | } 31 | } 32 | 33 | mp_uint_t mp_hal_ticks_us(void) { 34 | return mp_js_hal_ticks_ms() * 1000; 35 | } 36 | 37 | mp_uint_t mp_hal_ticks_ms(void) { 38 | return mp_js_hal_ticks_ms(); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /src/mphalport.h: -------------------------------------------------------------------------------- 1 | #include "py/obj.h" 2 | #include "py/ringbuf.h" 3 | #include "microbithal.h" 4 | #include "modmicrobit.h" 5 | 6 | // Constants for the nRF needed by the radio module. 7 | #define RADIO_MODE_MODE_Nrf_1Mbit (0) 8 | #define RADIO_MODE_MODE_Nrf_2Mbit (1) 9 | 10 | extern ringbuf_t stdin_ringbuf; 11 | 12 | void mp_hal_set_interrupt_char(int c); 13 | 14 | static inline uint32_t mp_hal_disable_irq(void) { 15 | return 0; 16 | } 17 | 18 | static inline void mp_hal_enable_irq(uint32_t state) { 19 | (void)state; 20 | } 21 | 22 | static inline void mp_hal_unique_id(uint32_t id[2]) { 23 | id[0] = 0; 24 | id[1] = 0; 25 | } 26 | -------------------------------------------------------------------------------- /src/simulator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Simulator 5 | 6 | 54 | 55 | 56 |
57 | 73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/simulator.ts: -------------------------------------------------------------------------------- 1 | import * as conversions from "./board/conversions"; 2 | import { FileSystem } from "./board/fs"; 3 | import { EmscriptenModule } from "./board/wasm"; 4 | import { 5 | Board, 6 | createBoard, 7 | createMessageListener, 8 | Notifications, 9 | } from "./board"; 10 | import { flags } from "./flags"; 11 | 12 | declare global { 13 | interface Window { 14 | // Provided by firmware.js 15 | createModule: (args: object) => Promise; 16 | } 17 | } 18 | 19 | function initServiceWorker() { 20 | window.addEventListener("load", () => { 21 | navigator.serviceWorker.register("sw.js").then( 22 | (registration) => { 23 | console.log("Simulator service worker registration successful"); 24 | // Reload the page when a new service worker is installed. 25 | registration.onupdatefound = function () { 26 | const installingWorker = registration.installing; 27 | if (installingWorker) { 28 | installingWorker.onstatechange = function () { 29 | if ( 30 | installingWorker.state === "installed" && 31 | navigator.serviceWorker.controller 32 | ) { 33 | window.location.reload(); 34 | } 35 | }; 36 | } 37 | }; 38 | }, 39 | (error) => { 40 | console.error(`Simulator service worker registration failed: ${error}`); 41 | } 42 | ); 43 | }); 44 | } 45 | 46 | if ("serviceWorker" in navigator) { 47 | if (flags.sw) { 48 | initServiceWorker(); 49 | } else { 50 | navigator.serviceWorker.getRegistration().then((registration) => { 51 | registration?.unregister().then(() => { 52 | window.location.reload(); 53 | }); 54 | }); 55 | } 56 | } 57 | 58 | const fs = new FileSystem(); 59 | const board = createBoard(new Notifications(window.parent), fs); 60 | window.addEventListener("message", createMessageListener(board)); 61 | -------------------------------------------------------------------------------- /src/sw.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // Empty export required due to --isolatedModules flag in tsconfig.json 3 | export type {}; 4 | declare const self: ServiceWorkerGlobalScope; 5 | declare const clients: Clients; 6 | 7 | const assets = ["simulator.html", "build/simulator.js", "build/firmware.js", "build/firmware.wasm"]; 8 | const cacheName = `simulator-${process.env.VERSION}`; 9 | 10 | self.addEventListener("install", (event) => { 11 | console.log("Installing simulator service worker..."); 12 | self.skipWaiting(); 13 | event.waitUntil( 14 | (async () => { 15 | const cache = await caches.open(cacheName); 16 | await cache.addAll(assets); 17 | })() 18 | ); 19 | }); 20 | 21 | self.addEventListener("activate", (event) => { 22 | console.log("Activating simulator service worker..."); 23 | event.waitUntil( 24 | (async () => { 25 | const names = await caches.keys(); 26 | await Promise.all( 27 | names.map((name) => { 28 | if (/^simulator-/.test(name) && name !== cacheName) { 29 | return caches.delete(name); 30 | } 31 | }) 32 | ); 33 | await clients.claim(); 34 | })() 35 | ); 36 | }); 37 | 38 | self.addEventListener("fetch", (event) => { 39 | event.respondWith( 40 | (async () => { 41 | const cachedResponse = await caches.match(event.request); 42 | if (cachedResponse) { 43 | return cachedResponse; 44 | } 45 | const response = await fetch(event.request); 46 | const cache = await caches.open(cacheName); 47 | cache.put(event.request, response.clone()); 48 | return response; 49 | })() 50 | ); 51 | }); 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2019", 4 | "lib": ["dom", "dom.iterable", "esnext", "WebWorker"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true 17 | }, 18 | "include": ["src"] 19 | } 20 | --------------------------------------------------------------------------------