├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .gitattributes ├── .github └── workflows │ ├── build.yml │ └── codeql-analysis.yml ├── .gitignore ├── .gitmodules ├── .nojekyll ├── .npm-ignore ├── .prettierrc ├── .releaserc.json ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DOCS.md ├── LICENSE ├── README.md ├── SECURITY.md ├── assets └── icons │ ├── chip.svg │ ├── jd-logo-pads.svg │ ├── makecode.svg │ └── microbit.svg ├── build.js ├── package.json ├── src ├── clients │ ├── cloudconfigurationclient.ts │ ├── modelrunnerclient.ts │ └── sensoraggregatorclient.ts ├── embed │ ├── filestorage.ts │ ├── jacdac-embed.ts │ ├── modelstore.ts │ ├── protocol.ts │ └── transport.ts ├── jacdac.ts ├── jdom │ ├── bridge.ts │ ├── bridges │ │ ├── iframebridge.ts │ │ └── websocketbridge.ts │ ├── buffer.ts │ ├── bus.ts │ ├── busstats.ts │ ├── catalog.ts │ ├── client.ts │ ├── clients │ │ ├── devicescriptmanagerclient.ts │ │ ├── rolemanagerclient.ts │ │ └── settingsclient.ts │ ├── color.ts │ ├── command.ts │ ├── constants.ts │ ├── device.ts │ ├── devtools.ts │ ├── error.ts │ ├── event.ts │ ├── eventsource.ts │ ├── field.ts │ ├── filters │ │ ├── devicefilter.ts │ │ └── servicefilter.ts │ ├── flags.ts │ ├── flashing.ts │ ├── iframeclient.ts │ ├── jacdac-jdom.ts │ ├── ledcontroller.ts │ ├── light.ts │ ├── lightconstants.ts │ ├── logparser.ts │ ├── node.ts │ ├── nodesetting.ts │ ├── observable.ts │ ├── pack.ts │ ├── packet.ts │ ├── packetfilter.ts │ ├── packobject.ts │ ├── pipes.ts │ ├── pretty.ts │ ├── random.ts │ ├── register.ts │ ├── rolemanager.ts │ ├── scheduler.ts │ ├── semver.ts │ ├── sensors.ts │ ├── servers │ │ ├── controlserver.ts │ │ ├── protocoltestserver.ts │ │ ├── registerserver.ts │ │ ├── rolemanagerserver.ts │ │ ├── serverserviceprovider.ts │ │ ├── serviceprovider.ts │ │ └── serviceserver.ts │ ├── service.ts │ ├── serviceclient.ts │ ├── servicemembernode.ts │ ├── setting.ts │ ├── sevensegment.ts │ ├── spec.ts │ ├── speedtest.ts │ ├── trace │ │ ├── trace.ts │ │ ├── traceplayer.ts │ │ ├── tracerecorder.ts │ │ └── traceview.ts │ ├── transport │ │ ├── bluetooth.ts │ │ ├── createbus.ts │ │ ├── eventtargetobservable.ts │ │ ├── hf2.ts │ │ ├── jdusb.ts │ │ ├── microbit.ts │ │ ├── nodesocket.ts │ │ ├── nodespi.ts │ │ ├── nodewebserialio.ts │ │ ├── nodewebusb.ts │ │ ├── proto.ts │ │ ├── transport.ts │ │ ├── transportmessages.ts │ │ ├── usb.ts │ │ ├── usbio.ts │ │ ├── webserial.ts │ │ ├── webserialio.ts │ │ ├── websockettransport.ts │ │ └── workertransport.ts │ └── utils.ts ├── p5 │ └── p5.jacdac.ts ├── servers │ ├── accelerometerserver.ts │ ├── analogsensorserver.ts │ ├── bitradioserver.ts │ ├── brailledisplayserver.ts │ ├── buttonserver.ts │ ├── buzzerserver.ts │ ├── capacitivebuttonserver.ts │ ├── characterscreenserver.ts │ ├── cloudadapterserver.ts │ ├── compassserver.ts │ ├── devicescriptmanagerserver.ts │ ├── dmxserver.ts │ ├── dotmatrixserver.ts │ ├── dualmotorsserver.ts │ ├── gamepadserver.ts │ ├── gamepadservermanager.ts │ ├── hidjoystickserver.ts │ ├── hidkeyboardserver.ts │ ├── hidmouseserver.ts │ ├── indexedscreenserver.ts │ ├── jacdac-servers.ts │ ├── ledserver.ts │ ├── ledstripserver.ts │ ├── leveldetector.ts │ ├── loggerserver.ts │ ├── magneticfieldlevelserver.ts │ ├── matrixkeypadserver.ts │ ├── motorserver.ts │ ├── pccontrollerserver.ts │ ├── pcmonitorserver.ts │ ├── planarpositionserver.ts │ ├── powerserver.ts │ ├── powersupplyserver.ts │ ├── raingaugeserver.ts │ ├── randomnumbergeneratorserver.ts │ ├── realtimeclockserver.ts │ ├── reflectedlightserver.ts │ ├── rosserver.ts │ ├── rotaryencoderserver.ts │ ├── satnavserver.ts │ ├── sensorserver.ts │ ├── serialserver.ts │ ├── servers.ts │ ├── servoserver.ts │ ├── settingsserver.ts │ ├── sevensegmentdisplayserver.ts │ ├── soundplayerserver.ts │ ├── speechsynthesisserver.ts │ ├── switchserver.ts │ ├── trafficlightserver.ts │ ├── verifiedtelemetryserver.ts │ ├── vibrationmotorserver.ts │ └── wifiserver.ts ├── testdom │ ├── compiler.ts │ ├── nodes.ts │ ├── spec.ts │ └── testrules.ts ├── tstester │ ├── base.ts │ ├── button.spec.ts │ ├── eventhold.ts │ ├── jacdac-tstester.ts │ ├── ledstrip.spec.ts │ ├── naming.ts │ ├── potentiometer.spec.ts │ ├── registerwrapper.ts │ ├── servicewrapper.ts │ └── testwrappers.ts └── worker │ ├── jacdac-worker.ts │ ├── transportproxy.ts │ ├── tsconfig.json │ ├── usbtransportproxy.ts │ ├── webusb.d.ts │ └── workerloader.js ├── tests ├── jdom │ ├── bus.spec.ts │ ├── button.spec.ts │ ├── command.spec.ts │ ├── csvtrace.spec.ts │ ├── fastforwardtester.ts │ ├── pack.spec.ts │ ├── rolemanager.spec.ts │ ├── scheduler.spec.ts │ ├── scheduler.ts │ ├── servercsvsource.ts │ └── testerfailure.spec.ts ├── p5 │ ├── index.html │ ├── jsconfig.json │ ├── sketch.js │ └── style.css ├── testutils.ts └── tsconfig.json ├── tools ├── console.html ├── devices.html ├── flashing.html ├── namer.html ├── packets.html ├── prepare.js ├── streaming-rickshaw.html ├── streaming.html ├── testtest.html └── tflite.html ├── tsconfig.json ├── tslint.json ├── typedoc.json └── yarn.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node/.devcontainer/base.Dockerfile 2 | ARG VARIANT="18" 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | # [Optional] Uncomment if you want to install an additional version of node using nvm 10 | # ARG EXTRA_NODE_VERSION=10 11 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 12 | 13 | # [Optional] Uncomment if you want to install more global node modules 14 | RUN sudo -u node npm install -g gatsby-cli 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/javascript-node 3 | { 4 | "name": "Node.js", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | // Update 'VARIANT' to pick a Node version: 10, 12, 14 8 | "args": { 9 | "VARIANT": "18" 10 | } 11 | }, 12 | // Set *default* container specific settings.json values on container create. 13 | "settings": {}, 14 | // Add the IDs of extensions you want installed when the container is created. 15 | "extensions": [ 16 | "dbaeumer.vscode-eslint", 17 | "esbenp.prettier-vscode" 18 | ], 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | "forwardPorts": [ 21 | 8080, 22 | 8000 23 | ], 24 | // Specifies a command that should be run after the container has been created. 25 | "postCreateCommand": "yarn setup", 26 | // Comment out the next line to run as root instead. 27 | "remoteUser": "node" 28 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # enforce unix style line endings 2 | *.ts text eol=lf 3 | *.tsx text eol=lf 4 | *.cpp text eol=lf 5 | *.h text eol=lf 6 | *.jres text eol=lf 7 | *.asm text eol=lf 8 | *.md text eol=lf 9 | *.txt text eol=lf 10 | *.js text eol=lf 11 | *.json text eol=lf 12 | *.xml text eol=lf 13 | *.svg text eol=lf 14 | *.yaml text eol=lf 15 | *.css text eol=lf 16 | *.html text eol=lf 17 | *.py text eol=lf 18 | *.exp text eol=lf 19 | *.manifest text eol=lf 20 | 21 | # do not enforce text for everything - it causes issues with random binary files 22 | 23 | *.sln text eol=crlf 24 | 25 | *.png binary 26 | *.jpg binary 27 | *.jpeg binary 28 | *.gif binary 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | # Controls when the action will run. 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | permissions: 13 | actions: read 14 | contents: write 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | submodules: recursive 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | cache: 'yarn' 30 | - run: yarn install --frozen-lockfile 31 | - run: yarn prettier 32 | - run: yarn dist 33 | - run: yarn test 34 | - name: semantic release 35 | run: npx -p=semantic-release -p=@semantic-release/git -p=@semantic-release/npm -p=@semantic-release/github -p=@semantic-release/exec semantic-release 36 | if: ${{ github.ref == 'refs/heads/dev' }} 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | - run: yarn builddocs 41 | - run: yarn disttools 42 | - name: github pages 43 | uses: peaceiris/actions-gh-pages@v3 44 | if: ${{ github.ref == 'refs/heads/dev' }} 45 | with: 46 | github_token: ${{ secrets.GITHUB_TOKEN }} 47 | enable_jekyll: false 48 | publish_dir: ./docs 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [dev, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [dev] 9 | schedule: 10 | - cron: '0 12 * * 3' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-20.04 16 | permissions: 17 | actions: read 18 | contents: read 19 | security-events: write 20 | 21 | steps: 22 | - name: Checkout repository 23 | uses: actions/checkout@v4 24 | with: 25 | # We must fetch at least the immediate parents so that if this is 26 | # a pull request then we can checkout the head. 27 | fetch-depth: 2 28 | 29 | # Initializes the CodeQL tools for scanning. 30 | - name: Initialize CodeQL 31 | uses: github/codeql-action/init@v3 32 | # Override language selection by uncommenting this and choosing your languages 33 | # with: 34 | # languages: go, javascript, csharp, python, cpp, java 35 | 36 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 37 | # If this step fails, then you should remove it and run the build manually (see below) 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v3 40 | 41 | # ℹ️ Command-line programs to run using the OS shell. 42 | # 📚 https://git.io/JvXDl 43 | 44 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 45 | # and modify them (or add more) to build your code if your project 46 | # uses a compiled language 47 | 48 | #- run: | 49 | # make bootstrap 50 | # make release 51 | 52 | - name: Perform CodeQL Analysis 53 | uses: github/codeql-action/analyze@v3 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | .DS_Store 4 | *.log 5 | .idea 6 | compiled 7 | .awcache 8 | .rpt2_cache 9 | built 10 | dist 11 | temp 12 | .lighthouseci 13 | package-lock.json 14 | stats.html 15 | docs 16 | tests/p5/libraries 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jacdac-spec"] 2 | path = jacdac-spec 3 | url = https://github.com/microsoft/jacdac 4 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microsoft/jacdac-ts/bd6b18b1102c8f3a6713c3e0d615f8c658122b01/.nojekyll -------------------------------------------------------------------------------- /.npm-ignore: -------------------------------------------------------------------------------- 1 | .* 2 | **/tsconfig.json 3 | 4 | node_modules 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "semi": false, 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branch": "main", 3 | "branches": ["main"], 4 | "plugins": [ 5 | [ 6 | "@semantic-release/commit-analyzer", 7 | { 8 | "preset": "angular", 9 | "releaseRules": [ 10 | { 11 | "type": "doc", 12 | "release": "patch" 13 | }, 14 | { 15 | "type": "fix", 16 | "release": "patch" 17 | }, 18 | { 19 | "type": "patch", 20 | "release": "patch" 21 | }, 22 | { 23 | "type": "minor", 24 | "release": "minor" 25 | }, 26 | { 27 | "type": "feat", 28 | "release": "minor" 29 | }, 30 | { 31 | "type": "feature", 32 | "release": "minor" 33 | }, 34 | { 35 | "scope": "no-release", 36 | "release": false 37 | } 38 | ] 39 | } 40 | ], 41 | ["@semantic-release/release-notes-generator"], 42 | [ 43 | "@semantic-release/github", 44 | { 45 | "successComment": false, 46 | "failComment": false 47 | } 48 | ], 49 | ["@semantic-release/npm"], 50 | [ 51 | "@semantic-release/git", 52 | { 53 | "assets": ["package.json", "src/**/*.ts"] 54 | } 55 | ] 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "cli usb", 11 | "skipFiles": ["/**"], 12 | "program": "${workspaceFolder}/dist/jacdac-cli.js", 13 | "args": ["--usb"], 14 | "outFiles": ["${workspaceFolder}/dist/*.js"] 15 | }, 16 | { 17 | "name": "Attach to Node Functions", 18 | "type": "node", 19 | "request": "attach", 20 | "port": 9229, 21 | "preLaunchTask": "func: host start" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "eslint.alwaysShowStatus": true, 4 | "eslint.format.enable": false, 5 | "eslint.debug": true, 6 | "eslint.lintTask.enable": true, 7 | "files.eol": "\n", 8 | "[typescript]": { 9 | "editor.defaultFormatter": "esbenp.prettier-vscode" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "vscode.json-language-features" 13 | }, 14 | "azureFunctions.deploySubpath": "device-models-function", 15 | "azureFunctions.postDeployTask": "npm install (functions)", 16 | "azureFunctions.projectLanguage": "TypeScript", 17 | "azureFunctions.projectRuntime": "~3", 18 | "debug.internalConsoleOptions": "neverOpen", 19 | } 20 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "func", 6 | "command": "host start", 7 | "problemMatcher": "$func-node-watch", 8 | "isBackground": true, 9 | "dependsOn": "npm build (functions)", 10 | "options": { 11 | "cwd": "${workspaceFolder}/device-models-function" 12 | } 13 | }, 14 | { 15 | "type": "shell", 16 | "label": "npm build (functions)", 17 | "command": "npm run build", 18 | "dependsOn": "npm install (functions)", 19 | "problemMatcher": "$tsc", 20 | "options": { 21 | "cwd": "${workspaceFolder}/device-models-function" 22 | } 23 | }, 24 | { 25 | "type": "shell", 26 | "label": "npm install (functions)", 27 | "command": "yarn install --frozen-lockfile", 28 | "options": { 29 | "cwd": "${workspaceFolder}/device-models-function" 30 | } 31 | }, 32 | { 33 | "type": "shell", 34 | "label": "npm prune (functions)", 35 | "command": "npm prune --production", 36 | "dependsOn": "npm build (functions)", 37 | "problemMatcher": [], 38 | "options": { 39 | "cwd": "${workspaceFolder}/device-models-function" 40 | } 41 | } 42 | ] 43 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Microsoft Open Source Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 4 | 5 | Resources: 6 | 7 | - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) 8 | - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 9 | - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project welcomes contributions and suggestions. Most contributions require you to 4 | agree to a Contributor License Agreement (CLA) declaring that you have the right to, 5 | and actually do, grant us the rights to use your contribution. For details, visit 6 | https://cla.microsoft.com. 7 | 8 | When you submit a pull request, a CLA-bot will automatically determine whether you need 9 | to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the 10 | instructions provided by the bot. You will only need to do this once across all repositories using our CLA. 11 | 12 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 13 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) 14 | or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 15 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # JavaScript/TypeScript package 2 | 3 | This package allows you to integrate Jacdac into web applications or Node.JS projects. 4 | The package exposes **JDOM**, a dependency-free JavaScript object model 5 | that reflects the state of the Jacdac elements and allows sending commands as well. 6 | JDOM also handles connection through WebUSB, WebBLE and other transports. 7 | 8 | To read guides and overview documents about JDOM, go to [JDOM documentation](https://microsoft.github.io/jacdac-docs/clients/javascript/jdom). 9 | 10 | To browser the API documentation, use the list on this page to explore classes. 11 | 12 | ## Installation 13 | 14 | Add the [jacdac-ts npm package](https://www.npmjs.com/package/jacdac-ts) module 15 | to your project 16 | 17 | ``` 18 | npm install --save jacdac-ts 19 | ``` 20 | 21 | or 22 | 23 | ``` 24 | yarn add jacdac-ts 25 | ``` 26 | 27 | then import components as needed using ES6 import syntax 28 | 29 | ```javascript 30 | import { createWebBus } from "jacdac-ts" 31 | 32 | const bus = createWebBus() 33 | ``` 34 | 35 | ### CDN / UMD 36 | 37 | You can also use CDN services to import `jacdac` into your html page directly. 38 | This will load the ES6 build of the library. 39 | 40 | ```html 41 | 42 | ``` 43 | 44 | where `@VERSION` is the desired version. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. 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 all 13 | 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 THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). 40 | 41 | -------------------------------------------------------------------------------- /assets/icons/chip.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/jd-logo-pads.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/icons/microbit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const esbuild = require("esbuild") 3 | const fs = require("fs") 4 | const childProcess = require("child_process") 5 | 6 | let watch = false 7 | let fast = false 8 | 9 | const args = process.argv.slice(2) 10 | if (args[0] == "--watch" || args[0] == "-watch" || args[0] == "-w") { 11 | args.shift() 12 | watch = true 13 | } 14 | 15 | if (args[0] == "--fast" || args[0] == "-fast" || args[0] == "-f") { 16 | args.shift() 17 | fast = true 18 | } 19 | 20 | if (args.length) { 21 | console.log("Usage: ./build.js [--watch]") 22 | process.exit(1) 23 | } 24 | 25 | function runTSC(args) { 26 | return new Promise((resolve, reject) => { 27 | let invoked = false 28 | if (watch) args.push("--watch", "--preserveWatchOutput") 29 | console.log("run tsc " + args.join(" ")) 30 | let tscPath = "node_modules/typescript/lib/tsc.js" 31 | if (!fs.existsSync(tscPath)) tscPath = "../" + tscPath 32 | const process = childProcess.fork(tscPath, args) 33 | process.on("error", err => { 34 | if (invoked) return 35 | invoked = true 36 | reject(err) 37 | }) 38 | 39 | process.on("exit", code => { 40 | if (invoked) return 41 | invoked = true 42 | if (code == 0) resolve() 43 | else reject(new Error("exit " + code)) 44 | }) 45 | 46 | // in watch mode "go in background" 47 | if (watch) 48 | setTimeout(() => { 49 | if (invoked) return 50 | invoked = true 51 | resolve() 52 | }, 500) 53 | }) 54 | } 55 | 56 | const files = { 57 | "dist/jacdac.cjs": "src/jacdac.ts", 58 | "dist/jacdac.js": "src/jacdac.ts", 59 | "dist/jacdac.mjs": "src/jacdac.ts", 60 | "dist/p5.jacdac.js": "src/p5/p5.jacdac.ts", 61 | "dist/jacdac-embed.js": "src/embed/jacdac-embed.ts", 62 | "dist/jacdac-worker.js": "src/worker/jacdac-worker.ts", 63 | "dist/jacdac-tstester.mjs": "src/tstester/jacdac-tstester.ts", 64 | } 65 | 66 | function check(pr) { 67 | pr.then( 68 | () => {}, 69 | err => { 70 | console.error("Error: " + err.message) 71 | process.exit(1) 72 | } 73 | ) 74 | } 75 | 76 | async function main() { 77 | try { 78 | for (const outfile of Object.keys(files)) { 79 | const src = files[outfile] 80 | const cjs = outfile.endsWith(".cjs") 81 | const mjs = outfile.endsWith(".mjs") 82 | await esbuild.build({ 83 | entryPoints: [src], 84 | bundle: true, 85 | sourcemap: true, 86 | outfile, 87 | logLevel: "warning", 88 | external: ["net", "webusb", "crypto", "fs"], 89 | platform: cjs ? "node" : "browser", 90 | target: "es2020", 91 | format: mjs ? "esm" : cjs ? "cjs" : "iife", 92 | globalName: "jacdac", 93 | watch, 94 | }) 95 | } 96 | console.log("bundle done") 97 | if (!fast) { 98 | await runTSC(["-b", "."]) 99 | await runTSC(["-b", "src/worker"]) 100 | } 101 | } catch {} 102 | } 103 | 104 | main() 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jacdac-ts", 3 | "version": "1.33.7", 4 | "description": "JavaScript/TypeScript library to interact with Jacdac devices", 5 | "keywords": [ 6 | "jacdac", 7 | "typescript", 8 | "i2c", 9 | "sensors", 10 | "iot" 11 | ], 12 | "main": "dist/jacdac.cjs", 13 | "module": "dist/jacdac.mjs", 14 | "exports": { 15 | ".": { 16 | "import": "./dist/jacdac.mjs", 17 | "require": "./dist/jacdac.cjs" 18 | } 19 | }, 20 | "typings": "dist/types/src/jacdac.d.ts", 21 | "files": [ 22 | "dist", 23 | "jacdac-spec", 24 | "service", 25 | "src" 26 | ], 27 | "author": "Microsoft Corporation", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/microsoft/jacdac-ts.git" 31 | }, 32 | "license": "MIT", 33 | "engines": { 34 | "node": ">=12.0.0" 35 | }, 36 | "scripts": { 37 | "setup": "yarn pullsubmodules && yarn install --frozen-lockfile", 38 | "pullsubmodules": "git submodule update --init --recursive", 39 | "lint": "node node_modules/eslint/bin/eslint.js src/**/*.ts", 40 | "prettier": "prettier --write src/**/*.ts tests/**/*.ts", 41 | "predist": "rm -rf dist", 42 | "dist": "node build.js", 43 | "build": "node build.js", 44 | "watch": "node build.js --watch", 45 | "test": "(cd tests && node ../node_modules/mocha/bin/mocha --exit --require ts-node/register --watch-extensions ts,tsx **/*.spec.ts)", 46 | "testone": "cd tests && node ../node_modules/mocha/bin/mocha --exit --require ts-node/register --watch-extensions ts,tsx **/rolemanager.spec.ts", 47 | "tools": "npx http-server . -c-1", 48 | "disttools": "cd docs && mkdir tools && cd ../tools && node ./prepare.js && cp * -t ../docs/tools", 49 | "buildspecs": "cd jacdac-spec/spectool && sh build.sh", 50 | "buildpxt": "cd pxt-jacdac && sh mk.sh", 51 | "builddocs": "node node_modules/typedoc/bin/typedoc && cp .nojekyll docs", 52 | "docs:watch": "node node_modules/typedoc/bin/typedoc --watch", 53 | "docs:server": "cd docs && npx live-server --port=8082" 54 | }, 55 | "devDependencies": { 56 | "@types/expect": "^24.3.0", 57 | "@types/mocha": "^10.0.1", 58 | "@typescript-eslint/eslint-plugin": "^5.48.0", 59 | "@typescript-eslint/parser": "^5.48.0", 60 | "colors": "^1.4.0", 61 | "concurrently": "^8.2.0", 62 | "cross-env": "^7.0.3", 63 | "esbuild": "^0.16.14", 64 | "eslint": "^8.45.0", 65 | "heap-js": "^2.3.0", 66 | "lodash.camelcase": "^4.3.0", 67 | "mem": "^9.0.2", 68 | "mocha": "^10.2.0", 69 | "prettier": "3.0.0", 70 | "prompt": "^1.3.0", 71 | "replace-in-file": "^7.0.1", 72 | "shelljs": "^0.8.5", 73 | "ts-node": "^10.9.1", 74 | "typedoc": "^0.24.8", 75 | "typescript": "^5.1.6", 76 | "yargs-parser": "^21.1.1", 77 | "@types/node": "^20.4.2", 78 | "@types/w3c-web-serial": "^1.0.3", 79 | "@types/w3c-web-usb": "^1.0.6", 80 | "@types/web-bluetooth": "^0.0.17" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/embed/filestorage.ts: -------------------------------------------------------------------------------- 1 | import { delay } from "../jdom/utils" 2 | import { EmbedSaveTextMessage } from "./protocol" 3 | import { EmbedTransport } from "./transport" 4 | 5 | export interface FileStorage { 6 | saveText(name: string, data: string): Promise 7 | } 8 | 9 | export async function downloadUrl(url: string, name: string): Promise { 10 | const a = document.createElement("a") as HTMLAnchorElement 11 | document.body.appendChild(a) 12 | a.style.display = "none" 13 | a.href = url 14 | a.download = name 15 | a.click() 16 | await delay(100) 17 | a.remove() 18 | } 19 | 20 | export class BrowserFileStorage implements FileStorage { 21 | saveText(name: string, data: string, mimeType?: string): Promise { 22 | if (!mimeType) { 23 | if (/\.(csv|txt)/i.test(name)) mimeType = "text/plain" 24 | else if (/\.json/i.test(name)) mimeType = "application/json" 25 | } 26 | const url = `data:${ 27 | mimeType || "text/plain" 28 | };charset=utf-8,${encodeURIComponent(data)}` 29 | return downloadUrl(url, name) 30 | } 31 | } 32 | 33 | export class HostedFileStorage implements FileStorage { 34 | constructor(public readonly transport: EmbedTransport) {} 35 | saveText(name: string, data: string): Promise { 36 | return this.transport 37 | .postMessage({ 38 | type: "save-text", 39 | data: { name, data }, 40 | } as EmbedSaveTextMessage) 41 | .then(resp => {}) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/embed/jacdac-embed.ts: -------------------------------------------------------------------------------- 1 | export * from "./transport" 2 | export * from "./filestorage" 3 | export * from "./modelstore" 4 | -------------------------------------------------------------------------------- /src/embed/modelstore.ts: -------------------------------------------------------------------------------- 1 | import { CHANGE } from "../jdom/constants" 2 | import { JDEventSource } from "../jdom/eventsource" 3 | import { 4 | EmbedFile, 5 | EmbedFileContent, 6 | EmbedFileLoadMessage, 7 | EmbedModelListMessage, 8 | } from "./protocol" 9 | import { EmbedTransport } from "./transport" 10 | 11 | export abstract class ModelStore extends JDEventSource { 12 | abstract models(): EmbedFile[] 13 | abstract inputConfigurations(): EmbedFile[] 14 | abstract loadFile(model: EmbedFile): Promise 15 | } 16 | 17 | export class HostedModelStore extends JDEventSource { 18 | private _models: EmbedModelListMessage 19 | 20 | constructor(public readonly transport: EmbedTransport) { 21 | super() 22 | this.handleModelList = this.handleModelList.bind(this) 23 | 24 | this.transport.onMessage("model-list", this.handleModelList) 25 | } 26 | 27 | private handleModelList(msg: EmbedModelListMessage) { 28 | this._models = msg 29 | this.emit(CHANGE) 30 | } 31 | 32 | models(): EmbedFile[] { 33 | return this._models?.data.models?.slice(0) 34 | } 35 | 36 | inputConfigurations(): EmbedFile[] { 37 | return this._models?.data.inputConfigurations?.slice(0) 38 | } 39 | 40 | async loadFile(model: EmbedFile): Promise { 41 | const { path } = model 42 | const ack = await this.transport.postMessage({ 43 | type: "file-load", 44 | requireAck: true, 45 | data: { path }, 46 | } as EmbedFileLoadMessage) 47 | 48 | const data = ack?.data?.data as EmbedFileContent 49 | if (!data) return undefined 50 | 51 | const base64 = data.mimetype === "application/octet-stream" 52 | const buffer = Buffer.from(data.content, base64 ? "base64" : undefined) 53 | return new Blob([buffer], { type: data.mimetype }) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/embed/protocol.ts: -------------------------------------------------------------------------------- 1 | /** Jacdac IFrame Message protocol */ 2 | export interface EmbedMessage { 3 | id?: string 4 | source: "jacdac" 5 | type: string 6 | data: any 7 | requireAck?: boolean 8 | } 9 | export interface EmbedAckMessage extends EmbedMessage { 10 | type: "ack" 11 | ackId?: string 12 | data: { 13 | status: "success" | "error" 14 | data?: any 15 | error?: any 16 | } 17 | } 18 | export type EmbedLogLevel = "error" | "warn" | "log" | "info" | "debug" 19 | export interface EmbedLogMessage extends EmbedMessage { 20 | type: "log" 21 | data: { 22 | level?: EmbedLogLevel 23 | message: any 24 | } 25 | } 26 | export interface EmbedThemeMessage extends EmbedMessage { 27 | type: "theme" 28 | data: { 29 | type: "light" | "dark" 30 | } 31 | } 32 | export interface EmbedSpecsMessage extends EmbedMessage { 33 | type: "specs" 34 | data: { 35 | services?: jdspec.ServiceSpec[] 36 | } 37 | } 38 | export type EmbedStatus = "unknown" | "ready" 39 | export interface EmbedStatusMessage extends EmbedMessage { 40 | type: "status" 41 | data: { 42 | status: EmbedStatus 43 | } 44 | } 45 | export interface EmbedSaveTextMessage extends EmbedMessage { 46 | type: "save-text" 47 | data: { 48 | name: string 49 | data: string 50 | } 51 | } 52 | export interface EmbedFile { 53 | name: string 54 | path: string 55 | size: number 56 | mimetype: string 57 | } 58 | 59 | export interface EmbedFileContent { 60 | content: string 61 | mimetype: string 62 | } 63 | 64 | export interface EmbedModelListMessage extends EmbedMessage { 65 | type: "model-list" 66 | data: { 67 | models: EmbedFile[] 68 | inputConfigurations: EmbedFile[] 69 | } 70 | } 71 | export interface EmbedFileLoadMessage extends EmbedMessage { 72 | type: "file-load" 73 | requireAck: true 74 | data: { 75 | path: string 76 | } 77 | } 78 | /** End Jacdac protocol */ 79 | -------------------------------------------------------------------------------- /src/embed/transport.ts: -------------------------------------------------------------------------------- 1 | import { JDClient } from "../jdom/client" 2 | import { EmbedAckMessage, EmbedMessage, EmbedStatusMessage } from "./protocol" 3 | 4 | export interface EmbedTransport { 5 | postMessage( 6 | msg: TMessage, 7 | ): Promise 8 | onMessage( 9 | type: string, 10 | handler: (msg: TMessage) => void, 11 | ): void 12 | } 13 | 14 | /** 15 | * @internal 16 | */ 17 | export class IFrameTransport extends JDClient implements EmbedTransport { 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | private readonly ackAwaiters: Record void> = {} 20 | 21 | constructor(readonly origin: string) { 22 | super() 23 | this.handleMessage = this.handleMessage.bind(this) 24 | 25 | window.addEventListener("message", this.handleMessage, false) 26 | this.mount(() => 27 | window.removeEventListener("message", this.handleMessage, false), 28 | ) 29 | } 30 | 31 | private isOriginValid(msg: MessageEvent) { 32 | return this.origin === "*" || msg.origin === this.origin 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | postReady() { 39 | this.postMessage({ 40 | type: "status", 41 | data: { 42 | status: "ready", 43 | }, 44 | } as EmbedStatusMessage) 45 | } 46 | 47 | /** 48 | * Post message to client and awaits for ack if needed 49 | * @internal 50 | */ 51 | postMessage( 52 | msg: TMessage, 53 | ): Promise { 54 | let p: Promise 55 | 56 | msg.id = "jd:" + Math.random() 57 | msg.source = "jacdac" 58 | 59 | if (msg.requireAck) { 60 | p = new Promise(resolve => { 61 | this.ackAwaiters[msg.id] = msg => { 62 | resolve(msg) 63 | } 64 | }) 65 | } 66 | 67 | window.parent.postMessage(msg, this.origin) 68 | return p || Promise.resolve(undefined) 69 | } 70 | 71 | onMessage( 72 | type: string, 73 | handler: (msg: TMessage) => void, 74 | ): void { 75 | this.on(`message:${type}`, handler) 76 | } 77 | 78 | private handleMessage(event: MessageEvent) { 79 | if (!this.isOriginValid(event)) return 80 | 81 | const msg = event.data as EmbedMessage 82 | if (!msg || msg.source !== "jacdac") return 83 | 84 | // handle acks separately 85 | if (msg.type === "ack") { 86 | const ack = msg as EmbedAckMessage 87 | const awaiter = this.ackAwaiters[ack.ackId] 88 | delete this.ackAwaiters[ack.ackId] 89 | if (awaiter) awaiter(msg) 90 | } else { 91 | this.emit(`message:${msg.type}`, msg) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/jacdac.ts: -------------------------------------------------------------------------------- 1 | export * from "./jdom/jacdac-jdom" 2 | export * from "./servers/jacdac-servers" 3 | -------------------------------------------------------------------------------- /src/jdom/bridge.ts: -------------------------------------------------------------------------------- 1 | import { JDBus } from "./bus" 2 | import { JDClient } from "./client" 3 | import { CHANGE, FRAME_PROCESS } from "./constants" 4 | import { JDFrameBuffer } from "./packet" 5 | import { randomDeviceId } from "./random" 6 | 7 | /** 8 | * A client that bridges received and sent packets to a parent iframe. 9 | * @category JDOM 10 | */ 11 | export abstract class JDBridge extends JDClient { 12 | private _bus: JDBus 13 | readonly bridgeId: string 14 | packetSent = 0 15 | packetProcessed = 0 16 | currFrame: JDFrameBuffer 17 | 18 | constructor( 19 | name: string, 20 | public readonly infrastructure: boolean = false, 21 | ) { 22 | super() 23 | this.bridgeId = `bridge-${name}-` + randomDeviceId() 24 | this.handleSendFrame = this.handleSendFrame.bind(this) 25 | } 26 | 27 | get bus() { 28 | return this._bus 29 | } 30 | 31 | set bus(newBus: JDBus) { 32 | if (newBus !== this._bus) { 33 | if (this._bus) this.unmount() 34 | this._bus = newBus 35 | if (this._bus) { 36 | this.mount( 37 | this._bus.subscribe(FRAME_PROCESS, this.handleSendFrame), 38 | ) 39 | this.mount(this._bus.addBridge(this)) 40 | } 41 | this.emit(CHANGE) 42 | } 43 | } 44 | 45 | /** 46 | * Decodes and distributes a payload 47 | * @param data 48 | */ 49 | receiveFrameOrPacket(data: JDFrameBuffer, sender?: string) { 50 | const bus = this._bus 51 | if (!bus) return 52 | 53 | this.packetProcessed++ 54 | if (sender) data._jacdac_sender = sender 55 | if (!data._jacdac_sender) data._jacdac_sender = this.bridgeId 56 | 57 | this.currFrame = data // block self-loops 58 | // send to native bus and process on JS bus 59 | bus.sendFrameAsync(data) 60 | this.emit(FRAME_PROCESS, data) 61 | this.currFrame = null 62 | } 63 | 64 | private handleSendFrame(frame: JDFrameBuffer) { 65 | if ( 66 | !this._bus || 67 | this.currFrame == frame || 68 | frame._jacdac_sender === this.bridgeId 69 | ) 70 | return 71 | this.packetSent++ 72 | this.sendPacket(frame, frame._jacdac_sender) 73 | } 74 | 75 | /** 76 | * Sends packet data over the bridge 77 | * @param pkt 78 | */ 79 | protected abstract sendPacket(data: Uint8Array, sender: string): void 80 | } 81 | 82 | class ProxyBridge extends JDBridge { 83 | constructor( 84 | readonly _sendPacket: (pkt: Uint8Array, sender: string) => void, 85 | ) { 86 | super("proxy", true) 87 | } 88 | protected sendPacket(data: Uint8Array, sender: string): void { 89 | this._sendPacket(data, sender) 90 | } 91 | } 92 | 93 | export function createProxyBridge( 94 | sendPacket: (pkt: Uint8Array, sender: string) => void, 95 | ) { 96 | return new ProxyBridge(sendPacket) 97 | } 98 | -------------------------------------------------------------------------------- /src/jdom/bridges/iframebridge.ts: -------------------------------------------------------------------------------- 1 | import { JDBridge } from "../bridge" 2 | import { inIFrame } from "../iframeclient" 3 | 4 | class IFrameBridge extends JDBridge { 5 | constructor(readonly targetOrigin: string) { 6 | super("iframe") 7 | this.handleMessage = this.handleMessage.bind(this) 8 | window.addEventListener("message", this.handleMessage, false) 9 | this.mount(() => 10 | window.removeEventListener("message", this.handleMessage) 11 | ) 12 | //console.debug(`jacdac: iframe bridge created`) 13 | } 14 | 15 | private handleMessage(msg: MessageEvent) { 16 | const { data } = msg 17 | if (data.channel === "jacdac" && data.type === "messagepacket") { 18 | const payload: Uint8Array = data.data 19 | this.receiveFrameOrPacket(payload) 20 | } 21 | } 22 | 23 | protected sendPacket(data: Uint8Array, sender: string): void { 24 | const msg = { 25 | type: "messagepacket", 26 | channel: "jacdac", 27 | data, 28 | sender, 29 | broadcast: true, 30 | } 31 | window.parent.postMessage(msg, this.targetOrigin) 32 | } 33 | } 34 | 35 | export function createIFrameBridge(parentOrigin = "*"): JDBridge { 36 | return inIFrame() && new IFrameBridge(parentOrigin) 37 | } 38 | -------------------------------------------------------------------------------- /src/jdom/bridges/websocketbridge.ts: -------------------------------------------------------------------------------- 1 | import { JDBridge } from "../bridge" 2 | import { CLOSE } from "../constants" 3 | 4 | export class WebSocketBridge extends JDBridge { 5 | private _ws: WebSocket 6 | private _startPromise: Promise 7 | 8 | constructor( 9 | name: string, 10 | readonly url: string, 11 | readonly protocols?: string | string[] 12 | ) { 13 | super(name, true) 14 | 15 | this.mount(() => this.close()) 16 | } 17 | 18 | private close() { 19 | console.debug(`web bridge closed`, { url: this.url }) 20 | const opened = !!this._ws || !!this._startPromise 21 | try { 22 | this._ws?.close() 23 | this._ws = undefined 24 | this._startPromise = undefined 25 | } catch (e) { 26 | console.warn(e) 27 | } 28 | if (opened) this.emit(CLOSE) 29 | } 30 | 31 | async connect() { 32 | if (this._ws) return Promise.resolve() 33 | if (!this._startPromise) { 34 | this._startPromise = new Promise((resolve, reject) => { 35 | const ws = new WebSocket(this.url, this.protocols) 36 | ws.binaryType = "arraybuffer" 37 | ws.onopen = () => { 38 | this._ws = ws 39 | this._startPromise = undefined 40 | console.debug(`web bridge opened`, { url: this.url }) 41 | resolve() 42 | } 43 | ws.onerror = e => { 44 | console.debug(`web bridge error`, { url: this.url }) 45 | this.close() 46 | reject() 47 | } 48 | ws.onclose = ev => { 49 | console.debug(`web bridge onclose`, { ev }) 50 | this.close() 51 | } 52 | ws.onmessage = (ev: MessageEvent) => { 53 | const { data } = ev 54 | const buffer = new Uint8Array(data) 55 | this.receiveFrameOrPacket(buffer) 56 | } 57 | }) 58 | } 59 | return this._startPromise 60 | } 61 | 62 | protected sendPacket(data: Uint8Array, sender: string): void { 63 | this.connect() 64 | this._ws?.send(data) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/jdom/busstats.ts: -------------------------------------------------------------------------------- 1 | import { JDBus } from "./bus" 2 | import { CHANGE, PACKET_PROCESS, PACKET_SEND, SELF_ANNOUNCE } from "./constants" 3 | import { JDEventSource } from "./eventsource" 4 | import { Packet } from "./packet" 5 | 6 | export interface BusStats { 7 | packets: number 8 | announce: number 9 | acks: number 10 | bytes: number 11 | 12 | devices: number 13 | simulators: number 14 | transport?: string 15 | } 16 | 17 | interface Stats { 18 | packets: number 19 | announce: number 20 | acks: number 21 | bytes: number 22 | } 23 | 24 | export class BusStatsMonitor extends JDEventSource { 25 | private readonly _prev: Stats[] = Array(4) 26 | .fill(0) 27 | .map(() => ({ 28 | packets: 0, 29 | announce: 0, 30 | acks: 0, 31 | bytes: 0, 32 | })) 33 | private _previ = 0 34 | private _temp: Stats = { 35 | packets: 0, 36 | announce: 0, 37 | acks: 0, 38 | bytes: 0, 39 | } 40 | 41 | /** 42 | * @internal 43 | */ 44 | constructor(private readonly bus: JDBus) { 45 | super() 46 | bus.on(PACKET_SEND, this.handlePacketSend.bind(this)) 47 | bus.on(PACKET_PROCESS, this.handlePacketProcess.bind(this)) 48 | bus.on(SELF_ANNOUNCE, this.handleSelfAnnounce.bind(this)) 49 | } 50 | 51 | /** 52 | * Computes the current packet statistics of the bus 53 | */ 54 | get current(): BusStats { 55 | const r: Stats = { 56 | packets: 0, 57 | announce: 0, 58 | acks: 0, 59 | bytes: 0, 60 | } 61 | const n = this._prev.length 62 | for (let i = 0; i < this._prev.length; ++i) { 63 | const p = this._prev[i] 64 | r.packets += p.packets 65 | r.announce += p.announce 66 | r.acks += p.acks 67 | r.bytes += p.bytes 68 | } 69 | // announce every 500ms 70 | const n2 = n / 2 71 | r.packets /= n2 72 | r.announce /= n2 73 | r.acks /= n2 74 | r.bytes /= n2 75 | return { 76 | devices: this.bus.devices({ ignoreInfrastructure: true }).length, 77 | simulators: this.bus.serviceProviders().length, 78 | transport: this.bus.transports.find( 79 | transport => transport.connected, 80 | )?.type, 81 | ...r, 82 | } 83 | } 84 | 85 | private accumulate(pkt: Packet) { 86 | this._temp.packets++ 87 | this._temp.bytes += (pkt.header?.length || 0) + (pkt.data?.length || 0) 88 | if (pkt.isAnnounce) this._temp.announce++ 89 | if (pkt.isCRCAck) this._temp.acks++ 90 | } 91 | 92 | private handleSelfAnnounce() { 93 | const changed = 94 | JSON.stringify(this._prev) !== JSON.stringify(this._temp) 95 | this._prev[this._previ] = this._temp 96 | this._previ = (this._previ + 1) % this._prev.length 97 | this._temp = { 98 | packets: 0, 99 | announce: 0, 100 | acks: 0, 101 | bytes: 0, 102 | } 103 | if (changed) this.emit(CHANGE) 104 | } 105 | 106 | private handlePacketSend(pkt: Packet) { 107 | this.accumulate(pkt) 108 | } 109 | 110 | private handlePacketProcess(pkt: Packet) { 111 | this.accumulate(pkt) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/jdom/client.ts: -------------------------------------------------------------------------------- 1 | import { JDEventSource } from "./eventsource" 2 | 3 | /** 4 | * Base class for clients 5 | * @category Clients 6 | */ 7 | export class JDClient extends JDEventSource { 8 | private unsubscribers: (() => void)[] = [] 9 | protected unmounted = false 10 | constructor() { 11 | super() 12 | } 13 | 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | protected log(msg: any, arg?: any) { 16 | if (arg) console.debug(msg, arg) 17 | else console.debug(msg) 18 | } 19 | 20 | mount(unsubscribe: () => void): () => void { 21 | this.unmounted = false 22 | if (unsubscribe && this.unsubscribers.indexOf(unsubscribe) < 0) 23 | this.unsubscribers.push(unsubscribe) 24 | return unsubscribe 25 | } 26 | 27 | unmount() { 28 | const us = this.unsubscribers 29 | this.unsubscribers = [] 30 | us.forEach(u => u()) 31 | this.unmounted = true 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/jdom/clients/devicescriptmanagerclient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | EVENT, 4 | DeviceScriptManagerCmd, 5 | DeviceScriptManagerEvent, 6 | DeviceScriptManagerReg, 7 | } from "../constants" 8 | import { jdpack } from "../pack" 9 | import { OutPipe } from "../pipes" 10 | import { JDService } from "../service" 11 | import { JDServiceClient } from "../serviceclient" 12 | 13 | export class DeviceScriptManagerClient extends JDServiceClient { 14 | constructor(service: JDService) { 15 | super(service) 16 | 17 | // report events 18 | const changeEvent = service.event( 19 | DeviceScriptManagerEvent.ProgramChange 20 | ) 21 | this.mount(changeEvent.subscribe(EVENT, () => this.emit(CHANGE))) 22 | this.mount( 23 | changeEvent.subscribe(EVENT, () => 24 | this.emit(DeviceScriptManagerClient.PROGRAM_CHANGE) 25 | ) 26 | ) 27 | 28 | const panicEvent = service.event(DeviceScriptManagerEvent.ProgramPanic) 29 | this.mount( 30 | panicEvent.subscribe(EVENT, (args: unknown[]) => 31 | this.emit( 32 | DeviceScriptManagerClient.PROGRAM_PANIC, 33 | ...(args || []) 34 | ) 35 | ) 36 | ) 37 | } 38 | 39 | static PROGRAM_CHANGE = "programChange" 40 | static PROGRAM_PANIC = "programPanic" 41 | 42 | deployBytecode(bytecode: Uint8Array, onProgress?: (p: number) => void) { 43 | return OutPipe.sendBytes( 44 | this.service, 45 | DeviceScriptManagerCmd.DeployBytecode, 46 | bytecode, 47 | onProgress 48 | ) 49 | } 50 | 51 | async setRunning(value: boolean) { 52 | const reg = this.service.register(DeviceScriptManagerReg.Running) 53 | await reg.sendSetAsync(jdpack("u8", [value ? 1 : 0])) 54 | } 55 | 56 | async setAutoStart(value: boolean) { 57 | const reg = this.service.register(DeviceScriptManagerReg.Autostart) 58 | await reg.sendSetAsync(jdpack("u8", [value ? 1 : 0])) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/jdom/clients/settingsclient.ts: -------------------------------------------------------------------------------- 1 | import { CHANGE, SettingsCmd } from "../constants" 2 | import { jdpack, jdunpack } from "../pack" 3 | import { Packet } from "../packet" 4 | import { InPipeReader } from "../pipes" 5 | import { JDService } from "../service" 6 | import { JDServiceClient } from "../serviceclient" 7 | import { bufferToString, stringToBuffer } from "../utils" 8 | 9 | /** 10 | * A settings service client implementation. 11 | * @category Clients 12 | */ 13 | export class SettingsClient extends JDServiceClient { 14 | constructor(service: JDService) { 15 | super(service) 16 | service.registersUseAcks = true 17 | } 18 | 19 | async clear() { 20 | await this.service.sendCmdAsync(SettingsCmd.Clear) 21 | } 22 | 23 | async listKeys(): Promise { 24 | const inp = new InPipeReader(this.bus) 25 | await this.service.sendPacketAsync( 26 | inp.openCommand(SettingsCmd.ListKeys), 27 | true 28 | ) 29 | const { output } = await inp.readAll() 30 | const keys = output.map(pkt => pkt.stringData) 31 | return keys.filter(k => !!k) 32 | } 33 | 34 | async list(): Promise<{ key: string; value?: Uint8Array }[]> { 35 | const inp = new InPipeReader(this.bus) 36 | await this.service.sendPacketAsync( 37 | inp.openCommand(SettingsCmd.List), 38 | true 39 | ) 40 | const { output } = await inp.readAll() 41 | return output 42 | .map(pkt => { 43 | const [key, value] = pkt.jdunpack<[string, Uint8Array]>("z b") 44 | return key && { key, value } 45 | }) 46 | .filter(kv => !!kv) 47 | } 48 | 49 | async setValue(key: string, value: Uint8Array) { 50 | key = key.trim() 51 | if (value === undefined) { 52 | await this.deleteValue(key) 53 | } else { 54 | const pkt = Packet.from( 55 | SettingsCmd.Set, 56 | jdpack("z b", [key, value]) 57 | ) 58 | await this.service.sendPacketAsync(pkt, true) 59 | this.emit(CHANGE) 60 | } 61 | } 62 | 63 | async setStringValue(key: string, value: string) { 64 | await this.setValue(key, value ? stringToBuffer(value) : undefined) 65 | } 66 | 67 | async getValue(key: string): Promise { 68 | if (!key) return undefined 69 | 70 | key = key.trim() 71 | const pkt = Packet.from(SettingsCmd.Get, jdpack("s", [key])) 72 | const resp = await this.service.sendCmdAwaitResponseAsync(pkt) 73 | const [rkey, value] = jdunpack<[string, Uint8Array]>(resp.data, "z b") 74 | if (key !== rkey) { 75 | console.error( 76 | `device returned different key, got "${rkey}", expected "${key}"` 77 | ) 78 | return undefined 79 | } 80 | return value 81 | } 82 | 83 | async getStringValue(key: string) { 84 | const value = await this.getValue(key) 85 | return value && bufferToString(value) 86 | } 87 | 88 | async deleteValue(key: string) { 89 | if (!key) return 90 | key = key.trim() 91 | const pkt = Packet.from(SettingsCmd.Delete, jdpack("s", [key])) 92 | await this.service.sendPacketAsync(pkt) 93 | 94 | this.emit(CHANGE) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/jdom/color.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export function hsvToCss( 5 | hue: number, 6 | saturation: number, 7 | value: number, 8 | brightness: number, 9 | monochrome?: boolean, 10 | ) { 11 | const csshue = (hue * 360) / 0xff 12 | const csssat = (monochrome ? 0xff : saturation) / 0xff 13 | const cssval = value / 0xff 14 | const [h, s, l] = hsv_to_hsl(csshue, csssat, cssval) 15 | const mixl = 0.3 16 | const alpha = (mixl + (1 - mixl) * l) * brightness 17 | 18 | return `hsla(${h}, ${s * 100}%, ${l * 100}%, ${alpha}` 19 | } 20 | 21 | function hsv_to_hsl(h: number, s: number, v: number) { 22 | // both hsv and hsl values are in [0, 1] 23 | const l = ((2 - s) * v) / 2 24 | 25 | if (l != 0) { 26 | if (l == 1) { 27 | s = 0 28 | } else if (l < 0.5) { 29 | s = (s * v) / (l * 2) 30 | } else { 31 | s = (s * v) / (2 - l * 2) 32 | } 33 | } 34 | 35 | return [h, s, l] 36 | } 37 | -------------------------------------------------------------------------------- /src/jdom/command.ts: -------------------------------------------------------------------------------- 1 | import { setNumber, sizeOfNumberFormat } from "./buffer" 2 | import { CMD_SET_REG, JD_SERIAL_MAX_PAYLOAD_SIZE } from "./constants" 3 | import { PackedValues } from "./pack" 4 | import { Packet } from "./packet" 5 | import { 6 | clampToStorage, 7 | isRegister, 8 | numberFormatFromStorageType, 9 | scaleFloatToInt, 10 | } from "./spec" 11 | import { stringToUint8Array, toUTF8 } from "./utils" 12 | 13 | /** 14 | * @internal 15 | */ 16 | export function packArguments(info: jdspec.PacketInfo, args: PackedValues) { 17 | let repeatIdx = -1 18 | let numReps = 0 19 | let argIdx = 0 20 | let dst = 0 21 | 22 | const buf = new Uint8Array(256) 23 | 24 | for (let i = 0; i < info.fields.length; ++argIdx, ++i) { 25 | if (argIdx >= args.length && numReps > 0) break 26 | const arg0 = argIdx < args.length ? args[argIdx] : 0 27 | const fld = info.fields[i] 28 | 29 | if (repeatIdx == -1 && fld.startRepeats) repeatIdx = i 30 | 31 | const arg1 = 32 | fld.type == "string0" && typeof arg0 == "string" 33 | ? arg0 + "\u0000" 34 | : arg0 35 | 36 | const arg = 37 | typeof arg1 == "boolean" 38 | ? arg1 39 | ? 1 40 | : 0 41 | : typeof arg1 == "string" 42 | ? stringToUint8Array(toUTF8(arg1)) 43 | : arg1 44 | 45 | if (typeof arg == "number") { 46 | const intVal = scaleFloatToInt(arg, fld) 47 | if (fld.storage == 0) 48 | throw new Error(`expecting ${fld.type} got number`) 49 | 50 | const fmt = numberFormatFromStorageType(fld.storage) 51 | setNumber(buf, fmt, dst, clampToStorage(intVal, fld.storage)) 52 | dst += sizeOfNumberFormat(fmt) 53 | } else { 54 | let size = Math.abs(fld.storage) 55 | if (typeof arg1 == "string") { 56 | if (size == 0) size = arg.length 57 | const argCut = arg.slice(0, size) 58 | buf.set(argCut, dst) 59 | dst += size 60 | } else if (size == 0 || size == arg.length) { 61 | buf.set(arg, dst) 62 | dst += arg.length 63 | } else { 64 | throw new Error( 65 | `expecting ${Math.abs(fld.storage)} bytes; got ${ 66 | arg.length 67 | }`, 68 | ) 69 | } 70 | } 71 | 72 | if (dst >= JD_SERIAL_MAX_PAYLOAD_SIZE) 73 | throw new Error( 74 | `jacdac packet length too large, ${dst} > ${JD_SERIAL_MAX_PAYLOAD_SIZE} bytes`, 75 | ) 76 | 77 | if (repeatIdx != -1 && i + 1 >= info.fields.length) { 78 | i = repeatIdx - 1 79 | numReps++ 80 | } 81 | } 82 | 83 | const cmd = isRegister(info) 84 | ? info.identifier | CMD_SET_REG 85 | : info.identifier 86 | const pkt = Packet.from(cmd, buf.slice(0, dst)) 87 | if (info.kind != "report") pkt.isCommand = true 88 | return pkt 89 | } 90 | -------------------------------------------------------------------------------- /src/jdom/error.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERROR_NO_ACK, 3 | ERROR_TIMEOUT, 4 | ERROR_TRANSPORT_DEVICE_LOCKED, 5 | JACDAC_ERROR, 6 | } from "./constants" 7 | 8 | export interface JDErrorOptions { 9 | /** 10 | * The error code 11 | */ 12 | code?: string 13 | /** 14 | * If true, the error is not reported to the user 15 | */ 16 | cancel?: boolean 17 | } 18 | /** 19 | * Common Jacdac error type 20 | * @category Runtime 21 | */ 22 | export class JDError extends Error { 23 | readonly code: string 24 | readonly cancel: boolean 25 | constructor(message: string, options?: JDErrorOptions) { 26 | super(message) 27 | this.name = JACDAC_ERROR 28 | this.code = options?.code 29 | this.cancel = !!options?.cancel 30 | } 31 | } 32 | 33 | export function throwError(msg: string, options?: JDErrorOptions) { 34 | const e = new JDError(msg, options) 35 | throw e 36 | } 37 | 38 | export function isCancelError(e: Error) { 39 | const res = e?.name === JACDAC_ERROR ? (e as JDError)?.cancel : false 40 | return res 41 | } 42 | 43 | export function isAckError(e: Error) { 44 | return isCodeError(e, ERROR_NO_ACK) 45 | } 46 | 47 | export function isTimeoutError(e: Error) { 48 | return isCodeError(e, ERROR_TIMEOUT) 49 | } 50 | 51 | export function isCodeError(e: Error, code: string) { 52 | return errorCode(e) === code 53 | } 54 | 55 | /** 56 | * Extract the Jacdac error code if any 57 | * @param e 58 | * @returns 59 | * @category Runtime 60 | */ 61 | export function errorCode(e: Error): string { 62 | const code = e?.name === JACDAC_ERROR ? (e as JDError)?.code : undefined 63 | if (code) return code 64 | 65 | const deviceLocked = 66 | e.name == "NetworkError" && /unable to claim interface/i.test(e.message) 67 | if (deviceLocked) return ERROR_TRANSPORT_DEVICE_LOCKED 68 | 69 | return undefined 70 | } 71 | -------------------------------------------------------------------------------- /src/jdom/event.ts: -------------------------------------------------------------------------------- 1 | import { JDNode } from "./node" 2 | import { JDService } from "./service" 3 | import { Packet } from "./packet" 4 | import { CHANGE, EVENT, EVENT_NODE_NAME } from "./constants" 5 | import { isEvent } from "./spec" 6 | import { JDServiceMemberNode } from "./servicemembernode" 7 | import { DecodedPacket } from "./pretty" 8 | import { JDField } from "./field" 9 | 10 | /** 11 | * A Jacdac event client. 12 | * @category JDOM 13 | */ 14 | export class JDEvent extends JDServiceMemberNode { 15 | private _lastReportPkt: Packet 16 | private _fields: JDField[] 17 | private _count = 0 18 | 19 | /** 20 | * @internal 21 | */ 22 | constructor(service: JDService, code: number) { 23 | super(service, code, isEvent) 24 | } 25 | 26 | /** 27 | * Returns the ``EVENT_NODE_NAME`` identifier 28 | * @category JDOM 29 | */ 30 | get nodeKind() { 31 | return EVENT_NODE_NAME 32 | } 33 | 34 | /** 35 | * Gets the field node 36 | * @category Service Clients 37 | */ 38 | get fields() { 39 | if (!this._fields) 40 | this._fields = this.specification?.fields.map( 41 | (field, index) => new JDField(this, index, field), 42 | ) 43 | return this._fields.slice() 44 | } 45 | 46 | /** 47 | * Gets the list of fields 48 | * @category JDOM 49 | */ 50 | get children(): JDNode[] { 51 | return this.fields 52 | } 53 | 54 | /** 55 | * Gets the raw data attached to the last event packet 56 | * @category Data 57 | */ 58 | get data() { 59 | return this._lastReportPkt?.data 60 | } 61 | 62 | /** 63 | * Gets the unpacked data attached to the last event packet, if the event specification is known. 64 | * @category Data 65 | */ 66 | get unpackedValue() { 67 | const { packFormat } = this.specification || {} 68 | return packFormat && this._lastReportPkt?.jdunpack(packFormat) 69 | } 70 | 71 | /** 72 | * Gets a counter of occurences for this event. 73 | * @category Data 74 | */ 75 | get count() { 76 | return this._count 77 | } 78 | 79 | /** 80 | * Gets the timestamp of the last packet with data received for this event. 81 | * @category Data 82 | */ 83 | get lastDataTimestamp() { 84 | return this._lastReportPkt?.timestamp 85 | } 86 | /** 87 | * @internal 88 | */ 89 | get decoded(): DecodedPacket { 90 | return this._lastReportPkt?.decoded 91 | } 92 | 93 | /** 94 | * @internal 95 | */ 96 | processEvent(pkt: Packet) { 97 | this._lastReportPkt = pkt 98 | this._count++ 99 | this.emitPropagated(EVENT, this) 100 | this.emit(CHANGE) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/jdom/filters/devicefilter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A device filter 3 | * @category JDOM 4 | */ 5 | export interface DeviceFilter { 6 | /** 7 | * Matches devices with a service matching this name 8 | */ 9 | serviceName?: string 10 | /** 11 | * Matches devices that have this service 12 | */ 13 | serviceClass?: number 14 | /** 15 | * Excludes the infrastructure devices 16 | */ 17 | ignoreInfrastructure?: boolean 18 | /** 19 | * Matches devices that have already announced their services 20 | */ 21 | announced?: boolean 22 | /** 23 | * Ignore virtual devices used as state simulators 24 | */ 25 | ignoreSimulators?: boolean 26 | /** 27 | * Matches devices with a specific product identifier 28 | */ 29 | productIdentifier?: boolean 30 | /** 31 | * Matches physical devices exclusively 32 | */ 33 | physical?: boolean 34 | /** 35 | * Ignore or select lost devices 36 | */ 37 | lost?: boolean 38 | } 39 | -------------------------------------------------------------------------------- /src/jdom/filters/servicefilter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A service filter 3 | * @category JDOM 4 | */ 5 | export interface ServiceFilter { 6 | /** 7 | * Match services at the given service index 8 | */ 9 | serviceIndex?: number 10 | /** 11 | * Match services with the given name 12 | */ 13 | serviceName?: string 14 | /** 15 | * Match services with the given service class 16 | */ 17 | serviceClass?: number 18 | /** 19 | * Match services which have a known specifications 20 | */ 21 | specification?: boolean 22 | /** 23 | * Match or excludes mixin services 24 | */ 25 | mixins?: boolean 26 | /** 27 | * Is a sensor service 28 | */ 29 | sensor?: boolean 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/jdom/flags.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Various flags to control the runtime environment 3 | * @category Runtime 4 | */ 5 | export class Flags { 6 | /** 7 | * Enables additional logging and diagnostics 8 | */ 9 | static diagnostics = false 10 | /** 11 | * Trace who and what generates packets 12 | */ 13 | static trace = false 14 | /** 15 | * Enables/disabled WebUSB 16 | */ 17 | static webUSB = true 18 | /** 19 | * Enables/disabled WebSerial 20 | */ 21 | static webSerial = true 22 | 23 | /** 24 | * Enables/disables WebBLE 25 | */ 26 | static webBluetooth = false 27 | 28 | /** 29 | * Enables developer mode when connecting devices 30 | */ 31 | static developerMode = false 32 | } 33 | -------------------------------------------------------------------------------- /src/jdom/iframeclient.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export function inIFrame() { 5 | try { 6 | return typeof window !== "undefined" && window.self !== window.top 7 | } catch (e) { 8 | return typeof window !== "undefined" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/jdom/jacdac-jdom.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants" 2 | export * from "../../jacdac-spec/spectool/jdspec" 3 | export * from "./random" 4 | export * from "./utils" 5 | export * from "./buffer" 6 | export * from "./observable" 7 | export * from "./packet" 8 | export * from "./eventsource" 9 | export * from "./node" 10 | export * from "./servicemembernode" 11 | export * from "./device" 12 | export * from "./service" 13 | export * from "./register" 14 | export * from "./field" 15 | export * from "./event" 16 | export * from "./scheduler" 17 | export * from "./bus" 18 | export * from "./logparser" 19 | export * from "./pretty" 20 | export * from "./flashing" 21 | export * from "./speedtest" 22 | export * from "./spec" 23 | export * from "./client" 24 | export * from "./serviceclient" 25 | export * from "./iframeclient" 26 | export * from "./trace/trace" 27 | export * from "./trace/traceplayer" 28 | export * from "./command" 29 | export * from "./packetfilter" 30 | export * from "./trace/tracerecorder" 31 | export * from "./trace/traceview" 32 | export * from "./flags" 33 | export * from "./pack" 34 | export * from "./packobject" 35 | export * from "./pipes" 36 | export * from "./light" 37 | export * from "./setting" 38 | export * from "./nodesetting" 39 | export * from "./servers/registerserver" 40 | export * from "./servers/serviceserver" 41 | export * from "./servers/serviceprovider" 42 | export * from "./servers/serverserviceprovider" 43 | export * from "./servers/controlserver" 44 | export * from "./servers/rolemanagerserver" 45 | export * from "./color" 46 | export * from "./bridge" 47 | export * from "./clients/settingsclient" 48 | export * from "./clients/rolemanagerclient" 49 | export * from "./clients/devicescriptmanagerclient" 50 | export * from "./transport/transport" 51 | export * from "./transport/bluetooth" 52 | export * from "./transport/usbio" 53 | export * from "./transport/webserialio" 54 | export * from "./transport/proto" 55 | export * from "./transport/hf2" 56 | export * from "./transport/microbit" 57 | export * from "./transport/workertransport" 58 | export * from "./transport/usb" 59 | export * from "./transport/webserial" 60 | export * from "./transport/createbus" 61 | export * from "./transport/websockettransport" 62 | export * from "./error" 63 | export * from "./rolemanager" 64 | export * from "./filters/servicefilter" 65 | export * from "./filters/devicefilter" 66 | export * from "./sensors" 67 | export * from "./bridges/iframebridge" 68 | export * from "./bridges/websocketbridge" 69 | export * from "./catalog" 70 | export * from "./transport/nodesocket" 71 | export * from "./transport/nodespi" 72 | export * from "./transport/nodewebserialio" 73 | export * from "./transport/nodewebusb" 74 | export * from "./devtools" 75 | export * from "./sevensegment" 76 | export * from "./semver" 77 | -------------------------------------------------------------------------------- /src/jdom/ledcontroller.ts: -------------------------------------------------------------------------------- 1 | import { CHANGE, ControlCmdPack } from "./constants" 2 | import { JDEventSource } from "./eventsource" 3 | import { jdpack, jdunpack } from "./pack" 4 | import { Packet } from "./packet" 5 | import { JDService } from "./service" 6 | 7 | function trgbToValues(trgb: number) { 8 | return [ 9 | (trgb >> 16) & 0xff, 10 | (trgb >> 8) & 0xff, 11 | trgb & 0xff, 12 | (trgb >> 24) & 0xff, 13 | ] 14 | } 15 | 16 | export class LEDController extends JDEventSource { 17 | private _color: number 18 | private _announces = 0 19 | 20 | constructor( 21 | public readonly service: JDService, 22 | public readonly command: number, 23 | ) { 24 | super() 25 | } 26 | 27 | get color(): number { 28 | return this._color 29 | } 30 | 31 | async setColor(color: number) { 32 | if (color !== this._color) { 33 | this._color = color 34 | this._announces = 0 35 | 36 | if (this._color !== undefined) { 37 | const data = jdpack( 38 | ControlCmdPack.SetStatusLight, 39 | trgbToValues(color), 40 | ) 41 | await this.service.sendCmdAsync(this.command, data) 42 | } 43 | this.emit(CHANGE) 44 | } 45 | } 46 | 47 | async blink(from: number, to: number, interval: number, repeat: number) { 48 | const { bus } = this.service.device 49 | for (let i = 0; i < repeat; ++i) { 50 | await this.setColor(from) 51 | await bus.delay(interval - 1) 52 | await this.setColor(to) 53 | await bus.delay(interval - 1) 54 | } 55 | } 56 | 57 | processAnnouncement() { 58 | if (this._color === undefined) return 59 | this._announces++ 60 | if (this._announces > 2) { 61 | // jacdac will blink at least once per announce cycle 62 | this._color = undefined 63 | this._announces = 0 64 | this.emit(CHANGE) 65 | } 66 | } 67 | 68 | processPacket(pkt: Packet) { 69 | const [toRed, toGreen, toBlue] = jdunpack< 70 | [number, number, number, number] 71 | >(pkt.data, ControlCmdPack.SetStatusLight) 72 | this._color = (toRed << 16) | (toGreen << 8) | toBlue 73 | this.emit(CHANGE) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/jdom/lightconstants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * `0xD0: set_all(C+)` - set all pixels in current range to given color pattern 3 | * `0xD1: fade(C+)` - set `N` pixels to color between colors in sequence 4 | * `0xD2: fade_hsv(C+)` - similar to `fade()`, but colors are specified and faded in HSV 5 | * `0xD3: rotate_fwd(K)` - rotate (shift) pixels by `K` positions away from the connector 6 | * `0xD4: rotate_back(K)` - same, but towards the connector 7 | * `0xD5: show(M=50)` - send buffer to strip and wait `M` milliseconds 8 | * `0xD6: range(P=0, N=length)` - range from pixel `P`, `N` pixels long 9 | * `0xD7: mode(K=0)` - set update mode 10 | * `0xD8: mode1(K=0)` - set update mode for next command only 11 | */ 12 | 13 | export const LIGHT_PROG_SET_ALL = 0xd0 14 | export const LIGHT_PROG_FADE = 0xd1 15 | export const LIGHT_PROG_FADE_HSV = 0xd2 16 | export const LIGHT_PROG_ROTATE_FWD = 0xd3 17 | export const LIGHT_PROG_ROTATE_BACK = 0xd4 18 | export const LIGHT_PROG_SHOW = 0xd5 19 | export const LIGHT_PROG_RANGE = 0xd6 20 | export const LIGHT_PROG_MODE = 0xd7 21 | export const LIGHT_PROG_MODE1 = 0xd8 22 | 23 | export const LIGHT_MODE_REPLACE = 0x00 24 | export const LIGHT_MODE_ADD_RGB = 0x01 25 | export const LIGHT_MODE_SUBTRACT_RGB = 0x02 26 | export const LIGHT_MODE_MULTIPLY_RGB = 0x03 27 | export const LIGHT_MODE_LAST = 0x03 28 | 29 | export const LIGHT_PROG_COLN = 0xc0 30 | export const LIGHT_PROG_COL1 = 0xc1 31 | export const LIGHT_PROG_COL2 = 0xc2 32 | export const LIGHT_PROG_COL3 = 0xc3 33 | 34 | export const LIGHT_PROG_COL1_SET = 0xcf 35 | -------------------------------------------------------------------------------- /src/jdom/node.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { JDEventSource } from "./eventsource" 3 | 4 | /** 5 | * Base class for JDOM Node classes. 6 | * @category JDOM 7 | */ 8 | export abstract class JDNode extends JDEventSource { 9 | private _nodeData: Record 10 | 11 | constructor() { 12 | super() 13 | } 14 | 15 | /** 16 | * Globally unique identifier in the tree 17 | * @category JDOM 18 | */ 19 | abstract get id(): string 20 | 21 | /** 22 | * Gets a kind identifier useful for UI descriptions 23 | * @category JDOM 24 | */ 25 | abstract get nodeKind(): string 26 | 27 | /** 28 | * Gets the local name 29 | * @category JDOM 30 | */ 31 | abstract get name(): string 32 | 33 | /** 34 | * A human friendly name 35 | * @category JDOM 36 | */ 37 | get friendlyName(): string { 38 | return this.name 39 | } 40 | 41 | /** 42 | * Gets the name including parents 43 | * @category JDOM 44 | */ 45 | abstract get qualifiedName(): string 46 | 47 | /** 48 | * Gets the parent node in the Jacdac dom 49 | * @category JDOM 50 | */ 51 | abstract get parent(): JDNode 52 | 53 | /** 54 | * Gets the children of the current node 55 | * @category JDOM 56 | */ 57 | abstract get children(): JDNode[] 58 | 59 | /** 60 | * Gets a databag to store custom information 61 | * @category JDOM 62 | */ 63 | get nodeData() { 64 | if (!this._nodeData) this._nodeData = {} 65 | return this._nodeData 66 | } 67 | 68 | /** 69 | * Emit event in current node and parent nodes 70 | * @param event event to emit 71 | * @param arg event arguments 72 | * @category JDOM 73 | */ 74 | emitPropagated(event: string, arg?: any) { 75 | let current = this as JDNode 76 | while (current) { 77 | current.emit(event, arg || this) 78 | current = current.parent 79 | } 80 | } 81 | 82 | /** 83 | * @hidden 84 | */ 85 | toString() { 86 | return this.friendlyName 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/jdom/nodesetting.ts: -------------------------------------------------------------------------------- 1 | import { Setting } from "./setting" 2 | 3 | let settingsPath = "" 4 | export function nodeSetting(key: string): Setting { 5 | let v: string 6 | let keyPath: string 7 | const fs = require("fs") 8 | function init() { 9 | if (!settingsPath) { 10 | const jd = process.env["HOME"] + "/.jacdac" 11 | try { 12 | fs.mkdirSync(jd) 13 | } catch {} 14 | settingsPath = jd + "/settings" 15 | try { 16 | fs.mkdirSync(settingsPath) 17 | } catch {} 18 | } 19 | if (!keyPath) { 20 | keyPath = 21 | settingsPath + 22 | "/" + 23 | key.replace(/[^a-z_\-]/g, c => c.charCodeAt(0) + "") 24 | try { 25 | v = fs.readFileSync(keyPath, "utf8") 26 | } catch { 27 | v = undefined 28 | } 29 | } 30 | } 31 | function get() { 32 | init() 33 | return v 34 | } 35 | function set(nv: string) { 36 | init() 37 | if (v !== nv) { 38 | v = nv 39 | fs.writeFileSync(keyPath, nv, "utf8") 40 | } 41 | } 42 | return { get, set } 43 | } 44 | -------------------------------------------------------------------------------- /src/jdom/observable.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @internal 3 | */ 4 | export interface Observer { 5 | next?: (value: T) => void 6 | error?: (error: Error) => void 7 | complete?: () => void 8 | } 9 | 10 | /** 11 | * @internal 12 | */ 13 | export interface Observable { 14 | subscribe(observer: Observer): { 15 | unsubscribe: () => void 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/jdom/random.ts: -------------------------------------------------------------------------------- 1 | import { toHex } from "./utils" 2 | 3 | function cryptoRandomUint32(length: number): Uint32Array { 4 | const crypto = 5 | (typeof self !== "undefined" ? self.crypto : undefined) || 6 | (typeof globalThis !== "undefined" ? globalThis.crypto : undefined) 7 | if (!crypto) return undefined 8 | 9 | const vals = new Uint32Array(length) 10 | crypto.getRandomValues(vals) 11 | return vals 12 | } 13 | 14 | export function anyRandomUint32(length: number): Uint32Array { 15 | let r = cryptoRandomUint32(length) 16 | if (!r) { 17 | r = new Uint32Array(length) 18 | for (let i = 0; i < r.length; ++i) 19 | r[i] = (Math.random() * 0x1_0000_0000) >>> 0 20 | } 21 | return r 22 | } 23 | 24 | export function randomUInt(max: number) { 25 | const arr = anyRandomUint32(1) 26 | return arr[0] % max 27 | } 28 | 29 | export function randomBytes(n: number) { 30 | const buf = anyRandomUint32(n) 31 | const r = new Uint8Array(buf.length) 32 | for (let i = 0; i < n; ++i) r[i] = buf[i] & 0xff 33 | return r 34 | } 35 | 36 | export function randomDeviceId() { 37 | const devId = anyRandomUint32(8) 38 | for (let i = 0; i < 8; ++i) devId[i] &= 0xff 39 | return toHex(devId) 40 | } 41 | -------------------------------------------------------------------------------- /src/jdom/scheduler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /** 4 | * A time scheduler to orchestrate time in the bus. 5 | * @category JDOM 6 | */ 7 | export interface Scheduler { 8 | /** 9 | * Gets the current timestamp 10 | */ 11 | get timestamp(): number 12 | /** 13 | * Reset time 14 | * @param delta 15 | */ 16 | resetTime(delta: number): void 17 | /** 18 | * Start a timeout timer 19 | */ 20 | setTimeout( 21 | handler: (...args: any[]) => void, 22 | delay: number, 23 | ...args: any[] 24 | ): any 25 | /** 26 | * Cancel an existing timeout timer 27 | */ 28 | clearTimeout(handle: any): void 29 | /** 30 | * Start an interval timer 31 | */ 32 | setInterval( 33 | handler: (...args: any[]) => void, 34 | delay: number, 35 | ...args: any[] 36 | ): any 37 | /** 38 | * Clear an interval timer 39 | */ 40 | clearInterval(handle: any): void 41 | } 42 | 43 | /** @internal */ 44 | export class WallClockScheduler implements Scheduler { 45 | private _now: () => number 46 | private _startTime: number 47 | 48 | constructor() { 49 | this._now = 50 | typeof performance !== "undefined" 51 | ? () => performance.now() 52 | : () => Date.now() 53 | this._startTime = this._now() 54 | } 55 | 56 | get timestamp(): number { 57 | return this._now() - this._startTime 58 | } 59 | resetTime(delta = 0) { 60 | this._startTime = this._now() - delta 61 | } 62 | setTimeout( 63 | handler: (...args: any[]) => void, 64 | delay: number, 65 | ...args: any[] 66 | ): any { 67 | return setTimeout(handler, delay, args) 68 | } 69 | clearTimeout(handle: any): void { 70 | clearTimeout(handle) 71 | } 72 | setInterval( 73 | handler: (...args: any[]) => void, 74 | delay: number, 75 | ...args: any[] 76 | ): any { 77 | return setInterval(handler, delay, args) 78 | } 79 | clearInterval(handle: any): void { 80 | clearInterval(handle) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/jdom/semver.ts: -------------------------------------------------------------------------------- 1 | export interface Version { 2 | major: number 3 | minor: number 4 | patch: number 5 | pre: string[] 6 | build: string[] 7 | } 8 | 9 | function cmp(a: Version, b: Version) { 10 | if (!a) 11 | if (!b) return 0 12 | else return 1 13 | else if (!b) return -1 14 | else { 15 | let d = a.major - b.major || a.minor - b.minor || a.patch - b.patch 16 | if (d) return d 17 | if (a.pre.length == 0 && b.pre.length > 0) return 1 18 | if (a.pre.length > 0 && b.pre.length == 0) return -1 19 | for (let i = 0; i < a.pre.length + 1; ++i) { 20 | const aa = a.pre[i] 21 | const bb = b.pre[i] 22 | if (!aa) 23 | if (!bb) return 0 24 | else return -1 25 | else if (!bb) return 1 26 | else if (/^\d+$/.test(aa)) 27 | if (/^\d+$/.test(bb)) { 28 | d = parseInt(aa) - parseInt(bb) 29 | if (d) return d 30 | } else return -1 31 | else if (/^\d+$/.test(bb)) return 1 32 | else { 33 | d = strcmp(aa, bb) 34 | if (d) return d 35 | } 36 | } 37 | return 0 38 | } 39 | } 40 | 41 | export function versionTryParse(v: string): Version { 42 | if (!v) return null 43 | if ("*" === v) { 44 | return { 45 | major: Number.MAX_SAFE_INTEGER, 46 | minor: Number.MAX_SAFE_INTEGER, 47 | patch: Number.MAX_SAFE_INTEGER, 48 | pre: [], 49 | build: [], 50 | } 51 | } 52 | if (/^v\d/i.test(v)) v = v.slice(1) 53 | const m = 54 | /^(\d+)\.(\d+)\.(\d+)(-([0-9a-zA-Z\-\.]+))?(\+([0-9a-zA-Z\-\.]+))?$/.exec( 55 | v, 56 | ) 57 | if (m) 58 | return { 59 | major: parseInt(m[1]), 60 | minor: parseInt(m[2]), 61 | patch: parseInt(m[3]), 62 | pre: m[5] ? m[5].split(".") : [], 63 | build: m[7] ? m[7].split(".") : [], 64 | } 65 | return null 66 | } 67 | 68 | function strcmp(a: string, b: string) { 69 | if (a === b) return 0 70 | if (a < b) return -1 71 | else return 1 72 | } 73 | 74 | export function semverCmp(a: string, b: string) { 75 | const aa = versionTryParse(a) 76 | const bb = versionTryParse(b) 77 | if (!aa && !bb) return strcmp(a, b) 78 | else return cmp(aa, bb) 79 | } 80 | -------------------------------------------------------------------------------- /src/jdom/sensors.ts: -------------------------------------------------------------------------------- 1 | import { JDBus } from "./bus" 2 | import { isSensor, serviceSpecifications } from "./spec" 3 | import { toMap } from "./utils" 4 | 5 | let _sensorSpecs: jdspec.ServiceSpec[] 6 | 7 | /** 8 | * Gets the list of sensor specifications available 9 | * @returns 10 | */ 11 | export function sensorSpecifications() { 12 | if (!_sensorSpecs) { 13 | _sensorSpecs = serviceSpecifications().filter( 14 | srv => !srv.shortName.startsWith("_") && isSensor(srv), 15 | ) 16 | } 17 | return _sensorSpecs 18 | } 19 | 20 | /** 21 | * Collects and flattens all sensor data into a serializable object 22 | * @param bus 23 | * @returns 24 | */ 25 | export function snapshotSensors( 26 | bus: JDBus, 27 | sparse?: boolean, 28 | ): Record[]> { 29 | const r = toMap( 30 | sensorSpecifications(), 31 | srv => srv.camelName, 32 | srv => { 33 | const r = bus 34 | .services({ 35 | serviceClass: srv.classIdentifier, 36 | ignoreInfrastructure: true, 37 | announced: true, 38 | }) 39 | .map(srv => { 40 | const reg = srv.readingRegister 41 | const spec = reg.specification 42 | return spec.fields.length === 1 43 | ? reg.unpackedValue?.[0] || 0 44 | : reg.objectValue || {} 45 | }) 46 | return sparse && !r.length ? undefined : r 47 | }, 48 | sparse, 49 | ) 50 | return r 51 | } 52 | -------------------------------------------------------------------------------- /src/jdom/servers/protocoltestserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SRV_PROTO_TEST, 3 | CHANGE, 4 | ProtoTestReg, 5 | ProtoTestCmd, 6 | ProtoTestEvent, 7 | } from "../constants" 8 | import { jdpack, jdunpack } from "../pack" 9 | import { Packet } from "../packet" 10 | import { OutPipe } from "../pipes" 11 | import { JDRegisterServer } from "./registerserver" 12 | import { JDServiceServer } from "./serviceserver" 13 | 14 | export class ProtocolTestServer extends JDServiceServer { 15 | private rwBytes: JDRegisterServer<[Uint8Array]> 16 | 17 | constructor() { 18 | super(SRV_PROTO_TEST) 19 | 20 | this.init<[boolean]>( 21 | ProtoTestReg.RwBool, 22 | ProtoTestReg.RoBool, 23 | ProtoTestCmd.CBool, 24 | ProtoTestEvent.EBool, 25 | false 26 | ) 27 | this.init<[number]>( 28 | ProtoTestReg.RwI32, 29 | ProtoTestReg.RoI32, 30 | ProtoTestCmd.CI32, 31 | ProtoTestEvent.EI32, 32 | 0 33 | ) 34 | this.init<[number]>( 35 | ProtoTestReg.RwU32, 36 | ProtoTestReg.RoU32, 37 | ProtoTestCmd.CU32, 38 | ProtoTestEvent.EU32, 39 | 0 40 | ) 41 | this.init<[string]>( 42 | ProtoTestReg.RwString, 43 | ProtoTestReg.RoString, 44 | ProtoTestCmd.CString, 45 | ProtoTestEvent.EString, 46 | "" 47 | ) 48 | this.rwBytes = this.init<[Uint8Array]>( 49 | ProtoTestReg.RwBytes, 50 | ProtoTestReg.RoBytes, 51 | ProtoTestCmd.CBytes, 52 | ProtoTestEvent.EBytes, 53 | new Uint8Array(0) 54 | ) 55 | this.init<[number, number, number, number]>( 56 | ProtoTestReg.RwI8U8U16I32, 57 | ProtoTestReg.RoI8U8U16I32, 58 | ProtoTestCmd.CI8U8U16I32, 59 | ProtoTestEvent.EI8U8U16I32, 60 | 0, 61 | 0, 62 | 0, 63 | 0 64 | ) 65 | this.init<[number, string]>( 66 | ProtoTestReg.RwU8String, 67 | ProtoTestReg.RoU8String, 68 | ProtoTestCmd.CU8String, 69 | ProtoTestEvent.EU8String, 70 | 0, 71 | "" 72 | ) 73 | 74 | this.addCommand( 75 | ProtoTestCmd.CReportPipe, 76 | this.handleReportPipe.bind(this) 77 | ) 78 | } 79 | 80 | private init( 81 | rwi: number, 82 | roi: number, 83 | ci: number, 84 | ei: number, 85 | ...values: TValues 86 | ) { 87 | const rw = this.addRegister(rwi, values) 88 | const ro = this.addRegister(roi, rw.values()) 89 | rw.on(CHANGE, () => { 90 | ro.setValues(rw.values()) 91 | this.sendEvent(ei, rw.data) 92 | }) 93 | this.addCommand(ci, pkt => 94 | rw.setValues(jdunpack(pkt.data, rw.specification.packFormat)) 95 | ) 96 | return rw 97 | } 98 | 99 | private async handleReportPipe(pkt: Packet) { 100 | const pipe = OutPipe.from(this.device.bus, pkt, true) 101 | await pipe.respondForEach(this.rwBytes.data, (b: number) => { 102 | const buf = new Uint8Array(1) 103 | buf[0] = b 104 | return jdpack<[Uint8Array]>("b", [buf]) 105 | }) 106 | } 107 | } 108 | 109 | -------------------------------------------------------------------------------- /src/jdom/servers/serviceprovider.ts: -------------------------------------------------------------------------------- 1 | import { JDBus } from "../bus" 2 | import { 3 | CHANGE, 4 | PACKET_PROCESS, 5 | SELF_ANNOUNCE, 6 | } from "../constants" 7 | import { JDEventSource } from "../eventsource" 8 | import { Packet } from "../packet" 9 | import { shortDeviceId } from "../pretty" 10 | import { randomDeviceId } from "../random" 11 | import { JDServiceServer } from "./serviceserver" 12 | 13 | /** 14 | * Implements a device with service servers. 15 | * @category Servers 16 | */ 17 | export abstract class JDServiceProvider extends JDEventSource { 18 | private _bus: JDBus 19 | public readonly template: string 20 | public readonly deviceId: string 21 | public readonly shortId: string 22 | 23 | constructor(template: string, deviceId?: string) { 24 | super() 25 | this.template = template 26 | this.deviceId = deviceId 27 | if (!this.deviceId) this.deviceId = randomDeviceId() 28 | this.shortId = shortDeviceId(this.deviceId) 29 | this.handleSelfAnnounce = this.handleSelfAnnounce.bind(this) 30 | this.handlePacket = this.handlePacket.bind(this) 31 | } 32 | 33 | get bus() { 34 | return this._bus 35 | } 36 | 37 | set bus(value: JDBus) { 38 | if (value !== this._bus) { 39 | this.stop() 40 | this._bus = value 41 | if (this._bus) this.start() 42 | this.emit(CHANGE) 43 | } 44 | } 45 | 46 | protected start() { 47 | if (this._bus) { 48 | this._bus.on(SELF_ANNOUNCE, this.handleSelfAnnounce) 49 | this._bus.on(PACKET_PROCESS, this.handlePacket) 50 | } 51 | } 52 | 53 | protected stop() { 54 | if (this._bus) { 55 | this._bus.off(SELF_ANNOUNCE, this.handleSelfAnnounce) 56 | this._bus.off(PACKET_PROCESS, this.handlePacket) 57 | this._bus = undefined 58 | } 59 | } 60 | 61 | abstract service(serviceIndex: number): JDServiceServer 62 | 63 | protected handleSelfAnnounce(): void { 64 | this.emit(SELF_ANNOUNCE) 65 | } 66 | protected abstract handlePacket(pkt: Packet): void 67 | } 68 | -------------------------------------------------------------------------------- /src/jdom/serviceclient.ts: -------------------------------------------------------------------------------- 1 | import { JDService } from "./service" 2 | import { JDDevice } from "./device" 3 | import { JDBus } from "./bus" 4 | import { JDClient } from "./client" 5 | import { 6 | CHANGE, 7 | EVENT, 8 | SystemEvent, 9 | SystemReg, 10 | SystemStatusCodes, 11 | } from "./constants" 12 | 13 | /** 14 | * Base class for service clients 15 | * @category Clients 16 | */ 17 | export class JDServiceClient extends JDClient { 18 | constructor(public readonly service: JDService) { 19 | super() 20 | 21 | const statusCodeChanged = this.service.event( 22 | SystemEvent.StatusCodeChanged, 23 | ) 24 | this.mount(statusCodeChanged?.subscribe(EVENT, () => this.emit(CHANGE))) 25 | } 26 | 27 | protected get device(): JDDevice { 28 | return this.service.device 29 | } 30 | 31 | protected get bus(): JDBus { 32 | return this.device.bus 33 | } 34 | 35 | get statusCode(): SystemStatusCodes { 36 | const reg = this.service.register(SystemReg.StatusCode) 37 | return reg.unpackedValue?.[0] 38 | } 39 | 40 | toString(): string { 41 | return `client of ${this.service}` 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/jdom/servicemembernode.ts: -------------------------------------------------------------------------------- 1 | import { JDNode } from "./node" 2 | import { JDService } from "./service" 3 | import { DecodedPacket } from "./pretty" 4 | import { CHANGE } from "./constants" 5 | 6 | /** 7 | * Base class for JDOM service member classes. 8 | * @category JDOM 9 | */ 10 | export abstract class JDServiceMemberNode extends JDNode { 11 | private _specification: jdspec.PacketInfo 12 | private _notImplemented = false 13 | 14 | /** 15 | * Parent service 16 | * @category JDOM 17 | */ 18 | public readonly service: JDService 19 | /** 20 | * Identifier of the event. 21 | * @category Specification 22 | */ 23 | public readonly code: number 24 | 25 | private readonly isPacket: (pkt: jdspec.PacketInfo) => boolean 26 | 27 | /** 28 | * @internal 29 | */ 30 | constructor( 31 | service: JDService, 32 | code: number, 33 | isPacket: (pkt: jdspec.PacketInfo) => boolean, 34 | ) { 35 | super() 36 | this._specification = null 37 | this.service = service 38 | this.code = code 39 | this.isPacket = isPacket 40 | } 41 | 42 | /** 43 | * Gets the node identifier in the JDOM tree 44 | * @category JDOM 45 | */ 46 | get id() { 47 | return `${this.nodeKind}:${ 48 | this.service.device.deviceId 49 | }:${this.service.serviceIndex.toString(16)}:${this.code.toString(16)}` 50 | } 51 | 52 | /** 53 | * Gets the event name, if specified. 54 | * @category JDOM 55 | */ 56 | get name() { 57 | return this.specification?.name || this.code.toString(16) 58 | } 59 | 60 | /** 61 | * Gets the qualitified event name, if specified. 62 | * @category JDOM 63 | */ 64 | get qualifiedName() { 65 | return `${this.service.qualifiedName}.${this.name}` 66 | } 67 | 68 | /** 69 | * Gets the event specification if known. 70 | * @category Specification 71 | */ 72 | get specification() { 73 | if (this._specification === null) 74 | // lookup once 75 | this._specification = this.service.specification?.packets.find( 76 | packet => 77 | this.isPacket(packet) && packet.identifier === this.code, 78 | ) 79 | return this._specification 80 | } 81 | 82 | /** 83 | * Gets the parent service client instance. 84 | * @category JDOM 85 | */ 86 | get parent(): JDNode { 87 | return this.service 88 | } 89 | 90 | /** 91 | * Gets the event friendly name. 92 | * @category JDOM 93 | */ 94 | get friendlyName() { 95 | const parts = [this.service.friendlyName, this.name] 96 | return parts.join(".") 97 | } 98 | 99 | /** 100 | * Indicates if the member is not implemented on the server side 101 | * @category JDOM 102 | */ 103 | get notImplemented() { 104 | return this._notImplemented 105 | } 106 | 107 | /** 108 | * Internal 109 | * @internal 110 | */ 111 | setNotImplemented() { 112 | if (!this._notImplemented) { 113 | this._notImplemented = true 114 | this.emit(CHANGE) 115 | } 116 | } 117 | 118 | /** 119 | * @internal 120 | */ 121 | abstract get decoded(): DecodedPacket 122 | } 123 | -------------------------------------------------------------------------------- /src/jdom/setting.ts: -------------------------------------------------------------------------------- 1 | export interface Setting { 2 | get(): string | undefined 3 | set(v: string): void 4 | } 5 | 6 | export function localStorageSetting(key: string): Setting { 7 | if (typeof self !== "undefined" && self.localStorage) { 8 | return { 9 | get: () => self.localStorage.getItem(key) ?? undefined, 10 | set: v => self.localStorage.setItem(key, v), 11 | } 12 | } 13 | return undefined 14 | } 15 | -------------------------------------------------------------------------------- /src/jdom/sevensegment.ts: -------------------------------------------------------------------------------- 1 | export function sevenSegmentDigitEncode(value: number, digitCount: number) { 2 | const ns = isNaN(value) ? "" : value.toString() 3 | const length = digitCount 4 | if (isNaN(length)) return // unknown size 5 | 6 | const digits = new Uint8Array(length) 7 | const n = Math.min(length, ns.length) 8 | /* 9 | * ```text 10 | * - A - 11 | * F B 12 | * | | 13 | * - G - 14 | * | | - 15 | * E C |DP| 16 | * - D - - 17 | * ``` 18 | */ 19 | const digitBits = [ 20 | 0b00111111, // 0 21 | 0b00000110, // 1 22 | 0b01011011, // 2 23 | 0b01001111, // 3 24 | 0b01100110, // 4 25 | 0b01101101, // 5 26 | 0b01111101, // 6 27 | 0b00000111, // 7 28 | 0b01111111, // 8 29 | 0b01101111, // 9 30 | ] 31 | 32 | let k = digits.length - 1 33 | for (let i = n - 1; i >= 0; --i) { 34 | let c = ns.charCodeAt(i) 35 | let value = 0 36 | // dot 37 | if (c == 46) { 38 | i-- 39 | if (i > -1) c = ns.charCodeAt(i) 40 | value |= 0b10000000 41 | } 42 | // 0-9 43 | if (c >= 48 && c < 48 + digitBits.length) value |= digitBits[c - 48] 44 | // - 45 | else if (c == 45) value |= 0b01000000 46 | // e, E 47 | else if (c == 69 || c == 101) value |= 0b01001111 48 | digits[k--] = value 49 | } 50 | 51 | return digits 52 | } 53 | -------------------------------------------------------------------------------- /src/jdom/speedtest.ts: -------------------------------------------------------------------------------- 1 | import * as U from "./utils" 2 | import { Packet } from "./packet" 3 | import { JDDevice } from "./device" 4 | import { PACKET_REPORT, CMD_GET_REG, JD_SERVICE_INDEX_CTRL } from "./constants" 5 | import { ControlReg } from "../../jacdac-spec/dist/specconstants" 6 | 7 | /** 8 | * @internal 9 | */ 10 | export async function packetSpeedTest(dev: JDDevice) { 11 | const pingCmd = CMD_GET_REG | ControlReg.ProductIdentifier 12 | 13 | dev.on(PACKET_REPORT, onPacket) 14 | const t0 = Date.now() 15 | let lastSend = Date.now() 16 | let numpkt = 0 17 | let timeouts = 0 18 | let numrecv = 0 19 | let done = false 20 | 21 | await ask() 22 | while (numpkt < 100) { 23 | await U.delay(50) 24 | const now = Date.now() 25 | if (now - t0 > 3000) break 26 | if (now - lastSend > 100) { 27 | timeouts++ 28 | await ask() 29 | } 30 | } 31 | done = true 32 | await U.delay(250) 33 | dev.off(PACKET_REPORT, onPacket) 34 | const ms = Date.now() - t0 35 | 36 | const pktsPerSecond = numpkt / (ms / 1000) 37 | const dropRate = (100 * (numpkt - numrecv)) / numpkt 38 | 39 | return { 40 | msg: `${pktsPerSecond.toFixed(1)} pkts/s; ${dropRate.toFixed( 41 | 2, 42 | )}% dropped`, 43 | pktsPerSecond, 44 | dropRate, 45 | } 46 | 47 | async function ask() { 48 | lastSend = Date.now() 49 | numpkt++ 50 | await dev.sendCtrlCommand(pingCmd) 51 | } 52 | 53 | async function onPacket(p: Packet) { 54 | if ( 55 | p.serviceIndex == JD_SERVICE_INDEX_CTRL && 56 | p.serviceCommand == pingCmd 57 | ) { 58 | numrecv++ 59 | if (!done) await ask() 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/jdom/trace/tracerecorder.ts: -------------------------------------------------------------------------------- 1 | import { JDBus } from "../bus" 2 | import { JDClient } from "../client" 3 | import { CHANGE, FRAME_PROCESS, START, STOP } from "../constants" 4 | import { JDFrameBuffer } from "../packet" 5 | import { Trace } from "./trace" 6 | 7 | const RECORDING_TRACE_MAX_ITEMS = 100000 8 | 9 | /** 10 | * A recorder of packets to create traces. 11 | * @category Trace 12 | */ 13 | export class TraceRecorder extends JDClient { 14 | public maxRecordingLength = RECORDING_TRACE_MAX_ITEMS 15 | private _trace: Trace 16 | private _subscription: () => void 17 | 18 | constructor(public readonly bus: JDBus) { 19 | super() 20 | this.handleFrame = this.handleFrame.bind(this) 21 | 22 | this.mount(() => this._subscription?.()) 23 | } 24 | 25 | start() { 26 | if (this.recording) return 27 | 28 | this._subscription = this.bus.subscribe(FRAME_PROCESS, this.handleFrame) 29 | this._trace = new Trace([], { maxLength: this.maxRecordingLength }) 30 | this.emit(START) 31 | this.emit(CHANGE) 32 | } 33 | 34 | stop() { 35 | if (!this.recording) return 36 | 37 | this._subscription?.() 38 | this._subscription = undefined 39 | const t = this._trace 40 | this._trace = undefined 41 | this.emit(STOP) 42 | this.emit(CHANGE) 43 | 44 | return t 45 | } 46 | 47 | get recording() { 48 | return !!this._trace 49 | } 50 | 51 | get trace() { 52 | return this._trace 53 | } 54 | 55 | private handleFrame(pkt: JDFrameBuffer) { 56 | // record packets in traces 57 | this._trace.addFrame(pkt) 58 | // notify that this packet has been processed 59 | this.emit(FRAME_PROCESS, pkt) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/jdom/transport/createbus.ts: -------------------------------------------------------------------------------- 1 | import { JDBus, BusOptions } from "../bus" 2 | import { createUSBTransport, isWebUSBSupported } from "./usb" 3 | import { 4 | createWebSerialTransport, 5 | isWebSerialSupported, 6 | WebSerialOptions, 7 | } from "./webserial" 8 | import { 9 | createBluetoothTransport, 10 | isWebBluetoothSupported, 11 | WebBluetoothOptions, 12 | } from "./bluetooth" 13 | import { USBOptions } from "./usbio" 14 | import { createIFrameBridge } from "../bridges/iframebridge" 15 | import { 16 | createWebSocketTransport, 17 | WebSocketTransportOptions, 18 | } from "./websockettransport" 19 | 20 | /** 21 | * Options to instantiate a bus. By default, the bus acts as a client. 22 | */ 23 | export interface WebBusOptions extends BusOptions { 24 | /** 25 | * USB connection options, set to null to disable USB 26 | */ 27 | usbOptions?: USBOptions | null 28 | 29 | /** 30 | * WebSerial connection options, set to null to disable serial 31 | */ 32 | serialOptions?: WebSerialOptions | null 33 | 34 | /** 35 | * WebBluetooth connection options, set to null to disable BLE 36 | */ 37 | bluetoothOptions?: WebBluetoothOptions | null 38 | 39 | /** 40 | * Specify target origin for iframe messages 41 | */ 42 | iframeTargetOrigin?: string 43 | } 44 | 45 | /** 46 | * Creates a Jacdac bus using WebUSB, WebSerial or WebBluetooth 47 | * @param options 48 | * @returns 49 | * @category Transport 50 | */ 51 | export function createWebBus(options?: WebBusOptions) { 52 | const { 53 | usbOptions, 54 | serialOptions, 55 | bluetoothOptions, 56 | iframeTargetOrigin, 57 | client = true, 58 | ...rest 59 | } = options || {} 60 | const bus = new JDBus( 61 | [ 62 | usbOptions !== null ? createUSBTransport(usbOptions) : undefined, 63 | serialOptions !== null 64 | ? createWebSerialTransport(serialOptions) 65 | : undefined, 66 | bluetoothOptions !== null 67 | ? createBluetoothTransport(bluetoothOptions) 68 | : undefined, 69 | ], 70 | { client, ...rest } 71 | ) 72 | const iframeBridge = 73 | iframeTargetOrigin !== null && createIFrameBridge(iframeTargetOrigin) 74 | if (iframeBridge) iframeBridge.bus = bus 75 | return bus 76 | } 77 | 78 | /** 79 | * Indicates if any of the USB/Serial/Bluetooth transports is supported 80 | * @returns 81 | * @category Transport 82 | */ 83 | export function isWebTransportSupported() { 84 | return ( 85 | isWebUSBSupported() || 86 | isWebSerialSupported() || 87 | isWebBluetoothSupported() 88 | ) 89 | } 90 | 91 | /** 92 | * Create a bus that opens a websocket connection to the local debug server (ws://127.0.0.1:8081) 93 | * @param options 94 | * @returns 95 | */ 96 | export function createWebSocketBus(options?: { 97 | url?: string 98 | busOptions?: BusOptions 99 | webSocketOptions?: WebSocketTransportOptions 100 | }) { 101 | const { 102 | url = "ws://127.0.0.1:8081/", 103 | webSocketOptions, 104 | busOptions = {}, 105 | } = options || {} 106 | const ws = createWebSocketTransport(url, webSocketOptions) 107 | const bus = new JDBus([ws], { 108 | disableRoleManager: true, 109 | client: false, 110 | ...busOptions, 111 | }) 112 | bus.autoConnect = true 113 | return bus 114 | } 115 | -------------------------------------------------------------------------------- /src/jdom/transport/eventtargetobservable.ts: -------------------------------------------------------------------------------- 1 | import { Observable, Observer } from "../observable" 2 | 3 | /** 4 | * @internal 5 | */ 6 | export class EventTargetObservable implements Observable { 7 | constructor( 8 | public readonly element: EventTarget, 9 | public readonly eventName: string 10 | ) {} 11 | 12 | subscribe(observer: Observer): { unsubscribe: () => void } { 13 | const handler: EventListener = (ev: Event) => 14 | !!observer.next && observer.next((ev)) 15 | this.element.addEventListener(this.eventName, handler, false) 16 | return { 17 | unsubscribe: () => 18 | this.element.removeEventListener(this.eventName, handler), 19 | } 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/jdom/transport/nodesocket.ts: -------------------------------------------------------------------------------- 1 | import { NODESOCKET_TRANSPORT } from "../constants" 2 | import { bufferConcat } from "../utils" 3 | import { Transport, TransportOptions } from "./transport" 4 | 5 | /** 6 | * Transport creation options for TCP sockets 7 | * @category Transport 8 | */ 9 | // eslint-disable-next-line @typescript-eslint/no-empty-interface 10 | export interface NodeSocketTransportOptions extends TransportOptions {} 11 | 12 | class NodeSocketTransport extends Transport { 13 | private sock: any 14 | 15 | constructor( 16 | readonly port: number = 8082, 17 | readonly host: string = "127.0.0.1", 18 | options?: NodeSocketTransportOptions 19 | ) { 20 | super(NODESOCKET_TRANSPORT, options) 21 | } 22 | 23 | description(): string { 24 | return `${this.host}:${this.port}` 25 | } 26 | 27 | protected transportConnectAsync(background?: boolean): Promise { 28 | return new Promise(resolve => { 29 | // eslint-disable-next-line @typescript-eslint/no-var-requires 30 | const net = require("net") 31 | this.sock = net.createConnection(this.port, this.host, resolve) 32 | this.sock.on("error", (e: any) => { 33 | if (!background) console.error(e) 34 | this.disconnect(background) 35 | }) 36 | this.sock.on("end", () => this.disconnect(background)) 37 | this.sock.setNoDelay() 38 | 39 | let acc: Uint8Array 40 | this.sock.on("data", (buf: Uint8Array) => { 41 | if (acc) { 42 | buf = bufferConcat(acc, buf) 43 | acc = null 44 | } else { 45 | buf = new Uint8Array(buf) 46 | } 47 | while (buf) { 48 | const endp = buf[0] + 1 49 | if (buf.length >= endp) { 50 | const pkt = buf.slice(1, endp) 51 | if (buf.length > endp) buf = buf.slice(endp) 52 | else buf = null 53 | this.handleFrame(pkt) 54 | } else { 55 | acc = buf 56 | buf = null 57 | } 58 | } 59 | }) 60 | }) 61 | } 62 | 63 | protected transportSendPacketAsync(data: Uint8Array): Promise { 64 | const buf = new Uint8Array(1 + data.length) 65 | buf[0] = data.length 66 | buf.set(data, 1) 67 | this.sock.write(buf) 68 | return Promise.resolve() 69 | } 70 | 71 | protected transportDisconnectAsync(background?: boolean): Promise { 72 | try { 73 | this.sock?.end() 74 | this.sock = undefined 75 | } catch (e) { 76 | if (!background) throw e 77 | } 78 | return Promise.resolve() 79 | } 80 | 81 | toString() { 82 | return `socket transport (local port: ${this.sock?.localPort})` 83 | } 84 | } 85 | 86 | /** 87 | * Creates a transport over a TCP socket connection 88 | * @category transport 89 | */ 90 | export function createNodeSocketTransport( 91 | port?: number, 92 | host?: string, 93 | options?: NodeSocketTransportOptions 94 | ) { 95 | return new NodeSocketTransport(port, host, options) 96 | } 97 | -------------------------------------------------------------------------------- /src/jdom/transport/nodewebusb.ts: -------------------------------------------------------------------------------- 1 | import { isCancelError } from "../error" 2 | import { EventTargetObservable } from "./eventtargetobservable" 3 | import { HF2_DEVICE_MAJOR } from "./hf2" 4 | import { MICROBIT_V2_PRODUCT_ID, MICROBIT_V2_VENDOR_ID } from "./microbit" 5 | import { USBOptions } from "./usbio" 6 | 7 | export function createNodeUSBOptions(WebUSB: any): USBOptions { 8 | console.debug(`jacdac: creating usb transport`) 9 | async function devicesFound(devices: USBDevice[]): Promise { 10 | for (const device of devices) { 11 | const { vendorId, productId, deviceVersionMajor } = device 12 | // microbit v2 13 | if ( 14 | vendorId === MICROBIT_V2_VENDOR_ID && 15 | productId === MICROBIT_V2_PRODUCT_ID 16 | ) { 17 | console.debug(`usb: found micro:bit v2`) 18 | return device 19 | } 20 | // jacdac device 21 | else if (deviceVersionMajor == HF2_DEVICE_MAJOR) { 22 | for (const iface of device.configuration.interfaces) { 23 | const alt = iface.alternates[0] 24 | if ( 25 | alt.interfaceClass == 0xff && 26 | alt.interfaceSubclass == HF2_DEVICE_MAJOR 27 | ) { 28 | return device 29 | } 30 | } 31 | } 32 | } 33 | 34 | return undefined 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-var-requires 38 | const usb = new WebUSB({ 39 | devicesFound, 40 | allowAllDevices: true 41 | }) 42 | 43 | async function requestDevice( 44 | options: USBDeviceRequestOptions 45 | ): Promise { 46 | console.debug(`usb: requesting device...`) 47 | try { 48 | const device = await usb.requestDevice(options) 49 | return device 50 | } catch (e) { 51 | if (!isCancelError(e)) console.debug(e) 52 | return undefined 53 | } 54 | } 55 | 56 | async function getDevices( 57 | options: USBDeviceRequestOptions 58 | ): Promise { 59 | //const devices = await usb.getDevices() 60 | //return devices 61 | const dev = await requestDevice(options) 62 | return dev ? [dev] : [] 63 | } 64 | 65 | const connectObservable = new EventTargetObservable(usb, "connect") 66 | const disconnectObservable = new EventTargetObservable(usb, "disconnect") 67 | 68 | return { 69 | getDevices, 70 | requestDevice, 71 | connectObservable, 72 | disconnectObservable, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/jdom/transport/proto.ts: -------------------------------------------------------------------------------- 1 | export interface Proto { 2 | onJDMessage(f: (buf: Uint8Array) => void): void 3 | sendJDMessageAsync(buf: Uint8Array): Promise 4 | postConnectAsync(): Promise 5 | disconnectAsync(): Promise 6 | } 7 | -------------------------------------------------------------------------------- /src/jdom/transport/transportmessages.ts: -------------------------------------------------------------------------------- 1 | export interface TransportMessage { 2 | jacdac: true 3 | type: "connect" | "disconnect" | "send" | "packet" | "frame" | "error" 4 | id?: string 5 | background?: boolean 6 | error?: { 7 | message?: string 8 | stack?: string 9 | name?: string 10 | jacdacName?: string 11 | } 12 | } 13 | 14 | export interface TransportPacketMessage extends TransportMessage { 15 | type: "packet" | "frame" 16 | payload: Uint8Array 17 | } 18 | 19 | export interface TransportConnectMessage extends TransportMessage { 20 | type: "connect" 21 | deviceId?: string 22 | } 23 | -------------------------------------------------------------------------------- /src/jdom/transport/webserial.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from "../flags" 2 | import { SERIAL_TRANSPORT } from "../constants" 3 | import { Transport } from "./transport" 4 | import { Proto } from "./proto" 5 | import { WebSerialIO } from "./webserialio" 6 | import { HF2_IO } from "./hf2" 7 | import { Observable } from "../observable" 8 | import { EventTargetObservable } from "./eventtargetobservable" 9 | import { JDBus } from "../bus" 10 | 11 | export function isWebSerialEnabled(): boolean { 12 | return !!Flags.webSerial 13 | } 14 | 15 | export function isWebSerialSupported(): boolean { 16 | try { 17 | return ( 18 | typeof navigator !== "undefined" && 19 | !!navigator.serial && 20 | !!navigator.serial.getPorts 21 | ) 22 | } catch (e) { 23 | return false 24 | } 25 | } 26 | 27 | export interface WebSerialOptions { 28 | mkTransport: (bus: JDBus) => HF2_IO 29 | connectObservable?: Observable 30 | disconnectObservable?: Observable 31 | } 32 | 33 | export class WebSerialTransport extends Transport { 34 | private mkTransport: (bus: JDBus) => HF2_IO 35 | private hf2: Proto 36 | private transport: HF2_IO 37 | constructor(options: WebSerialOptions) { 38 | super(SERIAL_TRANSPORT, { ...options, checkPulse: true }) 39 | this.mkTransport = options.mkTransport 40 | } 41 | 42 | description(): string { 43 | return this.transport?.description() 44 | } 45 | 46 | protected async transportConnectAsync(background: boolean) { 47 | this.transport = this.mkTransport(this.bus) 48 | this.transport.onError = e => this.errorHandler(SERIAL_TRANSPORT, e) 49 | this.transport.onLog = line => this.handleLog(line) 50 | this.hf2 = await this.transport.connectAsync(background) 51 | this.hf2.onJDMessage(this.handleFrame.bind(this)) 52 | } 53 | 54 | protected async transportSendPacketAsync(buf: Uint8Array) { 55 | if (!this.hf2) throw new Error("hf2 transport disconnected") 56 | await this.hf2.sendJDMessageAsync(buf) 57 | } 58 | 59 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 60 | protected async transportDisconnectAsync(background?: boolean) { 61 | const h = this.hf2 62 | this.hf2 = undefined 63 | this.transport = undefined 64 | if (h) await h.disconnectAsync() 65 | } 66 | } 67 | 68 | function defaultOptions(): WebSerialOptions { 69 | if (!isWebSerialSupported()) return undefined 70 | const connectObservable = new EventTargetObservable( 71 | navigator.serial, 72 | "connect" 73 | ) 74 | const disconnectObservable = new EventTargetObservable( 75 | navigator.serial, 76 | "disconnect" 77 | ) 78 | return { 79 | mkTransport: (bus: JDBus) => new WebSerialIO(bus), 80 | connectObservable, 81 | disconnectObservable, 82 | } 83 | } 84 | 85 | /** 86 | * Creates a transport over a Web Serial connection 87 | * @category Transport 88 | */ 89 | export function createWebSerialTransport( 90 | options?: WebSerialOptions 91 | ): Transport { 92 | if (!options) options = defaultOptions() 93 | return options && new WebSerialTransport(options) 94 | } 95 | -------------------------------------------------------------------------------- /src/servers/accelerometerserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccelerometerReg, 3 | SRV_ACCELEROMETER, 4 | } from "../../jacdac-spec/dist/specconstants" 5 | import { JDRegisterServer } from "../jdom/servers/registerserver" 6 | import { SensorServer } from "./sensorserver" 7 | 8 | export class AccelerometerServer extends SensorServer< 9 | [number, number, number] 10 | > { 11 | maxForce: JDRegisterServer<[number]> 12 | 13 | constructor() { 14 | super(SRV_ACCELEROMETER, { 15 | readingValues: [0.5, 0.5, -(1 - (0.5 * 0.5 + 0.5 * 0.5))], 16 | preferredStreamingInterval: 20, 17 | }) 18 | 19 | this.maxForce = this.addRegister<[number]>(AccelerometerReg.MaxForce, [ 20 | 2, 21 | ]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/servers/analogsensorserver.ts: -------------------------------------------------------------------------------- 1 | import { SystemReg } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { LevelDetector } from "./leveldetector" 4 | import { SensorServer, SensorServiceOptions } from "./sensorserver" 5 | 6 | /** 7 | * Creation options for AnalogSensorServer 8 | * @category Servers 9 | * @internal 10 | */ 11 | export interface AnalogSensorServerOptions 12 | extends SensorServiceOptions<[number]> { 13 | minReading?: number 14 | maxReading?: number 15 | inactiveThreshold?: number 16 | activeThreshold?: number 17 | readingResolution?: number 18 | } 19 | 20 | /** 21 | * Base class for analog sensor servers 22 | * @category Servers 23 | */ 24 | export class AnalogSensorServer extends SensorServer<[number]> { 25 | readonly inactiveThreshold: JDRegisterServer<[number]> 26 | readonly activeThreshold: JDRegisterServer<[number]> 27 | readonly levelDetector: LevelDetector 28 | 29 | constructor(serviceClass: number, options?: AnalogSensorServerOptions) { 30 | super(serviceClass, options) 31 | const { 32 | minReading, 33 | maxReading, 34 | inactiveThreshold, 35 | activeThreshold, 36 | readingResolution, 37 | } = options || {} 38 | if (minReading !== undefined) 39 | this.addRegister<[number]>(SystemReg.MinReading, [minReading]) 40 | if (maxReading !== undefined) 41 | this.addRegister<[number]>(SystemReg.MaxReading, [maxReading]) 42 | if (readingResolution !== undefined) 43 | this.addRegister<[number]>(SystemReg.ReadingResolution, [ 44 | readingResolution, 45 | ]) 46 | if ( 47 | inactiveThreshold !== undefined || 48 | this.activeThreshold !== undefined 49 | ) { 50 | if (inactiveThreshold !== undefined) 51 | this.inactiveThreshold = this.addRegister<[number]>( 52 | SystemReg.InactiveThreshold, 53 | [inactiveThreshold], 54 | ) 55 | if (activeThreshold !== undefined) 56 | this.activeThreshold = this.addRegister<[number]>( 57 | SystemReg.ActiveThreshold, 58 | [activeThreshold], 59 | ) 60 | this.levelDetector = new LevelDetector(this) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/servers/brailledisplayserver.ts: -------------------------------------------------------------------------------- 1 | import { BrailleDisplayReg, SRV_BRAILLE_DISPLAY } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 4 | 5 | export class BrailleDisplayServer extends JDServiceServer { 6 | readonly patterns: JDRegisterServer<[string]> 7 | readonly enabled: JDRegisterServer<[boolean]> 8 | readonly length: JDRegisterServer<[number]> 9 | 10 | constructor( 11 | options?: { patterns?: string; length?: number } & JDServerOptions, 12 | ) { 13 | super(SRV_BRAILLE_DISPLAY, options) 14 | const { patterns = "", length = 12 } = options || {} 15 | 16 | this.patterns = this.addRegister<[string]>(BrailleDisplayReg.Patterns, [ 17 | patterns, 18 | ]) 19 | this.enabled = this.addRegister<[boolean]>(BrailleDisplayReg.Enabled, [ 20 | false, 21 | ]) 22 | this.length = this.addRegister<[number]>(BrailleDisplayReg.Length, [ 23 | length, 24 | ]) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/servers/buttonserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ButtonEvent, 3 | ButtonReg, 4 | CHANGE, 5 | REFRESH, 6 | SRV_BUTTON, 7 | } from "../jdom/constants" 8 | import { SensorServer } from "./sensorserver" 9 | import { JDRegisterServer } from "../jdom/servers/registerserver" 10 | import { jdpack } from "../jdom/pack" 11 | 12 | /** 13 | * Server implementation for the button service 14 | * @category Servers 15 | */ 16 | export class ButtonServer extends SensorServer<[number]> { 17 | public static readonly HOLD_TIME = 500 18 | public static readonly INACTIVE_VALUE = 0 19 | public static readonly ACTIVE_VALUE = 1 20 | 21 | private _downTime: number 22 | private _nextHold: number 23 | 24 | readonly analog: JDRegisterServer<[boolean]> 25 | private _threshold: JDRegisterServer<[number]> 26 | 27 | constructor(instanceName?: string, analog?: boolean) { 28 | super(SRV_BUTTON, { 29 | instanceName, 30 | readingValues: [ButtonServer.INACTIVE_VALUE], 31 | streamingInterval: 50, 32 | }) 33 | this.analog = this.addRegister(ButtonReg.Analog, [!!analog]) 34 | this.on(REFRESH, this.handleRefresh.bind(this)) 35 | } 36 | 37 | get threshold() { 38 | return this._threshold 39 | } 40 | 41 | set threshold(value: JDRegisterServer<[number]>) { 42 | if (value !== this._threshold) { 43 | this._threshold = value 44 | this.analog.setValues([!!this._threshold]) 45 | this.emit(CHANGE) 46 | } 47 | } 48 | 49 | private isActive() { 50 | // TODO: debouncing 51 | const [v] = this.reading.values() 52 | const t = this.threshold?.values()?.[0] || 0.5 53 | 54 | return v > t 55 | } 56 | 57 | private async handleRefresh() { 58 | const now = this.device.bus.timestamp 59 | if (this.isActive()) { 60 | // down event 61 | if (this._downTime === undefined) { 62 | this._downTime = now 63 | this._nextHold = this._downTime + ButtonServer.HOLD_TIME 64 | await this.sendEvent(ButtonEvent.Down) 65 | // hold 66 | } else if (now > this._nextHold) { 67 | const time = now - this._downTime 68 | this._nextHold = 69 | this.device.bus.timestamp + ButtonServer.HOLD_TIME 70 | await this.sendEvent( 71 | ButtonEvent.Hold, 72 | jdpack<[number]>("u32", [time]), 73 | ) 74 | } 75 | } else { 76 | // up event 77 | if (this._downTime !== undefined) { 78 | const time = now - this._downTime 79 | this._downTime = undefined 80 | this._nextHold = undefined 81 | await this.sendEvent( 82 | ButtonEvent.Up, 83 | jdpack<[number]>("u32", [time]), 84 | ) 85 | } 86 | } 87 | } 88 | 89 | async down() { 90 | this.reading.setValues([ButtonServer.ACTIVE_VALUE]) 91 | } 92 | 93 | async up() { 94 | this.reading.setValues([ButtonServer.INACTIVE_VALUE]) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/servers/buzzerserver.ts: -------------------------------------------------------------------------------- 1 | import { BuzzerCmd, BuzzerReg, SRV_BUZZER } from "../jdom/constants" 2 | import { jdpack, jdunpack } from "../jdom/pack" 3 | import { Packet } from "../jdom/packet" 4 | import { JDRegisterServer } from "../jdom/servers/registerserver" 5 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 6 | 7 | /** 8 | * @internal 9 | */ 10 | export interface BuzzerTone { 11 | frequency: number 12 | duration: number 13 | volume: number 14 | } 15 | 16 | /** 17 | * Encodes a buzzer tone information into a data payload 18 | * @param frequency sound frequency in Hz 19 | * @param ms sound duration in milliseconds 20 | * @param volume volume from [0..1] 21 | * @returns data payload 22 | * @category Data Packing 23 | */ 24 | export function tonePayload(frequency: number, ms: number, volume: number) { 25 | const period = Math.round(1000000 / frequency) 26 | const duty = (period * volume) >> 11 27 | return jdpack("u16 u16 u16", [period, duty, ms]) 28 | } 29 | 30 | /** 31 | * Server implementation for the buzzer service 32 | * @category Servers 33 | */ 34 | export class BuzzerServer extends JDServiceServer { 35 | readonly volume: JDRegisterServer<[number]> 36 | 37 | static PLAY_TONE = "playTone" 38 | 39 | constructor(options?: JDServerOptions) { 40 | super(SRV_BUZZER, options) 41 | 42 | this.volume = this.addRegister<[number]>(BuzzerReg.Volume, [0.2]) 43 | this.addCommand(BuzzerCmd.PlayTone, this.handlePlayTone.bind(this)) 44 | } 45 | 46 | private handlePlayTone(pkt: Packet) { 47 | const [period, , duration] = jdunpack<[number, number, number]>( 48 | pkt.data, 49 | "u16 u16 u16", 50 | ) 51 | const frequency = 1000000 / period 52 | const [volume] = this.volume.values() 53 | 54 | this.emit(BuzzerServer.PLAY_TONE, { 55 | frequency, 56 | duration, 57 | volume, 58 | }) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/servers/capacitivebuttonserver.ts: -------------------------------------------------------------------------------- 1 | import { CapacitiveButtonReg, SRV_CAPACITIVE_BUTTON } from "../jdom/constants" 2 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 3 | import { JDRegisterServer } from "../jdom/servers/registerserver" 4 | 5 | export class CapacitiveButtonServer extends JDServiceServer { 6 | readonly threshold: JDRegisterServer<[number]> 7 | 8 | constructor(options?: { threshold?: number } & JDServerOptions) { 9 | super(SRV_CAPACITIVE_BUTTON, options) 10 | const { threshold = 0.5 } = options || {} 11 | 12 | this.threshold = this.addRegister(CapacitiveButtonReg.Threshold, [ 13 | threshold, 14 | ]) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/servers/characterscreenserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CharacterScreenReg, 3 | CharacterScreenTextDirection, 4 | CharacterScreenVariant, 5 | SRV_CHARACTER_SCREEN, 6 | } from "../jdom/constants" 7 | import { Packet } from "../jdom/packet" 8 | import { JDRegisterServer } from "../jdom/servers/registerserver" 9 | import { JDServiceServer } from "../jdom/servers/serviceserver" 10 | 11 | export class CharacterScreenServer extends JDServiceServer { 12 | readonly message: JDRegisterServer<[string]> 13 | readonly brightness: JDRegisterServer<[number]> 14 | readonly rows: JDRegisterServer<[number]> 15 | readonly columns: JDRegisterServer<[number]> 16 | readonly variant: JDRegisterServer<[CharacterScreenVariant]> 17 | readonly textDirection: JDRegisterServer<[CharacterScreenTextDirection]> 18 | 19 | constructor(options?: { 20 | message?: string 21 | brightness?: number 22 | rows?: number 23 | columns?: number 24 | variant?: CharacterScreenVariant 25 | textDirection?: CharacterScreenTextDirection 26 | }) { 27 | super(SRV_CHARACTER_SCREEN) 28 | const { 29 | message = "", 30 | rows = 2, 31 | columns = 16, 32 | variant, 33 | textDirection, 34 | brightness = 100, 35 | } = options || {} 36 | 37 | this.message = this.addRegister<[string]>(CharacterScreenReg.Message, [ 38 | message, 39 | ]) 40 | this.brightness = this.addRegister<[number]>( 41 | CharacterScreenReg.Brightness, 42 | [brightness], 43 | ) 44 | this.rows = this.addRegister<[number]>(CharacterScreenReg.Rows, [rows]) 45 | this.columns = this.addRegister<[number]>(CharacterScreenReg.Columns, [ 46 | columns, 47 | ]) 48 | this.variant = this.addRegister<[CharacterScreenVariant]>( 49 | CharacterScreenReg.Variant, 50 | [variant || CharacterScreenVariant.LCD], 51 | ) 52 | this.message = this.addRegister<[string]>(CharacterScreenReg.Message, [ 53 | "", 54 | ]) 55 | this.textDirection = this.addRegister<[CharacterScreenTextDirection]>( 56 | CharacterScreenReg.TextDirection, 57 | [textDirection || CharacterScreenTextDirection.LeftToRight], 58 | ) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/servers/cloudadapterserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CloudAdapterCmd, 3 | CloudAdapterEvent, 4 | CloudAdapterReg, 5 | SRV_CLOUD_ADAPTER, 6 | CHANGE, 7 | CloudAdapterCmdPack, 8 | } from "../jdom/constants" 9 | import { Packet } from "../jdom/packet" 10 | import { JDRegisterServer } from "../jdom/servers/registerserver" 11 | import { JDServerOptions, JDServiceServer } from "../jdom/servers/serviceserver" 12 | 13 | export const UPLOAD_JSON = "upload" 14 | export const UPLOAD_BIN = "uploadBin" 15 | export const CLOUD_COMMAND = "cloudCommand" 16 | 17 | export interface CloudAdapterUploadJSONRequest { 18 | json: string 19 | } 20 | 21 | export interface CloudAdapterUploadBinRequest { 22 | data: Uint8Array 23 | } 24 | 25 | export class CloudAdapterServer extends JDServiceServer { 26 | readonly connectedRegister: JDRegisterServer<[boolean]> 27 | readonly connectionNameRegister: JDRegisterServer<[string]> 28 | readonly controlled: boolean 29 | 30 | constructor( 31 | options?: { 32 | connectionName?: string 33 | controlled?: boolean 34 | } & JDServerOptions, 35 | ) { 36 | super(SRV_CLOUD_ADAPTER, options) 37 | 38 | this.controlled = !!options?.controlled 39 | this.connectedRegister = this.addRegister(CloudAdapterReg.Connected, [ 40 | false, 41 | ]) 42 | this.connectionNameRegister = this.addRegister( 43 | CloudAdapterReg.ConnectionName, 44 | [options?.connectionName || ""], 45 | ) 46 | this.addCommand( 47 | CloudAdapterCmd.UploadJson, 48 | this.handleUpload.bind(this), 49 | ) 50 | this.addCommand( 51 | CloudAdapterCmd.UploadBinary, 52 | this.handleUploadBin.bind(this), 53 | ) 54 | this.connectedRegister.on(CHANGE, () => 55 | this.sendEvent(CloudAdapterEvent.Change), 56 | ) 57 | this.connectionNameRegister.on(CHANGE, () => 58 | this.sendEvent(CloudAdapterEvent.Change), 59 | ) 60 | } 61 | 62 | get connected() { 63 | return this.connectedRegister.values()[0] 64 | } 65 | 66 | set connected(value: boolean) { 67 | if (value !== this.connected) { 68 | this.connectedRegister.setValues([!!value]) 69 | this.sendEvent(CloudAdapterEvent.Change) 70 | } 71 | } 72 | 73 | private async handleUpload(pkt: Packet) { 74 | if (!this.connected) { 75 | console.debug(`cloud: cancel upload, not connected`) 76 | return 77 | } 78 | 79 | const [json] = pkt.jdunpack<[string]>(CloudAdapterCmdPack.UploadJson) 80 | this.uploadJSON(json) 81 | } 82 | 83 | private async handleUploadBin(pkt: Packet) { 84 | if (!this.connected) { 85 | console.debug(`cloud: cancel upload, not connected`) 86 | return 87 | } 88 | 89 | const data = pkt.data 90 | this.uploadBin(data) 91 | } 92 | 93 | uploadJSON(json: string) { 94 | this.emit(UPLOAD_JSON, { 95 | json, 96 | }) 97 | } 98 | 99 | uploadBin(data: Uint8Array) { 100 | this.emit(UPLOAD_BIN, { data }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/servers/compassserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | CompassReg, 4 | SRV_COMPASS, 5 | SystemStatusCodes, 6 | } from "../jdom/constants" 7 | import { JDRegisterServer } from "../jdom/servers/registerserver" 8 | import { AnalogSensorServer } from "./analogsensorserver" 9 | 10 | export class CompassServer extends AnalogSensorServer { 11 | private enabled: JDRegisterServer<[boolean]> 12 | constructor() { 13 | super(SRV_COMPASS, { 14 | readingValues: [0], 15 | minReading: 0, 16 | maxReading: 360, 17 | readingError: [1], 18 | }) 19 | 20 | this.enabled = this.addRegister(CompassReg.Enabled, [false]) 21 | this.enabled.on(CHANGE, () => { 22 | const [status] = this.statusCode.values() 23 | if (status === SystemStatusCodes.CalibrationNeeded) { 24 | console.debug("start calibration") 25 | this.calibrate() 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/servers/devicescriptmanagerserver.ts: -------------------------------------------------------------------------------- 1 | import { fnv1 } from "../jdom/utils" 2 | import { 3 | CHANGE, 4 | DeviceScriptManagerCmd, 5 | DeviceScriptManagerEvent, 6 | DeviceScriptManagerReg, 7 | SRV_DEVICE_SCRIPT_MANAGER, 8 | } from "../jdom/constants" 9 | import { JDRegisterServer } from "../jdom/servers/registerserver" 10 | import { JDServiceServer } from "../jdom/servers/serviceserver" 11 | import { OutPipe } from "../jdom/pipes" 12 | import { Packet } from "../jdom/packet" 13 | 14 | export class DeviceScriptManagerServer extends JDServiceServer { 15 | readonly running: JDRegisterServer<[boolean]> 16 | readonly autoStart: JDRegisterServer<[boolean]> 17 | readonly programSize: JDRegisterServer<[number]> 18 | readonly programHash: JDRegisterServer<[number]> 19 | 20 | private _binary: Uint8Array = new Uint8Array(0) 21 | private _debugInfo: unknown 22 | 23 | static PROGRAM_CHANGE = "programChange" 24 | 25 | constructor() { 26 | super(SRV_DEVICE_SCRIPT_MANAGER) 27 | 28 | this.running = this.addRegister(DeviceScriptManagerReg.Running, [false]) 29 | this.autoStart = this.addRegister(DeviceScriptManagerReg.Autostart, [ 30 | true, 31 | ]) 32 | this.programSize = this.addRegister( 33 | DeviceScriptManagerReg.ProgramSize, 34 | [this._binary.length], 35 | ) 36 | this.programHash = this.addRegister( 37 | DeviceScriptManagerReg.ProgramHash, 38 | [fnv1(this._binary)], 39 | ) 40 | 41 | this.addCommand( 42 | DeviceScriptManagerCmd.DeployBytecode, 43 | this.handleDeployBytecode.bind(this), 44 | ) 45 | this.addCommand( 46 | DeviceScriptManagerCmd.ReadBytecode, 47 | this.handleReadBytecode.bind(this), 48 | ) 49 | } 50 | 51 | get binary() { 52 | return this._binary 53 | } 54 | 55 | get debugInfo() { 56 | return this._debugInfo 57 | } 58 | 59 | setBytecode(binary: Uint8Array, debugInfo: unknown) { 60 | binary = binary || new Uint8Array(0) 61 | const [hash] = this.programHash.values() 62 | const valueHash = fnv1(binary) 63 | 64 | if (hash !== valueHash) { 65 | this._binary = binary 66 | this._debugInfo = debugInfo 67 | this.programSize.setValues([binary.length]) 68 | this.programHash.setValues([valueHash]) 69 | this.emit(DeviceScriptManagerServer.PROGRAM_CHANGE) 70 | this.emit(CHANGE) 71 | this.sendEvent(DeviceScriptManagerEvent.ProgramChange) 72 | } 73 | } 74 | 75 | private handleDeployBytecode(pkt: Packet) { 76 | console.debug(`devicescript server: deploy`, { pkt }) 77 | } 78 | 79 | private async handleReadBytecode(pkt: Packet) { 80 | const pipe = OutPipe.from(this.device.bus, pkt, true) 81 | await pipe.sendBytes(this._binary) 82 | await pipe.close() 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/servers/dmxserver.ts: -------------------------------------------------------------------------------- 1 | import { DmxCmd, SRV_DMX } from "../jdom/constants" 2 | import { Packet } from "../jdom/packet" 3 | import { JDServiceServer } from "../jdom/servers/serviceserver" 4 | import { toHex } from "../jdom/utils" 5 | 6 | export class DMXServer extends JDServiceServer { 7 | constructor() { 8 | super(SRV_DMX, { 9 | intensityValues: [0], 10 | }) 11 | 12 | this.addCommand(DmxCmd.Send, this.handleSend.bind(this)) 13 | } 14 | 15 | private handleSend(pkt: Packet) { 16 | // ignore 17 | console.debug(`dmx send`, toHex(pkt.data)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/servers/dotmatrixserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | DotMatrixReg, 4 | DotMatrixVariant, 5 | SensorReg, 6 | SRV_DOT_MATRIX, 7 | } from "../jdom/constants" 8 | import { JDRegisterServer } from "../jdom/servers/registerserver" 9 | import { JDServiceServer } from "../jdom/servers/serviceserver" 10 | 11 | export class DotMatrixServer extends JDServiceServer { 12 | readonly dots: JDRegisterServer<[Uint8Array]> 13 | readonly rows: JDRegisterServer<[number]> 14 | readonly columns: JDRegisterServer<[number]> 15 | readonly brightness: JDRegisterServer<[number]> 16 | readonly variant: JDRegisterServer<[DotMatrixVariant]> 17 | 18 | constructor( 19 | columns: number, 20 | rows: number, 21 | options?: { 22 | brightness?: number 23 | variant?: DotMatrixVariant 24 | }, 25 | ) { 26 | super(SRV_DOT_MATRIX) 27 | const { brightness, variant } = options || {} 28 | this.dots = this.addRegister(DotMatrixReg.Dots, [new Uint8Array(0)]) 29 | this.rows = this.addRegister(DotMatrixReg.Rows, [rows]) 30 | this.columns = this.addRegister(DotMatrixReg.Columns, [columns]) 31 | if (brightness !== undefined) 32 | this.brightness = this.addRegister(DotMatrixReg.Brightness, [128]) 33 | if (variant !== undefined) 34 | this.variant = this.addRegister(DotMatrixReg.Variant, [variant]) 35 | this.rows.skipBoundaryCheck = true 36 | this.rows.skipErrorInjection = true 37 | 38 | if (variant === DotMatrixVariant.LED) 39 | this.addRegister<[number]>(SensorReg.StreamingPreferredInterval, [ 40 | 50, 41 | ]) 42 | 43 | this.rows.on(CHANGE, this.updateDotsBuffer.bind(this)) 44 | this.columns.on(CHANGE, this.updateDotsBuffer.bind(this)) 45 | 46 | this.updateDotsBuffer() 47 | } 48 | 49 | private updateDotsBuffer() { 50 | // columns must be byte aligned 51 | const [rows] = this.rows.values() 52 | const [columns] = this.columns.values() 53 | 54 | // total bytes needed 55 | const n = columns * ((rows + 7) >> 3) 56 | 57 | if (this.dots.data?.length !== n) { 58 | this.dots.data = new Uint8Array(n) 59 | this.dots.emit(CHANGE) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/servers/dualmotorsserver.ts: -------------------------------------------------------------------------------- 1 | import { DualMotorsReg, SRV_DUAL_MOTORS } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServiceServer } from "../jdom/servers/serviceserver" 4 | 5 | export class DualMotorsServer extends JDServiceServer { 6 | readonly speed: JDRegisterServer<[number, number]> 7 | readonly enabled: JDRegisterServer<[boolean]> 8 | readonly loadTorque: JDRegisterServer<[number]> 9 | readonly loadRotationSpeed: JDRegisterServer<[number]> 10 | 11 | constructor( 12 | instanceName?: string, 13 | options?: { 14 | loadTorque?: number 15 | loadRotationSpeed?: number 16 | }, 17 | ) { 18 | super(SRV_DUAL_MOTORS, { instanceName }) 19 | 20 | const { loadTorque, loadRotationSpeed } = options || {} 21 | 22 | this.speed = this.addRegister<[number, number]>( 23 | DualMotorsReg.Speed, 24 | [0, 0], 25 | ) 26 | this.enabled = this.addRegister<[boolean]>(DualMotorsReg.Enabled, [ 27 | false, 28 | ]) 29 | if (loadTorque) 30 | this.loadTorque = this.addRegister<[number]>( 31 | DualMotorsReg.LoadTorque, 32 | [loadTorque], 33 | ) 34 | if (loadRotationSpeed) 35 | this.loadRotationSpeed = this.addRegister<[number]>( 36 | DualMotorsReg.LoadRotationSpeed, 37 | [loadRotationSpeed], 38 | ) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/servers/hidjoystickserver.ts: -------------------------------------------------------------------------------- 1 | import { bufferEq } from "../jdom/utils" 2 | import { 3 | CHANGE, 4 | HidJoystickCmd, 5 | HidJoystickReg, 6 | SRV_HID_JOYSTICK, 7 | } from "../jdom/constants" 8 | import { Packet } from "../jdom/packet" 9 | import { JDServerOptions, JDServiceServer } from "../jdom/servers/serviceserver" 10 | 11 | export class HIDJoystickServer extends JDServiceServer { 12 | buttons: Uint8Array = new Uint8Array() 13 | axis: Uint8Array = new Uint8Array() 14 | 15 | constructor( 16 | options?: { 17 | buttonCount: number 18 | axisCount: number 19 | buttonsAnalog: boolean 20 | } & JDServerOptions, 21 | ) { 22 | super(SRV_HID_JOYSTICK, options) 23 | const { 24 | buttonCount = 16, 25 | axisCount = 6, 26 | buttonsAnalog = false, 27 | } = options || {} 28 | 29 | this.addRegister(HidJoystickReg.ButtonCount, [buttonCount]) 30 | this.addRegister(HidJoystickReg.AxisCount, [axisCount]) 31 | this.addRegister(HidJoystickReg.ButtonsAnalog, [buttonsAnalog]) 32 | 33 | this.addCommand( 34 | HidJoystickCmd.SetButtons, 35 | this.handleSetButtons.bind(this), 36 | ) 37 | this.addCommand(HidJoystickCmd.SetAxis, this.handleSetAxis.bind(this)) 38 | } 39 | 40 | private handleSetButtons(pkt: Packet) { 41 | if (!bufferEq(this.buttons, pkt.data)) { 42 | this.buttons = pkt.data 43 | this.emit(CHANGE) 44 | } 45 | } 46 | private handleSetAxis(pkt: Packet) { 47 | if (!bufferEq(this.axis, pkt.data)) { 48 | this.axis = pkt.data 49 | this.emit(CHANGE) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/servers/hidmouseserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | HidMouseButton, 4 | HidMouseButtonEvent, 5 | HidMouseCmd, 6 | SRV_HID_MOUSE, 7 | } from "../jdom/constants" 8 | import { Packet } from "../jdom/packet" 9 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 10 | 11 | /** 12 | * @internal 13 | */ 14 | export function renderHidMouseButtons(buttons: HidMouseButton) { 15 | const btns = [ 16 | buttons & HidMouseButton.Left ? "left" : "", 17 | buttons & HidMouseButton.Right ? "right" : "", 18 | buttons & HidMouseButton.Middle ? "middle" : "", 19 | ] 20 | .filter(b => !!b) 21 | .join(", ") 22 | return btns 23 | } 24 | 25 | export class HIDMouseServer extends JDServiceServer { 26 | private _lastCommand: string 27 | 28 | constructor(options?: JDServerOptions) { 29 | super(SRV_HID_MOUSE, options) 30 | 31 | this.addCommand(HidMouseCmd.Move, this.handleMove.bind(this)) 32 | this.addCommand(HidMouseCmd.SetButton, this.handleSetButton.bind(this)) 33 | this.addCommand(HidMouseCmd.Wheel, this.handleWheel.bind(this)) 34 | } 35 | 36 | get lastCommand() { 37 | return this._lastCommand 38 | } 39 | 40 | setLastCommand(s: string) { 41 | if (this._lastCommand !== s) { 42 | this._lastCommand = s 43 | this.emit(CHANGE) 44 | } 45 | } 46 | 47 | private handleMove(pkt: Packet) { 48 | const [dx, dy, time] = 49 | pkt.jdunpack<[number, number, number]>("i16 i16 u16") 50 | this.setLastCommand(`move ${dx} ${dy} ${time}`) 51 | } 52 | 53 | private handleSetButton(pkt: Packet) { 54 | const [buttons, event] = 55 | pkt.jdunpack<[HidMouseButton, HidMouseButtonEvent]>("u16 u8") 56 | const btns = renderHidMouseButtons(buttons) 57 | this.setLastCommand( 58 | `set buttons ${btns || "?"} ${( 59 | HidMouseButtonEvent[event] || "?" 60 | ).toLocaleLowerCase()}`, 61 | ) 62 | } 63 | 64 | private handleWheel(pkt: Packet) { 65 | const [dy, time] = pkt.jdunpack<[number, number]>("i16 u16") 66 | this.setLastCommand(`wheel ${dy} ${time}`) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/servers/jacdac-servers.ts: -------------------------------------------------------------------------------- 1 | export * from "./analogsensorserver" 2 | export * from "./buttonserver" 3 | export * from "./buzzerserver" 4 | export * from "./characterscreenserver" 5 | export * from "./compassserver" 6 | export * from "./gamepadservermanager" 7 | export * from "./servers" 8 | export * from "./gamepadserver" 9 | export * from "./dotmatrixserver" 10 | export * from "./ledstripserver" 11 | export * from "./ledserver" 12 | export * from "./leveldetector" 13 | export * from "./loggerserver" 14 | export * from "./matrixkeypadserver" 15 | export * from "./motorserver" 16 | export * from "./raingaugeserver" 17 | export * from "./randomnumbergeneratorserver" 18 | export * from "./realtimeclockserver" 19 | export * from "./reflectedlightserver" 20 | export * from "./rotaryencoderserver" 21 | export * from "./sensorserver" 22 | export * from "./servoserver" 23 | export * from "./settingsserver" 24 | export * from "./soundplayerserver" 25 | export * from "./speechsynthesisserver" 26 | export * from "./switchserver" 27 | export * from "./trafficlightserver" 28 | export * from "./powerserver" 29 | export * from "./hidkeyboardserver" 30 | export * from "./hidmouseserver" 31 | export * from "./hidjoystickserver" 32 | export * from "./verifiedtelemetryserver" 33 | export * from "./vibrationmotorserver" 34 | export * from "./wifiserver" 35 | export * from "./brailledisplayserver" 36 | export * from "./devicescriptmanagerserver" 37 | export * from "./cloudadapterserver" 38 | export * from "./powersupplyserver" 39 | export * from "./magneticfieldlevelserver" 40 | export * from "./dualmotorsserver" 41 | export * from "./satnavserver" 42 | export * from "./planarpositionserver" 43 | export * from "./serialserver" 44 | export * from "./rosserver" 45 | export * from "./indexedscreenserver" 46 | export * from "./sevensegmentdisplayserver" 47 | export * from "./pcmonitorserver" 48 | export * from "./pccontrollerserver" 49 | -------------------------------------------------------------------------------- /src/servers/leveldetector.ts: -------------------------------------------------------------------------------- 1 | import { JDClient } from "../jdom/client" 2 | import { CHANGE, SystemEvent, SystemReadingThreshold } from "../jdom/constants" 3 | import { AnalogSensorServer } from "./analogsensorserver" 4 | 5 | export class LevelDetector extends JDClient { 6 | private _state: SystemReadingThreshold 7 | 8 | constructor(readonly service: AnalogSensorServer) { 9 | super() 10 | this.reset() 11 | if (this.service.inactiveThreshold) 12 | this.mount( 13 | this.service.inactiveThreshold.subscribe( 14 | CHANGE, 15 | this.reset.bind(this), 16 | ), 17 | ) 18 | if (this.service.activeThreshold) 19 | this.mount( 20 | this.service.activeThreshold.subscribe( 21 | CHANGE, 22 | this.reset.bind(this), 23 | ), 24 | ) 25 | this.mount( 26 | this.service.reading.subscribe(CHANGE, this.update.bind(this)), 27 | ) 28 | } 29 | 30 | reset() { 31 | this._state = SystemReadingThreshold.Neutral 32 | } 33 | 34 | update() { 35 | const [level] = this.service.reading.values() 36 | if (level === undefined) { 37 | this.setState(SystemReadingThreshold.Neutral) 38 | return 39 | } 40 | 41 | const [active] = this.service.activeThreshold?.values() || [] 42 | if (active !== undefined && level >= active) { 43 | this.setState(SystemReadingThreshold.Active) 44 | return 45 | } 46 | 47 | const [inactive] = this.service.inactiveThreshold?.values() || [] 48 | if (inactive !== undefined && level <= inactive) { 49 | this.setState(SystemReadingThreshold.Inactive) 50 | return 51 | } 52 | 53 | // neutral 54 | this.setState(SystemReadingThreshold.Neutral) 55 | } 56 | 57 | private setState(state: number) { 58 | if (state === this._state) return 59 | 60 | this._state = state 61 | switch (state) { 62 | case SystemReadingThreshold.Active: 63 | this.service.sendEvent(SystemEvent.Active) 64 | break 65 | case SystemReadingThreshold.Inactive: 66 | this.service.sendEvent(SystemEvent.Inactive) 67 | break 68 | case SystemReadingThreshold.Neutral: 69 | this.service.sendEvent(SystemEvent.Neutral) 70 | break 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/servers/loggerserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LoggerCmd, 3 | LoggerPriority, 4 | LoggerReg, 5 | SRV_LOGGER, 6 | } from "../jdom/constants" 7 | import { Packet } from "../jdom/packet" 8 | import { JDRegisterServer } from "../jdom/servers/registerserver" 9 | import { JDServiceServer } from "../jdom/servers/serviceserver" 10 | 11 | export class LoggerServer extends JDServiceServer { 12 | readonly minPriority: JDRegisterServer<[LoggerPriority]> 13 | 14 | constructor() { 15 | super(SRV_LOGGER) 16 | 17 | this.minPriority = this.addRegister(LoggerReg.MinPriority, [ 18 | LoggerPriority.Silent, 19 | ]) 20 | } 21 | 22 | async report(priority: LoggerCmd, msg: string) { 23 | const pkt = Packet.jdpacked<[string]>(priority, "s", [msg]) 24 | await this.sendPacketAsync(pkt) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/servers/magneticfieldlevelserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | MagneticFieldLevelEvent, 4 | MagneticFieldLevelReg, 5 | MagneticFieldLevelVariant, 6 | SRV_MAGNETIC_FIELD_LEVEL, 7 | SystemReadingThreshold, 8 | } from "../jdom/constants" 9 | import { JDServerOptions } from "../jdom/servers/serviceserver" 10 | import { SensorServer } from "./sensorserver" 11 | 12 | export interface MagneticFieldLevelServerOptions extends JDServerOptions { 13 | variant: MagneticFieldLevelVariant 14 | } 15 | 16 | export class MagneticFieldLevelServer extends SensorServer<[number]> { 17 | private _state = SystemReadingThreshold.Neutral 18 | 19 | static ACTIVE_THRESHOLD = 0.3 20 | static INACTIVE_THRESHOLD = 0.1 21 | 22 | constructor(options: MagneticFieldLevelServerOptions) { 23 | super(SRV_MAGNETIC_FIELD_LEVEL, { 24 | ...options, 25 | readingValues: [0], 26 | }) 27 | 28 | this.reading.on(CHANGE, this.update.bind(this)) 29 | } 30 | 31 | active() { 32 | this.reading.setValues([1]) 33 | } 34 | 35 | inactive() { 36 | this.reading.setValues([0]) 37 | } 38 | 39 | get variant() { 40 | const reg = this.register(MagneticFieldLevelReg.Variant) 41 | const [v] = reg.values() as [MagneticFieldLevelVariant] 42 | return v 43 | } 44 | 45 | private update() { 46 | const [strength] = this.reading.values() 47 | if (Math.abs(strength) >= MagneticFieldLevelServer.ACTIVE_THRESHOLD) { 48 | this.setState(SystemReadingThreshold.Active) 49 | } else if ( 50 | Math.abs(strength) <= MagneticFieldLevelServer.INACTIVE_THRESHOLD 51 | ) { 52 | this.setState(SystemReadingThreshold.Inactive) 53 | } else this.setState(SystemReadingThreshold.Neutral) 54 | } 55 | 56 | private setState(state: number) { 57 | if (state === this._state) return 58 | 59 | const variant = this.variant 60 | this._state = state 61 | switch (state) { 62 | case SystemReadingThreshold.Active: 63 | this.sendEvent(MagneticFieldLevelEvent.Active) 64 | break 65 | case SystemReadingThreshold.Inactive: 66 | this.sendEvent(MagneticFieldLevelEvent.Inactive) 67 | break 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/servers/matrixkeypadserver.ts: -------------------------------------------------------------------------------- 1 | import { MatrixKeypadReg, SRV_MATRIX_KEYPAD } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { SensorServer } from "./sensorserver" 4 | 5 | export class MatrixKeypadServer extends SensorServer<[[number][]]> { 6 | readonly rows: JDRegisterServer<[number]> 7 | readonly columns: JDRegisterServer<[number]> 8 | readonly labels: JDRegisterServer<[[string][]]> 9 | 10 | constructor(columns: number, rows: number, labels?: string[]) { 11 | super(SRV_MATRIX_KEYPAD, { 12 | readingValues: [[]], 13 | }) 14 | 15 | this.columns = this.addRegister(MatrixKeypadReg.Columns, [columns]) 16 | this.rows = this.addRegister(MatrixKeypadReg.Rows, [rows]) 17 | this.labels = this.addRegister( 18 | MatrixKeypadReg.Labels, 19 | labels ? [labels.map(l => [l])] : undefined, 20 | ) 21 | } 22 | 23 | async down(button: number) { 24 | const [values] = this.reading.values() 25 | const valuei = values.findIndex(v => v[0] === button) 26 | if (valuei < 0) { 27 | values.push([button]) 28 | this.reading.setValues([values]) 29 | } 30 | } 31 | 32 | async up(button: number) { 33 | const [values] = this.reading.values() 34 | const valuei = values.findIndex(v => v[0] === button) 35 | if (valuei > -1) { 36 | values.splice(valuei, 1) 37 | this.reading.setValues([values]) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/servers/motorserver.ts: -------------------------------------------------------------------------------- 1 | import { MotorReg, SRV_MOTOR } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServiceServer } from "../jdom/servers/serviceserver" 4 | 5 | export class MotorServer extends JDServiceServer { 6 | readonly speed: JDRegisterServer<[number]> 7 | readonly enabled: JDRegisterServer<[boolean]> 8 | readonly loadTorque: JDRegisterServer<[number]> 9 | readonly loadRotationSpeed: JDRegisterServer<[number]> 10 | 11 | constructor( 12 | instanceName?: string, 13 | options?: { 14 | loadTorque?: number 15 | loadRotationSpeed?: number 16 | }, 17 | ) { 18 | super(SRV_MOTOR, { instanceName }) 19 | 20 | const { loadTorque, loadRotationSpeed } = options || {} 21 | 22 | this.speed = this.addRegister<[number]>(MotorReg.Speed, [0]) 23 | this.enabled = this.addRegister<[boolean]>(MotorReg.Enabled, [false]) 24 | if (loadTorque) 25 | this.loadTorque = this.addRegister<[number]>(MotorReg.LoadTorque, [ 26 | loadTorque, 27 | ]) 28 | if (loadRotationSpeed) 29 | this.loadRotationSpeed = this.addRegister<[number]>( 30 | MotorReg.LoadRotationSpeed, 31 | [loadRotationSpeed], 32 | ) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/servers/pccontrollerserver.ts: -------------------------------------------------------------------------------- 1 | import { PCControllerCmd, SRV_PCCONTROLLER } from "../jdom/constants" 2 | import { Packet } from "../jdom/packet" 3 | import { JDServerOptions, JDServiceServer } from "../jdom/servers/serviceserver" 4 | 5 | export class PCControllerServer extends JDServiceServer { 6 | constructor(options?: JDServerOptions) { 7 | super(SRV_PCCONTROLLER, options) 8 | 9 | this.addCommand(PCControllerCmd.OpenUrl, this.handleOpenUrl.bind(this)) 10 | this.addCommand( 11 | PCControllerCmd.SendText, 12 | this.handleSendText.bind(this), 13 | ) 14 | this.addCommand( 15 | PCControllerCmd.StartApp, 16 | this.handleStartApp.bind(this), 17 | ) 18 | this.addCommand( 19 | PCControllerCmd.RunScript, 20 | this.handleRunScript.bind(this), 21 | ) 22 | } 23 | 24 | public static readonly OPEN_URL = "openUrl" 25 | public static readonly SEND_TEXT = "sendText" 26 | public static readonly START_APP = "startApp" 27 | public static readonly RUN_SCRIPT = "runScript" 28 | 29 | private handleOpenUrl(pkt: Packet) { 30 | const url = pkt.stringData 31 | this.emit(PCControllerServer.OPEN_URL, url) 32 | } 33 | 34 | private handleSendText(pkt: Packet) { 35 | const text = pkt.stringData 36 | this.emit(PCControllerServer.SEND_TEXT, text) 37 | } 38 | 39 | private handleStartApp(pkt: Packet) { 40 | const text = pkt.stringData 41 | this.emit(PCControllerServer.START_APP, text) 42 | } 43 | 44 | private handleRunScript(pkt: Packet) { 45 | const text = pkt.stringData 46 | this.emit(PCControllerServer.RUN_SCRIPT, text) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/servers/pcmonitorserver.ts: -------------------------------------------------------------------------------- 1 | import { PCMonitorReg, SRV_PCMONITOR } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServerOptions, JDServiceServer } from "../jdom/servers/serviceserver" 4 | 5 | export class PCMonitorServer extends JDServiceServer { 6 | readonly CPUUsage: JDRegisterServer<[number]> 7 | readonly CPUTemperature: JDRegisterServer<[number]> 8 | readonly RAMUsage: JDRegisterServer<[number]> 9 | readonly GPUInfo: JDRegisterServer<[number, number]> 10 | readonly NetworkInfo: JDRegisterServer<[number, number]> 11 | 12 | constructor(options?: JDServerOptions) { 13 | super(SRV_PCMONITOR, options) 14 | 15 | this.CPUUsage = this.addRegister<[number]>(PCMonitorReg.CpuUsage, [0]) 16 | this.CPUTemperature = this.addRegister<[number]>( 17 | PCMonitorReg.CpuTemperature, 18 | [21], 19 | ) 20 | this.RAMUsage = this.addRegister<[number]>(PCMonitorReg.RamUsage, [0]) 21 | this.GPUInfo = this.addRegister<[number, number]>( 22 | PCMonitorReg.GpuInformation, 23 | [0, 0], 24 | ) 25 | this.NetworkInfo = this.addRegister<[number, number]>( 26 | PCMonitorReg.NetworkInformation, 27 | [0, 0], 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/servers/planarpositionserver.ts: -------------------------------------------------------------------------------- 1 | import { SRV_PLANAR_POSITION } from "../jacdac" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { SensorServer } from "./sensorserver" 4 | 5 | export class PlanarPositionServer extends SensorServer<[number, number]> { 6 | position: JDRegisterServer<[number]> 7 | 8 | constructor() { 9 | super(SRV_PLANAR_POSITION, { 10 | readingValues: [0, 0], 11 | preferredStreamingInterval: 500, 12 | }) 13 | } 14 | 15 | move(dx: number, dy: number) { 16 | if (!dx && !dy) return 17 | let [x = 0, y = 0] = this.reading.values() 18 | x += dx 19 | y += dy 20 | this.reading.setValues([x, y]) 21 | // always send update immediately 22 | this.reading.sendGetAsync() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/servers/powerserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | PowerPowerStatus, 4 | PowerReg, 5 | SRV_POWER, 6 | } from "../jdom/constants" 7 | import { JDRegisterServer } from "../jdom/servers/registerserver" 8 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 9 | 10 | export class PowerServer extends JDServiceServer { 11 | readonly allowed: JDRegisterServer<[boolean]> 12 | readonly maxPower: JDRegisterServer<[number]> 13 | readonly powerStatus: JDRegisterServer<[PowerPowerStatus]> 14 | readonly currentDraw: JDRegisterServer<[number]> 15 | readonly keepOnPulseDuration: JDRegisterServer<[number]> 16 | readonly keepOnPulsePeriod: JDRegisterServer<[number]> 17 | 18 | constructor(options?: JDServerOptions) { 19 | super(SRV_POWER, options) 20 | this.allowed = this.addRegister<[boolean]>(PowerReg.Allowed, [false]) 21 | this.maxPower = this.addRegister<[number]>(PowerReg.MaxPower, [500]) 22 | this.powerStatus = this.addRegister<[PowerPowerStatus]>( 23 | PowerReg.PowerStatus, 24 | [PowerPowerStatus.Disallowed], 25 | ) 26 | this.currentDraw = this.addRegister<[number]>(PowerReg.CurrentDraw, [0]) 27 | this.keepOnPulseDuration = this.addRegister<[number]>( 28 | PowerReg.KeepOnPulseDuration, 29 | [10], 30 | ) 31 | this.keepOnPulsePeriod = this.addRegister<[number]>( 32 | PowerReg.KeepOnPulsePeriod, 33 | [1000], 34 | ) 35 | 36 | this.allowed.on(CHANGE, this.handleAllowedChange.bind(this)) 37 | } 38 | 39 | private handleAllowedChange() { 40 | const allowed = !!this.allowed.values()[0] 41 | if (allowed) { 42 | this.powerStatus.setValues([PowerPowerStatus.Powering]) 43 | this.currentDraw.setValues([250]) 44 | } else { 45 | this.powerStatus.setValues([PowerPowerStatus.Disallowed]) 46 | this.currentDraw.setValues([0]) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/servers/powersupplyserver.ts: -------------------------------------------------------------------------------- 1 | import { PowerSupplyReg, SRV_POWER_SUPPLY } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServiceServer } from "../jdom/servers/serviceserver" 4 | 5 | export class PowerSupplyServer extends JDServiceServer { 6 | readonly enabled: JDRegisterServer<[boolean]> 7 | readonly outputVoltage: JDRegisterServer<[number]> 8 | readonly minVoltage: JDRegisterServer<[number]> 9 | readonly maxVoltage: JDRegisterServer<[number]> 10 | 11 | constructor(options?: { 12 | outputVoltage: number 13 | minVoltage: number 14 | maxVoltage: number 15 | }) { 16 | super(SRV_POWER_SUPPLY) 17 | const { outputVoltage, minVoltage, maxVoltage } = options || {} 18 | 19 | this.enabled = this.addRegister(PowerSupplyReg.Enabled, [false]) 20 | this.outputVoltage = this.addRegister(PowerSupplyReg.OutputVoltage, [ 21 | outputVoltage, 22 | ]) 23 | this.minVoltage = this.addRegister(PowerSupplyReg.MinimumVoltage, [ 24 | minVoltage, 25 | ]) 26 | this.maxVoltage = this.addRegister(PowerSupplyReg.MaximumVoltage, [ 27 | maxVoltage, 28 | ]) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/servers/raingaugeserver.ts: -------------------------------------------------------------------------------- 1 | import { CHANGE, RainGaugeReg, SRV_RAIN_GAUGE } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { AnalogSensorServer } from "./analogsensorserver" 4 | 5 | export class RainGaugeServer extends AnalogSensorServer { 6 | readonly precipitationPrecision: JDRegisterServer<[number]> 7 | private _tiltCount = 0 8 | private _level = 0 9 | 10 | constructor(options?: { bucketSize?: number }) { 11 | super(SRV_RAIN_GAUGE, { 12 | readingValues: [0], 13 | }) 14 | const { bucketSize } = options || {} 15 | 16 | this.precipitationPrecision = this.addRegister<[number]>( 17 | RainGaugeReg.PrecipitationPrecision, 18 | [bucketSize || 0.2794], 19 | ) 20 | this._level = 0 21 | } 22 | 23 | get tiltCount() { 24 | return this._tiltCount 25 | } 26 | 27 | get level() { 28 | return this._level 29 | } 30 | 31 | async rain(fraction: number) { 32 | if (!fraction) return 33 | 34 | this._level += fraction 35 | if (this._level >= 0.7) await this.tilt() 36 | else this.emit(CHANGE) 37 | } 38 | 39 | async tilt() { 40 | this._tiltCount++ 41 | this._level = 0 42 | 43 | const [bucket] = this.precipitationPrecision.values() 44 | const [current] = this.reading.values() 45 | this.reading.setValues([current + (bucket || 0.2)]) 46 | 47 | this.emit(CHANGE) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/servers/randomnumbergeneratorserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | REGISTER_PRE_GET, 3 | RngReg, 4 | RngVariant, 5 | SRV_RNG, 6 | } from "../jdom/constants" 7 | import { JDRegisterServer } from "../jdom/servers/registerserver" 8 | import { JDServiceServer } from "../jdom/servers/serviceserver" 9 | 10 | export class RandomNumberGeneratorServer extends JDServiceServer { 11 | readonly reading: JDRegisterServer<[Uint8Array]> 12 | constructor() { 13 | super(SRV_RNG, { 14 | variant: RngVariant.WebCrypto, 15 | }) 16 | 17 | this.reading = this.addRegister(RngReg.Random, [new Uint8Array(64)]) 18 | this.reading.on(REGISTER_PRE_GET, this.handleRefresh.bind(this)) 19 | } 20 | 21 | private handleRefresh() { 22 | // generate new data 23 | const data = new Uint8Array(64) 24 | if (typeof window !== "undefined") window.crypto.getRandomValues(data) 25 | this.reading.setValues([data], true) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/servers/realtimeclockserver.ts: -------------------------------------------------------------------------------- 1 | import { SensorServer } from "./sensorserver" 2 | import { 3 | RealTimeClockCmd, 4 | RealTimeClockReg, 5 | RealTimeClockVariant, 6 | REFRESH, 7 | SRV_REAL_TIME_CLOCK, 8 | } from "../jdom/constants" 9 | import { JDRegisterServer } from "../jdom/servers/registerserver" 10 | import { JDBus } from "../jdom/bus" 11 | import { Packet } from "../jdom/packet" 12 | 13 | /** 14 | * @internal 15 | */ 16 | export type RealTimeClockReadingType = [ 17 | number, 18 | number, 19 | number, 20 | number, 21 | number, 22 | number, 23 | number, 24 | ] 25 | 26 | /** 27 | * @internal 28 | */ 29 | export function dateToClock(n: Date): RealTimeClockReadingType { 30 | const year = n.getFullYear() 31 | const month = n.getMonth() + 1 32 | const dayOfMonth = n.getDate() 33 | const dayOfWeek = n.getDay() 34 | const hour = n.getHours() 35 | const min = n.getMinutes() 36 | const sec = n.getSeconds() 37 | 38 | return [year, month, dayOfMonth, dayOfWeek, hour, min, sec] 39 | } 40 | 41 | export class RealTimeClockServer extends SensorServer { 42 | readonly drift: JDRegisterServer<[number]> 43 | readonly precision: JDRegisterServer<[number]> 44 | private lastSecond = 0 45 | 46 | constructor() { 47 | super(SRV_REAL_TIME_CLOCK, { 48 | readingValues: dateToClock(new Date()), 49 | variant: RealTimeClockVariant.Computer, 50 | streamingInterval: 1000, 51 | }) 52 | 53 | this.drift = this.addRegister<[number]>(RealTimeClockReg.Drift, [0]) 54 | this.precision = this.addRegister<[number]>( 55 | RealTimeClockReg.Precision, 56 | [0], 57 | ) 58 | 59 | this.addCommand(RealTimeClockCmd.SetTime, this.handleSetTime.bind(this)) 60 | this.on(REFRESH, this.refreshTime.bind(this)) 61 | } 62 | 63 | static async syncTime(bus: JDBus) { 64 | const values = dateToClock(new Date()) 65 | const pkt = Packet.jdpacked( 66 | RealTimeClockCmd.SetTime, 67 | "u16 u8 u8 u8 u8 u8 u8", 68 | values, 69 | ) 70 | await pkt.sendAsMultiCommandAsync(bus, SRV_REAL_TIME_CLOCK) 71 | } 72 | 73 | private handleSetTime(pkt: Packet) { 74 | //console.debug(`set time`, { pkt }) 75 | } 76 | 77 | private refreshTime() { 78 | const d = new Date() 79 | const s = d.getSeconds() 80 | if (s !== this.lastSecond) { 81 | const r = dateToClock(d) 82 | this.reading.setValues(r) 83 | this.lastSecond = s 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/servers/reflectedlightserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ReflectedLightReg, 3 | ReflectedLightVariant, 4 | SRV_REFLECTED_LIGHT, 5 | } from "../jdom/constants" 6 | import { JDRegisterServer } from "../jdom/servers/registerserver" 7 | import { AnalogSensorServer } from "./analogsensorserver" 8 | 9 | export class ReflectedLightServer extends AnalogSensorServer { 10 | readonly variant: JDRegisterServer<[ReflectedLightVariant]> 11 | 12 | constructor(options?: { variant?: ReflectedLightVariant }) { 13 | super(SRV_REFLECTED_LIGHT, { readingValues: [0] }) 14 | const { variant } = options || {} 15 | 16 | this.variant = this.addRegister<[ReflectedLightVariant]>( 17 | ReflectedLightReg.Variant, 18 | [variant || ReflectedLightVariant.InfraredDigital], 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/servers/rotaryencoderserver.ts: -------------------------------------------------------------------------------- 1 | import { RotaryEncoderReg, SRV_ROTARY_ENCODER } from "../jdom/constants" 2 | import { SensorServer } from "./sensorserver" 3 | import { JDRegisterServer } from "../jdom/servers/registerserver" 4 | 5 | export class RotaryEncoderServer extends SensorServer<[number]> { 6 | readonly clicksPerTurn: JDRegisterServer<[number]> 7 | 8 | constructor() { 9 | super(SRV_ROTARY_ENCODER, { readingValues: [0], streamingInterval: 50 }) 10 | 11 | this.clicksPerTurn = this.addRegister<[number]>( 12 | RotaryEncoderReg.ClicksPerTurn, 13 | [12], 14 | ) 15 | } 16 | 17 | async rotate(clicks: number) { 18 | const [position] = this.reading.values() 19 | this.reading.setValues([position + (clicks >> 0)]) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/servers/satnavserver.ts: -------------------------------------------------------------------------------- 1 | import { SatNavReg, SRV_SAT_NAV } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { SensorServer, SensorServiceOptions } from "./sensorserver" 4 | 5 | export type SatNavReadingType = [ 6 | number, // time 7 | number, // latitude 8 | number, // longitude 9 | number, // accuracy 10 | number, // altitude, 11 | number, // altitude accuracy 12 | ] 13 | 14 | export class SatNavServer extends SensorServer { 15 | readonly enabled: JDRegisterServer<[boolean]> 16 | 17 | constructor(options?: SensorServiceOptions) { 18 | super( 19 | SRV_SAT_NAV, 20 | options || { 21 | streamingInterval: 1000, 22 | }, 23 | ) 24 | this.enabled = this.addRegister(SatNavReg.Enabled, [false]) 25 | } 26 | 27 | setGeolocationPosition( 28 | loc: GeolocationPosition, 29 | skipChangeEvent?: boolean, 30 | ) { 31 | const { timestamp, coords } = loc 32 | const { latitude, longitude, accuracy, altitude, altitudeAccuracy } = 33 | coords 34 | this.reading.setValues( 35 | [ 36 | timestamp, 37 | latitude, 38 | longitude, 39 | accuracy, 40 | altitude || 0, 41 | altitudeAccuracy || 0, 42 | ], 43 | skipChangeEvent, 44 | ) 45 | } 46 | } 47 | 48 | export function watchLocation( 49 | server: SatNavServer, 50 | options?: PositionOptions, 51 | ): () => void { 52 | let id: number = undefined 53 | const success: PositionCallback = pos => { 54 | console.log("geo: pos", { id, pos }) 55 | if (id !== undefined) server.setGeolocationPosition(pos) 56 | } 57 | const unmount = () => { 58 | if (id !== undefined) navigator.geolocation.clearWatch(id) 59 | console.log("geo: unmount", { id }) 60 | id = undefined 61 | } 62 | const error: PositionErrorCallback = err => { 63 | console.log(err) 64 | unmount() 65 | } 66 | if (typeof navigator !== "undefined" && navigator.geolocation) { 67 | id = navigator.geolocation.watchPosition( 68 | success, 69 | error, 70 | options || { 71 | enableHighAccuracy: false, 72 | timeout: 5000, 73 | maximumAge: 0, 74 | }, 75 | ) 76 | console.log("geo: mount", { id }) 77 | navigator.geolocation.getCurrentPosition(success, error) 78 | } 79 | return unmount 80 | } 81 | -------------------------------------------------------------------------------- /src/servers/serialserver.ts: -------------------------------------------------------------------------------- 1 | import { Packet, toHex } from "../jacdac" 2 | import { 3 | SerialCmd, 4 | SerialParityType, 5 | SerialReg, 6 | SRV_SERIAL, 7 | } from "../jdom/constants" 8 | import { JDRegisterServer } from "../jdom/servers/registerserver" 9 | import { JDServiceServer } from "../jdom/servers/serviceserver" 10 | 11 | export class SerialServer extends JDServiceServer { 12 | readonly connected: JDRegisterServer<[boolean]> 13 | readonly baudRate: JDRegisterServer<[number]> 14 | readonly dataBits: JDRegisterServer<[number]> 15 | readonly stopBits: JDRegisterServer<[number]> 16 | readonly parityMode: JDRegisterServer<[SerialParityType]> 17 | readonly bufferSize: JDRegisterServer<[number]> 18 | 19 | constructor(options?: { 20 | baudRate?: number 21 | dataBits?: number 22 | stopBits?: number 23 | parityMode?: SerialParityType 24 | bufferSize?: number 25 | connectionName?: string 26 | }) { 27 | super(SRV_SERIAL) 28 | const { 29 | baudRate = 115200, 30 | dataBits = 8, 31 | stopBits = 1, 32 | parityMode = SerialParityType.None, 33 | bufferSize = 64, 34 | connectionName, 35 | } = options || {} 36 | 37 | this.connected = this.addRegister(SerialReg.Connected, [false]) 38 | this.baudRate = this.addRegister(SerialReg.BaudRate, [baudRate]) 39 | this.dataBits = this.addRegister(SerialReg.DataBits, [dataBits]) 40 | this.stopBits = this.addRegister(SerialReg.StopBits, [stopBits]) 41 | this.parityMode = this.addRegister(SerialReg.ParityMode, [parityMode]) 42 | this.bufferSize = this.addRegister(SerialReg.BufferSize, [bufferSize]) 43 | if (connectionName) 44 | this.addRegister(SerialReg.ConnectionName, [connectionName]) 45 | this.addCommand(SerialCmd.Send, this.handleSend.bind(this)) 46 | } 47 | 48 | private handleSend(pkt: Packet) { 49 | const [connected] = this.connected.values() 50 | if (!connected) return // ignore 51 | 52 | console.debug(`serial send`, toHex(pkt.data)) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/servers/servoserver.ts: -------------------------------------------------------------------------------- 1 | import { PACKET_DATA_NORMALIZE, ServoReg, SRV_SERVO } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 4 | 5 | export class ServoServer extends JDServiceServer { 6 | readonly angle: JDRegisterServer<[number]> 7 | readonly offset: JDRegisterServer<[number]> 8 | readonly enabled: JDRegisterServer<[boolean]> 9 | readonly minAngle: JDRegisterServer<[number]> 10 | readonly maxAngle: JDRegisterServer<[number]> 11 | readonly responseSpeed: JDRegisterServer<[number]> 12 | readonly stallTorque: JDRegisterServer<[number]> 13 | 14 | constructor( 15 | options?: { 16 | minAngle?: number 17 | maxAngle?: number 18 | responseSpeed?: number 19 | stallTorque?: number 20 | } & JDServerOptions, 21 | ) { 22 | super(SRV_SERVO, options) 23 | const { 24 | minAngle = 0, 25 | maxAngle = 180, 26 | responseSpeed, 27 | stallTorque, 28 | } = options || {} 29 | 30 | this.angle = this.addRegister<[number]>(ServoReg.Angle, [0]) 31 | this.enabled = this.addRegister<[boolean]>(ServoReg.Enabled, [false]) 32 | this.minAngle = this.addRegister<[number]>(ServoReg.MinAngle, [ 33 | minAngle, 34 | ]) 35 | this.maxAngle = this.addRegister<[number]>(ServoReg.MaxAngle, [ 36 | maxAngle, 37 | ]) 38 | this.offset = this.addRegister<[number]>(ServoReg.Offset, [0]) 39 | this.responseSpeed = this.addRegister<[number]>( 40 | ServoReg.ResponseSpeed, 41 | responseSpeed !== undefined ? [responseSpeed] : undefined, 42 | ) 43 | this.stallTorque = this.addRegister<[number]>( 44 | ServoReg.StallTorque, 45 | stallTorque !== undefined ? [stallTorque] : undefined, 46 | ) 47 | 48 | this.angle.on(PACKET_DATA_NORMALIZE, (values: [number]) => { 49 | let angle = values[0] 50 | const [minAngle] = this.minAngle.values() 51 | const [maxAngle] = this.maxAngle.values() 52 | if (minAngle !== undefined) angle = Math.max(minAngle, angle) 53 | if (maxAngle !== undefined) angle = Math.min(maxAngle, angle) 54 | values[0] = angle 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/servers/sevensegmentdisplayserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SRV_SEVEN_SEGMENT_DISPLAY, 3 | SevenSegmentDisplayCmd, 4 | SevenSegmentDisplayCmdPack, 5 | SevenSegmentDisplayReg, 6 | } from "../jdom/constants" 7 | import { Packet } from "../jdom/packet" 8 | import { JDRegisterServer } from "../jdom/servers/registerserver" 9 | import { JDServiceServer } from "../jdom/servers/serviceserver" 10 | import { sevenSegmentDigitEncode } from "../jdom/sevensegment" 11 | 12 | export class SevenSegmentDisplayServer extends JDServiceServer { 13 | readonly digits: JDRegisterServer<[Uint8Array]> 14 | readonly digitCount: JDRegisterServer<[number]> 15 | readonly decimalPoint: JDRegisterServer<[boolean]> 16 | constructor(options: { digits: Uint8Array; decimalPoint?: boolean }) { 17 | super(SRV_SEVEN_SEGMENT_DISPLAY, { 18 | intensityValues: [0xffff], 19 | }) 20 | 21 | const { digits, decimalPoint } = options 22 | 23 | this.digitCount = this.addRegister<[number]>( 24 | SevenSegmentDisplayReg.DigitCount, 25 | [digits.length], 26 | ) 27 | this.decimalPoint = this.addRegister<[boolean]>( 28 | SevenSegmentDisplayReg.DecimalPoint, 29 | [!!decimalPoint], 30 | ) 31 | this.digits = this.addRegister<[Uint8Array]>( 32 | SevenSegmentDisplayReg.Digits, 33 | [digits], 34 | ) 35 | 36 | this.addCommand( 37 | SevenSegmentDisplayCmd.SetNumber, 38 | this.handleSetNumber.bind(this), 39 | ) 40 | } 41 | 42 | private async handleSetNumber(pkt: Packet) { 43 | const [digitCount] = this.digitCount.values() 44 | const [value] = pkt.jdunpack<[number]>( 45 | SevenSegmentDisplayCmdPack.SetNumber, 46 | ) 47 | const digits = isNaN(value) 48 | ? new Uint8Array(0) 49 | : sevenSegmentDigitEncode(value, digitCount) 50 | if (this.digits.setValues([digits])) await this.digits.sendGetAsync() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/servers/soundplayerserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SoundPlayerCmd, 3 | SoundPlayerReg, 4 | SRV_SOUND_PLAYER, 5 | } from "../jdom/constants" 6 | import { jdpack } from "../jdom/pack" 7 | import { Packet } from "../jdom/packet" 8 | import { OutPipe } from "../jdom/pipes" 9 | import { JDRegisterServer } from "../jdom/servers/registerserver" 10 | import { JDServiceServer } from "../jdom/servers/serviceserver" 11 | 12 | /** 13 | * @internal 14 | */ 15 | export type SoundPlayerSound = [number, string] 16 | 17 | export class SoundPlayerServer extends JDServiceServer { 18 | readonly volume: JDRegisterServer<[number]> 19 | onPlay?: (name: string) => void 20 | constructor(private readonly sounds: SoundPlayerSound[]) { 21 | super(SRV_SOUND_PLAYER) 22 | 23 | this.volume = this.addRegister(SoundPlayerReg.Volume, [0.5]) 24 | this.addCommand( 25 | SoundPlayerCmd.ListSounds, 26 | this.handleListSounds.bind(this), 27 | ) 28 | this.addCommand(SoundPlayerCmd.Play, this.handlePlay.bind(this)) 29 | } 30 | 31 | private async handleListSounds(pkt: Packet) { 32 | const pipe = OutPipe.from(this.device.bus, pkt, true) 33 | await pipe.respondForEach(this.sounds, sound => 34 | jdpack<[number, string]>("u32 s", sound), 35 | ) 36 | } 37 | 38 | private handlePlay(pkt: Packet) { 39 | const [name] = pkt.jdunpack("s") 40 | this.onPlay?.(name) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/servers/speechsynthesisserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SpeechSynthesisCmd, 3 | SpeechSynthesisReg, 4 | SRV_SPEECH_SYNTHESIS, 5 | } from "../jdom/constants" 6 | import { Packet } from "../jdom/packet" 7 | import { JDRegisterServer } from "../jdom/servers/registerserver" 8 | import { JDServiceServer } from "../jdom/servers/serviceserver" 9 | 10 | export class SpeechSynthesisServer extends JDServiceServer { 11 | readonly enabled: JDRegisterServer<[boolean]> 12 | readonly pitch: JDRegisterServer<[number]> 13 | readonly rate: JDRegisterServer<[number]> 14 | readonly lang: JDRegisterServer<[string]> 15 | readonly volume: JDRegisterServer<[number]> 16 | 17 | readonly synthesis: SpeechSynthesis 18 | 19 | constructor() { 20 | super(SRV_SPEECH_SYNTHESIS) 21 | 22 | this.synthesis = typeof self !== "undefined" && window.speechSynthesis 23 | 24 | this.enabled = this.addRegister<[boolean]>(SpeechSynthesisReg.Enabled, [ 25 | !this.synthesis?.paused, 26 | ]) 27 | this.pitch = this.addRegister<[number]>(SpeechSynthesisReg.Pitch, [1]) 28 | this.rate = this.addRegister<[number]>(SpeechSynthesisReg.Rate, [1]) 29 | this.lang = this.addRegister<[string]>(SpeechSynthesisReg.Lang, [""]) 30 | this.volume = this.addRegister<[number]>(SpeechSynthesisReg.Volume, [ 31 | 0.5, 32 | ]) 33 | 34 | this.addCommand(SpeechSynthesisCmd.Speak, this.handleSpeak.bind(this)) 35 | this.addCommand(SpeechSynthesisCmd.Cancel, this.handleCancel.bind(this)) 36 | } 37 | 38 | private handleSpeak(pkt: Packet) { 39 | const [text] = pkt.jdunpack("s") 40 | if (!this.synthesis || !text) return 41 | 42 | const [pitch] = this.pitch.values() 43 | const [rate] = this.pitch.values() 44 | const [lang] = this.lang.values() 45 | const [volume] = this.volume.values() 46 | 47 | const utterance = new SpeechSynthesisUtterance(text) 48 | utterance.pitch = pitch 49 | utterance.rate = rate 50 | utterance.lang = lang 51 | utterance.volume = volume 52 | 53 | this.synthesis.speak(utterance) 54 | } 55 | 56 | private handleCancel(pkt: Packet) { 57 | this.synthesis?.cancel() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/servers/switchserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SRV_SWITCH, 3 | SwitchEvent, 4 | SwitchReg, 5 | SwitchVariant, 6 | } from "../jdom/constants" 7 | import { JDRegisterServer } from "../jdom/servers/registerserver" 8 | import { SensorServer } from "./sensorserver" 9 | 10 | export class SwitchServer extends SensorServer<[boolean]> { 11 | readonly variant: JDRegisterServer<[SwitchVariant]> 12 | 13 | constructor(options?: { variant?: SwitchVariant }) { 14 | super(SRV_SWITCH, { readingValues: [false], streamingInterval: 50 }) 15 | const { variant } = options || {} 16 | 17 | this.variant = this.addRegister( 18 | SwitchReg.Variant, 19 | variant !== undefined ? [variant] : undefined, 20 | ) 21 | } 22 | 23 | async toggle() { 24 | const [v] = this.reading.values() 25 | if (!v) await this.switchOn() 26 | else await this.switchOff() 27 | } 28 | 29 | async switchOn() { 30 | const [v] = this.reading.values() 31 | if (!v) { 32 | this.reading.setValues([true]) 33 | await this.sendEvent(SwitchEvent.On) 34 | } 35 | } 36 | 37 | async switchOff() { 38 | const [v] = this.reading.values() 39 | if (v) { 40 | this.reading.setValues([false]) 41 | await this.sendEvent(SwitchEvent.Off) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/servers/trafficlightserver.ts: -------------------------------------------------------------------------------- 1 | import { SRV_TRAFFIC_LIGHT, TrafficLightReg } from "../jdom/constants" 2 | import { JDRegisterServer } from "../jdom/servers/registerserver" 3 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 4 | 5 | export class TrafficLightServer extends JDServiceServer { 6 | readonly red: JDRegisterServer<[boolean]> 7 | readonly yellow: JDRegisterServer<[boolean]> 8 | readonly green: JDRegisterServer<[boolean]> 9 | 10 | constructor(options?: JDServerOptions) { 11 | super(SRV_TRAFFIC_LIGHT, options) 12 | 13 | this.red = this.addRegister(TrafficLightReg.Red, [false]) 14 | this.yellow = this.addRegister(TrafficLightReg.Yellow, [false]) 15 | this.green = this.addRegister(TrafficLightReg.Green, [false]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/servers/verifiedtelemetryserver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CHANGE, 3 | SRV_VERIFIED_TELEMETRY, 4 | VerifiedTelemetryCmd, 5 | VerifiedTelemetryEvent, 6 | VerifiedTelemetryFingerprintType, 7 | VerifiedTelemetryReg, 8 | VerifiedTelemetryStatus, 9 | } from "../jdom/constants" 10 | import { JDRegisterServer } from "../jdom/servers/registerserver" 11 | import { JDServiceServer, JDServerOptions } from "../jdom/servers/serviceserver" 12 | 13 | export class VerifiedTelemetryServer extends JDServiceServer { 14 | readonly telemetryStatus: JDRegisterServer<[VerifiedTelemetryStatus]> 15 | readonly telemetryStatusInterval: JDRegisterServer<[number]> 16 | readonly fingerprintType: JDRegisterServer< 17 | [VerifiedTelemetryFingerprintType] 18 | > 19 | readonly fingerprintTemplate: JDRegisterServer<[number, Uint8Array]> 20 | 21 | constructor( 22 | options?: { 23 | fingerprintType?: VerifiedTelemetryFingerprintType 24 | telemetryStatusInterval?: number 25 | } & JDServerOptions, 26 | ) { 27 | super(SRV_VERIFIED_TELEMETRY, options) 28 | 29 | const { 30 | fingerprintType = VerifiedTelemetryFingerprintType.FallCurve, 31 | telemetryStatusInterval = 5000, 32 | } = options || {} 33 | 34 | this.telemetryStatus = this.addRegister( 35 | VerifiedTelemetryReg.TelemetryStatus, 36 | [VerifiedTelemetryStatus.Working], 37 | ) 38 | this.telemetryStatusInterval = this.addRegister( 39 | VerifiedTelemetryReg.TelemetryStatusInterval, 40 | [telemetryStatusInterval], 41 | ) 42 | this.fingerprintType = this.addRegister< 43 | [VerifiedTelemetryFingerprintType] 44 | >(VerifiedTelemetryReg.FingerprintType, [fingerprintType]) 45 | 46 | this.fingerprintTemplate = this.addRegister( 47 | VerifiedTelemetryReg.FingerprintTemplate, 48 | [50, new Uint8Array(0)], 49 | ) 50 | this.addCommand( 51 | VerifiedTelemetryCmd.ResetFingerprintTemplate, 52 | this.handleResetTelemetryTemplate.bind(this), 53 | ) 54 | this.addCommand( 55 | VerifiedTelemetryCmd.RetrainFingerprintTemplate, 56 | this.handleRetrainTelemetryTemplate.bind(this), 57 | ) 58 | 59 | // events 60 | this.telemetryStatus.on(CHANGE, () => 61 | this.sendEvent( 62 | VerifiedTelemetryEvent.TelemetryStatusChange, 63 | this.telemetryStatus.data, 64 | ), 65 | ) 66 | this.fingerprintTemplate.on(CHANGE, () => 67 | this.sendEvent(VerifiedTelemetryEvent.FingerprintTemplateChange), 68 | ) 69 | } 70 | 71 | private handleResetTelemetryTemplate() { 72 | this.fingerprintTemplate.setValues([50, new Uint8Array(0)]) 73 | } 74 | 75 | private handleRetrainTelemetryTemplate() { 76 | this.fingerprintTemplate.setValues([50, new Uint8Array(0)]) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/servers/vibrationmotorserver.ts: -------------------------------------------------------------------------------- 1 | import { JDRegisterServer } from "../jacdac" 2 | import { 3 | CHANGE, 4 | REFRESH, 5 | SRV_VIBRATION_MOTOR, 6 | VibrationMotorCmd, 7 | VibrationMotorReg, 8 | } from "../jdom/constants" 9 | import { Packet } from "../jdom/packet" 10 | import { JDServerOptions, JDServiceServer } from "../jdom/servers/serviceserver" 11 | 12 | export class VibrationMotorServer extends JDServiceServer { 13 | static VIBRATE_PATTERN = "vibratePattern" 14 | 15 | private _animation: { 16 | start: number 17 | pattern: [number, number][] 18 | } 19 | private _animationStep = -1 20 | readonly maxVibrations: JDRegisterServer<[number]> 21 | 22 | constructor(options?: { maxVibrations?: number } & JDServerOptions) { 23 | super(SRV_VIBRATION_MOTOR, options) 24 | const { maxVibrations = 10 } = options || {} 25 | 26 | this.maxVibrations = this.addRegister<[number]>( 27 | VibrationMotorReg.MaxVibrations, 28 | [maxVibrations], 29 | ) 30 | this.addCommand( 31 | VibrationMotorCmd.Vibrate, 32 | this.handleVibrate.bind(this), 33 | ) 34 | this.on(REFRESH, this.handleRefresh.bind(this)) 35 | } 36 | 37 | private handleRefresh() { 38 | if (!this._animation) return // nothing to do 39 | 40 | const { start, pattern } = this._animation 41 | const now = this.device.bus.timestamp 42 | const elapsed = now - start 43 | let t = 0 44 | for (let i = 0; i < pattern.length; ++i) { 45 | const [duration, speed] = pattern[i] 46 | const dt = duration << 3 47 | t += dt 48 | if (t - dt <= elapsed && t > elapsed) { 49 | // we're playing this note 50 | if (this._animationStep !== i) { 51 | this._animationStep = i 52 | this.emit(VibrationMotorServer.VIBRATE_PATTERN, { 53 | duration, 54 | speed, 55 | }) 56 | } 57 | break 58 | } 59 | } 60 | if (elapsed > t) { 61 | // animation finished 62 | this._animation = undefined 63 | this._animationStep = -1 64 | this.emit(VibrationMotorServer.VIBRATE_PATTERN, { 65 | duration: 0, 66 | speed: 0, 67 | }) 68 | this.emit(CHANGE) 69 | } 70 | } 71 | 72 | private handleVibrate(pkt: Packet) { 73 | const [pattern] = pkt.jdunpack<[[number, number][]]>("r: u8 u0.8") 74 | this._animation = { 75 | start: this.device.bus.timestamp, 76 | pattern, 77 | } 78 | this._animationStep = -1 79 | if (pattern.length) { 80 | const [duration, speed] = pattern[0] 81 | this._animationStep = 0 82 | this.emit(VibrationMotorServer.VIBRATE_PATTERN, { 83 | duration, 84 | speed, 85 | }) 86 | } 87 | this.emit(CHANGE) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/testdom/spec.ts: -------------------------------------------------------------------------------- 1 | export enum TestState { 2 | Pass, 3 | Indeterminate, 4 | Running, 5 | Fail, 6 | } 7 | 8 | export enum TestUploadState { 9 | Local, 10 | Uploading, 11 | Uploaded, 12 | UploadError, 13 | } 14 | 15 | export interface TestResult { 16 | state: TestState 17 | output?: string 18 | } 19 | 20 | export interface PanelTestSpec { 21 | id?: string 22 | /** 23 | * Tests should be fast and automated 24 | */ 25 | factory?: boolean 26 | devices: DeviceTestSpec[] 27 | oracles?: OracleTestSpec[] 28 | } 29 | 30 | export interface OracleTestSpec { 31 | serviceClass: number 32 | deviceId: string 33 | serviceIndex?: number 34 | tolerance?: number 35 | } 36 | 37 | export interface DeviceTestSpec { 38 | productIdentifier?: number 39 | count: number 40 | firmwareVersion?: string 41 | services: ServiceTestSpec[] 42 | factory?: boolean 43 | } 44 | 45 | export interface ServiceTestSpec { 46 | name?: string 47 | serviceClass: number 48 | count?: number 49 | rules?: ServiceTestRule[] 50 | disableBuiltinRules?: boolean 51 | } 52 | 53 | export interface ManualSteps { 54 | prepare?: string 55 | validate?: string 56 | } 57 | 58 | export interface ServiceTestRule { 59 | type: 60 | | "reading" 61 | | "intensity" 62 | | "value" 63 | | "oracleReading" 64 | | "event" 65 | | "setIntensityAndValue" 66 | name?: string 67 | manualSteps?: ManualSteps 68 | factory?: boolean 69 | } 70 | export interface ReadingTestRule extends ServiceTestRule { 71 | type: "reading" | "intensity" | "value" 72 | value: number 73 | tolerance?: number 74 | samples?: number 75 | op?: ">" | "<" | "==" 76 | } 77 | export interface SetIntensityAndValueTestRule extends ServiceTestRule { 78 | type: "setIntensityAndValue" 79 | steps: { 80 | duration: number 81 | intensity?: number 82 | value?: number 83 | }[] 84 | } 85 | export interface OracleReadingTestRule extends ServiceTestRule { 86 | type: "oracleReading" 87 | oracle: OracleTestSpec 88 | tolerance?: number 89 | } 90 | export interface EventTestRule extends ServiceTestRule { 91 | type: "event" 92 | eventName: string 93 | } 94 | -------------------------------------------------------------------------------- /src/tstester/button.spec.ts: -------------------------------------------------------------------------------- 1 | import { ButtonEvent, ButtonReg } from "../jdom/constants" 2 | import { TestDriver } from "./base" 3 | import { ServiceTester } from "./servicewrapper" 4 | 5 | export class ButtonTestRoutine { 6 | constructor( 7 | readonly service: ServiceTester, 8 | readonly driver: TestDriver, 9 | ) {} 10 | 11 | public async testClick() { 12 | // Avoid over-use of "this" everywhere 13 | const service = this.service 14 | const register = this.service.register(ButtonReg.Pressure) 15 | 16 | this.driver.log("wait for down - press button") 17 | 18 | await this.driver.waitFor( 19 | [ 20 | service.onEvent(ButtonEvent.Down).hold(), 21 | register 22 | .onValue([0.5, 1], { 23 | precondition: [0, 0.5], 24 | }) 25 | .hold(), 26 | ], 27 | { synchronization: 50 }, 28 | ) 29 | this.driver.log("saw down") 30 | 31 | await this.driver.waitFor( 32 | [ 33 | service.nextEvent(ButtonEvent.Up).hold(), 34 | register 35 | .onValue([0, 0.5], { 36 | precondition: [0.5, 1], 37 | }) 38 | .hold(), 39 | ], 40 | { within: 500, synchronization: 50 }, 41 | ) 42 | this.driver.log("saw up") 43 | } 44 | 45 | public async testHold() { 46 | // Avoid over-use of "this" everywhere 47 | const service = this.service 48 | const register = this.service.register(ButtonReg.Pressure) 49 | 50 | this.driver.log("wait for down - press and hold button") 51 | 52 | await this.driver.waitFor( 53 | [ 54 | service.onEvent(ButtonEvent.Down).hold(), 55 | register 56 | .onValue([0.5, 1], { 57 | precondition: [0, 0.5], 58 | }) 59 | .hold(), 60 | ], 61 | { synchronization: 50 }, 62 | ) 63 | 64 | this.driver.log("saw down, continue holding") 65 | await this.driver.waitFor( 66 | [ 67 | service.nextEvent(ButtonEvent.Hold).hold(), 68 | register.hold([0.5, 1.0]), 69 | ], 70 | { after: 500, tolerance: 100 }, 71 | ) 72 | 73 | this.driver.log("saw hold (1), continue holding") 74 | await this.driver.waitFor( 75 | [ 76 | service.nextEvent(ButtonEvent.Hold).hold(), 77 | register.hold([0.5, 1.0]), 78 | ], 79 | { after: 500, tolerance: 100 }, 80 | ) 81 | 82 | this.driver.log("saw hold (2), continue holding") 83 | await this.driver.waitFor( 84 | [ 85 | service.nextEvent(ButtonEvent.Hold).hold(), 86 | register.hold([0.5, 1.0]), 87 | ], 88 | { after: 500, tolerance: 100 }, 89 | ) 90 | 91 | this.driver.log("done, release") 92 | await this.driver.waitFor( 93 | [ 94 | service.onEvent(ButtonEvent.Up).hold(), // ignore any continued hold events 95 | register.onValue([0, 0.5]).hold(), 96 | ], 97 | { synchronization: 50 }, 98 | ) 99 | 100 | this.driver.log("saw up") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/tstester/naming.ts: -------------------------------------------------------------------------------- 1 | import { JDDevice, JDRegister, JDService } from "../jdom/jacdac-jdom" 2 | 3 | // Naming helpers 4 | // This breaks the circular dependencies between eg the ServiceTester and RegisterTester, where 5 | // ServiceTester depends on RegisterTester to instantiate it 6 | // but RegisterTester depends on ServiceTester for its name prefix 7 | export class TestingNamer { 8 | public static nameOfRegister(register: JDRegister) { 9 | return `${TestingNamer.nameOfService(register.service)}.${ 10 | register.name 11 | }` 12 | } 13 | 14 | public static nameOfService(service: JDService) { 15 | return `${TestingNamer.nameOfDevice(service.device)}.${ 16 | service.specification.name 17 | }` 18 | } 19 | 20 | public static nameOfDevice(device: JDDevice) { 21 | return device.shortId 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/tstester/potentiometer.spec.ts: -------------------------------------------------------------------------------- 1 | import { PotentiometerReg } from "../jdom/constants" 2 | import { TestDriver } from "./base" 3 | import { ServiceTester } from "./servicewrapper" 4 | 5 | export class PotentiometerTestRoutine { 6 | constructor( 7 | readonly service: ServiceTester, 8 | readonly driver: TestDriver, 9 | ) {} 10 | 11 | public async testMin() { 12 | const register = this.service.register(PotentiometerReg.Position) 13 | 14 | this.driver.log("wait for min - move slider to min") 15 | 16 | // TODO hold-for 17 | await this.driver.waitFor(register.onValue([0, 0.1])) 18 | this.driver.log("saw min") 19 | } 20 | 21 | public async testMax() { 22 | const register = this.service.register(PotentiometerReg.Position) 23 | 24 | this.driver.log("wait for max - move slider to max") 25 | 26 | // TODO hold-for 27 | await this.driver.waitFor(register.onValue([0.9, 1])) 28 | this.driver.log("saw max") 29 | } 30 | 31 | public async testSlideUp() { 32 | // Avoid over-use of "this" everywhere 33 | const register = this.service.register(PotentiometerReg.Position) 34 | 35 | this.driver.log("wait for slide up: slide up over ~2 seconds") 36 | 37 | // First one isn't time bounded to give the user time to start the rest 38 | await this.driver.waitFor( 39 | register.onValue([0.1, 0.2], { 40 | precondition: [0, 0.1], 41 | }), 42 | ) 43 | this.driver.log(`saw approx 1 / 10`) 44 | 45 | for (let i = 2; i < 10; i++) { 46 | await this.driver.waitFor( 47 | register.onValue([i / 10.0, (i + 1) / 10.0], { 48 | precondition: [(i - 1) / 10.0, i / 1.0], 49 | }), 50 | { after: 200, tolerance: 200 }, 51 | ) 52 | this.driver.log(`saw approx ${i + 1} / 10`) 53 | } 54 | 55 | this.driver.log(`done!`) 56 | } 57 | 58 | public async testSlideDown() { 59 | // Avoid over-use of "this" everywhere 60 | const register = this.service.register(PotentiometerReg.Position) 61 | 62 | this.driver.log("wait for slide down: slide down over ~2 seconds") 63 | 64 | // First one isn't time bounded to give the user time to start the rest 65 | await this.driver.waitFor( 66 | register.onValue([0.8, 0.9], { 67 | precondition: [0.9, 1.0], 68 | }), 69 | ) 70 | this.driver.log(`saw approx 9 / 10`) 71 | 72 | for (let i = 7; i >= 0; i--) { 73 | await this.driver.waitFor( 74 | register.onValue([i / 10.0, (i + 1) / 10.0], { 75 | precondition: [(i + 1) / 10.0, (i + 2) / 10.0], 76 | }), 77 | { after: 200, tolerance: 200 }, 78 | ) 79 | this.driver.log(`saw approx ${i + 1} / 10`) 80 | } 81 | 82 | this.driver.log(`done!`) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/tstester/testwrappers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DeviceFilter, 3 | DEVICE_ANNOUNCE, 4 | JDBus, 5 | JDDevice, 6 | ServiceFilter, 7 | } from "../jdom/jacdac-jdom" 8 | import { TestingNamer } from "./naming" 9 | import { ServiceTester } from "./servicewrapper" 10 | 11 | export class BusTester { 12 | constructor(readonly bus: JDBus) {} 13 | 14 | public devices(options?: DeviceFilter) { 15 | return this.bus.devices(options).map(device => new DeviceTester(device)) 16 | } 17 | 18 | public async nextConnected(): Promise { 19 | const promise = new Promise(resolve => { 20 | this.bus.once(DEVICE_ANNOUNCE, (device: JDDevice) => { 21 | resolve(new DeviceTester(device)) 22 | }) 23 | }) 24 | 25 | return promise 26 | } 27 | } 28 | 29 | export class DeviceTester { 30 | constructor(readonly device: JDDevice) {} 31 | 32 | public get name() { 33 | return TestingNamer.nameOfDevice(this.device) 34 | } 35 | 36 | public services(options?: ServiceFilter) { 37 | return this.device 38 | .services(options) 39 | .map(service => new ServiceTester(service)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/worker/jacdac-worker.ts: -------------------------------------------------------------------------------- 1 | import { errorCode } from "../jdom/error" 2 | import { TransportProxy } from "./transportproxy" 3 | import { USBTransportProxy } from "./usbtransportproxy" 4 | 5 | const { debug } = console 6 | 7 | debug(`jdsw: starting...`) 8 | let proxy: TransportProxy 9 | 10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 11 | function handleError(resp: any, e: Error) { 12 | self.postMessage({ 13 | ...resp, 14 | error: { 15 | message: e.message, 16 | stack: e.stack, 17 | name: e.name, 18 | jacdacName: errorCode(e), 19 | }, 20 | }) 21 | } 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | async function handleCommand(resp: any, handler: () => Promise) { 25 | try { 26 | await handler() 27 | self.postMessage(resp) 28 | } catch (e) { 29 | handleError(resp, e) 30 | } 31 | } 32 | 33 | async function handleMessage(event: MessageEvent) { 34 | const { data } = event 35 | const { jacdac, type, payload } = data 36 | if (!jacdac) return // someone else's message 37 | switch (type) { 38 | case "connect": { 39 | if (proxy) await proxy.disconnect() 40 | const { deviceId } = data 41 | //debug(`jdsw: connecting`) 42 | proxy = new USBTransportProxy() 43 | await handleCommand(data, () => proxy.connect(deviceId)) 44 | break 45 | } 46 | case "packet": 47 | //info(`jdsw: send`) 48 | proxy?.send(payload).then( 49 | () => {}, 50 | e => handleError(payload, e), 51 | ) 52 | // don't wait or acknowledge 53 | break 54 | case "disconnect": 55 | if (proxy) { 56 | //debug(`jdsw: disconnecting`) 57 | await handleCommand(data, () => proxy?.disconnect()) 58 | } 59 | break 60 | } 61 | } 62 | 63 | self.addEventListener("message", handleMessage) 64 | 65 | debug(`jdsw: ready...`) 66 | -------------------------------------------------------------------------------- /src/worker/transportproxy.ts: -------------------------------------------------------------------------------- 1 | export interface TransportProxy { 2 | connect(deviceId?: string): Promise 3 | send(payload: Uint8Array): Promise 4 | disconnect(): Promise 5 | } 6 | -------------------------------------------------------------------------------- /src/worker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": [ 5 | "webworker", 6 | "es2020", 7 | "scripthost" 8 | ], 9 | "declarationDir": "../../dist/types", 10 | "declaration": true, 11 | "emitDeclarationOnly": true, 12 | "types": [ 13 | "@types/w3c-web-usb" 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /src/worker/usbtransportproxy.ts: -------------------------------------------------------------------------------- 1 | import { errorCode } from "../jdom/error" 2 | import { Flags } from "../jdom/flags" 3 | import { Proto } from "../jdom/transport/proto" 4 | import { USBIO } from "../jdom/transport/usbio" 5 | import { TransportProxy } from "./transportproxy" 6 | 7 | const { debug } = console 8 | 9 | export class USBTransportProxy implements TransportProxy { 10 | private hf2: Proto 11 | constructor() {} 12 | async connect(deviceId: string) { 13 | if (Flags.diagnostics) debug(`jdsw: connect`, { deviceId }) 14 | if (this.hf2) { 15 | if (Flags.diagnostics) debug(`jdsw: cleanup hf2`) 16 | await this.hf2.disconnectAsync() 17 | this.hf2 = undefined 18 | } 19 | const io = new USBIO({ 20 | getDevices: () => navigator.usb.getDevices(), 21 | }) 22 | io.onError = e => { 23 | debug(`jdsw: error`, e) 24 | postMessage({ 25 | jacdac: true, 26 | type: "error", 27 | error: { 28 | message: e.message, 29 | stack: e.stack, 30 | name: e.name, 31 | jacdacName: errorCode(e), 32 | }, 33 | }) 34 | } 35 | const onJDMessage = (buf: Uint8Array) => { 36 | self.postMessage({ 37 | jacdac: true, 38 | type: "frame", 39 | payload: buf, 40 | }) 41 | } 42 | this.hf2 = await io.connectAsync(true, deviceId) 43 | this.hf2.onJDMessage(onJDMessage) 44 | } 45 | async send(payload: Uint8Array) { 46 | await this.hf2?.sendJDMessageAsync(payload) 47 | } 48 | async disconnect() { 49 | if (Flags.diagnostics) debug(`jdsw: disconnect`) 50 | const h = this.hf2 51 | this.hf2 = undefined 52 | if (h) await h.disconnectAsync() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/worker/webusb.d.ts: -------------------------------------------------------------------------------- 1 | interface WorkerNavigator { 2 | readonly usb: USB 3 | } 4 | -------------------------------------------------------------------------------- /src/worker/workerloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * WebPack 5.0+ compatible loader. 3 | * @returns 4 | */ 5 | export default function createJacdacWorker() { 6 | return ( 7 | typeof Window !== "undefined" && 8 | new Worker(new URL("../dist/jacdac-worker.js", import.meta.url)) 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /tests/jdom/bus.spec.ts: -------------------------------------------------------------------------------- 1 | import { suite, test, afterEach } from "mocha" 2 | import { JDBus } from "../../src/jdom/bus" 3 | import { SELF_ANNOUNCE } from "../../src/jdom/constants" 4 | import { mkBus } from "../testutils" 5 | 6 | suite("bus", () => { 7 | let bus: JDBus 8 | afterEach(() => bus?.stop()) 9 | 10 | test("self announce", function (done) { 11 | bus = mkBus() 12 | bus.on(SELF_ANNOUNCE, () => { 13 | done() 14 | bus.stop() 15 | }) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /tests/jdom/command.spec.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from "mocha" 2 | import { ProtoTestCmd, SRV_PROTO_TEST } from "../../src/jdom/constants" 3 | import { serviceSpecificationFromClassIdentifier } from "../../src/jdom/spec" 4 | import { packArguments } from "../../src/jdom/command" 5 | import { loadSpecifications } from "../testutils" 6 | import { PackedValues } from "../../src/jdom/pack" 7 | 8 | loadSpecifications() 9 | 10 | suite("packArguments", () => { 11 | function testOne(cmdid: number, args: PackedValues) { 12 | const service = serviceSpecificationFromClassIdentifier(SRV_PROTO_TEST) 13 | const cmd = service.packets.find( 14 | pkt => pkt.kind === "command" && pkt.identifier === cmdid, 15 | ) 16 | const pkt = packArguments(cmd, args) 17 | console.log({ args, pkt }) 18 | } 19 | test("cbool", () => testOne(ProtoTestCmd.CBool, [true])) 20 | test("c32", () => testOne(ProtoTestCmd.CU32, [42])) 21 | test("cString", () => testOne(ProtoTestCmd.CString, ["hi"])) 22 | test("cI8U8U16I32", () => testOne(ProtoTestCmd.CI8U8U16I32, [-1, 2, 3, 4])) 23 | test("cU8String", () => testOne(ProtoTestCmd.CU8String, [42, "hi"])) 24 | }) 25 | -------------------------------------------------------------------------------- /tests/jdom/scheduler.spec.ts: -------------------------------------------------------------------------------- 1 | import { suite, test } from "mocha" 2 | import { assert } from "../../src/jdom/utils" 3 | import { FastForwardScheduler } from "./scheduler" 4 | import { makeTest } from "./fastforwardtester" 5 | 6 | suite("fast forward scheduler", () => { 7 | test( 8 | "fires setTimeout", 9 | makeTest(async tester => { 10 | let done = false 11 | tester.bus.scheduler.setTimeout(() => { 12 | done = true 13 | }, 500) 14 | await tester.waitForDelay(501) 15 | assert(done) 16 | }), 17 | ) 18 | 19 | test( 20 | "fires setInterval, repeatedly", 21 | makeTest(async tester => { 22 | let count = 0 23 | tester.bus.scheduler.setInterval(() => { 24 | count += 1 25 | }, 100) 26 | await tester.waitForDelay(501) 27 | assert(count == 5) 28 | }), 29 | ) 30 | 31 | test( 32 | "clear setTimeout", 33 | makeTest(async tester => { 34 | let called = false 35 | const handler = tester.bus.scheduler.setTimeout(() => { 36 | called = true 37 | }, 500) 38 | await tester.waitForDelay(400) 39 | tester.bus.scheduler.clearTimeout(handler) 40 | await tester.waitForDelay(101) 41 | assert(!called) 42 | }), 43 | ) 44 | 45 | test( 46 | "clear setInterval", 47 | makeTest(async tester => { 48 | assert(tester.bus.scheduler instanceof FastForwardScheduler) 49 | 50 | let count = 0 51 | const handler = tester.bus.scheduler.setInterval(() => { 52 | count += 1 53 | }, 100) 54 | await tester.waitForDelay(301) 55 | tester.bus.scheduler.clearInterval(handler) 56 | assert(count == 3) 57 | }), 58 | ) 59 | 60 | test( 61 | "fires concurrent setTimeout, with waitForDelay", 62 | makeTest(async tester => { 63 | let finished1: number 64 | let finished2: number 65 | tester.bus.scheduler.setTimeout(() => { 66 | finished1 = tester.bus.timestamp 67 | }, 400) 68 | tester.bus.scheduler.setTimeout(() => { 69 | finished2 = tester.bus.timestamp 70 | }, 500) 71 | await tester.waitForDelay(501) 72 | assert(finished1 == 400) 73 | assert(finished2 == 500) 74 | }), 75 | ) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/jdom/servercsvsource.ts: -------------------------------------------------------------------------------- 1 | import { REFRESH } from "../../src/jdom/constants" 2 | import { JDRegisterServer } from "../../src/jdom/servers/registerserver" 3 | import { JDServiceServer } from "../../src/jdom/servers/serviceserver" 4 | import { assert } from "../../src/jdom/utils" 5 | 6 | /** 7 | * Streams data from a "CSV" into a server, with a user-defined map from column name to register. 8 | * All registers must be on the same server. 9 | * 10 | * "CSV" is formatted as (for example): 11 | * [ 12 | * {time: 0.6, "BP95.position": 0.5}, 13 | * {time: 0.8, "BP95.position": 1.0}, 14 | * {time: 1.0, "BP95.position": 0.8}, 15 | * {time: 1.2, "BP95.position": 0.6}, 16 | * ... 17 | * ] 18 | * This is consistent with Papa Parse (used by csv.proxy.ts in jacdac-docs, see 19 | * https://www.papaparse.com/demo for an interactive demo) on a Jacdac recording CSV. 20 | * Columns without a mapped register are ignored. 21 | * 22 | * Time is specified in seconds, and there may be multiple data columns. Null cells are ignored. 23 | * 24 | * Note that this only writes to registers, and relies on other code (eg, ButtonServer's refresh) 25 | * to generate derived events where applicable. 26 | */ 27 | export class ServerCsvSource { 28 | protected nextDataIndex = 0 // index (in this.data) of next value 29 | protected server: JDServiceServer 30 | 31 | constructor( 32 | protected readonly registerMap: Record< 33 | string, 34 | JDRegisterServer<[number]> 35 | >, 36 | protected readonly data: Record[], 37 | ) { 38 | const servers = Object.entries(registerMap).map( 39 | ([colName, register]) => register.service, 40 | ) 41 | this.server = servers[0] 42 | assert( 43 | servers.every(serverElt => serverElt == this.server), 44 | "all registers must be on same server", 45 | ) 46 | 47 | // TODO timings are only approximate, perhaps this should use bus.scheduler.setTimeout 48 | // instead, but that needs a bus handle and there isn't an event when a device has its 49 | // bus assigned. 50 | this.server.on(REFRESH, this.handleRefresh.bind(this)) 51 | } 52 | 53 | protected handleRefresh() { 54 | const now = this.server.device.bus.timestamp // in ms 55 | while (this.nextDataIndex < this.data.length) { 56 | const thisData = this.data[this.nextDataIndex] 57 | assert("time" in thisData, "time field missing") 58 | const time = thisData.time as number // in s 59 | assert(typeof time == "number", "time field not a number") 60 | if (time * 1000 > now) { 61 | // s to ms conversion 62 | break // still in the future, handle later 63 | } 64 | 65 | Object.entries(thisData).forEach(([key, value]) => { 66 | if (value !== null && key in this.registerMap) { 67 | const register = this.registerMap[key] 68 | register.setValues([value]) 69 | } // drop unused columns 70 | }) 71 | 72 | this.nextDataIndex++ 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/p5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sketch 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/p5/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["*.js", "libraries/*.js"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/p5/sketch.js: -------------------------------------------------------------------------------- 1 | let counter = 0 2 | function setup() { 3 | createCanvas(400, 400) 4 | jacdac.debug() 5 | textAlign(CENTER, CENTER) 6 | textSize(64) 7 | 8 | // use jacdac.events to register events 9 | jacdac.events.button.down(() => counter++) 10 | } 11 | 12 | function draw() { 13 | // grabs an array with all potentiometer readings (number[]) connected to jacdac 14 | const { potentiometer } = jacdac.sensors 15 | // destructure readings into r,g,b variables 16 | // if sensors are missing, default to 0 17 | const [r = 0, g = 0, b = 0] = potentiometer 18 | // rescale 0..1 to 0..255 to repaint background 19 | background(r * 255, g * 255, b * 255) 20 | 21 | // move points for an accelerometer 22 | const { accelerometer } = jacdac.sensors 23 | // the acceleration is stored as a ``{ x: .., y: ..., z: ... }`` object 24 | for (const acceleration of accelerometer) { 25 | const { x: ax = 0, y: ay = 0, z: az = 0 } = acceleration 26 | 27 | // map g (gravities) to 100..300 on canvas 28 | const x = map(ax, -1, 1, 100, 300) 29 | const y = map(ay, -1, 1, 100, 300) 30 | const d = map(az, -1, 1, 5, 50) 31 | stroke("white") 32 | circle(x, y, d) 33 | } 34 | 35 | // show button counter 36 | fill("white") 37 | text(counter, 200, 200) 38 | } 39 | -------------------------------------------------------------------------------- /tests/p5/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | canvas { 7 | display: block; 8 | } 9 | -------------------------------------------------------------------------------- /tests/testutils.ts: -------------------------------------------------------------------------------- 1 | import { loadServiceSpecifications } from "../src/jdom/spec" 2 | import { readFileSync } from "fs" 3 | import { JDBus } from "../src/jdom/bus" 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 6 | let specs: any 7 | export function loadSpecifications() { 8 | if (!specs) { 9 | specs = JSON.parse( 10 | readFileSync("../jacdac-spec/dist/services.json", { 11 | encoding: "utf-8", 12 | }) 13 | ) 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | loadServiceSpecifications(specs as any) 16 | } 17 | } 18 | 19 | export function mkBus() { 20 | loadSpecifications() 21 | return new JDBus([], { client: false }) 22 | } 23 | 24 | loadSpecifications() 25 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } -------------------------------------------------------------------------------- /tools/prepare.js: -------------------------------------------------------------------------------- 1 | const replace = require("replace-in-file") 2 | const pkg = require("../package.json") 3 | const version = pkg.version 4 | console.log(`patching version: ${version}`) 5 | 6 | // patch with cdn files 7 | async function patchDocs() { 8 | const fn = `jacdac.js` 9 | await patch({ 10 | files: "./*.html", 11 | from: `/dist/${fn}`, 12 | to: `https://unpkg.com/jacdac-ts@${version}/dist/${fn}`, 13 | }) 14 | } 15 | 16 | async function patch(options) { 17 | try { 18 | const results = await replace(options) 19 | console.log("Replacement results:", results) 20 | } catch (error) { 21 | console.error("Error occurred:", error) 22 | } 23 | } 24 | 25 | patchDocs() 26 | -------------------------------------------------------------------------------- /tools/testtest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 25 | 26 | 27 | 28 |

