├── .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 | Kind
49 | Example
50 | Description
51 |
52 |
53 | 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 |
83 | 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 |
108 | 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 |
120 | 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 |
133 | 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 |
147 | 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 | Kind
167 | Example
168 | Description
169 |
170 |
171 | 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 |
188 | stop
189 |
190 |
191 | ```javascript
192 | {
193 | "kind": "stop"
194 | }
195 | ```
196 |
197 | Stop the program.
198 |
199 |
200 | reset
201 |
202 |
203 | ```javascript
204 | {
205 | "kind": "reset"
206 | }
207 | ```
208 |
209 | Reset the program.
210 |
211 |
212 | mute
213 |
214 |
215 | ```javascript
216 | {
217 | "kind": "mute"
218 | }
219 | ```
220 |
221 | Mute the simulator.
222 |
223 |
224 | unmute
225 |
226 |
227 | ```javascript
228 | {
229 | "kind": "unmute"
230 | }
231 | ```
232 |
233 | Unmute the simulator.
234 |
235 |
236 | 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 |
248 | 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 |
262 | 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 |
79 |
80 |
81 |
82 |
83 |
84 | Accelerometer
85 | Audio
86 | Background music and display
87 | Buttons
88 | Compass
89 | Data logging
90 | Display
91 | Inline assembler
92 | Microphone
93 | Music
94 | Pin logo
95 | Radio
96 | Random
97 | Sensors
98 |
99 | Sound effects (builtin)
100 |
101 | Sound effects (user)
102 | Speech
103 | Stack size
104 | Volume
105 |
106 |
114 |
115 | Stop
116 | Reset
117 | Mute
118 | Unmute
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 |
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 |
--------------------------------------------------------------------------------