├── .eslintrc.cjs ├── .github └── workflows │ ├── deploy.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── devPackageJson.cjs ├── global.d.ts ├── package.json ├── public └── images │ └── icon.png ├── rename-to-esm.mjs ├── src ├── __tests__ │ └── procedure │ │ └── utils │ │ └── board.test.ts ├── declarative │ ├── components │ │ ├── App.tsx │ │ ├── AppContext.ts │ │ ├── Box.tsx │ │ ├── ErrorOverview.tsx │ │ ├── Static.tsx │ │ ├── Text.tsx │ │ ├── analog │ │ │ ├── AnalogRotation.tsx │ │ │ ├── AnalogSoilMoisture.tsx │ │ │ ├── AnalogSteam.tsx │ │ │ ├── AnalogTemperature.tsx │ │ │ ├── AnalogVibration.tsx │ │ │ ├── LinearTemperature.tsx │ │ │ ├── Photocell.tsx │ │ │ └── WaterLevelSensor.tsx │ │ ├── input │ │ │ ├── Button.tsx │ │ │ ├── Collision.tsx │ │ │ ├── DigitalTiltSensor.tsx │ │ │ ├── HallEffectSensor.tsx │ │ │ ├── Input.tsx │ │ │ ├── PIRMotionSensor.tsx │ │ │ └── PhotoInterrupter.tsx │ │ ├── output │ │ │ ├── Buzzer.ts │ │ │ ├── Led.ts │ │ │ └── Output.ts │ │ ├── pwm │ │ │ └── VibrationMotorModule.ts │ │ └── servo │ │ │ └── Servo.tsx │ ├── examples │ │ ├── VibrationMotorModule.tsx │ │ ├── AnalogRotation.tsx │ │ ├── AnalogSoilMoisture.tsx │ │ ├── AnalogSteam.tsx │ │ ├── AnalogTemperature.tsx │ │ ├── AnalogVibrationSensor.tsx │ │ ├── App.tsx │ │ ├── CollisionSensor.tsx │ │ ├── DigiTaltiltSensor.tsx │ │ ├── HallEffectSensor.tsx │ │ ├── Inputs.tsx │ │ ├── Led.tsx │ │ ├── LinearTemperature.tsx │ │ ├── PIRMotionSensor.tsx │ │ ├── PhotoInterrupter.tsx │ │ ├── Photocell.tsx │ │ ├── Servo.tsx │ │ └── WaterLevelSensor.tsx │ ├── rendere │ │ ├── dom.ts │ │ ├── edison.tsx │ │ ├── global.d.ts │ │ ├── instances.ts │ │ ├── output.ts │ │ ├── reconciler.ts │ │ ├── render-node-to-output.ts │ │ ├── render.ts │ │ └── renderer.ts │ └── utils │ │ └── Board.tsx ├── index.ts └── procedure │ ├── analog │ ├── analogPort.ts │ └── uniqueDevice │ │ ├── analog.ts │ │ └── pressureSensor.ts │ ├── complex │ └── ultrasonicSensor.ts │ ├── helper │ ├── Analog │ │ ├── bufferAnalog.ts │ │ └── setPinAnalog.ts │ ├── Input │ │ ├── setInputState.ts │ │ └── setPinInput.ts │ ├── Output │ │ ├── setAnalogOutput.ts │ │ ├── setOutputState.ts │ │ └── setPinOutput.ts │ ├── PWM │ │ └── setPwmState.ts │ ├── Servo │ │ ├── setPinToServo.ts │ │ └── setServoAngle.ts │ └── Utils │ │ └── bufferWrite.ts │ ├── input │ ├── inputPort.ts │ └── uniqueDevice │ │ └── input.ts │ ├── output │ ├── outputPort.ts │ └── uniqueDevice │ │ ├── led.ts │ │ └── output.ts │ ├── pwm │ ├── pwmPort.ts │ └── uniqueDevice │ │ ├── passiveBuzzer.ts │ │ └── vibrationSensor.ts │ ├── servo │ ├── servoPort.ts │ └── uniqueDevice │ │ ├── rotationServo.ts │ │ └── servo.ts │ ├── types │ ├── Mode.ts │ └── analog │ │ └── analog.ts │ ├── uniqueDevice │ └── prettierChange.ts │ └── utils │ ├── board.ts │ ├── delay.ts │ ├── findArduinoPath.ts │ ├── portClose.ts │ ├── portOpen.ts │ └── setup.ts ├── tsconfig.esm.json ├── tsconfig.json ├── vite-env.d.ts └── vite.config.ts /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:react/recommended', 6 | 'plugin:react-hooks/recommended', 7 | 'plugin:@typescript-eslint/recommended', 8 | 'plugin:@typescript-eslint/eslint-recommended', 9 | 'plugin:react-hooks/recommended', 10 | 'prettier', 11 | 'plugin:prettier/recommended', 12 | ], 13 | plugins: ['@typescript-eslint'], 14 | parser: '@typescript-eslint/parser', 15 | env: { 16 | browser: true, 17 | node: true, 18 | es6: true, 19 | }, 20 | settings: { 21 | react: { 22 | version: 'detect', 23 | }, 24 | }, 25 | parserOptions: { 26 | sourceType: 'module', 27 | }, 28 | rules: { 29 | '@typescript-eslint/consistent-type-imports': 'error', 30 | '@typescript-eslint/explicit-module-boundary-types': 'off', 31 | 'react/jsx-uses-react': 'off', 32 | 'react/react-in-jsx-scope': 'off', 33 | 'no-dupe-args': ['error'], 34 | 'no-dupe-keys': ['error'], 35 | 'no-unreachable': ['error'], 36 | //"brace-style": ["error", "stroustrup"], 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | node-version: lts 17 | - run: npm run build 18 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | jobs: 9 | test: 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: latest 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Run lint 24 | run: npm run lint 25 | 26 | - name: Run tests 27 | run: npm test 28 | 29 | - name: Modify package.json and Reinstall dependencies 30 | if: success() 31 | run: | 32 | rm -rf package-lock.json node_modules 33 | npm install 34 | 35 | - name: Run type-check 36 | run: npm run type-check 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src/*.js 3 | dist 4 | dist/* 5 | package-lock.json 6 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/test/* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "quoteProps": "consistent", 5 | "jsxBracketSameLine": false, 6 | "singleAttributePerLine": true, 7 | "braceStyle": "stroustrup" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "editor.formatOnType": true, 5 | "editor.formatOnPaste": true, 6 | "[typescript]": { 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit" 9 | } 10 | }, 11 | "[typescriptreact]": { 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "explicit" 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to the project 2 | 3 | This project welcomes any change, improvement, or suggestion! 4 | 5 | If you'd like to help its development feel free to open a new issue and raise a pull request. 6 | 7 | ## IMPORTANT 8 | 9 | If you'd like to work on an existing issue, kindly **ask** for it to be assigned to you. 10 | 11 | Do you have any struggles with the issue you are working on? Feel free to **tag [me](https://github.com/AllenShintani)** in it _and/or_ open a draft pull request. Please add the appropriate labels to the issues. It's not necessary if an appropriate label does not exist. 12 | 13 | We will collaborate with users and contributors to develop the necessary features, devices, etc. This OSS will be developed in a scrum style. We appreciate and respect all contributors and users. 14 | 15 | ### How do I make a contribution 16 | 17 | If you've never made an open source contribution before or are curious about how contributions operate in our project? Here's a quick rundown! 18 | 19 | #### Fork this repository 20 | 21 | Fork this repository by clicking on the fork button on the top of [this](https://github.com/AllenShintani/Edison) page. 22 | This will create a copy of this repository in your account `/`. 23 | 24 | #### Clone the repository 25 | 26 | Now clone the forked repository to your machine. Go to your GitHub account, open the forked repository, and copy the link provided under `HTTPS` when you click on the green button labeled `code` on the repository page 27 | 28 | Open a terminal and run the following git command: 29 | 30 | ``` 31 | git clone "url you just copied" 32 | ``` 33 | 34 | where "URL you just copied" (without quotation marks) is the URL to this repository (your fork of this project). 35 | 36 | For example: 37 | 38 | ``` 39 | git clone https://github.com/AllenShintani/Edison.git 40 | ``` 41 | 42 | #### Create a new branch for your changes or fix 43 | 44 | ```sh 45 | $ git checkout -b 46 | ``` 47 | 48 | #### Setup the project in your local by following the steps listed in the [README.md](https://github.com/AllenShintani/Edison/blob/master/README.md) file 49 | 50 | #### Open the project in a code editor and begin working on it 51 | #### Add the contents of the changed files to the "snapshot" git uses to manage the state of the project, also known as the index 52 | 53 | ```sh 54 | $ git add . 55 | ``` 56 | 57 | #### Add a descriptive commit message 58 | 59 | ```sh 60 | $ git commit -m "Insert a short message of the changes made here" 61 | ``` 62 | 63 | #### Push the changes to the remote repository 64 | 65 | ```sh 66 | $ git push -u origin 67 | ``` 68 | 69 | #### Submit a pull request to the upstream repository 70 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 vczb 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Something nice img 4 | 5 |

6 | 7 |

Edison

8 | 9 |

Edison can control microcontroller board with TypeScript or JavaScript!