Jacdac/Testing

29 |
30 | 31 | 32 |
33 | 34 |
35 | 36 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es2020", 5 | "module": "es2020", 6 | "lib": [ 7 | "es2015", 8 | "es2016", 9 | "es2017", 10 | "dom" 11 | ], 12 | "strict": false, 13 | "strictNullChecks": false, 14 | "sourceMap": true, 15 | "declaration": true, 16 | "declarationMap": true, 17 | "allowSyntheticDefaultImports": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true, 20 | "preserveConstEnums": true, 21 | "noImplicitThis": true, 22 | "noImplicitAny": true, 23 | "declarationDir": "dist/types", 24 | "resolveJsonModule": true, 25 | "emitDeclarationOnly": true, 26 | "outDir": "dist/lib", 27 | "pretty": true, 28 | "newLine": "LF", 29 | "types": [ 30 | "w3c-web-serial", 31 | "w3c-web-usb", 32 | "web-bluetooth", 33 | "node" 34 | ] 35 | }, 36 | "include": [ 37 | "src", 38 | "jacdac-spec/dist/specconstants.ts" 39 | ], 40 | "exclude": [ 41 | "src/worker/*" 42 | ] 43 | } -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src/jacdac.ts"], 3 | "out": "docs", 4 | "exclude": [ 5 | "src/worker/*.ts", 6 | "src/jdom/*constants.ts", 7 | "src/jdom/transport/*.ts", 8 | "src/jdom/pretty.ts", 9 | "src/jdom/utils.ts", 10 | "src/jdom/buffer.ts", 11 | "src/jdom/makecode.ts", 12 | "src/azure-iot/*", 13 | "jacdac-spec/dist/*.ts" 14 | ], 15 | "excludePrivate": true, 16 | "excludeInternal": true, 17 | "excludeExternals": true, 18 | "name": "Jacdac TypeScript", 19 | "categoryOrder": ["JDOM", "Clients", "Servers", "Trace", "*"], 20 | "readme": "./DOCS.md", 21 | "includeVersion": true, 22 | "categorizeByGroup": false 23 | } 24 | --------------------------------------------------------------------------------