10 | 11 |
12 | 13 | 14 | GitHub stars 15 | 16 | 17 | [![NPM 18 | version](https://img.shields.io/npm/v/edison.svg?style=flat)](https://www.npmjs.com/package/edison) 19 | [![NPM 20 | downloads](https://img.shields.io/npm/dm/edison.svg?style=flat)](https://www.npmjs.com/package/edison) 21 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/edison-js/Edison/blob/main/LICENSE) 22 | 23 |
24 | 25 | ## Documentation 26 | 27 | Our documentation site is [here](https://edison-js-document.vercel.app/)! 28 | 29 | ## If you have not yet installed the Arduino IDE ? 30 | 31 | please click on [the official site](https://www.arduino.cc/en/software) and install it. 32 | 33 | ## How to use in WSL 34 | 35 | Please read [this article](https://zenn.dev/konjikun/articles/e905f4ce99d3ea). 36 | 37 | ## Installation 38 | 39 | Install Edison your project 40 | 41 | ```console 42 | npm install edison 43 | ``` 44 | 45 | or 46 | 47 | ```console 48 | yarn add edison 49 | ``` 50 | 51 | ## Getting Started 52 | 53 | ```.ts 54 | import { Board, Button, Led, render } from "edison" 55 | import React from "react" 56 | 57 | const App: React.FC = () => { 58 | return ( 59 | // Please replace with your port 60 | 64 | 65 | ) 66 | } 67 | 68 | render() 69 | ``` 70 | 71 | ## Contributing 72 | 73 | We love collaborating with folks inside and outside of GitHub and welcome contributions! 74 | 75 | > 👉 [Discord](eHB5dBkZyW) 76 | -------------------------------------------------------------------------------- /devPackageJson.cjs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edison-js/Edison/6be2109cee7aa8606524bfc0eff454e6a7ae9405/devPackageJson.cjs -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edison-js/Edison/6be2109cee7aa8606524bfc0eff454e6a7ae9405/global.d.ts -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "edison", 3 | "version": "0.1.37", 4 | "description": "This package can control Arduino with TypeScript!", 5 | "main": "./dist/esm/index.js", 6 | "module": "./dist/esm/index.js", 7 | "type": "module", 8 | "types": "./dist/esm/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/esm/index.js", 12 | "types": "./dist/esm/index.d.ts" 13 | }, 14 | "./package.json": "./package.json", 15 | "./dist/*": "./dist/*" 16 | }, 17 | "files": [ 18 | "dist", 19 | "src", 20 | "package.json", 21 | "README.md", 22 | "LICENSE.md", 23 | "CONTRIBUTING.md" 24 | ], 25 | "scripts": { 26 | "build:esm": "tsc -p tsconfig.esm.json && node rename-to-esm.mjs", 27 | "build": "npm run build:esm", 28 | "dev": "vite", 29 | "serve": "vite preview", 30 | "lint": "npx eslint src/**/*.ts* ", 31 | "type-check": "tsc --noEmit", 32 | "test": "vitest", 33 | "test:coverage": "vitest run --coverage", 34 | "format": "prettier --write \"./src/**/*.{ts}\"" 35 | }, 36 | "dependencies": { 37 | "@types/lodash-es": "^4.17.12", 38 | "ansi-escapes": "^4.3.1", 39 | "auto-bind": "^5.0.1", 40 | "code-excerpt": "^4.0.0", 41 | "is-in-ci": "^0.1.0", 42 | "lodash-es": "^4.17.21", 43 | "ora": "^8.0.1", 44 | "react": "^18.2.0", 45 | "react-reconciler": "^0.29.0", 46 | "rxjs": "^7.8.1", 47 | "serialport": "^12.0.0", 48 | "stack-utils": "^2.0.6", 49 | "yoga": "^0.0.20", 50 | "yoga-wasm-web": "^0.3.3" 51 | }, 52 | "devDependencies": { 53 | "@types/ansi-escapes": "^4.0.0", 54 | "@types/auto-bind": "^2.1.0", 55 | "@types/lodash": "^4.14.202", 56 | "@types/node": "^20.8.8", 57 | "@types/react": "^18.2.55", 58 | "@types/react-reconciler": "^0.28.8", 59 | "@types/serialport": "^8.0.3", 60 | "@types/stack-utils": "^2.0.3", 61 | "@typescript-eslint/eslint-plugin": "^7.2.0", 62 | "@typescript-eslint/parser": "^7.2.0", 63 | "@vitejs/plugin-react": "^4.2.1", 64 | "@vitest/coverage-v8": "^1.4.0", 65 | "eslint": "^8.56.0", 66 | "eslint-config-prettier": "^9.0.0", 67 | "eslint-plugin-prettier": "^5.0.0", 68 | "eslint-plugin-react": "^7.33.2", 69 | "eslint-plugin-react-hooks": "^4.6.0", 70 | "is-in-ci": "^0.1.0", 71 | "prettier": "^3.0.3", 72 | "source-map-loader": "^5.0.0", 73 | "ts-loader": "^9.5.1", 74 | "typescript": "^5.4.2", 75 | "vite": "^5.1.6", 76 | "vite-node": "^1.2.2", 77 | "vite-plugin-checker": "^0.6.2", 78 | "vitest": "^1.4.0", 79 | "webpack": "^5.91.0", 80 | "webpack-cli": "^5.1.4", 81 | "webpack-node-externals": "^3.0.0" 82 | }, 83 | "repository": { 84 | "type": "git", 85 | "url": "git+https://github.com/edison-js/Edison.git" 86 | }, 87 | "keywords": [ 88 | "IoT", 89 | "Arduino", 90 | "TypeScript", 91 | "JavaScript", 92 | "Robotics", 93 | "edison", 94 | "edison.ts", 95 | "edison.js" 96 | ], 97 | "author": "aluta", 98 | "license": "MIT", 99 | "bugs": { 100 | "url": "https://github.com/edison-js/Edison/issues" 101 | }, 102 | "homepage": "https://github.com/edison-js/Edison#readme" 103 | } 104 | -------------------------------------------------------------------------------- /public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edison-js/Edison/6be2109cee7aa8606524bfc0eff454e6a7ae9405/public/images/icon.png -------------------------------------------------------------------------------- /rename-to-esm.mjs: -------------------------------------------------------------------------------- 1 | // rename-and-fix-imports.mjs 2 | import { fileURLToPath } from 'url' 3 | import { dirname, join } from 'path' 4 | import { readFileSync, writeFileSync } from 'fs' 5 | import { promisify } from 'util' 6 | import glob from 'glob' 7 | 8 | const globPromise = promisify(glob) 9 | 10 | const __filename = fileURLToPath(import.meta.url) 11 | const __dirname = dirname(__filename) 12 | 13 | // Import文のパスを修正する関数 14 | const fixImportPaths = async (file) => { 15 | let content = readFileSync(file, 'utf8') 16 | // 正規表現で、'./' または '../'で始まるパスのimport文を対象にし、'.js'がない場合は追加 17 | content = content.replace( 18 | /(from\s+['"])(\.\/|\.\.\/)([^'"]+?)['"]/g, 19 | (match, p1, p2, p3) => { 20 | // '.js'で終わっていないパスに'.js'を追加 21 | if (!p3.endsWith('.js')) { 22 | return `${p1}${p2}${p3}.js'` 23 | } 24 | return match 25 | }, 26 | ) 27 | writeFileSync(file, content, 'utf8') 28 | } 29 | 30 | // 特定のディレクトリ内のすべての.jsファイルを走査し、import文のパスを修正 31 | const fixImportsInDirectory = async (directory) => { 32 | const files = await globPromise(`${directory}/**/*.js`) // ディレクトリ内の全ての.jsファイルを取得 33 | // biome-ignore lint/complexity/noForEach: 34 | files.forEach((file) => { 35 | fixImportPaths(file) 36 | }) 37 | } 38 | 39 | // 実行するディレクトリを指定 40 | fixImportsInDirectory(join(__dirname, 'dist')) 41 | -------------------------------------------------------------------------------- /src/__tests__/procedure/utils/board.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, beforeEach, afterEach, vi, expect } from 'vitest' 2 | import { SerialPort } from 'serialport' 3 | import type { Ora } from 'ora' 4 | import ora from 'ora' 5 | import { board } from '../../../procedure/utils/board' 6 | 7 | vi.mock('serialport') 8 | vi.mock('ora') 9 | const ARDUINO_PATH = '/dev/ttyUSB0' 10 | const BAUD_RATE = 57600 11 | 12 | describe('board', () => { 13 | let mockPort: SerialPort 14 | let mockSpinner: Ora 15 | 16 | beforeEach(() => { 17 | mockPort = new SerialPort({ path: '/dev/ttyUSB0', baudRate: 57600 }) 18 | vi.mocked(SerialPort).mockImplementation(() => mockPort) 19 | 20 | mockSpinner = { 21 | start: vi.fn().mockReturnThis(), 22 | succeed: vi.fn().mockReturnThis(), 23 | fail: vi.fn().mockReturnThis(), 24 | } as unknown as Ora 25 | vi.mocked(ora).mockReturnValue(mockSpinner) 26 | }) 27 | 28 | afterEach(() => { 29 | vi.clearAllMocks() 30 | }) 31 | 32 | describe('connectManual', () => { 33 | it('should handle port close event', async () => { 34 | vi.spyOn(process, 'exit').mockImplementation(vi.fn()) 35 | 36 | board.connectManual(ARDUINO_PATH, BAUD_RATE) 37 | 38 | mockPort.on('close', () => { 39 | expect(mockSpinner.fail).toHaveBeenCalledWith('Board is closed.') 40 | expect(board.getCurrentPort()).toBeNull() 41 | expect(board.isReady()).toBe(false) 42 | expect(process.exit).toHaveBeenCalledWith(1) 43 | }) 44 | 45 | mockPort.emit('data', '*data*') 46 | await new Promise((resolve) => setTimeout(resolve, 0)) 47 | 48 | mockPort.emit('close') 49 | }) 50 | }) 51 | 52 | describe('on', () => { 53 | it('should return the port status', async () => { 54 | const listener = vi.fn() 55 | board.on('ready', listener) 56 | 57 | board.connectManual(ARDUINO_PATH, BAUD_RATE) 58 | 59 | mockPort.on('data', () => { 60 | expect(board.isReady()).toBe(true) 61 | expect(mockSpinner.succeed).toHaveBeenCalledWith( 62 | 'Device is connected successfully!', 63 | ) 64 | }) 65 | 66 | mockPort.emit('data', '*data*') 67 | await new Promise((resolve) => setTimeout(resolve, 0)) 68 | }) 69 | }) 70 | 71 | describe('off', () => { 72 | it('should remove event listeners', async () => { 73 | const listener = vi.fn() 74 | board.on('ready', listener) 75 | board.off('ready', listener) 76 | 77 | board.connectManual(ARDUINO_PATH, BAUD_RATE) 78 | mockPort.emit('data', '*data*') 79 | await new Promise((resolve) => setTimeout(resolve, 0)) 80 | 81 | expect(listener).not.toHaveBeenCalled() 82 | }) 83 | }) 84 | 85 | describe('isReady', () => { 86 | it('should return the port status', async () => { 87 | vi.spyOn(process, 'exit').mockImplementation(vi.fn()) 88 | 89 | expect(board.isReady()).toBe(false) 90 | 91 | board.connectManual(ARDUINO_PATH, BAUD_RATE) 92 | 93 | mockPort.on('data', () => { 94 | expect(board.isReady()).toBe(true) 95 | expect(mockSpinner.succeed).toHaveBeenCalledWith( 96 | 'Device is connected successfully!', 97 | ) 98 | }) 99 | 100 | mockPort.emit('data', '*data*') 101 | await new Promise((resolve) => setTimeout(resolve, 0)) 102 | }) 103 | }) 104 | }) 105 | -------------------------------------------------------------------------------- /src/declarative/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import AppContext from './AppContext' 3 | 4 | type Props = { 5 | readonly children: React.ReactNode 6 | readonly stdin: NodeJS.ReadStream 7 | readonly stdout: NodeJS.WriteStream 8 | readonly stderr: NodeJS.WriteStream 9 | readonly exitOnCtrlC: boolean 10 | readonly onExit: (error?: Error) => void 11 | } 12 | 13 | const App = ({ children, onExit }: Props) => { 14 | const contextValue = { 15 | exit: onExit, 16 | } 17 | 18 | return ( 19 | {children} 20 | ) 21 | } 22 | 23 | export default App 24 | -------------------------------------------------------------------------------- /src/declarative/components/AppContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react' 2 | 3 | export type Props = { 4 | /** 5 | * Exit (unmount) the whole Edison app. 6 | */ 7 | readonly exit: (error?: Error) => void 8 | } 9 | 10 | /** 11 | * `AppContext` is a React context, which exposes a method to manually exit the app (unmount). 12 | */ 13 | const AppContext = createContext({ 14 | exit() {}, 15 | }) 16 | 17 | AppContext.displayName = 'InternalAppContext' 18 | 19 | export default AppContext 20 | -------------------------------------------------------------------------------- /src/declarative/components/Box.tsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, type PropsWithChildren } from 'react' 2 | import { type DOMElement } from '../rendere/dom' 3 | 4 | /** 5 | * `` is an essential Edison component to build. It's like `
` in the browser. 6 | */ 7 | const Box = forwardRef(({ children }, ref) => { 8 | return {children} 9 | }) 10 | 11 | Box.displayName = 'Box' 12 | 13 | export default Box 14 | -------------------------------------------------------------------------------- /src/declarative/components/ErrorOverview.tsx: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs' 2 | import { cwd } from 'node:process' 3 | import StackUtils from 'stack-utils' 4 | import codeExcerpt, { type CodeExcerpt } from 'code-excerpt' 5 | import Box from './Box' 6 | import Text from './Text' 7 | 8 | // Error's source file is reported as file:///home/user/file.js 9 | // This function removes the file://[cwd] part 10 | const cleanupPath = (path: string | undefined): string | undefined => { 11 | return path?.replace(`file://${cwd()}/`, '') 12 | } 13 | 14 | const stackUtils = new StackUtils({ 15 | cwd: cwd(), 16 | internals: StackUtils.nodeInternals(), 17 | }) 18 | 19 | type Props = { 20 | readonly error: Error 21 | } 22 | 23 | export default function ErrorOverview({ error }: Props) { 24 | const stack = error.stack ? error.stack.split('\n').slice(1) : undefined 25 | // biome-ignore lint/style/noNonNullAssertion: 26 | const origin = stack ? stackUtils.parseLine(stack[0]!) : undefined 27 | const filePath = cleanupPath(origin?.file) 28 | let excerpt: CodeExcerpt[] | undefined 29 | let lineWidth = 0 30 | 31 | if (filePath && origin?.line && fs.existsSync(filePath)) { 32 | const sourceCode = fs.readFileSync(filePath, 'utf8') 33 | excerpt = codeExcerpt(sourceCode, origin.line) 34 | 35 | if (excerpt) { 36 | for (const { line } of excerpt) { 37 | lineWidth = Math.max(lineWidth, String(line).length) 38 | } 39 | } 40 | } 41 | 42 | return ( 43 | 44 | 45 | ERROR 46 | 47 | {error.message} 48 | 49 | 50 | {origin && filePath && ( 51 | 52 | 53 | {filePath}:{origin.line}:{origin.column} 54 | 55 | 56 | )} 57 | 58 | {origin && excerpt && ( 59 | 60 | {excerpt.map(({ line, value }) => ( 61 | 62 | 63 | {String(line).padStart(lineWidth, ' ')}: 64 | 65 | 66 | {` ${value}`} 67 | 68 | ))} 69 | 70 | )} 71 | 72 | {error.stack && ( 73 | 74 | {error.stack 75 | .split('\n') 76 | .slice(1) 77 | .map((line) => { 78 | const parsedLine = stackUtils.parseLine(line) 79 | 80 | // If the line from the stack cannot be parsed, we print out the unparsed line. 81 | if (!parsedLine) { 82 | return ( 83 | 84 | - 85 | {line} 86 | 87 | ) 88 | } 89 | 90 | return ( 91 | 92 | - 93 | {parsedLine.function} 94 | 95 | {' '} 96 | ({cleanupPath(parsedLine.file) ?? ''}:{parsedLine.line}: 97 | {parsedLine.column}) 98 | 99 | 100 | ) 101 | })} 102 | 103 | )} 104 | 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /src/declarative/components/Static.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState, useLayoutEffect, type ReactNode } from 'react' 2 | 3 | export type Props = { 4 | /** 5 | * Array of items of any type to render using a function you pass as a component child. 6 | */ 7 | readonly items: T[] 8 | 9 | /** 10 | * Function that is called to render every item in `items` array. 11 | * First argument is an item itself and second argument is index of that item in `items` array. 12 | * Note that `key` must be assigned to the root component. 13 | */ 14 | readonly children: (item: T, index: number) => ReactNode 15 | } 16 | 17 | /** 18 | * `` component permanently renders its output above everything else. 19 | * It's useful for displaying activity like completed tasks or logs - things that 20 | * are not changing after they're rendered (hence the name "Static"). 21 | * 22 | * It's preferred to use `` for use cases like these, when you can't know 23 | * or control the amount of items that need to be rendered. 24 | * 25 | * For example, [Tap](https://github.com/tapjs/node-tap) uses `` to display 26 | * a list of completed tests. [Gatsby](https://github.com/gatsbyjs/gatsby) uses it 27 | * to display a list of generated pages, while still displaying a live progress bar. 28 | */ 29 | export default function Static(props: Props) { 30 | const { items, children: render } = props 31 | const [index, setIndex] = useState(0) 32 | 33 | const itemsToRender: T[] = useMemo(() => { 34 | return items.slice(index) 35 | }, [items, index]) 36 | 37 | useLayoutEffect(() => { 38 | setIndex(items.length) 39 | }, [items.length]) 40 | 41 | const children = itemsToRender.map((item, itemIndex) => { 42 | return render(item, index + itemIndex) 43 | }) 44 | 45 | return {children} 46 | } 47 | -------------------------------------------------------------------------------- /src/declarative/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react' 2 | 3 | export type Props = { 4 | /** 5 | * This property tells Edison to wrap or truncate text if its width is larger than container. 6 | * If `wrap` is passed (by default), Edison will wrap text and split it into multiple lines. 7 | * If `truncate-*` is passed, Edison will truncate text instead, which will result in one line of text with the rest cut off. 8 | */ 9 | 10 | readonly children?: ReactNode 11 | } 12 | 13 | /** 14 | * This component can display text, and change its style to make it colorful, bold, underline, italic or strikethrough. 15 | */ 16 | export default function Text({ children }: Props) { 17 | if (children === undefined || children === null) { 18 | return null 19 | } 20 | 21 | return {children} 22 | } 23 | -------------------------------------------------------------------------------- /src/declarative/components/analog/AnalogRotation.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const AnalogRotationContext = createContext(null) 8 | 9 | type AnalogRotationProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const AnalogRotation: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue(sensorValue) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/analog/AnalogSoilMoisture.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const AnalogSoilMoistureContext = createContext(null) 8 | 9 | type AnalogSoilMoistureProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const AnalogSoilMoisture: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue(sensorValue) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/analog/AnalogSteam.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const AnalogSteamContext = createContext(null) 8 | 9 | type AnalogSteamProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const AnalogSteam: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue(sensorValue) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/analog/AnalogTemperature.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const AnalogTemperatureContext = createContext(null) 8 | 9 | type AnalogTemperatureProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const AnalogTemperature: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | const fenya = (sensorValue / 1023) * 5 29 | const r = ((5 - fenya) / fenya) * 4700 30 | setValue(1 / (Math.log(r / 10000) / 3950 + 1 / (25 + 273.15)) - 273.15) 31 | if (onValueChange) { 32 | onValueChange(sensorValue) 33 | } 34 | }) 35 | } 36 | 37 | const handleReady = (port: SerialPort) => { 38 | setupSensor(port) 39 | setPort(port) 40 | board.off('ready', handleReady) 41 | } 42 | 43 | if (board.isReady()) { 44 | const currentPort = board.getCurrentPort() 45 | if (currentPort) { 46 | setupSensor(currentPort) 47 | setPort(currentPort) 48 | } 49 | } else { 50 | board.on('ready', handleReady) 51 | } 52 | 53 | return () => { 54 | board.off('ready', handleReady) 55 | } 56 | }, [pin, onValueChange]) 57 | 58 | return ( 59 | 60 | {children(value)} 61 | 62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /src/declarative/components/analog/AnalogVibration.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const AnalogVibrationContext = createContext(null) 8 | 9 | type AnalogVibrationProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const AnalogVibration: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue(sensorValue) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/analog/LinearTemperature.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const LinearTemperatureContext = createContext(null) 8 | 9 | type LinearTemperatureProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const LinearTemperature: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue((500 * sensorValue) / 1024) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/analog/Photocell.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const PhotocellContext = createContext(null) 8 | 9 | type PhotocellProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const Photocell: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue(sensorValue) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/analog/WaterLevelSensor.tsx: -------------------------------------------------------------------------------- 1 | import { attachAnalog } from '../../../procedure/analog/uniqueDevice/analog' 2 | import type { AnalogPin } from '../../../procedure/types/analog/analog' 3 | import { board } from '../../../procedure/utils/board' 4 | import React, { createContext, useEffect, useState } from 'react' 5 | import type { SerialPort } from 'serialport' 6 | 7 | export const WaterLevelContext = createContext(null) 8 | 9 | type WaterLevelSensorProps = { 10 | pin: AnalogPin 11 | onValueChange?: (value: number) => void 12 | children: (value: number) => React.ReactNode 13 | } 14 | 15 | export const WaterLevel: React.FC = ({ 16 | pin, 17 | onValueChange, 18 | children, 19 | }) => { 20 | const [port, setPort] = useState(null) 21 | const [value, setValue] = useState(0) 22 | 23 | useEffect(() => { 24 | const setupSensor = (port: SerialPort) => { 25 | const waterSensor = attachAnalog(port, pin) 26 | 27 | waterSensor.read('change', async (sensorValue: number) => { 28 | setValue(sensorValue) 29 | if (onValueChange) { 30 | onValueChange(sensorValue) 31 | } 32 | }) 33 | } 34 | 35 | const handleReady = (port: SerialPort) => { 36 | setupSensor(port) 37 | setPort(port) 38 | board.off('ready', handleReady) 39 | } 40 | 41 | if (board.isReady()) { 42 | const currentPort = board.getCurrentPort() 43 | if (currentPort) { 44 | setupSensor(currentPort) 45 | setPort(currentPort) 46 | } 47 | } else { 48 | board.on('ready', handleReady) 49 | } 50 | 51 | return () => { 52 | board.off('ready', handleReady) 53 | } 54 | }, [pin, onValueChange]) 55 | 56 | return ( 57 | 58 | {children(value)} 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/declarative/components/input/Button.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const ButtonContext = createContext(null) 7 | 8 | type ButtonProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | children: React.ReactNode 13 | } 14 | 15 | export const Button: React.FC = ({ 16 | pin, 17 | triggered, 18 | untriggered, 19 | children, 20 | }) => { 21 | const setupButton = (port: SerialPort) => { 22 | const pushButton = attachInput(port, pin) 23 | 24 | if (untriggered) { 25 | pushButton.read('off', untriggered) 26 | } 27 | 28 | if (triggered) { 29 | pushButton.read('on', triggered) 30 | } 31 | } 32 | 33 | if (board.isReady()) { 34 | const port = board.getCurrentPort() 35 | if (port) { 36 | setupButton(port) 37 | } 38 | } else { 39 | const handleReady = (port: SerialPort) => { 40 | setupButton(port) 41 | board.off('ready', handleReady) 42 | } 43 | board.on('ready', handleReady) 44 | } 45 | 46 | return ( 47 | {children} 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/declarative/components/input/Collision.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const CollisionContext = createContext(null) 7 | 8 | type CollisionProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | children: React.ReactNode 13 | } 14 | 15 | export const Collision: React.FC = ({ 16 | pin, 17 | triggered, 18 | untriggered, 19 | children, 20 | }) => { 21 | const setupCollision = (port: SerialPort) => { 22 | const collisionSensor = attachInput(port, pin) 23 | 24 | if (untriggered) { 25 | collisionSensor.read('off', untriggered) 26 | } 27 | 28 | if (triggered) { 29 | collisionSensor.read('on', triggered) 30 | } 31 | } 32 | 33 | if (board.isReady()) { 34 | const port = board.getCurrentPort() 35 | if (port) { 36 | setupCollision(port) 37 | } 38 | } else { 39 | const handleReady = (port: SerialPort) => { 40 | setupCollision(port) 41 | board.off('ready', handleReady) 42 | } 43 | board.on('ready', handleReady) 44 | } 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/declarative/components/input/DigitalTiltSensor.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const DigitalTiltSensorContext = createContext(null) 7 | 8 | type DigitalTiltSensorProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | children: React.ReactNode 13 | } 14 | 15 | export const DigitalTilt: React.FC = ({ 16 | pin, 17 | triggered, 18 | untriggered, 19 | children, 20 | }) => { 21 | const setupDigitalTiltSensor = (port: SerialPort) => { 22 | const DigitalTiltSensor = attachInput(port, pin) 23 | 24 | if (untriggered) { 25 | DigitalTiltSensor.read('off', untriggered) 26 | } 27 | 28 | if (triggered) { 29 | DigitalTiltSensor.read('on', triggered) 30 | } 31 | } 32 | 33 | if (board.isReady()) { 34 | const port = board.getCurrentPort() 35 | if (port) { 36 | setupDigitalTiltSensor(port) 37 | } 38 | } else { 39 | const handleReady = (port: SerialPort) => { 40 | setupDigitalTiltSensor(port) 41 | board.off('ready', handleReady) 42 | } 43 | board.on('ready', handleReady) 44 | } 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/declarative/components/input/HallEffectSensor.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const HallEffectiveContext = createContext(null) 7 | 8 | type HallEffectProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | children: React.ReactNode 13 | } 14 | 15 | export const HallEffective: React.FC = ({ 16 | pin, 17 | triggered, 18 | untriggered, 19 | children, 20 | }) => { 21 | const setupHallEffective = (port: SerialPort) => { 22 | const hallEffectiveSensor = attachInput(port, pin) 23 | 24 | if (untriggered) { 25 | hallEffectiveSensor.read('off', untriggered) 26 | } 27 | 28 | if (triggered) { 29 | hallEffectiveSensor.read('on', triggered) 30 | } 31 | } 32 | 33 | if (board.isReady()) { 34 | const port = board.getCurrentPort() 35 | if (port) { 36 | setupHallEffective(port) 37 | } 38 | } else { 39 | const handleReady = (port: SerialPort) => { 40 | setupHallEffective(port) 41 | board.off('ready', handleReady) 42 | } 43 | board.on('ready', handleReady) 44 | } 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/declarative/components/input/Input.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const InputContext = createContext(null) 7 | 8 | type InputProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | children: React.ReactNode 13 | } 14 | 15 | export const Input: React.FC = ({ 16 | pin, 17 | triggered, 18 | untriggered, 19 | children, 20 | }) => { 21 | const setupInput = (port: SerialPort) => { 22 | const input = attachInput(port, pin) 23 | 24 | if (untriggered) { 25 | input.read('off', untriggered) 26 | } 27 | 28 | if (triggered) { 29 | input.read('on', triggered) 30 | } 31 | } 32 | 33 | if (board.isReady()) { 34 | const port = board.getCurrentPort() 35 | if (port) { 36 | setupInput(port) 37 | } 38 | } else { 39 | const handleReady = (port: SerialPort) => { 40 | setupInput(port) 41 | board.off('ready', handleReady) 42 | } 43 | board.on('ready', handleReady) 44 | } 45 | 46 | return {children} 47 | } 48 | -------------------------------------------------------------------------------- /src/declarative/components/input/PIRMotionSensor.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const PIRMotionSensorContext = createContext(null) 7 | 8 | type PIRMotionSensorProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | delayTime?: number 13 | children: React.ReactNode 14 | } 15 | 16 | export const PIRMotion: React.FC = ({ 17 | pin, 18 | triggered, 19 | untriggered, 20 | children, 21 | }) => { 22 | const setupPIRMotionSensor = async (port: SerialPort) => { 23 | const pirMotionSensor = attachInput(port, pin) 24 | 25 | if (untriggered) { 26 | pirMotionSensor.read('off', untriggered) 27 | } 28 | 29 | if (triggered) { 30 | pirMotionSensor.read('on', triggered) 31 | } 32 | } 33 | 34 | if (board.isReady()) { 35 | const port = board.getCurrentPort() 36 | if (port) { 37 | setupPIRMotionSensor(port) 38 | } 39 | } else { 40 | const handleReady = (port: SerialPort) => { 41 | setupPIRMotionSensor(port) 42 | board.off('ready', handleReady) 43 | } 44 | board.on('ready', handleReady) 45 | } 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/declarative/components/input/PhotoInterrupter.tsx: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import React, { createContext } from 'react' 3 | import { board } from '../../../procedure/utils/board' 4 | import { attachInput } from '../../../procedure/input/uniqueDevice/input' 5 | 6 | export const PhotoInterrupterContext = createContext(null) 7 | 8 | type PhotoInterrupterProps = { 9 | pin: number 10 | triggered?: () => void 11 | untriggered?: () => void 12 | children: React.ReactNode 13 | } 14 | 15 | export const PhotoInterrupter: React.FC = ({ 16 | pin, 17 | triggered, 18 | untriggered, 19 | children, 20 | }) => { 21 | const setupPhotoInterrupter = (port: SerialPort) => { 22 | const photointerrupter = attachInput(port, pin) 23 | 24 | if (untriggered) { 25 | photointerrupter.read('off', untriggered) 26 | } 27 | 28 | if (triggered) { 29 | photointerrupter.read('on', triggered) 30 | } 31 | } 32 | 33 | if (board.isReady()) { 34 | const port = board.getCurrentPort() 35 | if (port) { 36 | setupPhotoInterrupter(port) 37 | } 38 | } else { 39 | const handleReady = (port: SerialPort) => { 40 | setupPhotoInterrupter(port) 41 | board.off('ready', handleReady) 42 | } 43 | board.on('ready', handleReady) 44 | } 45 | 46 | return ( 47 | 48 | {children} 49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/declarative/components/output/Buzzer.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { board } from '../../../procedure/utils/board' 3 | import { attachOutput } from '../../../procedure/output/uniqueDevice/output' 4 | 5 | type BuzzerProps = { 6 | pin: number 7 | isOn?: boolean 8 | } 9 | 10 | const setupBuzzer = (props: BuzzerProps) => { 11 | const { pin, isOn } = props 12 | const port = board.getCurrentPort() 13 | 14 | if (!port) { 15 | console.error('Board is not connected.') 16 | return 17 | } 18 | 19 | const buzzer = attachOutput(port, pin) 20 | 21 | if (isOn === true) { 22 | buzzer.on() 23 | } else if (isOn === false) { 24 | buzzer.off() 25 | } 26 | } 27 | 28 | export const Buzzer: React.FC = (props) => { 29 | if (board.isReady()) { 30 | setupBuzzer(props) 31 | } else { 32 | const handleReady = () => { 33 | setupBuzzer(props) 34 | } 35 | board.on('ready', handleReady) 36 | } 37 | return null 38 | } 39 | -------------------------------------------------------------------------------- /src/declarative/components/output/Led.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { board } from '../../../procedure/utils/board' 3 | import { attachLed } from '../../../procedure/output/uniqueDevice/led' 4 | 5 | type LEDProps = { 6 | pin: number 7 | isOn?: boolean 8 | blink?: number 9 | } 10 | 11 | const setupLed = (props: LEDProps) => { 12 | const { pin, isOn, blink } = props 13 | const port = board.getCurrentPort() 14 | 15 | if (!port) { 16 | console.error('Board is not connected.') 17 | return 18 | } 19 | 20 | const led = attachLed(port, pin) 21 | 22 | if (isOn === true) { 23 | led.on() 24 | } else if (isOn === false) { 25 | led.off() 26 | } 27 | 28 | if (blink) { 29 | led.blink(blink) 30 | } 31 | } 32 | 33 | export const Led: React.FC = (props) => { 34 | if (board.isReady()) { 35 | setupLed(props) 36 | } else { 37 | const handleReady = () => { 38 | setupLed(props) 39 | board.off('ready', handleReady) 40 | } 41 | board.on('ready', handleReady) 42 | } 43 | return null 44 | } 45 | -------------------------------------------------------------------------------- /src/declarative/components/output/Output.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { board } from '../../../procedure/utils/board' 3 | import { attachOutput } from '../../../procedure/output/uniqueDevice/output' 4 | 5 | type OutputProps = { 6 | pin: number 7 | isOn?: boolean 8 | } 9 | 10 | const setupOutput = (props: OutputProps) => { 11 | const { pin, isOn } = props 12 | const port = board.getCurrentPort() 13 | 14 | if (!port) { 15 | console.error('Board is not connected.') 16 | return 17 | } 18 | 19 | const output = attachOutput(port, pin) 20 | 21 | if (isOn === true) { 22 | output.on() 23 | } else if (isOn === false) { 24 | output.off() 25 | } 26 | } 27 | 28 | export const Output: React.FC = (props) => { 29 | if (board.isReady()) { 30 | setupOutput(props) 31 | } else { 32 | const handleReady = () => { 33 | setupOutput(props) 34 | board.off('ready', handleReady) 35 | } 36 | board.on('ready', handleReady) 37 | } 38 | return null 39 | } 40 | -------------------------------------------------------------------------------- /src/declarative/components/pwm/VibrationMotorModule.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react' 2 | import { board } from '../../../procedure/utils/board' 3 | import { pwmPort } from '../../../procedure/pwm/pwmPort' 4 | 5 | type VibrationMotorModuleProps = { 6 | pin: number 7 | val: number 8 | isOn?: boolean 9 | } 10 | 11 | const setupVibrationMotorModule = (props: VibrationMotorModuleProps) => { 12 | const { pin, val, isOn } = props 13 | const port = board.getCurrentPort() 14 | 15 | if (!port) { 16 | console.error('Board is not connected.') 17 | return 18 | } 19 | 20 | const vibrationmotormodule = pwmPort(port)(pin) 21 | 22 | if (isOn === true) { 23 | vibrationmotormodule.analogWrite(val) 24 | } else if (isOn === false) { 25 | vibrationmotormodule.off() 26 | } 27 | } 28 | 29 | export const VibrationMotorModule: React.FC = ( 30 | props, 31 | ) => { 32 | if (board.isReady()) { 33 | setupVibrationMotorModule(props) 34 | } else { 35 | const handleReady = () => { 36 | setupVibrationMotorModule(props) 37 | board.off('ready', handleReady) 38 | } 39 | board.on('ready', handleReady) 40 | } 41 | return null 42 | } 43 | -------------------------------------------------------------------------------- /src/declarative/components/servo/Servo.tsx: -------------------------------------------------------------------------------- 1 | import { attachServo } from '../../../procedure/servo/uniqueDevice/servo' 2 | import { board } from '../../../procedure/utils/board' 3 | import type React from 'react' 4 | 5 | type ServoProps = { 6 | pin: number 7 | angle: number 8 | } 9 | 10 | export const Servo: React.FC = ({ pin, angle }) => { 11 | const port = board.getCurrentPort() 12 | if (!port) { 13 | console.error('Board is not connected.') 14 | return null 15 | } 16 | 17 | const servo = attachServo(port, pin) 18 | 19 | if (board.isReady()) { 20 | servo.setAngle(angle) 21 | } 22 | return null 23 | } 24 | -------------------------------------------------------------------------------- /src/declarative/examples/ VibrationMotorModule.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Button } from '../components/input/Button' 5 | import { VibrationMotorModule } from '../components/pwm/VibrationMotorModule' 6 | 7 | const App: React.FC = () => { 8 | const [value, setValue] = useState(0) 9 | 10 | return ( 11 | 15 | 32 | 33 | ) 34 | } 35 | render() 36 | -------------------------------------------------------------------------------- /src/declarative/examples/AnalogRotation.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { AnalogRotation } from '../../declarative/components/analog/AnalogRotation' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/AnalogSoilMoisture.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { AnalogSoilMoisture } from '../../declarative/components/analog/AnalogSoilMoisture' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/AnalogSteam.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { AnalogSteam } from '../../declarative/components/analog/AnalogSteam' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/AnalogTemperature.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { AnalogTemperature } from '../../declarative/components/analog/AnalogTemperature' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/AnalogVibrationSensor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { AnalogVibration } from '../../declarative/components/analog/AnalogVibration' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../components/output/Led' 5 | 6 | const App: React.FC = () => { 7 | return ( 8 | 12 | 16 | 17 | ) 18 | } 19 | render() 20 | -------------------------------------------------------------------------------- /src/declarative/examples/CollisionSensor.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Collision } from '../components/input/Collision' 5 | import { Led } from '../components/output/Led' 6 | 7 | const App: React.FC = () => { 8 | const [isOn, setIsOn] = useState(false) 9 | 10 | return ( 11 | 15 | setIsOn(true)} 18 | untriggered={() => setIsOn(false)} 19 | > 20 | 24 | 25 | 26 | ) 27 | } 28 | render() 29 | -------------------------------------------------------------------------------- /src/declarative/examples/DigiTaltiltSensor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState } from 'react' 3 | import { Board } from '../utils/Board' 4 | import { render } from '../rendere/render' 5 | import { Led } from '../components/output/Led' 6 | import { DigitalTilt } from '../../declarative/components/input/DigitalTiltSensor' 7 | 8 | const App: React.FC = () => { 9 | const [isOn, setIsOn] = useState(false) 10 | 11 | return ( 12 | 16 | { 19 | setIsOn(true) 20 | }} 21 | untriggered={() => { 22 | setIsOn(false) 23 | }} 24 | > 25 | 29 | 30 | 31 | ) 32 | } 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/HallEffectSensor.tsx: -------------------------------------------------------------------------------- 1 | import { HallEffective } from '../../declarative/components/input/HallEffectSensor' 2 | import { Led } from '../../declarative/components/output/Led' 3 | import { render } from '../../declarative/rendere/render' 4 | import { Board } from '../../declarative/utils/Board' 5 | import { useState } from 'react' 6 | 7 | const App: React.FC = () => { 8 | const [isOn, setIsOn] = useState(false) 9 | 10 | return ( 11 | 15 | setIsOn(true)} 18 | untriggered={() => setIsOn(false)} 19 | > 20 | 24 | 25 | 26 | ) 27 | } 28 | render() 29 | -------------------------------------------------------------------------------- /src/declarative/examples/Inputs.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState } from 'react' 3 | import { Board } from '../utils/Board' 4 | import { render } from '../rendere/render' 5 | import { Button } from '../components/input/Button' 6 | import { Led } from '../components/output/Led' 7 | 8 | const App: React.FC = () => { 9 | const [isOn, setIsOn] = useState(false) 10 | const [isOn1, setIsOn1] = useState(false) 11 | 12 | return ( 13 | 17 | 27 | 37 | 38 | ) 39 | } 40 | render() 41 | -------------------------------------------------------------------------------- /src/declarative/examples/Led.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState } from 'react' 3 | import { Board } from '../utils/Board' 4 | import { render } from '../rendere/render' 5 | import { Button } from '../components/input/Button' 6 | import { Led } from '../components/output/Led' 7 | 8 | const App: React.FC = () => { 9 | const [ledOne, setLedOne] = useState(false) 10 | 11 | return ( 12 | 16 | 26 | 27 | ) 28 | } 29 | render() 30 | -------------------------------------------------------------------------------- /src/declarative/examples/LinearTemperature.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { LinearTemperature } from '../../declarative/components/analog/LinearTemperature' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/PIRMotionSensor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState } from 'react' 3 | import { Board } from '../utils/Board' 4 | import { render } from '../rendere/render' 5 | import { Led } from '../components/output/Led' 6 | import { PIRMotion } from '../components/input/PIRMotionSensor' 7 | 8 | const App: React.FC = () => { 9 | const [isOn, setIsOn] = useState(false) 10 | 11 | return ( 12 | 16 | { 19 | setIsOn(true) 20 | }} 21 | untriggered={() => { 22 | setIsOn(false) 23 | }} 24 | delayTime={4000} 25 | > 26 | 30 | 31 | 32 | ) 33 | } 34 | render() 35 | -------------------------------------------------------------------------------- /src/declarative/examples/PhotoInterrupter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState } from 'react' 3 | import { Board } from '../utils/Board' 4 | import { render } from '../rendere/render' 5 | import { Led } from '../components/output/Led' 6 | import { PhotoInterrupter } from '../components/input/PhotoInterrupter' 7 | 8 | const App: React.FC = () => { 9 | const [isOn, setIsOn] = useState(false) 10 | 11 | return ( 12 | 16 | { 19 | setIsOn(true) 20 | }} 21 | untriggered={() => { 22 | setIsOn(false) 23 | }} 24 | > 25 | 29 | 30 | 31 | ) 32 | } 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/Photocell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { LinearTemperature } from '../../declarative/components/analog/LinearTemperature' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => { 15 | console.log(value) 16 | return value < 400 ? ( 17 | 21 | ) : ( 22 | 26 | ) 27 | }} 28 | 29 | 30 | ) 31 | } 32 | 33 | render() 34 | -------------------------------------------------------------------------------- /src/declarative/examples/Servo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useState } from 'react' 3 | import { Board } from '../utils/Board' 4 | import { render } from '../rendere/render' 5 | import { Button } from '../components/input/Button' 6 | import { Servo } from '../components/servo/Servo' 7 | 8 | const App: React.FC = () => { 9 | const [angle, setAngle] = useState(0) 10 | 11 | const handlePress = () => { 12 | if (angle >= 150) { 13 | setAngle(0) 14 | } 15 | } 16 | 17 | const handleRelease = () => { 18 | setAngle((prevAngle) => { 19 | const newAngle = prevAngle + 10 20 | 21 | return newAngle 22 | }) 23 | } 24 | 25 | return ( 26 | 30 | 44 | 45 | ) 46 | } 47 | render() 48 | -------------------------------------------------------------------------------- /src/declarative/examples/WaterLevelSensor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Board } from '../utils/Board' 3 | import { render } from '../rendere/render' 4 | import { Led } from '../../declarative/components/output/Led' 5 | import { WaterLevel } from '../../declarative/components/analog/WaterLevelSensor' 6 | 7 | const App: React.FC = () => { 8 | return ( 9 | 13 | 14 | {(value) => 15 | value < 400 ? ( 16 | 20 | ) : ( 21 | 25 | ) 26 | } 27 | 28 | 29 | ) 30 | } 31 | 32 | render() 33 | -------------------------------------------------------------------------------- /src/declarative/rendere/dom.ts: -------------------------------------------------------------------------------- 1 | import Yoga, { type Node as YogaNode } from 'yoga-wasm-web/auto' 2 | import { type OutputTransformer } from './render-node-to-output' 3 | 4 | type EdisonNode = { 5 | parentNode: DOMElement | undefined 6 | yogaNode?: YogaNode 7 | internal_static?: boolean 8 | } 9 | 10 | export type TextName = '#text' 11 | export type ElementNames = 12 | | 'edison-root' 13 | | 'edison-box' 14 | | 'edison-text' 15 | | 'edison-virtual-text' 16 | 17 | export type NodeNames = ElementNames | TextName 18 | 19 | export type DOMElement = { 20 | nodeName: ElementNames 21 | attributes: Record 22 | childNodes: DOMNode[] 23 | internal_transform?: OutputTransformer 24 | 25 | // Internal properties 26 | isStaticDirty?: boolean 27 | staticNode?: DOMElement 28 | onComputeLayout?: () => void 29 | onRender?: () => void 30 | onImmediateRender?: () => void 31 | } & EdisonNode 32 | 33 | export type TextNode = { 34 | nodeName: TextName 35 | nodeValue: string 36 | } & EdisonNode 37 | 38 | export type DOMNode = T extends { 39 | nodeName: infer U 40 | } 41 | ? U extends '#text' 42 | ? TextNode 43 | : DOMElement 44 | : never 45 | 46 | export type DOMNodeAttribute = boolean | string | number 47 | 48 | export const createNode = (nodeName: ElementNames): DOMElement => { 49 | const node: DOMElement = { 50 | nodeName, 51 | attributes: {}, 52 | childNodes: [], 53 | parentNode: undefined, 54 | yogaNode: 55 | nodeName === 'edison-virtual-text' ? undefined : Yoga.Node.create(), 56 | } 57 | 58 | return node 59 | } 60 | 61 | export const appendChildNode = ( 62 | node: DOMElement, 63 | childNode: DOMElement, 64 | ): void => { 65 | if (childNode.parentNode) { 66 | removeChildNode(childNode.parentNode, childNode) 67 | } 68 | 69 | childNode.parentNode = node 70 | node.childNodes.push(childNode) 71 | 72 | if (childNode.yogaNode) { 73 | node.yogaNode?.insertChild( 74 | childNode.yogaNode, 75 | node.yogaNode.getChildCount(), 76 | ) 77 | } 78 | 79 | if ( 80 | node.nodeName === 'edison-text' || 81 | node.nodeName === 'edison-virtual-text' 82 | ) { 83 | markNodeAsDirty(node) 84 | } 85 | } 86 | 87 | export const insertBeforeNode = ( 88 | node: DOMElement, 89 | newChildNode: DOMNode, 90 | beforeChildNode: DOMNode, 91 | ): void => { 92 | if (newChildNode.parentNode) { 93 | removeChildNode(newChildNode.parentNode, newChildNode) 94 | } 95 | 96 | newChildNode.parentNode = node 97 | 98 | const index = node.childNodes.indexOf(beforeChildNode) 99 | if (index >= 0) { 100 | node.childNodes.splice(index, 0, newChildNode) 101 | if (newChildNode.yogaNode) { 102 | node.yogaNode?.insertChild(newChildNode.yogaNode, index) 103 | } 104 | 105 | return 106 | } 107 | 108 | node.childNodes.push(newChildNode) 109 | 110 | if (newChildNode.yogaNode) { 111 | node.yogaNode?.insertChild( 112 | newChildNode.yogaNode, 113 | node.yogaNode.getChildCount(), 114 | ) 115 | } 116 | 117 | if ( 118 | node.nodeName === 'edison-text' || 119 | node.nodeName === 'edison-virtual-text' 120 | ) { 121 | markNodeAsDirty(node) 122 | } 123 | } 124 | 125 | export const removeChildNode = ( 126 | node: DOMElement, 127 | removeNode: DOMNode, 128 | ): void => { 129 | if (removeNode.yogaNode) { 130 | removeNode.parentNode?.yogaNode?.removeChild(removeNode.yogaNode) 131 | } 132 | 133 | removeNode.parentNode = undefined 134 | 135 | const index = node.childNodes.indexOf(removeNode) 136 | if (index >= 0) { 137 | node.childNodes.splice(index, 1) 138 | } 139 | 140 | if ( 141 | node.nodeName === 'edison-text' || 142 | node.nodeName === 'edison-virtual-text' 143 | ) { 144 | markNodeAsDirty(node) 145 | } 146 | } 147 | 148 | export const setAttribute = ( 149 | node: DOMElement, 150 | key: string, 151 | value: DOMNodeAttribute, 152 | ): void => { 153 | node.attributes[key] = value 154 | } 155 | 156 | export const createTextNode = (text: string): TextNode => { 157 | const node: TextNode = { 158 | nodeName: '#text', 159 | nodeValue: text, 160 | yogaNode: undefined, 161 | parentNode: undefined, 162 | } 163 | 164 | setTextNodeValue(node, text) 165 | 166 | return node 167 | } 168 | 169 | const findClosestYogaNode = (node?: DOMNode): YogaNode | undefined => { 170 | if (!node?.parentNode) { 171 | return undefined 172 | } 173 | 174 | return node.yogaNode ?? findClosestYogaNode(node.parentNode) 175 | } 176 | 177 | const markNodeAsDirty = (node?: DOMNode): void => { 178 | // Mark closest Yoga node as dirty to measure text dimensions again 179 | const yogaNode = findClosestYogaNode(node) 180 | yogaNode?.markDirty() 181 | } 182 | 183 | export const setTextNodeValue = (node: TextNode, text: string): void => { 184 | node.nodeValue = text 185 | markNodeAsDirty(node) 186 | } 187 | -------------------------------------------------------------------------------- /src/declarative/rendere/edison.tsx: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import { type ReactNode } from 'react' 3 | import throttle from 'lodash-es/throttle.js' 4 | import isInCi from 'is-in-ci' 5 | import autoBind from 'auto-bind' 6 | import { type FiberRoot } from 'react-reconciler' 7 | import Yoga from 'yoga-wasm-web/auto' 8 | import reconciler from './reconciler' 9 | import render from './renderer' 10 | import * as dom from './dom' 11 | import instances from './instances' 12 | import App from '../components/App' 13 | 14 | const noop = () => {} 15 | 16 | export type Options = { 17 | stdout: NodeJS.WriteStream 18 | stdin: NodeJS.ReadStream 19 | stderr: NodeJS.WriteStream 20 | debug: boolean 21 | exitOnCtrlC: boolean 22 | patchConsole: boolean 23 | waitUntilExit?: () => Promise 24 | } 25 | 26 | export default class Edison { 27 | private readonly options: Options 28 | // Ignore last render after unmounting a tree to prevent empty output before exit 29 | private isUnmounted: boolean 30 | private lastOutput: string 31 | private readonly container: FiberRoot 32 | private readonly rootNode: dom.DOMElement 33 | // This variable is used only in debug mode to store full static output 34 | // so that it's rerendered every time, not just new static parts, like in non-debug mode 35 | private fullStaticOutput: string 36 | private exitPromise?: Promise 37 | private restoreConsole?: () => void 38 | private readonly unsubscribeResize?: () => void 39 | 40 | constructor(options: Options) { 41 | autoBind(this) 42 | 43 | this.options = options 44 | this.rootNode = dom.createNode('edison-root') 45 | this.rootNode.onComputeLayout = this.calculateLayout 46 | 47 | this.rootNode.onRender = options.debug 48 | ? this.onRender 49 | : throttle(this.onRender, 32, { 50 | leading: true, 51 | trailing: true, 52 | }) 53 | 54 | this.rootNode.onImmediateRender = this.onRender 55 | 56 | // Ignore last render after unmounting a tree to prevent empty output before exit 57 | this.isUnmounted = false 58 | 59 | // Store last output to only rerender when needed 60 | this.lastOutput = '' 61 | 62 | // This variable is used only in debug mode to store full static output 63 | // so that it's rerendered every time, not just new static parts, like in non-debug mode 64 | this.fullStaticOutput = '' 65 | 66 | this.container = reconciler.createContainer( 67 | this.rootNode, 68 | // Legacy mode 69 | 0, 70 | null, 71 | false, 72 | null, 73 | 'id', 74 | () => {}, 75 | null, 76 | ) 77 | 78 | // Unmount when process exits 79 | // this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false }) 80 | 81 | if (process.env.DEV === 'true') { 82 | reconciler.injectIntoDevTools({ 83 | bundleType: 0, 84 | // Reporting React DOM's version, not Edison's 85 | // See https://github.com/facebook/react/issues/16666#issuecomment-532639905 86 | version: '16.13.1', 87 | rendererPackageName: 'edison', 88 | }) 89 | } 90 | 91 | if (options.patchConsole) { 92 | this.patchConsole() 93 | } 94 | 95 | if (!isInCi) { 96 | options.stdout.on('resize', this.resized) 97 | 98 | this.unsubscribeResize = () => { 99 | options.stdout.off('resize', this.resized) 100 | } 101 | } 102 | } 103 | 104 | resized = () => { 105 | this.calculateLayout() 106 | this.onRender() 107 | } 108 | 109 | resolveExitPromise: () => void = () => {} 110 | rejectExitPromise: (reason?: Error) => void = () => {} 111 | unsubscribeExit: () => void = () => {} 112 | 113 | calculateLayout = () => { 114 | // The 'columns' property can be undefined or 0 when not using a TTY. 115 | // In that case we fall back to 80. 116 | const terminalWidth = this.options.stdout.columns || 80 117 | 118 | this.rootNode.yogaNode?.setWidth(terminalWidth) 119 | 120 | this.rootNode.yogaNode?.calculateLayout( 121 | undefined, 122 | undefined, 123 | Yoga.DIRECTION_LTR, 124 | ) 125 | } 126 | 127 | onRender: () => void = () => { 128 | if (this.isUnmounted) { 129 | return 130 | } 131 | 132 | // If use ansi-escapes to clear the terminal, add outputHeight to Destructuring assignmen 133 | const { output, staticOutput } = render() 134 | 135 | // If output isn't empty, it means new children have been added to it 136 | const hasStaticOutput = staticOutput && staticOutput !== '\n' 137 | 138 | if (this.options.debug) { 139 | if (hasStaticOutput) { 140 | this.fullStaticOutput += staticOutput 141 | } 142 | 143 | this.options.stdout.write(this.fullStaticOutput + output) 144 | return 145 | } 146 | 147 | if (isInCi) { 148 | if (hasStaticOutput) { 149 | this.options.stdout.write(staticOutput) 150 | } 151 | 152 | this.lastOutput = output 153 | return 154 | } 155 | 156 | if (hasStaticOutput) { 157 | this.fullStaticOutput += staticOutput 158 | } 159 | 160 | // if (outputHeight >= this.options.stdout.rows) { 161 | // this.options.stdout.write( 162 | // ansiEscapes.clearTerminal + this.fullStaticOutput + output, 163 | // ) 164 | // this.lastOutput = output 165 | // return 166 | // } 167 | 168 | this.lastOutput = output 169 | } 170 | 171 | render(node: ReactNode): void { 172 | const tree = ( 173 | 180 | {node} 181 | 182 | ) 183 | 184 | reconciler.updateContainer(tree, this.container, null, noop) 185 | } 186 | 187 | writeToStdout(data: string): void { 188 | if (this.isUnmounted) { 189 | return 190 | } 191 | 192 | if (this.options.debug) { 193 | this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput) 194 | return 195 | } 196 | 197 | if (isInCi) { 198 | this.options.stdout.write(data) 199 | return 200 | } 201 | } 202 | 203 | writeToStderr(data: string): void { 204 | if (this.isUnmounted) { 205 | return 206 | } 207 | 208 | if (this.options.debug) { 209 | this.options.stderr.write(data) 210 | this.options.stdout.write(this.fullStaticOutput + this.lastOutput) 211 | return 212 | } 213 | 214 | if (isInCi) { 215 | this.options.stderr.write(data) 216 | return 217 | } 218 | } 219 | 220 | unmount(error?: Error | number | null): void { 221 | if (this.isUnmounted) { 222 | return 223 | } 224 | 225 | this.calculateLayout() 226 | this.onRender() 227 | this.unsubscribeExit() 228 | 229 | if (typeof this.restoreConsole === 'function') { 230 | this.restoreConsole() 231 | } 232 | 233 | if (typeof this.unsubscribeResize === 'function') { 234 | this.unsubscribeResize() 235 | } 236 | 237 | this.isUnmounted = true 238 | 239 | reconciler.updateContainer(null, this.container, null, noop) 240 | instances.delete(this.options.stdout) 241 | 242 | if (error instanceof Error) { 243 | this.rejectExitPromise(error) 244 | } else { 245 | this.resolveExitPromise() 246 | } 247 | } 248 | 249 | async waitUntilExit(): Promise { 250 | if (!this.exitPromise) { 251 | this.exitPromise = new Promise((resolve, reject) => { 252 | this.resolveExitPromise = resolve 253 | this.rejectExitPromise = reject 254 | }) 255 | } 256 | 257 | return this.exitPromise 258 | } 259 | 260 | patchConsole(): void { 261 | if (this.options.debug) { 262 | return 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/declarative/rendere/global.d.ts: -------------------------------------------------------------------------------- 1 | import { type ReactNode, type Key, type LegacyRef } from 'react' 2 | import { type DOMElement } from './dom.js' 3 | 4 | declare global { 5 | namespace JSX { 6 | interface IntrinsicElements { 7 | 'edison-box': Edison.Box 8 | 'edison-text': Edison.Text 9 | } 10 | } 11 | } 12 | 13 | declare namespace Edison { 14 | type Box = { 15 | internal_static?: boolean 16 | children?: ReactNode 17 | key?: Key 18 | ref?: LegacyRef 19 | } 20 | 21 | type Text = { 22 | children?: ReactNode 23 | key?: Key 24 | 25 | internal_transform?: (children: string, index: number) => string 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/declarative/rendere/instances.ts: -------------------------------------------------------------------------------- 1 | // Store all instances of Edison (instance.js) to ensure that consecutive render() calls 2 | // use the same instance of Edison and don't create a new one 3 | // 4 | // This map has to be stored in a separate file, because render.js creates instances, 5 | // but instance.js should delete itself from the map on unmount 6 | 7 | import type Edison from './edison' 8 | 9 | const instances = new WeakMap() 10 | export default instances 11 | -------------------------------------------------------------------------------- /src/declarative/rendere/output.ts: -------------------------------------------------------------------------------- 1 | import { type OutputTransformer } from './render-node-to-output.js' 2 | 3 | /** 4 | * "Virtual" output class 5 | * 6 | * Handles the positioning and saving of the output of each node in the tree. 7 | * Also responsible for applying transformations to each character of the output. 8 | * 9 | * Used to generate the final output of all nodes before writing it to actual output stream (e.g. stdout) 10 | */ 11 | 12 | type Operation = WriteOperation | ClipOperation | UnclipOperation 13 | 14 | type WriteOperation = { 15 | type: 'write' 16 | x: number 17 | y: number 18 | text: string 19 | transformers: OutputTransformer[] 20 | } 21 | 22 | type ClipOperation = { 23 | type: 'clip' 24 | clip: Clip 25 | } 26 | 27 | type Clip = { 28 | x1: number | undefined 29 | x2: number | undefined 30 | y1: number | undefined 31 | y2: number | undefined 32 | } 33 | 34 | type UnclipOperation = { 35 | type: 'unclip' 36 | } 37 | 38 | export default class Output { 39 | private readonly operations: Operation[] = [] 40 | 41 | write( 42 | x: number, 43 | y: number, 44 | text: string, 45 | options: { transformers: OutputTransformer[] }, 46 | ): void { 47 | const { transformers } = options 48 | this.operations.push({ 49 | type: 'write', 50 | x, 51 | y, 52 | text, 53 | transformers, 54 | }) 55 | } 56 | 57 | clip(clip: Clip) { 58 | this.operations.push({ 59 | type: 'clip', 60 | clip, 61 | }) 62 | } 63 | 64 | unclip() { 65 | this.operations.push({ 66 | type: 'unclip', 67 | }) 68 | } 69 | 70 | get(): undefined { 71 | // Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved 72 | 73 | const clips: Clip[] = [] 74 | 75 | for (const operation of this.operations) { 76 | if (operation.type === 'clip') { 77 | clips.push(operation.clip) 78 | } 79 | 80 | if (operation.type === 'unclip') { 81 | clips.pop() 82 | } 83 | 84 | if (operation.type === 'write') { 85 | const { text, transformers } = operation 86 | const lines = text.split('\n') 87 | 88 | // eslint-disable-next-line prefer-const 89 | for (let [index, line] of lines.entries()) { 90 | // Line can be missing if `text` is taller than height of pre-initialized `this.output` 91 | for (const transformer of transformers) { 92 | line = transformer(line, index) 93 | } 94 | } 95 | } 96 | } 97 | 98 | return 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/declarative/rendere/reconciler.ts: -------------------------------------------------------------------------------- 1 | import createReconciler from 'react-reconciler' 2 | import { DefaultEventPriority } from 'react-reconciler/constants.js' 3 | import Yoga, { type Node as YogaNode } from 'yoga-wasm-web/auto' 4 | import { 5 | createTextNode, 6 | appendChildNode, 7 | insertBeforeNode, 8 | removeChildNode, 9 | setTextNodeValue, 10 | createNode, 11 | setAttribute, 12 | type DOMNodeAttribute, 13 | type TextNode, 14 | type ElementNames, 15 | type DOMElement, 16 | } from './dom' 17 | import { type OutputTransformer } from './render-node-to-output' 18 | 19 | type AnyObject = Record 20 | 21 | const diff = (before: AnyObject, after: AnyObject): AnyObject | undefined => { 22 | if (before === after) { 23 | return 24 | } 25 | 26 | if (!before) { 27 | return after 28 | } 29 | 30 | const changed: AnyObject = {} 31 | let isChanged = false 32 | 33 | for (const key of Object.keys(before)) { 34 | const isDeleted = after ? !Object.hasOwnProperty.call(after, key) : true 35 | 36 | if (isDeleted) { 37 | changed[key] = undefined 38 | isChanged = true 39 | } 40 | } 41 | 42 | if (after) { 43 | for (const key of Object.keys(after)) { 44 | if (after[key] !== before[key]) { 45 | changed[key] = after[key] 46 | isChanged = true 47 | } 48 | } 49 | } 50 | 51 | return isChanged ? changed : undefined 52 | } 53 | 54 | const cleanupYogaNode = (node?: YogaNode): void => { 55 | node?.unsetMeasureFunc() 56 | node?.freeRecursive() 57 | } 58 | 59 | type Props = Record 60 | 61 | type HostContext = { 62 | isInsideText: boolean 63 | } 64 | 65 | type UpdatePayload = { 66 | props: Props | undefined 67 | } 68 | 69 | export default createReconciler< 70 | ElementNames, 71 | Props, 72 | DOMElement, 73 | DOMElement, 74 | TextNode, 75 | DOMElement, 76 | unknown, 77 | unknown, 78 | HostContext, 79 | UpdatePayload, 80 | unknown, 81 | unknown, 82 | unknown 83 | >({ 84 | getRootHostContext: () => ({ 85 | isInsideText: false, 86 | }), 87 | prepareForCommit: () => null, 88 | preparePortalMount: () => null, 89 | clearContainer: () => false, 90 | resetAfterCommit(rootNode) { 91 | if (typeof rootNode.onComputeLayout === 'function') { 92 | rootNode.onComputeLayout() 93 | } 94 | 95 | // Since renders are throttled at the instance level and component children 96 | // are rendered only once and then get deleted, we need an escape hatch to 97 | // trigger an immediate render to ensure children are written to output before they get erased 98 | if (rootNode.isStaticDirty) { 99 | rootNode.isStaticDirty = false 100 | if (typeof rootNode.onImmediateRender === 'function') { 101 | rootNode.onImmediateRender() 102 | } 103 | 104 | return 105 | } 106 | 107 | if (typeof rootNode.onRender === 'function') { 108 | rootNode.onRender() 109 | } 110 | }, 111 | getChildHostContext(parentHostContext, type) { 112 | const previousIsInsideText = parentHostContext.isInsideText 113 | const isInsideText = 114 | type === 'edison-text' || type === 'edison-virtual-text' 115 | 116 | if (previousIsInsideText === isInsideText) { 117 | return parentHostContext 118 | } 119 | 120 | return { isInsideText } 121 | }, 122 | shouldSetTextContent: () => false, 123 | createInstance(originalType, newProps, _root, hostContext) { 124 | if (hostContext.isInsideText && originalType === 'edison-box') { 125 | throw new Error(` can't be nested inside component`) 126 | } 127 | 128 | const type = 129 | originalType === 'edison-text' && hostContext.isInsideText 130 | ? 'edison-virtual-text' 131 | : originalType 132 | 133 | const node = createNode(type) 134 | 135 | for (const [key, value] of Object.entries(newProps)) { 136 | if (key === 'children') { 137 | continue 138 | } 139 | 140 | if (key === 'internal_transform') { 141 | node.internal_transform = value as OutputTransformer 142 | continue 143 | } 144 | 145 | if (key === 'internal_static') { 146 | node.internal_static = true 147 | continue 148 | } 149 | 150 | setAttribute(node, key, value as DOMNodeAttribute) 151 | } 152 | 153 | return node 154 | }, 155 | createTextInstance(text, _root, hostContext) { 156 | if (!hostContext.isInsideText) { 157 | throw new Error( 158 | `Text string "${text}" must be rendered inside component`, 159 | ) 160 | } 161 | 162 | return createTextNode(text) 163 | }, 164 | resetTextContent() {}, 165 | hideTextInstance(node) { 166 | setTextNodeValue(node, '') 167 | }, 168 | unhideTextInstance(node, text) { 169 | setTextNodeValue(node, text) 170 | }, 171 | getPublicInstance: (instance) => instance, 172 | hideInstance(node) { 173 | node.yogaNode?.setDisplay(Yoga.DISPLAY_NONE) 174 | }, 175 | unhideInstance(node) { 176 | node.yogaNode?.setDisplay(Yoga.DISPLAY_FLEX) 177 | }, 178 | appendInitialChild: appendChildNode, 179 | appendChild: appendChildNode, 180 | insertBefore: insertBeforeNode, 181 | finalizeInitialChildren(node, _type, _props, rootNode) { 182 | if (node.internal_static) { 183 | rootNode.isStaticDirty = true 184 | 185 | // Save reference to node to skip traversal of entire 186 | // node tree to find it 187 | rootNode.staticNode = node 188 | } 189 | 190 | return false 191 | }, 192 | isPrimaryRenderer: true, 193 | supportsMutation: true, 194 | supportsPersistence: false, 195 | supportsHydration: false, 196 | scheduleTimeout: setTimeout, 197 | cancelTimeout: clearTimeout, 198 | noTimeout: -1, 199 | getCurrentEventPriority: () => DefaultEventPriority, 200 | beforeActiveInstanceBlur() {}, 201 | afterActiveInstanceBlur() {}, 202 | detachDeletedInstance() {}, 203 | getInstanceFromNode: () => null, 204 | prepareScopeUpdate() {}, 205 | getInstanceFromScope: () => null, 206 | appendChildToContainer: appendChildNode, 207 | insertInContainerBefore: insertBeforeNode, 208 | removeChildFromContainer(node, removeNode) { 209 | removeChildNode(node, removeNode) 210 | cleanupYogaNode(removeNode.yogaNode) 211 | }, 212 | prepareUpdate(node, _type, oldProps, newProps, rootNode) { 213 | if (node.internal_static) { 214 | rootNode.isStaticDirty = true 215 | } 216 | 217 | const props = diff(oldProps, newProps) 218 | 219 | if (!props) { 220 | return null 221 | } 222 | 223 | return { props } 224 | }, 225 | commitUpdate(node, { props }) { 226 | if (props) { 227 | for (const [key, value] of Object.entries(props)) { 228 | if (key === 'internal_transform') { 229 | node.internal_transform = value as OutputTransformer 230 | continue 231 | } 232 | 233 | if (key === 'internal_static') { 234 | node.internal_static = true 235 | continue 236 | } 237 | 238 | setAttribute(node, key, value as DOMNodeAttribute) 239 | } 240 | } 241 | }, 242 | commitTextUpdate(node, _oldText, newText) { 243 | setTextNodeValue(node, newText) 244 | }, 245 | removeChild(node, removeNode) { 246 | removeChildNode(node, removeNode) 247 | cleanupYogaNode(removeNode.yogaNode) 248 | }, 249 | }) 250 | -------------------------------------------------------------------------------- /src/declarative/rendere/render-node-to-output.ts: -------------------------------------------------------------------------------- 1 | import Yoga from 'yoga-wasm-web/auto' 2 | import { type DOMElement } from './dom' 3 | import type Output from './output' 4 | 5 | // If parent container is ``, text nodes will be treated as separate nodes in 6 | // the tree and will have their own coordinates in the layout. 7 | // To ensure text nodes are aligned correctly, take X and Y of the first text node 8 | // and use it as offset for the rest of the nodes 9 | // Only first node is taken into account, because other text nodes can't have margin or padding, 10 | // so their coordinates will be relative to the first node anyway 11 | 12 | export type OutputTransformer = (s: string, index: number) => string 13 | 14 | // After nodes are laid out, render each to output object, which later gets rendered to terminal 15 | const renderNodeToOutput = ( 16 | node: DOMElement, 17 | output: Output, 18 | options: { 19 | offsetX?: number 20 | offsetY?: number 21 | transformers?: OutputTransformer[] 22 | skipStaticElements: boolean 23 | }, 24 | ) => { 25 | const { 26 | offsetX = 0, 27 | offsetY = 0, 28 | transformers = [], 29 | skipStaticElements, 30 | } = options 31 | 32 | if (skipStaticElements && node.internal_static) { 33 | return 34 | } 35 | 36 | const { yogaNode } = node 37 | 38 | if (yogaNode) { 39 | if (yogaNode.getDisplay() === Yoga.DISPLAY_NONE) { 40 | return 41 | } 42 | 43 | // Left and top positions in Yoga are relative to their parent node 44 | const x = offsetX + yogaNode.getComputedLeft() 45 | const y = offsetY + yogaNode.getComputedTop() 46 | 47 | // Transformers are functions that transform final text output of each component 48 | // See Output class for logic that applies transformers 49 | let newTransformers = transformers 50 | 51 | if (typeof node.internal_transform === 'function') { 52 | newTransformers = [node.internal_transform, ...transformers] 53 | } 54 | 55 | const clipped = false 56 | 57 | if (node.nodeName === 'edison-root' || node.nodeName === 'edison-box') { 58 | for (const childNode of node.childNodes) { 59 | renderNodeToOutput(childNode as DOMElement, output, { 60 | offsetX: x, 61 | offsetY: y, 62 | transformers: newTransformers, 63 | skipStaticElements, 64 | }) 65 | } 66 | 67 | if (clipped) { 68 | output.unclip() 69 | } 70 | } 71 | } 72 | } 73 | 74 | export default renderNodeToOutput 75 | -------------------------------------------------------------------------------- /src/declarative/rendere/render.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process' 2 | import type { ReactNode } from 'react' 3 | import Edison, { type Options as edisonOptions } from './edison' 4 | import instances from './instances' 5 | 6 | export type RenderOptions = { 7 | /** 8 | * Output stream where app will be rendered. 9 | * 10 | * @default process.stdout 11 | */ 12 | stdout?: NodeJS.WriteStream 13 | /** 14 | * Input stream where app will listen for input. 15 | * 16 | * @default process.stdin 17 | */ 18 | stdin?: NodeJS.ReadStream 19 | /** 20 | * Error stream. 21 | * @default process.stderr 22 | */ 23 | stderr?: NodeJS.WriteStream 24 | /** 25 | * If true, each update will be rendered as a separate output, without replacing the previous one. 26 | * 27 | * @default false 28 | */ 29 | debug?: boolean 30 | /** 31 | * Configure whether Edison should listen to Ctrl+C keyboard input and exit the app. This is needed in case `process.stdin` is in raw mode, because then Ctrl+C is ignored by default and process is expected to handle it manually. 32 | * 33 | * @default true 34 | */ 35 | exitOnCtrlC?: boolean 36 | 37 | /** 38 | * Patch console methods to ensure console output doesn't mix with Edison output. 39 | * 40 | * @default true 41 | */ 42 | patchConsole?: boolean 43 | } 44 | 45 | export type Instance = { 46 | /** 47 | * Replace previous root node with a new one or update props of the current root node. 48 | */ 49 | rerender: Edison['render'] 50 | /** 51 | * Manually unmount the whole Edison app. 52 | */ 53 | unmount: Edison['unmount'] 54 | /** 55 | * Returns a promise, which resolves when app is unmounted. 56 | */ 57 | waitUntilExit: Edison['waitUntilExit'] 58 | cleanup: () => void 59 | } 60 | 61 | /** 62 | * Mount a component and render the output. 63 | */ 64 | export const render = (node: ReactNode): Instance => { 65 | const edisonOptions: edisonOptions = { 66 | stdout: process.stdout, 67 | stdin: process.stdin, 68 | stderr: process.stderr, 69 | debug: false, 70 | exitOnCtrlC: true, 71 | patchConsole: true, 72 | } 73 | 74 | const instance: Edison = getInstance( 75 | edisonOptions.stdout, 76 | () => new Edison(edisonOptions), 77 | ) 78 | 79 | instance.render(node) 80 | 81 | return { 82 | rerender: instance.render, 83 | unmount() { 84 | instance.unmount() 85 | }, 86 | waitUntilExit: instance.waitUntilExit, 87 | cleanup: () => instances.delete(edisonOptions.stdout), 88 | } 89 | } 90 | 91 | const getInstance = ( 92 | stdout: NodeJS.WriteStream, 93 | createInstance: () => Edison, 94 | ): Edison => { 95 | let instance = instances.get(stdout) 96 | 97 | if (!instance) { 98 | instance = createInstance() 99 | instances.set(stdout, instance) 100 | } 101 | 102 | return instance 103 | } 104 | -------------------------------------------------------------------------------- /src/declarative/rendere/renderer.ts: -------------------------------------------------------------------------------- 1 | type Result = { 2 | output: string 3 | outputHeight: number 4 | staticOutput: string 5 | } 6 | 7 | const renderer = (): Result => { 8 | return { 9 | output: '', 10 | outputHeight: 0, 11 | staticOutput: '', 12 | } 13 | } 14 | 15 | export default renderer 16 | -------------------------------------------------------------------------------- /src/declarative/utils/Board.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext } from 'react' 2 | import type { SerialPort } from 'serialport' 3 | import { board } from '../../procedure/utils/board' 4 | 5 | export const BoardContext = createContext(null) 6 | 7 | type BoardProps = { 8 | children: React.ReactNode 9 | port: string 10 | baudRate: number 11 | } 12 | 13 | export const Board: React.FC = ({ children, port, baudRate }) => { 14 | const currentPort = board.getCurrentPort() 15 | 16 | if (currentPort) { 17 | return ( 18 | 19 | {children} 20 | 21 | ) 22 | } 23 | board.connectManual(port, baudRate) 24 | return ( 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { board } from './procedure/utils/board' 2 | //------------utils-----------------// 3 | export { render } from './declarative/rendere/render' 4 | export { delay } from './procedure/utils/delay' 5 | 6 | //-----------declarative----------------// 7 | export { Board } from './declarative/utils/Board' 8 | export { Led } from './declarative/components/output/Led' 9 | export { Buzzer } from './declarative/components/output/Buzzer' 10 | export { Button } from './declarative/components/input/Button' 11 | export { Collision } from './declarative/components/input/Collision' 12 | export { HallEffective } from './declarative/components/input/HallEffectSensor' 13 | export { DigitalTilt } from './declarative/components/input/DigitalTiltSensor' 14 | export { PIRMotion } from './declarative/components/input/PIRMotionSensor' 15 | export { Input } from './declarative/components/input/Input' 16 | export { Servo } from './declarative/components/servo/Servo' 17 | export { PhotoInterrupter } from './declarative/components/input/PhotoInterrupter' 18 | export { Output } from './declarative/components/output/Output' 19 | 20 | export type { SerialPort } from 'serialport' 21 | -------------------------------------------------------------------------------- /src/procedure/analog/analogPort.ts: -------------------------------------------------------------------------------- 1 | // analogPort.ts 2 | import type { SerialPort } from 'serialport' 3 | import { Observable } from 'rxjs' 4 | import type { AnalogPin, Sensor } from '../types/analog/analog' 5 | import { analogPinMapping } from '../types/analog/analog' 6 | import type { Subscription } from 'rxjs' 7 | 8 | export const analogPort = (port: SerialPort) => { 9 | return (analogPin: AnalogPin) => { 10 | const pin = analogPinMapping[analogPin] 11 | let subscription: Subscription 12 | let prevValue: number | undefined 13 | const currentValue: number = 0 14 | 15 | const setAnalogState = () => { 16 | const REPORT_ANALOG = 0xc0 17 | const ANALOG_MESSAGE = 0xe0 18 | 19 | return new Observable((observer) => { 20 | const buffer = Buffer.from([REPORT_ANALOG | pin, 1]) 21 | port.write(buffer) 22 | 23 | const onData = (data: Buffer) => { 24 | if ((data[0] & 0xf0) === ANALOG_MESSAGE && pin === (data[0] & 0x0f)) { 25 | const value = data[1] | (data[2] << 7) 26 | observer.next(value) 27 | } 28 | } 29 | port.on('data', onData) 30 | 31 | return () => { 32 | port.off('data', onData) 33 | } 34 | }) 35 | } 36 | 37 | return { 38 | read: async ( 39 | method: Sensor, 40 | func: ( 41 | value: number, 42 | ) => Promise | Promise | void | number, 43 | ): Promise => { 44 | const observable = setAnalogState() 45 | 46 | subscription = observable.subscribe((value: number) => { 47 | if (prevValue !== undefined) { 48 | // first emit will be skipped 49 | if (value && method === 'on') { 50 | func(Number(value)) 51 | } else if (value === 0 && method === 'off') { 52 | func(Number(value)) 53 | } else if (method === 'change' && value !== prevValue) { 54 | func(Number(value)) 55 | } 56 | } 57 | prevValue = value 58 | }) 59 | }, 60 | stop: () => { 61 | if (subscription) { 62 | subscription.unsubscribe() 63 | } 64 | }, 65 | getValue: () => currentValue, 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/procedure/analog/uniqueDevice/analog.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import type { AnalogPin, Sensor } from '../../types/analog/analog' 3 | import { analogPort } from '../analogPort' 4 | 5 | export const attachAnalog = (port: SerialPort, pin: AnalogPin) => { 6 | const analogSensor = analogPort(port)(pin) 7 | let currentValue: number = 0 8 | 9 | return { 10 | read: async ( 11 | method: Sensor, 12 | func: (value: number) => Promise | Promise | void | number, 13 | ): Promise => { 14 | return analogSensor.read(method, async (value: number) => { 15 | currentValue = value 16 | await func(value) 17 | }) 18 | }, 19 | getValue: () => currentValue, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/procedure/analog/uniqueDevice/pressureSensor.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import type { AnalogPin, Sensor } from '../../types/analog/analog' 3 | import { analogPort } from '../analogPort' 4 | 5 | export const attachPressureSensor = (port: SerialPort, pin: AnalogPin) => { 6 | const pressureSensor = analogPort(port)(pin) 7 | let isTriggered = false 8 | 9 | return { 10 | read: async ( 11 | method: Sensor, 12 | func: () => Promise | Promise | void | number, 13 | ): Promise => { 14 | return pressureSensor.read(method, async () => { 15 | //triger once 16 | if (isTriggered === false) { 17 | isTriggered = true 18 | await func() 19 | } 20 | }) 21 | }, 22 | // onOver: async ( 23 | // method: Sensor, 24 | // func: () => Promise | Promise | void | number, 25 | // ): Promise => { 26 | // return 27 | // }, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/procedure/complex/ultrasonicSensor.ts: -------------------------------------------------------------------------------- 1 | import { outputPort } from '../output/outputPort' 2 | import { inputPort } from '../input/inputPort' 3 | import { delay } from '../../index' 4 | import type { SerialPort } from '../../index' 5 | import type { Sensor } from '../types/analog/analog' 6 | 7 | export const attachUltrasonicSensor = ( 8 | port: SerialPort, 9 | trigPin: number, 10 | echoPin: number, 11 | ) => { 12 | const trig = outputPort(port)(trigPin) 13 | const echo = inputPort(port)(echoPin) 14 | 15 | return { 16 | measure: async ( 17 | method: Sensor, 18 | func: () => Promise | Promise | void | number, 19 | ): Promise => { 20 | await echo.read(method, async () => { 21 | await func() 22 | }) 23 | 24 | // eslint-disable-next-line no-constant-condition 25 | while (true) { 26 | await trig.on() 27 | await delay(20) 28 | await trig.off() 29 | await delay(20) 30 | } 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/procedure/helper/Analog/bufferAnalog.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | export const bufferAnalog = ( 4 | port: SerialPort, 5 | buffer: Buffer, 6 | ): Promise => { 7 | return new Promise((resolve, reject) => { 8 | port.write(buffer, (err) => { 9 | if (err) { 10 | reject(err) 11 | } else { 12 | resolve() 13 | } 14 | }) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/procedure/helper/Analog/setPinAnalog.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | export const setPinAnalog = (pin: number, port: SerialPort): Promise => { 4 | const SET_PIN_MODE = 0xf4 5 | // const REPORT_ANALOG = 0xc0 // Enable analog reporting for pin 6 | // const ANALOG_MESSAGE = 0xe0 // Analog message command 7 | return new Promise((resolve, reject) => { 8 | const setPinModeOutput = Buffer.from([SET_PIN_MODE, pin, 2]) 9 | port.write(setPinModeOutput, (err) => { 10 | if (err) { 11 | reject(err) 12 | } else { 13 | resolve() 14 | } 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /src/procedure/helper/Input/setInputState.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { bufferWrite } from '../Utils/bufferWrite' 3 | import type { Observable } from 'rxjs' 4 | import { fromEventPattern } from 'rxjs' 5 | import { distinctUntilChanged, filter, map } from 'rxjs/operators' 6 | import { Buffer } from 'node:buffer' 7 | 8 | const SET_PIN_MODE = 0xf4 9 | const INPUT_MODE = 0x00 10 | const DIGITAL_CHANGE_MESSAGE = 0xd0 11 | 12 | export const setInputState = ( 13 | pin: number, 14 | port: SerialPort, 15 | ): Observable => { 16 | bufferWrite(port, Buffer.from([SET_PIN_MODE, pin, INPUT_MODE])) 17 | bufferWrite( 18 | port, 19 | Buffer.from([DIGITAL_CHANGE_MESSAGE + Math.floor(pin / 8), 1]), 20 | ) 21 | 22 | return fromEventPattern( 23 | (handler) => port.on('data', handler), 24 | (handler) => port.removeListener('data', handler), 25 | ).pipe( 26 | map((data) => (data.length <= 3 ? data : data.subarray(0, 3))), 27 | filter((data) => (data[0] & 0x0f) === Math.floor(pin / 8)), 28 | map((data) => ((data[1] >> pin % 8) & 1) === 1), 29 | distinctUntilChanged(), 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/procedure/helper/Input/setPinInput.ts: -------------------------------------------------------------------------------- 1 | // import type { SerialPort } from 'serialport' 2 | 3 | // export const setPinInput = (pin: number, port: SerialPort): Promise => { 4 | // return new Promise((resolve, reject) => { 5 | // const setPinModeInput = Buffer.from([0xf4, pin, 0]) 6 | // port.write(setPinModeInput, (err) => { 7 | // if (err) { 8 | // //console.log('Error on write: ', err.message); 9 | // reject(err) 10 | // } else { 11 | // resolve() 12 | // } 13 | // }) 14 | // }) 15 | // } 16 | -------------------------------------------------------------------------------- /src/procedure/helper/Output/setAnalogOutput.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { bufferWrite } from '../Utils/bufferWrite' 3 | 4 | export const setAnalogOutput = async ( 5 | pin: number, 6 | value: number, 7 | port: SerialPort, 8 | ) => { 9 | const ANALOG_MESSAGE = 0xe0 10 | 11 | const buffer = Buffer.from([ 12 | ANALOG_MESSAGE + pin, 13 | value & 0x7f, 14 | (value >> 7) & 0x7f, 15 | ]) 16 | 17 | await bufferWrite(port, buffer) 18 | } 19 | -------------------------------------------------------------------------------- /src/procedure/helper/Output/setOutputState.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { setPinOutput } from './setPinOutput' 3 | import { bufferWrite } from '../Utils/bufferWrite' 4 | import { delay } from '../../utils/delay' 5 | 6 | export const setOutputState = async ( 7 | pin: number, 8 | onoff: boolean, 9 | port: SerialPort, 10 | ) => { 11 | const IOMESSAGE = 0x90 12 | 13 | const on = async (): Promise => { 14 | await setPinOutput(pin, port) 15 | const bufferValue = 1 << (pin & 0x07) 16 | const buffer = Buffer.from([IOMESSAGE + (pin >> 3), bufferValue, 0x00]) 17 | await bufferWrite(port, buffer) 18 | return 19 | } 20 | 21 | const off = async (): Promise => { 22 | await setPinOutput(pin, port) 23 | const bufferValue = 1 << 0x00 24 | const buffer = Buffer.from([IOMESSAGE + (pin >> 3), bufferValue, 0x00]) 25 | await bufferWrite(port, buffer) 26 | return 27 | } 28 | 29 | if (onoff) { 30 | on() 31 | await delay(20) 32 | } 33 | if (!onoff) { 34 | off() 35 | await delay(20) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/procedure/helper/Output/setPinOutput.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | export const setPinOutput = (pin: number, port: SerialPort): Promise => { 4 | return new Promise((resolve, reject) => { 5 | const setPinModeOutput = Buffer.from([0xf4, pin, 1]) 6 | port.write(setPinModeOutput, (err) => { 7 | if (err) { 8 | reject(err) 9 | } else { 10 | resolve() 11 | } 12 | }) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/procedure/helper/PWM/setPwmState.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { bufferWrite } from '../Utils/bufferWrite' 3 | 4 | const SET_PIN_MODE = 0xf4 5 | const PWM_MODE = 0x03 6 | const ANALOG_MESSAGE = 0xe0 7 | 8 | export const setPwmState = async ( 9 | pin: number, 10 | value: number, 11 | port: SerialPort, 12 | ) => { 13 | const modeBuffer = Buffer.from([SET_PIN_MODE, pin, PWM_MODE]) 14 | await bufferWrite(port, modeBuffer) 15 | const pwmBuffer = Buffer.from([ 16 | ANALOG_MESSAGE | (pin & 0x0f), 17 | value & 0x7f, 18 | (value >> 7) & 0x7f, 19 | ]) 20 | await bufferWrite(port, pwmBuffer) 21 | } 22 | -------------------------------------------------------------------------------- /src/procedure/helper/Servo/setPinToServo.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | // Switch pin 9 to servo mode 4 | export const setPinToServo = (pin: number, port: SerialPort): Promise => { 5 | const PIN_MODE_SERVO = 0x04 6 | const PIN = pin 7 | const data = [ 8 | 0xf4, // Set pin mode command 9 | PIN, 10 | PIN_MODE_SERVO, // Pin mode 11 | ] 12 | port.write(Buffer.from(data)) 13 | return Promise.resolve() 14 | } 15 | -------------------------------------------------------------------------------- /src/procedure/helper/Servo/setServoAngle.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | // Set servo angle on Arduino 4 | export const setServoAngle = ( 5 | pin: number, 6 | angle: number, 7 | port: SerialPort, 8 | ): Promise => { 9 | const PIN = pin 10 | const data = [ 11 | 0xe0 | PIN, // Start analog message for pin 12 | angle & 0x7f, // Send 7 bits only 13 | (angle >> 7) & 0x7f, // bitwise shift and take next 7 bits 14 | ] 15 | port.write(Buffer.from(data)) 16 | return Promise.resolve() 17 | } 18 | -------------------------------------------------------------------------------- /src/procedure/helper/Utils/bufferWrite.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | export const bufferWrite = ( 4 | port: SerialPort, 5 | buffer: Buffer, 6 | ): Promise => { 7 | return new Promise((resolve, reject) => { 8 | port.write(buffer, (err) => { 9 | if (err) { 10 | reject(err) 11 | } else { 12 | resolve() 13 | } 14 | }) 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /src/procedure/input/inputPort.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { setInputState } from '../helper/Input/setInputState' 3 | import type { Sensor } from '../types/analog/analog' 4 | 5 | export const inputPort = (port: SerialPort) => { 6 | return (pin: number) => { 7 | let prevValue: boolean = false 8 | 9 | return { 10 | read: ( 11 | method: Sensor, 12 | func: () => Promise | Promise | void | number, 13 | ): void => { 14 | const observable = setInputState(pin, port) 15 | 16 | observable.subscribe((value: boolean) => { 17 | if (method === 'change' && value !== prevValue) { 18 | prevValue = value 19 | func() 20 | } 21 | if (method === 'off' && !value && prevValue !== value) { 22 | prevValue = value 23 | func() 24 | } 25 | if (method === 'on' && value && prevValue !== value) { 26 | prevValue = value 27 | func() 28 | } 29 | }) 30 | }, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/procedure/input/uniqueDevice/input.ts: -------------------------------------------------------------------------------- 1 | import { inputPort } from '../inputPort' 2 | import type { SerialPort } from 'serialport' 3 | import type { Sensor } from '../../types/analog/analog' 4 | 5 | export const attachInput = (port: SerialPort, pin: number) => { 6 | const collisionSensor = inputPort(port)(pin) 7 | 8 | return { 9 | read: async ( 10 | method: Sensor, 11 | func: () => Promise | Promise | void | number, 12 | ): Promise => { 13 | return collisionSensor.read(method, async () => { 14 | //triger once 15 | await func() 16 | }) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/procedure/output/outputPort.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { setOutputState } from '../helper/Output/setOutputState' 3 | import { setAnalogOutput } from '../helper/Output/setAnalogOutput' 4 | 5 | export const outputPort = (port: SerialPort) => { 6 | return (pin: number) => { 7 | return { 8 | on: async () => { 9 | await setOutputState(pin, true, port) 10 | }, 11 | off: async () => { 12 | await setOutputState(pin, false, port) 13 | }, 14 | analogWrite: async (value: number) => { 15 | await setAnalogOutput(pin, value, port) 16 | }, 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/procedure/output/uniqueDevice/led.ts: -------------------------------------------------------------------------------- 1 | import { outputPort } from '../outputPort' 2 | import type { SerialPort } from 'serialport' 3 | import { delay } from '../../utils/delay' 4 | 5 | export const attachLed = (port: SerialPort, pin: number) => { 6 | const led = outputPort(port)(pin) 7 | 8 | return { 9 | blink: async (duration: number) => { 10 | // eslint-disable-next-line no-constant-condition 11 | while (true) { 12 | await led.on() 13 | await delay(duration) 14 | await led.off() 15 | await delay(duration) 16 | } 17 | }, 18 | on: async () => { 19 | await led.on() 20 | }, 21 | off: async () => { 22 | await led.off() 23 | }, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/procedure/output/uniqueDevice/output.ts: -------------------------------------------------------------------------------- 1 | import { outputPort } from '../outputPort' 2 | import type { SerialPort } from 'serialport' 3 | 4 | export const attachOutput = (port: SerialPort, pin: number) => { 5 | const output = outputPort(port)(pin) 6 | 7 | return { 8 | on: async () => { 9 | await output.on() 10 | }, 11 | off: async () => { 12 | await output.off() 13 | }, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/procedure/pwm/pwmPort.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { setPwmState } from '../helper/PWM/setPwmState' 3 | 4 | export const pwmPort = (port: SerialPort) => { 5 | return (pin: number) => { 6 | return { 7 | analogWrite: async (value: number) => { 8 | await setPwmState(pin, value, port) 9 | }, 10 | off: async () => { 11 | await setPwmState(pin, 0, port) 12 | }, 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/procedure/pwm/uniqueDevice/passiveBuzzer.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { pwmPort } from '../pwmPort' 3 | 4 | export const attachPassiveBuzzer = (port: SerialPort, pin: number) => { 5 | // Initialize pins to OUTPUT and set them to OFF 6 | const passiveBuzzer = pwmPort(port)(pin) 7 | passiveBuzzer.off() 8 | return { 9 | // r will be changed from number to template literal in the future 10 | sound: async (r: number) => { 11 | await passiveBuzzer.analogWrite(r) 12 | }, 13 | off: async () => { 14 | await passiveBuzzer.off() 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/procedure/pwm/uniqueDevice/vibrationSensor.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { pwmPort } from '../pwmPort' 3 | 4 | export const attachVibrationSensor = (port: SerialPort, pin: number) => { 5 | // Initialize pins to OUTPUT and set them to OFF 6 | const vibrationSensor = pwmPort(port)(pin) 7 | vibrationSensor.off() 8 | return { 9 | // r will be changed from number to template literal in the future 10 | write: async (r: number) => { 11 | await vibrationSensor.analogWrite(r) 12 | }, 13 | off: async () => { 14 | await vibrationSensor.off() 15 | }, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/procedure/servo/servoPort.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { setPinToServo } from '../helper/Servo/setPinToServo' 3 | import { setServoAngle } from '../helper/Servo/setServoAngle' 4 | import { delay } from '../utils/delay' 5 | 6 | export const servoPort = (port: SerialPort) => { 7 | return (pin: number) => { 8 | return { 9 | setAngle: async (angle: number) => { 10 | const TAKE_TIME_SECOND = 2 11 | 12 | await setPinToServo(pin, port) 13 | await setServoAngle(pin, angle, port) 14 | 15 | const time = 180 + Math.abs(angle - 90) * TAKE_TIME_SECOND 16 | await delay(time) 17 | }, 18 | rotate: async (speed: number) => { 19 | await setPinToServo(pin, port) 20 | await setServoAngle(pin, speed, port) 21 | }, 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/procedure/servo/uniqueDevice/rotationServo.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { servoPort } from '../servoPort' 3 | 4 | export const attachRotationServo = (port: SerialPort, pin: number) => { 5 | const servo = servoPort(port)(pin) 6 | 7 | return { 8 | rotate: async (speed: number) => { 9 | //0 <=speed <= 180. Stops when speed = 90. speed < 90 or 90 < speed to change direction of rotation 10 | 11 | await servo.rotate(speed) 12 | }, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/procedure/servo/uniqueDevice/servo.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { servoPort } from '../servoPort' 3 | 4 | export const attachServo = (port: SerialPort, pin: number) => { 5 | const servo = servoPort(port)(pin) 6 | 7 | return { 8 | setAngle: async (angle: number) => { 9 | await servo.setAngle(angle) 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/procedure/types/Mode.ts: -------------------------------------------------------------------------------- 1 | export type Mode = 'Servo' | 'Output' | 'Input' | 'Pwm' 2 | 3 | export type InputMode = { 4 | mode: 'Input' 5 | } 6 | 7 | export type OutputMode = { 8 | mode: 'Output' 9 | } 10 | 11 | export type PwmMode = { 12 | on: () => void 13 | off: () => void 14 | } 15 | 16 | export type ServoMode = { 17 | rotate: (angle: number) => Promise 18 | } 19 | -------------------------------------------------------------------------------- /src/procedure/types/analog/analog.ts: -------------------------------------------------------------------------------- 1 | export type AnalogpinMapping = { 2 | A0: 0 3 | A1: 1 4 | A2: 2 5 | A3: 3 6 | A4: 4 7 | A5: 5 8 | } 9 | 10 | export type AnalogPin = keyof AnalogpinMapping 11 | 12 | export const analogPinMapping: AnalogpinMapping = { 13 | A0: 0, 14 | A1: 1, 15 | A2: 2, 16 | A3: 3, 17 | A4: 4, 18 | A5: 5, 19 | } 20 | 21 | export type Sensor = 'on' | 'off' | 'change' | 'sonic' 22 | -------------------------------------------------------------------------------- /src/procedure/uniqueDevice/prettierChange.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | import { bufferAnalog } from '../helper/Analog/bufferAnalog' 3 | import { Observable } from 'rxjs' 4 | 5 | export const prettierChange = (pin: number, port: SerialPort) => { 6 | const REPORT_ANALOG = 0xc0 7 | const ANALOG_MESSAGE = 0xe0 8 | const CONSECUTIVE_ON = 5 9 | const CONSSECUTIVE_OFF = 5 10 | 11 | let onCount = 0 12 | let offCount = 0 13 | return new Observable((observer) => { 14 | const buffer = Buffer.from([REPORT_ANALOG | pin, 1]) 15 | bufferAnalog(port, buffer) 16 | 17 | const onData = (data: Buffer) => { 18 | if ((data[0] & 0xf0) === ANALOG_MESSAGE) { 19 | const pinData = data[0] & 0x0f 20 | if (pin === pinData) { 21 | const value = data[1] | (data[2] << 7) 22 | // is 0 consecutive? 23 | if (value < 10) { 24 | onCount++ 25 | offCount = 0 26 | if (onCount >= CONSECUTIVE_ON) { 27 | observer.next(true) 28 | onCount = 0 // reset Count 29 | } 30 | } else { 31 | offCount++ 32 | onCount = 0 // if not consecutive, reset Count 33 | if (offCount >= CONSSECUTIVE_OFF) { 34 | observer.next(false) 35 | offCount = 0 // reset Count 36 | } 37 | } 38 | } 39 | } 40 | } 41 | port.on('data', onData) 42 | 43 | return () => { 44 | port.off('data', onData) 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /src/procedure/utils/board.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | import { SerialPort } from 'serialport' 3 | import ora from 'ora' 4 | import { delay } from './delay' 5 | 6 | const boardEmitter = new EventEmitter() 7 | let currentPort: SerialPort | null = null 8 | let isPortActive = false 9 | 10 | const MAX_RECENT_LISTENERS = 2 11 | 12 | const confirmConnection = async (port: SerialPort) => { 13 | const endTimeout = Date.now() + 10000 14 | 15 | while (Date.now() < endTimeout) { 16 | port.write(Buffer.from([0xf0, 0x79, 0xf7])) 17 | if (isPortActive) { 18 | break 19 | } 20 | await delay(100) 21 | } 22 | } 23 | 24 | const connectManual = async (path: string, baudRate: number) => { 25 | const spinner = ora('Now Connecting to Device...').start() 26 | 27 | if (currentPort) { 28 | spinner.fail('Port is already used.') 29 | process.exit(1) 30 | } 31 | 32 | try { 33 | const port = new SerialPort({ path, baudRate }, (error) => { 34 | if (error) { 35 | spinner.fail( 36 | 'Failed to open port\n ' + 37 | error + 38 | `.\n ---------------------------------------------------------\nWSL: If you are using WSL, run the command 39 | \`\`\`sh 40 | ls/dev/tty* 41 | \`\`\` 42 | ---------------------------------------------------------- 43 | to see if the path you are passing to the "port" property of the component exists.\n 44 | Windows: the "port" is the COMx uploaded to microcomputer.`, 45 | ) 46 | currentPort = null 47 | process.exit(1) 48 | } 49 | }) 50 | 51 | currentPort = port 52 | 53 | const onData = () => { 54 | if (!isPortActive) { 55 | spinner.succeed('Device is connected successfully!') 56 | boardEmitter.emit('ready', port) 57 | isPortActive = true 58 | } 59 | 60 | const allListeners = port.listeners('data') as ((...args: []) => void)[] 61 | const oldListeners = allListeners.slice(0, -MAX_RECENT_LISTENERS) 62 | 63 | //console.log('data', port.listenerCount('data')) 64 | oldListeners.forEach((listener) => { 65 | if (listener !== onData) { 66 | port.removeListener('data', listener) 67 | } 68 | }) 69 | } 70 | port.on('data', onData) 71 | 72 | port.on('close', () => { 73 | spinner.fail('Port is closed.') 74 | currentPort = null 75 | port.removeAllListeners() 76 | isPortActive = false 77 | process.exit(1) 78 | }) 79 | 80 | port.on('error', (error) => { 81 | console.error('Serial port error:', error) 82 | }) 83 | 84 | await confirmConnection(port) 85 | 86 | if (!isPortActive) { 87 | spinner.fail('Failed to connect to device within 10 seconds.') 88 | process.exit(1) 89 | } 90 | } catch (error) { 91 | spinner.fail('Failed to open port: ' + error) 92 | currentPort = null 93 | process.exit(1) 94 | } 95 | } 96 | 97 | export const board = { 98 | on: boardEmitter.on.bind(boardEmitter), 99 | off: boardEmitter.off.bind(boardEmitter), 100 | connectManual, 101 | getCurrentPort: () => currentPort, 102 | isReady: () => isPortActive, 103 | } 104 | -------------------------------------------------------------------------------- /src/procedure/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export const delay = (ms: number): Promise => { 2 | return new Promise((resolve) => setTimeout(resolve, ms)) 3 | } 4 | -------------------------------------------------------------------------------- /src/procedure/utils/findArduinoPath.ts: -------------------------------------------------------------------------------- 1 | import { SerialPort } from 'serialport' 2 | 3 | export const findArduinoPath = async (): Promise => { 4 | try { 5 | const ports = await SerialPort.list() 6 | for (const port of ports) { 7 | if (port.manufacturer?.includes('Arduino')) { 8 | return port.path 9 | } 10 | } 11 | //There is no arduino board 12 | return null 13 | } catch (error) { 14 | console.error( 15 | 'An error occurred while finding the Arduino port. Check your connection with your device:', 16 | error, 17 | ) 18 | return null 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/procedure/utils/portClose.ts: -------------------------------------------------------------------------------- 1 | import type { SerialPort } from 'serialport' 2 | 3 | // This function closes the serial port. Console will be closed. 4 | export const portClose = (port: SerialPort): Promise => { 5 | return new Promise((resolve, reject) => { 6 | port.close((err) => { 7 | if (err) { 8 | reject(err) 9 | } else { 10 | resolve() 11 | } 12 | }) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /src/procedure/utils/portOpen.ts: -------------------------------------------------------------------------------- 1 | import { SerialPort } from 'serialport' 2 | 3 | export const portOpen = (path: string): SerialPort => { 4 | const port = new SerialPort({ path, baudRate: 57600 }) 5 | return port 6 | } 7 | -------------------------------------------------------------------------------- /src/procedure/utils/setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | import type { SerialPort } from 'serialport' 3 | import { findArduinoPath } from './findArduinoPath' 4 | import { outputPort } from '../factory/output/outputPort' 5 | import { servoPort } from '../factory/servo/servoPort' 6 | import { analogPort } from '../factory/analog/analogPort' 7 | 8 | // This func is assured to be called after the arduino is ready 9 | export const setup = async () => { 10 | const START_SYSEX = 0xf0 11 | const END_SYSEX = 0xf7 12 | const SET_PIN_MODE = 0xf4 13 | const REPORT_ANALOG = 0xc0 // Enable analog reporting for pin 14 | const ANALOG_MESSAGE = 0xe0 // Analog message command 15 | 16 | const path = await findArduinoPath() 17 | 18 | const port = new SerialPort({ path, baudRate: 57600 }) 19 | 20 | let onDataReady: (() => void) | null = null 21 | const dataReady = new Promise((resolve) => { 22 | onDataReady = resolve 23 | }) 24 | 25 | port.on('data', (data) => { 26 | if (onDataReady) { 27 | onDataReady() 28 | } 29 | }) 30 | await dataReady 31 | 32 | return { 33 | servo: servoPort(port), 34 | led: outputPort(port), 35 | buzzer: outputPort(port), 36 | pressureSensor: analogPort(port), 37 | } 38 | } 39 | */ 40 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "outDir": "./dist/esm", 8 | "paths": {} 9 | }, 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "*": ["src/*"] 6 | }, 7 | "target": "ES2022", 8 | "module": "ESNext", 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "declarationDir": "dist/esm", 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "outDir": "./dist", 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "esModuleInterop": true, 19 | "jsx": "react-jsx", 20 | "allowJs": true 21 | }, 22 | "include": ["src", "global.d.ts"], 23 | "exclude": ["node_modules", "./dist", "./src/__tests__"] 24 | } 25 | -------------------------------------------------------------------------------- /vite-env.d.ts: -------------------------------------------------------------------------------- 1 | declare const __APP_VERSION__: string 2 | declare const __APP_NAME__: string 3 | declare const __APP_DESCRIPTION__: string 4 | declare const __APP_AUTHOR__: string 5 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { resolve } from 'path' 3 | 4 | export default defineConfig({ 5 | resolve: { 6 | alias: { 7 | '@': resolve(__dirname, './src'), 8 | }, 9 | }, 10 | }) 11 | --------------------------------------------------------------------------------