diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 650e463..22f9a52 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,6 +6,7 @@ module.exports = { 'plugin:react-hooks/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:react-hooks/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', diff --git a/README.md b/README.md index 4ef8a85..9230653 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

- なんかいい感じの画像 + Something nice img

diff --git a/package.json b/package.json index d28df0d..87c17c2 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,25 @@ { "dependencies": { - "@types/microtime": "^2.1.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "@types/stack-utils": "^2.0.3", + "ansi-escapes": "^6.2.0", + "auto-bind": "^5.0.1", + "cli-cursor": "^4.0.0", + "code-excerpt": "^4.0.0", + "is-in-ci": "^0.1.0", + "lodash": "^4.17.21", "rxjs": "^7.8.1", "serialport": "^12.0.0", + "stack-utils": "^2.0.6", "ts-node": "^10.9.1", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "yoga": "^0.0.20", + "yoga-wasm-web": "^0.3.3" }, "devDependencies": { + "@types/ansi-escapes": "^4.0.0", + "@types/auto-bind": "^2.1.0", + "@types/lodash": "^4.14.202", "@types/node": "^20.8.8", - "@types/react": "^18.2.52", - "@types/react-dom": "^18.2.18", "@types/serialport": "^8.0.3", "@typescript-eslint/eslint-plugin": "^6.18.1", "@vitejs/plugin-react": "^4.2.1", @@ -21,18 +29,27 @@ "eslint-plugin-prettier": "^5.0.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", + "is-in-ci": "^0.1.0", + "patch-console": "^2.0.0", "prettier": "^3.0.3", + "signal-exit": "^4.1.0", "vite": "^5.1.1", "vite-node": "^1.2.2", "vite-plugin-checker": "^0.6.2", "vitest": "^0.34.6" }, + "peerDependencies": { + "@types/react": "^18.2.55", + "@types/react-reconciler": "^0.28.8", + "react": "^18.2.0", + "react-reconciler": "^0.29.0" + }, "name": "edison", "type": "module", - "version": "0.1.33", + "version": "0.1.34", "exports": { ".": { - "import": "./dist/esm/index.mjs", + "import": "./dist/esm/index.js", "types": "./dist/index.d.ts" }, "./package.json": "./package.json", @@ -53,7 +70,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/AllenShintani/Edison.git" + "url": "git+https://github.com/edison-js/Edison.git" }, "keywords": [ "IoT", @@ -68,7 +85,7 @@ "author": "aluta", "license": "MIT", "bugs": { - "url": "https://github.com/AllenShintani/Edison/issues" + "url": "https://github.com/edison-js/Edison/issues" }, - "homepage": "https://github.com/AllenShintani/Edison#readme" + "homepage": "https://github.com/edison-js/Edison#readme" } diff --git a/rename-to-esm.mjs b/rename-to-esm.mjs index 52fff02..32a3c0e 100644 --- a/rename-to-esm.mjs +++ b/rename-to-esm.mjs @@ -1,21 +1,37 @@ -// rename-to-esm.mjs +// rename-and-fix-imports.mjs import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; -import { readdirSync, renameSync } from 'fs'; +import { readFileSync, writeFileSync } from 'fs'; +import { promisify } from 'util'; +import glob from 'glob'; + +const globPromise = promisify(glob); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// -const renameJsToMjs = (dir) => { - const items = readdirSync(dir); - for (const item of items) { - const currentPath = join(dir, item); - if (item.endsWith('.js')) { - renameSync(currentPath, currentPath.replace('.js', '.mjs')); +// Import文のパスを修正する関数 +const fixImportPaths = async (file) => { + let content = readFileSync(file, 'utf8'); + // 正規表現で、'./' または '../'で始まるパスのimport文を対象にし、'.js'がない場合は追加 + content = content.replace(/(from\s+['"])(\.\/|\.\.\/)([^'"]+?)['"]/g, (match, p1, p2, p3) => { + // '.js'で終わっていないパスに'.js'を追加 + if (!p3.endsWith('.js')) { + return `${p1}${p2}${p3}.js'`; } - } + return match; + }); + writeFileSync(file, content, 'utf8'); +}; + +// 特定のディレクトリ内のすべての.jsファイルを走査し、import文のパスを修正 +const fixImportsInDirectory = async (directory) => { + const files = await globPromise(`${directory}/**/*.js`); // ディレクトリ内の全ての.jsファイルを取得 + // biome-ignore lint/complexity/noForEach: +files.forEach(file => { + fixImportPaths(file); + }); }; -// -renameJsToMjs(join(__dirname, 'dist', 'esm')); \ No newline at end of file +// 実行するディレクトリを指定 +fixImportsInDirectory(join(__dirname, 'dist')); diff --git a/src/__tests__/utils/board.test.ts b/src/__tests__/utils/board.test.ts index 08ea7fa..45cd4b5 100644 --- a/src/__tests__/utils/board.test.ts +++ b/src/__tests__/utils/board.test.ts @@ -4,16 +4,16 @@ // import { board } from '../../utils/board' // import { findArduinoPath } from '../../utils/findArduinoPath' -// // SerialPort と findArduinoPath のモック +// // SerialPort と findArduinoPath // vi.mock('serialport', () => ({ // SerialPort: vi.fn().mockImplementation(() => ({ // on: vi.fn((event, callback) => { // if (event === 'data') { -// // 'data' イベントのモック処理 +// // 'data' // setTimeout(() => callback('some data'), 0) // } // }), -// // 他の必要なメソッドもモック化 +// // })), // })) diff --git a/src/declarative/App.tsx b/src/declarative/App.tsx deleted file mode 100644 index ce8d910..0000000 --- a/src/declarative/App.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import { Board } from './utils/Board' -import { Led } from './factory/output/uniqueDevice/Led' -import { renderToString } from 'react-dom/server' - -const App: React.FC = () => { - return ( - - - - ) -} - -renderToString() -export { App } diff --git a/src/declarative/components/App.tsx b/src/declarative/components/App.tsx new file mode 100644 index 0000000..b9574d6 --- /dev/null +++ b/src/declarative/components/App.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import AppContext from './AppContext' + +type Props = { + readonly children: React.ReactNode + readonly stdin: NodeJS.ReadStream + readonly stdout: NodeJS.WriteStream + readonly stderr: NodeJS.WriteStream + readonly exitOnCtrlC: boolean + readonly onExit: (error?: Error) => void +} + +const App = ({ children, onExit }: Props) => { + const contextValue = { + exit: onExit, + } + + return ( + {children} + ) +} + +export default App diff --git a/src/declarative/components/AppContext.ts b/src/declarative/components/AppContext.ts new file mode 100644 index 0000000..b28e82f --- /dev/null +++ b/src/declarative/components/AppContext.ts @@ -0,0 +1,19 @@ +import { createContext } from 'react' + +export type Props = { + /** + * Exit (unmount) the whole Edison app. + */ + readonly exit: (error?: Error) => void +} + +/** + * `AppContext` is a React context, which exposes a method to manually exit the app (unmount). + */ +const AppContext = createContext({ + exit() {}, +}) + +AppContext.displayName = 'InternalAppContext' + +export default AppContext diff --git a/src/declarative/components/Box.tsx b/src/declarative/components/Box.tsx new file mode 100644 index 0000000..65abf08 --- /dev/null +++ b/src/declarative/components/Box.tsx @@ -0,0 +1,13 @@ +import React, { forwardRef, type PropsWithChildren } from 'react' +import { type DOMElement } from '../rendere/dom' + +/** + * `` is an essential Edison component to build. It's like `
` in the browser. + */ +const Box = forwardRef(({ children }, ref) => { + return {children} +}) + +Box.displayName = 'Box' + +export default Box diff --git a/src/declarative/components/ErrorOverview.tsx b/src/declarative/components/ErrorOverview.tsx new file mode 100644 index 0000000..c0989b9 --- /dev/null +++ b/src/declarative/components/ErrorOverview.tsx @@ -0,0 +1,107 @@ +import * as fs from 'node:fs' +import { cwd } from 'node:process' +import React from 'react' +import StackUtils from 'stack-utils' +import codeExcerpt, { type CodeExcerpt } from 'code-excerpt' +import Box from './Box' +import Text from './Text' + +// Error's source file is reported as file:///home/user/file.js +// This function removes the file://[cwd] part +const cleanupPath = (path: string | undefined): string | undefined => { + return path?.replace(`file://${cwd()}/`, '') +} + +const stackUtils = new StackUtils({ + cwd: cwd(), + internals: StackUtils.nodeInternals(), +}) + +type Props = { + readonly error: Error +} + +export default function ErrorOverview({ error }: Props) { + const stack = error.stack ? error.stack.split('\n').slice(1) : undefined + // biome-ignore lint/style/noNonNullAssertion: + const origin = stack ? stackUtils.parseLine(stack[0]!) : undefined + const filePath = cleanupPath(origin?.file) + let excerpt: CodeExcerpt[] | undefined + let lineWidth = 0 + + if (filePath && origin?.line && fs.existsSync(filePath)) { + const sourceCode = fs.readFileSync(filePath, 'utf8') + excerpt = codeExcerpt(sourceCode, origin.line) + + if (excerpt) { + for (const { line } of excerpt) { + lineWidth = Math.max(lineWidth, String(line).length) + } + } + } + + return ( + + + ERROR + + {error.message} + + + {origin && filePath && ( + + + {filePath}:{origin.line}:{origin.column} + + + )} + + {origin && excerpt && ( + + {excerpt.map(({ line, value }) => ( + + + {String(line).padStart(lineWidth, ' ')}: + + + {` ${value}`} + + ))} + + )} + + {error.stack && ( + + {error.stack + .split('\n') + .slice(1) + .map((line) => { + const parsedLine = stackUtils.parseLine(line) + + // If the line from the stack cannot be parsed, we print out the unparsed line. + if (!parsedLine) { + return ( + + - + {line} + + ) + } + + return ( + + - + {parsedLine.function} + + {' '} + ({cleanupPath(parsedLine.file) ?? ''}:{parsedLine.line}: + {parsedLine.column}) + + + ) + })} + + )} + + ) +} diff --git a/src/declarative/components/Static.tsx b/src/declarative/components/Static.tsx new file mode 100644 index 0000000..4654085 --- /dev/null +++ b/src/declarative/components/Static.tsx @@ -0,0 +1,51 @@ +import React, { + useMemo, + useState, + useLayoutEffect, + type ReactNode, +} from 'react' + +export type Props = { + /** + * Array of items of any type to render using a function you pass as a component child. + */ + readonly items: T[] + + /** + * Function that is called to render every item in `items` array. + * First argument is an item itself and second argument is index of that item in `items` array. + * Note that `key` must be assigned to the root component. + */ + readonly children: (item: T, index: number) => ReactNode +} + +/** + * `` component permanently renders its output above everything else. + * It's useful for displaying activity like completed tasks or logs - things that + * are not changing after they're rendered (hence the name "Static"). + * + * It's preferred to use `` for use cases like these, when you can't know + * or control the amount of items that need to be rendered. + * + * For example, [Tap](https://github.com/tapjs/node-tap) uses `` to display + * a list of completed tests. [Gatsby](https://github.com/gatsbyjs/gatsby) uses it + * to display a list of generated pages, while still displaying a live progress bar. + */ +export default function Static(props: Props) { + const { items, children: render } = props + const [index, setIndex] = useState(0) + + const itemsToRender: T[] = useMemo(() => { + return items.slice(index) + }, [items, index]) + + useLayoutEffect(() => { + setIndex(items.length) + }, [items.length]) + + const children = itemsToRender.map((item, itemIndex) => { + return render(item, index + itemIndex) + }) + + return {children} +} diff --git a/src/declarative/components/Text.tsx b/src/declarative/components/Text.tsx new file mode 100644 index 0000000..d38c9e0 --- /dev/null +++ b/src/declarative/components/Text.tsx @@ -0,0 +1,22 @@ +import React, { type ReactNode } from 'react' + +export type Props = { + /** + * This property tells Edison to wrap or truncate text if its width is larger than container. + * If `wrap` is passed (by default), Edison will wrap text and split it into multiple lines. + * If `truncate-*` is passed, Edison will truncate text instead, which will result in one line of text with the rest cut off. + */ + + readonly children?: ReactNode +} + +/** + * This component can display text, and change its style to make it colorful, bold, underline, italic or strikethrough. + */ +export default function Text({ children }: Props) { + if (children === undefined || children === null) { + return null + } + + return {children} +} diff --git a/src/declarative/components/input/Button.tsx b/src/declarative/components/input/Button.tsx new file mode 100644 index 0000000..df99230 --- /dev/null +++ b/src/declarative/components/input/Button.tsx @@ -0,0 +1,50 @@ +import type { SerialPort } from 'serialport' +import React, { createContext } from 'react' +import { board } from '../../../procedure/utils/board' +import { attachButton } from '../../../procedure/examples/input/uniqueDevice/pushButton' + +export const ButtonContext = createContext(null) + +type ButtonProps = { + pin: number + onPress?: () => void + onRelease?: () => void + debounceTime?: number + children: React.ReactNode +} + +export const Button: React.FC = ({ + pin, + onPress, + onRelease, + children, +}) => { + const setupButton = (port: SerialPort) => { + const pushButton = attachButton(port, pin) + + if (onRelease) { + pushButton.read('off', onRelease) + } + + if (onPress) { + pushButton.read('on', onPress) + } + } + + if (board.isReady()) { + const port = board.getCurrentPort() + if (port) { + setupButton(port) + } + } else { + const handleReady = (port: SerialPort) => { + setupButton(port) + board.off('ready', handleReady) + } + board.on('ready', handleReady) + } + + return ( + {children} + ) +} diff --git a/src/declarative/components/input/Collision.tsx b/src/declarative/components/input/Collision.tsx new file mode 100644 index 0000000..948b2a8 --- /dev/null +++ b/src/declarative/components/input/Collision.tsx @@ -0,0 +1,51 @@ +import type { SerialPort } from 'serialport' +import React, { createContext } from 'react' +import { board } from '../../../procedure/utils/board' +import { attachCollisionSensor } from '../../../procedure/examples/input/uniqueDevice/collisionSensor' + +export const CollisionContext = createContext(null) + +type CollisionProps = { + pin: number + onPress?: () => void + onRelease?: () => void + children: React.ReactNode +} + +export const Collision: React.FC = ({ + pin, + onPress, + onRelease, + children, +}) => { + const setupCollision = (port: SerialPort) => { + const collisionSensor = attachCollisionSensor(port, pin) + + if (onPress) { + collisionSensor.read('on', onPress) + } + + if (onRelease) { + collisionSensor.read('off', onRelease) + } + } + + if (board.isReady()) { + const port = board.getCurrentPort() + if (port) { + setupCollision(port) + } + } else { + const handleReady = (port: SerialPort) => { + setupCollision(port) + board.off('ready', handleReady) + } + board.on('ready', handleReady) + } + + return ( + + {children} + + ) +} diff --git a/src/declarative/components/input/HallEffectSensor.tsx b/src/declarative/components/input/HallEffectSensor.tsx new file mode 100644 index 0000000..7b4d3ab --- /dev/null +++ b/src/declarative/components/input/HallEffectSensor.tsx @@ -0,0 +1,52 @@ +import type { SerialPort } from 'serialport' +import React, { createContext } from 'react' +import { board } from '../../../procedure/utils/board' +import { attachHallEffectSensor } from '../../../procedure/examples/input/uniqueDevice/hallEffectSensor' + +export const HallEffectiveContext = createContext(null) + +type HallEffectProps = { + pin: number + onPress?: () => void + onRelease?: () => void + debounceTime?: number + children: React.ReactNode +} + +export const HallEffective: React.FC = ({ + pin, + onPress, + onRelease, + children, +}) => { + const setupHallEffective = (port: SerialPort) => { + const hallEffectiveSensor = attachHallEffectSensor(port, pin) + + if (onPress) { + hallEffectiveSensor.read('on', onPress) + } + + if (onRelease) { + hallEffectiveSensor.read('off', onRelease) + } + } + + if (board.isReady()) { + const port = board.getCurrentPort() + if (port) { + setupHallEffective(port) + } + } else { + const handleReady = (port: SerialPort) => { + setupHallEffective(port) + board.off('ready', handleReady) + } + board.on('ready', handleReady) + } + + return ( + + {children} + + ) +} diff --git a/src/declarative/components/output/Buzzer.ts b/src/declarative/components/output/Buzzer.ts new file mode 100644 index 0000000..bc64153 --- /dev/null +++ b/src/declarative/components/output/Buzzer.ts @@ -0,0 +1,38 @@ +import type React from 'react' +import { board } from '../../../procedure/utils/board' +import { attachBuzzer } from '../../../procedure/examples/output/uniqueDevice/buzzer' + +type BuzzerProps = { + pin: number + isOn?: boolean +} + +const setupBuzzer = (props: BuzzerProps) => { + const { pin, isOn } = props + const port = board.getCurrentPort() + + if (!port) { + console.error('Board is not connected.') + return + } + + const buzzer = attachBuzzer(port, pin) + + if (isOn === true) { + buzzer.on() + } else if (isOn === false) { + buzzer.off() + } +} + +export const Buzzer: React.FC = (props) => { + if (board.isReady()) { + setupBuzzer(props) + } else { + const handleReady = () => { + setupBuzzer(props) + } + board.on('ready', handleReady) + } + return null +} diff --git a/src/declarative/components/output/Led.ts b/src/declarative/components/output/Led.ts new file mode 100644 index 0000000..144560f --- /dev/null +++ b/src/declarative/components/output/Led.ts @@ -0,0 +1,44 @@ +import type React from 'react' +import { attachLed } from '../../../procedure/examples/output/uniqueDevice/led' +import { board } from '../../../procedure/utils/board' + +type LEDProps = { + pin: number + isOn?: boolean + blink?: number +} + +const setupLed = (props: LEDProps) => { + const { pin, isOn, blink } = props + const port = board.getCurrentPort() + + if (!port) { + console.error('Board is not connected.') + return + } + + const led = attachLed(port, pin) + + if (isOn === true) { + led.on() + } else if (isOn === false) { + led.off() + } + + if (blink) { + led.blink(blink) + } +} + +export const Led: React.FC = (props) => { + if (board.isReady()) { + setupLed(props) + } else { + const handleReady = () => { + setupLed(props) + board.off('ready', handleReady) + } + board.on('ready', handleReady) + } + return null +} diff --git a/src/declarative/components/output/Output.ts b/src/declarative/components/output/Output.ts new file mode 100644 index 0000000..4822b27 --- /dev/null +++ b/src/declarative/components/output/Output.ts @@ -0,0 +1,39 @@ +import type React from 'react' +import { board } from '../../../procedure/utils/board' +import { attachOutput } from '../../../procedure/examples/output/uniqueDevice/output' + +type OutputProps = { + pin: number + isOn?: boolean +} + +const setupOutput = (props: OutputProps) => { + const { pin, isOn } = props + const port = board.getCurrentPort() + + if (!port) { + console.error('Board is not connected.') + return + } + + const output = attachOutput(port, pin) + + if (isOn === true) { + output.on() + } else if (isOn === false) { + output.off() + } +} + +export const Output: React.FC = (props) => { + if (board.isReady()) { + setupOutput(props) + } else { + const handleReady = () => { + setupOutput(props) + board.off('ready', handleReady) + } + board.on('ready', handleReady) + } + return null +} diff --git a/src/declarative/components/servo/Servo.tsx b/src/declarative/components/servo/Servo.tsx new file mode 100644 index 0000000..b56244f --- /dev/null +++ b/src/declarative/components/servo/Servo.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react' +import { attachServo } from '../../../procedure/examples/servo/uniqueDevice/servo' +import { board } from '../../../procedure/utils/board' +import type React from 'react' + +type ServoProps = { + pin: number + angle: number +} + +export const Servo: React.FC = ({ pin, angle }) => { + useEffect(() => { + if (board.isReady()) { + const port = board.getCurrentPort() + if (port) { + const servo = attachServo(port, pin) + servo.setAngle(angle) + } + } + }, [angle, pin]) + + return null +} diff --git a/src/declarative/examples/App.tsx b/src/declarative/examples/App.tsx new file mode 100644 index 0000000..b5f836a --- /dev/null +++ b/src/declarative/examples/App.tsx @@ -0,0 +1,34 @@ +import React from 'react' +import { useState } from 'react' +import { Board } from '../utils/Board' +import { render } from '../rendere/render' +import { Button } from '../components/input/Button' +import { Led } from '../components/output/Led' + +const App: React.FC = () => { + const [isOn, setIsOn] = useState(false) + + const handlePress = () => { + setIsOn(true) + } + + const handleRelease = () => { + setIsOn(false) + } + + return ( + + + + ) +} +render() diff --git a/src/declarative/examples/RunServo.tsx b/src/declarative/examples/RunServo.tsx new file mode 100644 index 0000000..fd558d8 --- /dev/null +++ b/src/declarative/examples/RunServo.tsx @@ -0,0 +1,47 @@ +import React from 'react' +import { useState } from 'react' +import { Board } from '../utils/Board' +import { render } from '../rendere/render' +import { Button } from '../components/input/Button' +import { Led } from '../components/output/Led' +import { Servo } from '../components/servo/Servo' + +const App: React.FC = () => { + const [angle, setAngle] = useState(0) + const [isOn, setIsOn] = useState(false) + + const handlePress = () => { + setAngle(angle + 10) + setIsOn(true) + } + + const handleRelease = () => { + if (angle >= 150) { + setAngle(0) + setIsOn(false) + return + } + setAngle(angle + 10) + setIsOn(false) + } + + return ( + + + + ) +} +render() diff --git a/src/declarative/factory/output/uniqueDevice/Buzzer.ts b/src/declarative/factory/output/uniqueDevice/Buzzer.ts deleted file mode 100644 index a098e63..0000000 --- a/src/declarative/factory/output/uniqueDevice/Buzzer.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type React from 'react' -import { attachBuzzer } from '../../../../procedure/factory/output/uniqueDevice/buzzer' -import { board } from '../../../../procedure/utils/board' -import type { SerialPort } from 'serialport' -import { useEffect } from 'react' - -type BuzzerProps = { - pin: number - isOn?: boolean -} - -export const Buzzer: React.FC = ({ pin, isOn }) => { - useEffect(() => { - const attachDevices = () => { - board.on('ready', (port: SerialPort) => { - const buzzer = attachBuzzer(port, pin) - if (isOn) { - buzzer.on() - } else { - buzzer.off() - } - }) - } - - attachDevices() - }, [isOn, pin]) - - return null -} diff --git a/src/declarative/factory/output/uniqueDevice/Led.ts b/src/declarative/factory/output/uniqueDevice/Led.ts deleted file mode 100644 index c0f3cc9..0000000 --- a/src/declarative/factory/output/uniqueDevice/Led.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type React from 'react' -import { attachLed } from '../../../../procedure/factory/output/uniqueDevice/led' -import { board } from '../../../../procedure/utils/board' -import type { SerialPort } from 'serialport' - -type LEDProps = { - pin: number - isOn?: boolean - blink?: number -} - -export const Led: React.FC = ({ pin, isOn, blink }) => { - board.on('ready', (port: SerialPort) => { - const led = attachLed(port, pin) - - if (isOn === true) { - led.on() - } - - if (isOn === false) { - led.off() - } - - if (blink) { - led.blink(blink) - } - }) - - return null -} diff --git a/src/declarative/rendere/dom.ts b/src/declarative/rendere/dom.ts new file mode 100644 index 0000000..5f2b979 --- /dev/null +++ b/src/declarative/rendere/dom.ts @@ -0,0 +1,186 @@ +import Yoga, { type Node as YogaNode } from 'yoga-wasm-web/auto' +import { type OutputTransformer } from './render-node-to-output' + +type EdisonNode = { + parentNode: DOMElement | undefined + yogaNode?: YogaNode + internal_static?: boolean +} + +export type TextName = '#text' +export type ElementNames = + | 'edison-root' + | 'edison-box' + | 'edison-text' + | 'edison-virtual-text' + +export type NodeNames = ElementNames | TextName + +export type DOMElement = { + nodeName: ElementNames + attributes: Record + childNodes: DOMNode[] + internal_transform?: OutputTransformer + + // Internal properties + isStaticDirty?: boolean + staticNode?: DOMElement + onComputeLayout?: () => void + onRender?: () => void + onImmediateRender?: () => void +} & EdisonNode + +export type TextNode = { + nodeName: TextName + nodeValue: string +} & EdisonNode + +export type DOMNode = T extends { + nodeName: infer U +} + ? U extends '#text' + ? TextNode + : DOMElement + : never + +export type DOMNodeAttribute = boolean | string | number + +export const createNode = (nodeName: ElementNames): DOMElement => { + const node: DOMElement = { + nodeName, + attributes: {}, + childNodes: [], + parentNode: undefined, + yogaNode: + nodeName === 'edison-virtual-text' ? undefined : Yoga.Node.create(), + } + + return node +} + +export const appendChildNode = ( + node: DOMElement, + childNode: DOMElement, +): void => { + if (childNode.parentNode) { + removeChildNode(childNode.parentNode, childNode) + } + + childNode.parentNode = node + node.childNodes.push(childNode) + + if (childNode.yogaNode) { + node.yogaNode?.insertChild( + childNode.yogaNode, + node.yogaNode.getChildCount(), + ) + } + + if ( + node.nodeName === 'edison-text' || + node.nodeName === 'edison-virtual-text' + ) { + markNodeAsDirty(node) + } +} + +export const insertBeforeNode = ( + node: DOMElement, + newChildNode: DOMNode, + beforeChildNode: DOMNode, +): void => { + if (newChildNode.parentNode) { + removeChildNode(newChildNode.parentNode, newChildNode) + } + + newChildNode.parentNode = node + + const index = node.childNodes.indexOf(beforeChildNode) + if (index >= 0) { + node.childNodes.splice(index, 0, newChildNode) + if (newChildNode.yogaNode) { + node.yogaNode?.insertChild(newChildNode.yogaNode, index) + } + + return + } + + node.childNodes.push(newChildNode) + + if (newChildNode.yogaNode) { + node.yogaNode?.insertChild( + newChildNode.yogaNode, + node.yogaNode.getChildCount(), + ) + } + + if ( + node.nodeName === 'edison-text' || + node.nodeName === 'edison-virtual-text' + ) { + markNodeAsDirty(node) + } +} + +export const removeChildNode = ( + node: DOMElement, + removeNode: DOMNode, +): void => { + if (removeNode.yogaNode) { + removeNode.parentNode?.yogaNode?.removeChild(removeNode.yogaNode) + } + + removeNode.parentNode = undefined + + const index = node.childNodes.indexOf(removeNode) + if (index >= 0) { + node.childNodes.splice(index, 1) + } + + if ( + node.nodeName === 'edison-text' || + node.nodeName === 'edison-virtual-text' + ) { + markNodeAsDirty(node) + } +} + +export const setAttribute = ( + node: DOMElement, + key: string, + value: DOMNodeAttribute, +): void => { + node.attributes[key] = value +} + +export const createTextNode = (text: string): TextNode => { + const node: TextNode = { + nodeName: '#text', + nodeValue: text, + yogaNode: undefined, + parentNode: undefined, + } + + setTextNodeValue(node, text) + + return node +} + +const findClosestYogaNode = (node?: DOMNode): YogaNode | undefined => { + if (!node?.parentNode) { + return undefined + } + + return node.yogaNode ?? findClosestYogaNode(node.parentNode) +} + +const markNodeAsDirty = (node?: DOMNode): void => { + // Mark closest Yoga node as dirty to measure text dimensions again + const yogaNode = findClosestYogaNode(node) + yogaNode?.markDirty() +} + +export const setTextNodeValue = (node: TextNode, text: string): void => { + node.nodeValue = text + markNodeAsDirty(node) +} diff --git a/src/declarative/rendere/edison.tsx b/src/declarative/rendere/edison.tsx new file mode 100644 index 0000000..2a54a7d --- /dev/null +++ b/src/declarative/rendere/edison.tsx @@ -0,0 +1,265 @@ +import process from 'node:process' +import React, { type ReactNode } from 'react' +import throttle from 'lodash/throttle' +import ansiEscapes from 'ansi-escapes' +import isInCi from 'is-in-ci' +import autoBind from 'auto-bind' +import { type FiberRoot } from 'react-reconciler' +import Yoga from 'yoga-wasm-web/auto' +import reconciler from './reconciler' +import render from './renderer' +import * as dom from './dom' +import instances from './instances' +import App from '../components/App' + +const noop = () => {} + +export type Options = { + stdout: NodeJS.WriteStream + stdin: NodeJS.ReadStream + stderr: NodeJS.WriteStream + debug: boolean + exitOnCtrlC: boolean + patchConsole: boolean + waitUntilExit?: () => Promise +} + +export default class Edison { + private readonly options: Options + // Ignore last render after unmounting a tree to prevent empty output before exit + private isUnmounted: boolean + private lastOutput: string + private readonly container: FiberRoot + private readonly rootNode: dom.DOMElement + // This variable is used only in debug mode to store full static output + // so that it's rerendered every time, not just new static parts, like in non-debug mode + private fullStaticOutput: string + private exitPromise?: Promise + private restoreConsole?: () => void + private readonly unsubscribeResize?: () => void + + constructor(options: Options) { + autoBind(this) + + this.options = options + this.rootNode = dom.createNode('edison-root') + this.rootNode.onComputeLayout = this.calculateLayout + + this.rootNode.onRender = options.debug + ? this.onRender + : throttle(this.onRender, 32, { + leading: true, + trailing: true, + }) + + this.rootNode.onImmediateRender = this.onRender + + // Ignore last render after unmounting a tree to prevent empty output before exit + this.isUnmounted = false + + // Store last output to only rerender when needed + this.lastOutput = '' + + // This variable is used only in debug mode to store full static output + // so that it's rerendered every time, not just new static parts, like in non-debug mode + this.fullStaticOutput = '' + + this.container = reconciler.createContainer( + this.rootNode, + // Legacy mode + 0, + null, + false, + null, + 'id', + () => {}, + null, + ) + + // Unmount when process exits + // this.unsubscribeExit = signalExit(this.unmount, { alwaysLast: false }) + + if (process.env.DEV === 'true') { + reconciler.injectIntoDevTools({ + bundleType: 0, + // Reporting React DOM's version, not Edison's + // See https://github.com/facebook/react/issues/16666#issuecomment-532639905 + version: '16.13.1', + rendererPackageName: 'edison', + }) + } + + if (options.patchConsole) { + this.patchConsole() + } + + if (!isInCi) { + options.stdout.on('resize', this.resized) + + this.unsubscribeResize = () => { + options.stdout.off('resize', this.resized) + } + } + } + + resized = () => { + this.calculateLayout() + this.onRender() + } + + resolveExitPromise: () => void = () => {} + rejectExitPromise: (reason?: Error) => void = () => {} + unsubscribeExit: () => void = () => {} + + calculateLayout = () => { + // The 'columns' property can be undefined or 0 when not using a TTY. + // In that case we fall back to 80. + const terminalWidth = this.options.stdout.columns || 80 + + this.rootNode.yogaNode?.setWidth(terminalWidth) + + this.rootNode.yogaNode?.calculateLayout( + undefined, + undefined, + Yoga.DIRECTION_LTR, + ) + } + + onRender: () => void = () => { + if (this.isUnmounted) { + return + } + + const { output, outputHeight, staticOutput } = render() + + // If output isn't empty, it means new children have been added to it + const hasStaticOutput = staticOutput && staticOutput !== '\n' + + if (this.options.debug) { + if (hasStaticOutput) { + this.fullStaticOutput += staticOutput + } + + this.options.stdout.write(this.fullStaticOutput + output) + return + } + + if (isInCi) { + if (hasStaticOutput) { + this.options.stdout.write(staticOutput) + } + + this.lastOutput = output + return + } + + if (hasStaticOutput) { + this.fullStaticOutput += staticOutput + } + + if (outputHeight >= this.options.stdout.rows) { + this.options.stdout.write( + ansiEscapes.clearTerminal + this.fullStaticOutput + output, + ) + this.lastOutput = output + return + } + + this.lastOutput = output + } + + render(node: ReactNode): void { + const tree = ( + + {node} + + ) + + reconciler.updateContainer(tree, this.container, null, noop) + } + + writeToStdout(data: string): void { + if (this.isUnmounted) { + return + } + + if (this.options.debug) { + this.options.stdout.write(data + this.fullStaticOutput + this.lastOutput) + return + } + + if (isInCi) { + this.options.stdout.write(data) + return + } + } + + writeToStderr(data: string): void { + if (this.isUnmounted) { + return + } + + if (this.options.debug) { + this.options.stderr.write(data) + this.options.stdout.write(this.fullStaticOutput + this.lastOutput) + return + } + + if (isInCi) { + this.options.stderr.write(data) + return + } + } + + unmount(error?: Error | number | null): void { + if (this.isUnmounted) { + return + } + + this.calculateLayout() + this.onRender() + this.unsubscribeExit() + + if (typeof this.restoreConsole === 'function') { + this.restoreConsole() + } + + if (typeof this.unsubscribeResize === 'function') { + this.unsubscribeResize() + } + + this.isUnmounted = true + + reconciler.updateContainer(null, this.container, null, noop) + instances.delete(this.options.stdout) + + if (error instanceof Error) { + this.rejectExitPromise(error) + } else { + this.resolveExitPromise() + } + } + + async waitUntilExit(): Promise { + if (!this.exitPromise) { + this.exitPromise = new Promise((resolve, reject) => { + this.resolveExitPromise = resolve + this.rejectExitPromise = reject + }) + } + + return this.exitPromise + } + + patchConsole(): void { + if (this.options.debug) { + return + } + } +} diff --git a/src/declarative/rendere/global.d.ts b/src/declarative/rendere/global.d.ts new file mode 100644 index 0000000..911f365 --- /dev/null +++ b/src/declarative/rendere/global.d.ts @@ -0,0 +1,27 @@ +import { type ReactNode, type Key, type LegacyRef } from 'react' +import { type DOMElement } from './dom.js' + +declare global { + namespace JSX { + interface IntrinsicElements { + 'edison-box': Edison.Box + 'edison-text': Edison.Text + } + } +} + +declare namespace Edison { + type Box = { + internal_static?: boolean + children?: ReactNode + key?: Key + ref?: LegacyRef + } + + type Text = { + children?: ReactNode + key?: Key + + internal_transform?: (children: string, index: number) => string + } +} diff --git a/src/declarative/rendere/instances.ts b/src/declarative/rendere/instances.ts new file mode 100644 index 0000000..963f358 --- /dev/null +++ b/src/declarative/rendere/instances.ts @@ -0,0 +1,10 @@ +// Store all instances of Edison (instance.js) to ensure that consecutive render() calls +// use the same instance of Edison and don't create a new one +// +// This map has to be stored in a separate file, because render.js creates instances, +// but instance.js should delete itself from the map on unmount + +import type Edison from './edison' + +const instances = new WeakMap() +export default instances diff --git a/src/declarative/rendere/output.ts b/src/declarative/rendere/output.ts new file mode 100644 index 0000000..d12b284 --- /dev/null +++ b/src/declarative/rendere/output.ts @@ -0,0 +1,100 @@ +import { type OutputTransformer } from './render-node-to-output.js' + +/** + * "Virtual" output class + * + * Handles the positioning and saving of the output of each node in the tree. + * Also responsible for applying transformations to each character of the output. + * + * Used to generate the final output of all nodes before writing it to actual output stream (e.g. stdout) + */ + +type Operation = WriteOperation | ClipOperation | UnclipOperation + +type WriteOperation = { + type: 'write' + x: number + y: number + text: string + transformers: OutputTransformer[] +} + +type ClipOperation = { + type: 'clip' + clip: Clip +} + +type Clip = { + x1: number | undefined + x2: number | undefined + y1: number | undefined + y2: number | undefined +} + +type UnclipOperation = { + type: 'unclip' +} + +export default class Output { + private readonly operations: Operation[] = [] + + write( + x: number, + y: number, + text: string, + options: { transformers: OutputTransformer[] }, + ): void { + const { transformers } = options + this.operations.push({ + type: 'write', + x, + y, + text, + transformers, + }) + } + + clip(clip: Clip) { + this.operations.push({ + type: 'clip', + clip, + }) + } + + unclip() { + this.operations.push({ + type: 'unclip', + }) + } + + get(): undefined { + // Initialize output array with a specific set of rows, so that margin/padding at the bottom is preserved + + const clips: Clip[] = [] + + for (const operation of this.operations) { + if (operation.type === 'clip') { + clips.push(operation.clip) + } + + if (operation.type === 'unclip') { + clips.pop() + } + + if (operation.type === 'write') { + const { text, transformers } = operation + const lines = text.split('\n') + + // eslint-disable-next-line prefer-const + for (let [index, line] of lines.entries()) { + // Line can be missing if `text` is taller than height of pre-initialized `this.output` + for (const transformer of transformers) { + line = transformer(line, index) + } + } + } + } + + return + } +} diff --git a/src/declarative/rendere/reconciler.ts b/src/declarative/rendere/reconciler.ts new file mode 100644 index 0000000..6dd808e --- /dev/null +++ b/src/declarative/rendere/reconciler.ts @@ -0,0 +1,249 @@ +import createReconciler from 'react-reconciler' +import { DefaultEventPriority } from 'react-reconciler/constants' +import Yoga, { type Node as YogaNode } from 'yoga-wasm-web/auto' +import { + createTextNode, + appendChildNode, + insertBeforeNode, + removeChildNode, + setTextNodeValue, + createNode, + setAttribute, + type DOMNodeAttribute, + type TextNode, + type ElementNames, + type DOMElement, +} from './dom' +import { type OutputTransformer } from './render-node-to-output' + +type AnyObject = Record + +const diff = (before: AnyObject, after: AnyObject): AnyObject | undefined => { + if (before === after) { + return + } + + if (!before) { + return after + } + + const changed: AnyObject = {} + let isChanged = false + + for (const key of Object.keys(before)) { + const isDeleted = after ? !Object.hasOwnProperty.call(after, key) : true + + if (isDeleted) { + changed[key] = undefined + isChanged = true + } + } + + if (after) { + for (const key of Object.keys(after)) { + if (after[key] !== before[key]) { + changed[key] = after[key] + isChanged = true + } + } + } + + return isChanged ? changed : undefined +} + +const cleanupYogaNode = (node?: YogaNode): void => { + node?.unsetMeasureFunc() + node?.freeRecursive() +} + +type Props = Record + +type HostContext = { + isInsideText: boolean +} + +type UpdatePayload = { + props: Props | undefined +} + +export default createReconciler< + ElementNames, + Props, + DOMElement, + DOMElement, + TextNode, + DOMElement, + unknown, + unknown, + HostContext, + UpdatePayload, + unknown, + unknown, + unknown +>({ + getRootHostContext: () => ({ + isInsideText: false, + }), + prepareForCommit: () => null, + preparePortalMount: () => null, + clearContainer: () => false, + resetAfterCommit(rootNode) { + if (typeof rootNode.onComputeLayout === 'function') { + rootNode.onComputeLayout() + } + + // Since renders are throttled at the instance level and component children + // are rendered only once and then get deleted, we need an escape hatch to + // trigger an immediate render to ensure children are written to output before they get erased + if (rootNode.isStaticDirty) { + rootNode.isStaticDirty = false + if (typeof rootNode.onImmediateRender === 'function') { + rootNode.onImmediateRender() + } + + return + } + + if (typeof rootNode.onRender === 'function') { + rootNode.onRender() + } + }, + getChildHostContext(parentHostContext, type) { + const previousIsInsideText = parentHostContext.isInsideText + const isInsideText = + type === 'edison-text' || type === 'edison-virtual-text' + + if (previousIsInsideText === isInsideText) { + return parentHostContext + } + + return { isInsideText } + }, + shouldSetTextContent: () => false, + createInstance(originalType, newProps, _root, hostContext) { + if (hostContext.isInsideText && originalType === 'edison-box') { + throw new Error(` can't be nested inside component`) + } + + const type = + originalType === 'edison-text' && hostContext.isInsideText + ? 'edison-virtual-text' + : originalType + + const node = createNode(type) + + for (const [key, value] of Object.entries(newProps)) { + if (key === 'children') { + continue + } + + if (key === 'internal_transform') { + node.internal_transform = value as OutputTransformer + continue + } + + if (key === 'internal_static') { + node.internal_static = true + continue + } + + setAttribute(node, key, value as DOMNodeAttribute) + } + + return node + }, + createTextInstance(text, _root, hostContext) { + if (!hostContext.isInsideText) { + throw new Error( + `Text string "${text}" must be rendered inside component`, + ) + } + + return createTextNode(text) + }, + resetTextContent() {}, + hideTextInstance(node) { + setTextNodeValue(node, '') + }, + unhideTextInstance(node, text) { + setTextNodeValue(node, text) + }, + getPublicInstance: (instance) => instance, + hideInstance(node) { + node.yogaNode?.setDisplay(Yoga.DISPLAY_NONE) + }, + unhideInstance(node) { + node.yogaNode?.setDisplay(Yoga.DISPLAY_FLEX) + }, + appendInitialChild: appendChildNode, + appendChild: appendChildNode, + insertBefore: insertBeforeNode, + finalizeInitialChildren(node, _type, _props, rootNode) { + if (node.internal_static) { + rootNode.isStaticDirty = true + + // Save reference to node to skip traversal of entire + // node tree to find it + rootNode.staticNode = node + } + + return false + }, + isPrimaryRenderer: true, + supportsMutation: true, + supportsPersistence: false, + supportsHydration: false, + scheduleTimeout: setTimeout, + cancelTimeout: clearTimeout, + noTimeout: -1, + getCurrentEventPriority: () => DefaultEventPriority, + beforeActiveInstanceBlur() {}, + afterActiveInstanceBlur() {}, + detachDeletedInstance() {}, + getInstanceFromNode: () => null, + prepareScopeUpdate() {}, + getInstanceFromScope: () => null, + appendChildToContainer: appendChildNode, + insertInContainerBefore: insertBeforeNode, + removeChildFromContainer(node, removeNode) { + removeChildNode(node, removeNode) + cleanupYogaNode(removeNode.yogaNode) + }, + prepareUpdate(node, _type, oldProps, newProps, rootNode) { + if (node.internal_static) { + rootNode.isStaticDirty = true + } + + const props = diff(oldProps, newProps) + + if (!props) { + return null + } + + return { props } + }, + commitUpdate(node, { props }) { + if (props) { + for (const [key, value] of Object.entries(props)) { + if (key === 'internal_transform') { + node.internal_transform = value as OutputTransformer + continue + } + + if (key === 'internal_static') { + node.internal_static = true + continue + } + + setAttribute(node, key, value as DOMNodeAttribute) + } + } + }, + commitTextUpdate(node, _oldText, newText) { + setTextNodeValue(node, newText) + }, + removeChild(node, removeNode) { + removeChildNode(node, removeNode) + cleanupYogaNode(removeNode.yogaNode) + }, +}) diff --git a/src/declarative/rendere/render-node-to-output.ts b/src/declarative/rendere/render-node-to-output.ts new file mode 100644 index 0000000..9c1277a --- /dev/null +++ b/src/declarative/rendere/render-node-to-output.ts @@ -0,0 +1,74 @@ +import Yoga from 'yoga-wasm-web/auto' +import { type DOMElement } from './dom' +import type Output from './output' + +// If parent container is ``, text nodes will be treated as separate nodes in +// the tree and will have their own coordinates in the layout. +// To ensure text nodes are aligned correctly, take X and Y of the first text node +// and use it as offset for the rest of the nodes +// Only first node is taken into account, because other text nodes can't have margin or padding, +// so their coordinates will be relative to the first node anyway + +export type OutputTransformer = (s: string, index: number) => string + +// After nodes are laid out, render each to output object, which later gets rendered to terminal +const renderNodeToOutput = ( + node: DOMElement, + output: Output, + options: { + offsetX?: number + offsetY?: number + transformers?: OutputTransformer[] + skipStaticElements: boolean + }, +) => { + const { + offsetX = 0, + offsetY = 0, + transformers = [], + skipStaticElements, + } = options + + if (skipStaticElements && node.internal_static) { + return + } + + const { yogaNode } = node + + if (yogaNode) { + if (yogaNode.getDisplay() === Yoga.DISPLAY_NONE) { + return + } + + // Left and top positions in Yoga are relative to their parent node + const x = offsetX + yogaNode.getComputedLeft() + const y = offsetY + yogaNode.getComputedTop() + + // Transformers are functions that transform final text output of each component + // See Output class for logic that applies transformers + let newTransformers = transformers + + if (typeof node.internal_transform === 'function') { + newTransformers = [node.internal_transform, ...transformers] + } + + const clipped = false + + if (node.nodeName === 'edison-root' || node.nodeName === 'edison-box') { + for (const childNode of node.childNodes) { + renderNodeToOutput(childNode as DOMElement, output, { + offsetX: x, + offsetY: y, + transformers: newTransformers, + skipStaticElements, + }) + } + + if (clipped) { + output.unclip() + } + } + } +} + +export default renderNodeToOutput diff --git a/src/declarative/rendere/render.ts b/src/declarative/rendere/render.ts new file mode 100644 index 0000000..4df8ca1 --- /dev/null +++ b/src/declarative/rendere/render.ts @@ -0,0 +1,103 @@ +import process from 'node:process' +import type { ReactNode } from 'react' +import Edison, { type Options as edisonOptions } from './edison' +import instances from './instances' + +export type RenderOptions = { + /** + * Output stream where app will be rendered. + * + * @default process.stdout + */ + stdout?: NodeJS.WriteStream + /** + * Input stream where app will listen for input. + * + * @default process.stdin + */ + stdin?: NodeJS.ReadStream + /** + * Error stream. + * @default process.stderr + */ + stderr?: NodeJS.WriteStream + /** + * If true, each update will be rendered as a separate output, without replacing the previous one. + * + * @default false + */ + debug?: boolean + /** + * 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. + * + * @default true + */ + exitOnCtrlC?: boolean + + /** + * Patch console methods to ensure console output doesn't mix with Edison output. + * + * @default true + */ + patchConsole?: boolean +} + +export type Instance = { + /** + * Replace previous root node with a new one or update props of the current root node. + */ + rerender: Edison['render'] + /** + * Manually unmount the whole Edison app. + */ + unmount: Edison['unmount'] + /** + * Returns a promise, which resolves when app is unmounted. + */ + waitUntilExit: Edison['waitUntilExit'] + cleanup: () => void +} + +/** + * Mount a component and render the output. + */ +export const render = (node: ReactNode): Instance => { + const edisonOptions: edisonOptions = { + stdout: process.stdout, + stdin: process.stdin, + stderr: process.stderr, + debug: false, + exitOnCtrlC: true, + patchConsole: true, + } + + const instance: Edison = getInstance( + edisonOptions.stdout, + () => new Edison(edisonOptions), + ) + + instance.render(node) + + return { + rerender: instance.render, + unmount() { + instance.unmount() + }, + waitUntilExit: instance.waitUntilExit, + cleanup: () => instances.delete(edisonOptions.stdout), + } +} + +const getInstance = ( + stdout: NodeJS.WriteStream, + createInstance: () => Edison, +): Edison => { + let instance = instances.get(stdout) + + if (!instance) { + instance = createInstance() + instances.set(stdout, instance) + } + + return instance +} diff --git a/src/declarative/rendere/renderer.ts b/src/declarative/rendere/renderer.ts new file mode 100644 index 0000000..1f58a80 --- /dev/null +++ b/src/declarative/rendere/renderer.ts @@ -0,0 +1,15 @@ +type Result = { + output: string + outputHeight: number + staticOutput: string +} + +const renderer = (): Result => { + return { + output: '', + outputHeight: 0, + staticOutput: '', + } +} + +export default renderer diff --git a/src/declarative/utils/Board.tsx b/src/declarative/utils/Board.tsx index 4eac9ce..a01b445 100644 --- a/src/declarative/utils/Board.tsx +++ b/src/declarative/utils/Board.tsx @@ -10,6 +10,13 @@ type BoardProps = { } export const Board: React.FC = ({ children, port }) => { + const currentPort = board.getCurrentPort() + + if (currentPort) { + return ( + {children} + ) + } board.connectManual(port) return {children} } diff --git a/src/index.ts b/src/index.ts index 6d6be41..8a7dd38 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,24 +1,25 @@ export { board } from './procedure/utils/board' -export { attachLed } from './procedure/factory/output/uniqueDevice/led' -export { attachBuzzer } from './procedure/factory/output/uniqueDevice/buzzer' -export { attachPressureSensor } from './procedure/factory/analog/uniqueDevice/pressureSensor' -export { attachRotationServo } from './procedure/factory/servo/uniqueDevice/rotationServo' -export { attachServo } from './procedure/factory/servo/uniqueDevice/servo' -export { attachRotationServo } from './procedure/factory/servo/uniqueDevice/rotationServo' -//export { attachUltrasonicSensor } from './factory/complex/ultrasonicSensor' -export { attachRgbLed } from './procedure/factory/output/uniqueDevice/rgbLed' -export { attachCollisionSensor } from './procedure/factory/input/uniqueDevice/collisionSensor' -export { attachHallEffectSensor } from './procedure/factory/input/uniqueDevice/hallEffectSensor' -export { attachButton } from './procedure/factory/input/uniqueDevice/pushButton' -//export { attachLineTracking } from './factory/input/uniqueDevice/lineTracking' -export { attachOutput } from './procedure/factory/output/uniqueDevice/output' -//export { attachInfraredObstacleAvoidanceSensor } from './factory/input/uniqueDevice/infraredObstacleAvoidanceSensor' +export { attachLed } from './procedure/examples/output/uniqueDevice/led' +export { attachBuzzer } from './procedure/examples/output/uniqueDevice/buzzer' +export { attachPressureSensor } from './procedure/examples/analog/uniqueDevice/pressureSensor' +export { attachServo } from './procedure/examples/servo/uniqueDevice/servo' +export { attachRotationServo } from './procedure/examples/servo/uniqueDevice/rotationServo' +export { attachRgbLed } from './procedure/examples/output/uniqueDevice/rgbLed' +export { attachCollisionSensor } from './procedure/examples/input/uniqueDevice/collisionSensor' +export { attachHallEffectSensor } from './procedure/examples/input/uniqueDevice/hallEffectSensor' +export { attachButton } from './procedure/examples/input/uniqueDevice/pushButton' +export { attachOutput } from './procedure/examples/output/uniqueDevice/output' +//------------utils-----------------// +export { render } from './declarative/rendere/render' export { delay } from './procedure/utils/delay' -//-----From here down to describe in declarative UI----- +//-----------declarative----------------// export { Board } from './declarative/utils/Board' -export { Led } from './declarative/factory/output/uniqueDevice/Led' -export { Buzzer } from './declarative/factory/output/uniqueDevice/Buzzer' -// export { App } from './declarative/App' -export { FC, createContext } from 'react' -export { SerialPort } from 'serialport' +export { Led } from './declarative/components/output/Led' +export { Buzzer } from './declarative/components/output/Buzzer' +export { Button } from './declarative/components/input/Button' +export { Collision } from './declarative/components/input/Collision' +export { HallEffective } from './declarative/components/input/HallEffectSensor' +export { Servo } from './declarative/components/servo/Servo' + +export type { SerialPort } from 'serialport' diff --git a/src/procedure/factory/analog/analogPort.ts b/src/procedure/examples/analog/analogPort.ts similarity index 94% rename from src/procedure/factory/analog/analogPort.ts rename to src/procedure/examples/analog/analogPort.ts index 50c2e4c..d3ac7d0 100644 --- a/src/procedure/factory/analog/analogPort.ts +++ b/src/procedure/examples/analog/analogPort.ts @@ -23,11 +23,9 @@ export const analogPort = (port: SerialPort) => { if (value && method === 'on') { func() } else if (value === false && method === 'off') { - //console.log(value) func() } else if (method === 'change' && value !== prevValue) { // is value changed? - //console.log('change') func() } } diff --git a/src/procedure/factory/analog/uniqueDevice/pressureSensor.ts b/src/procedure/examples/analog/uniqueDevice/pressureSensor.ts similarity index 100% rename from src/procedure/factory/analog/uniqueDevice/pressureSensor.ts rename to src/procedure/examples/analog/uniqueDevice/pressureSensor.ts diff --git a/src/procedure/factory/complex/ultrasonicSensor.ts b/src/procedure/examples/complex/ultrasonicSensor.ts similarity index 100% rename from src/procedure/factory/complex/ultrasonicSensor.ts rename to src/procedure/examples/complex/ultrasonicSensor.ts diff --git a/src/procedure/factory/input/inputPort.ts b/src/procedure/examples/input/inputPort.ts similarity index 69% rename from src/procedure/factory/input/inputPort.ts rename to src/procedure/examples/input/inputPort.ts index e8a9d27..cc716fe 100644 --- a/src/procedure/factory/input/inputPort.ts +++ b/src/procedure/examples/input/inputPort.ts @@ -2,10 +2,10 @@ import type { SerialPort } from 'serialport' import { setInputState } from '../../helper/Input/setInputState' import type { Sensor } from '../../types/analog/analog' +let prevValue: boolean | null = null + export const inputPort = (port: SerialPort) => { return (pin: number) => { - let prevValue: boolean - return { read: async ( method: Sensor, @@ -14,16 +14,19 @@ export const inputPort = (port: SerialPort) => { const observable = setInputState(pin, port) observable.subscribe((value: boolean) => { - if (value && method === 'off') { + if (prevValue !== value && value && method === 'off') { + // console.log('off') + prevValue = value func() } - if (value === false && method === 'on') { + if (prevValue !== value && value === false && method === 'on') { + // console.log('on') + prevValue = value func() } if (method === 'change' && value !== prevValue) { func() } - prevValue = value }) }, } diff --git a/src/procedure/factory/input/uniqueDevice/collisionSensor.ts b/src/procedure/examples/input/uniqueDevice/collisionSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/collisionSensor.ts rename to src/procedure/examples/input/uniqueDevice/collisionSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/flameSensor.ts b/src/procedure/examples/input/uniqueDevice/flameSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/flameSensor.ts rename to src/procedure/examples/input/uniqueDevice/flameSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/hallEffectSensor.ts b/src/procedure/examples/input/uniqueDevice/hallEffectSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/hallEffectSensor.ts rename to src/procedure/examples/input/uniqueDevice/hallEffectSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/infraredObstacleAvoidanceSensor.ts b/src/procedure/examples/input/uniqueDevice/infraredObstacleAvoidanceSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/infraredObstacleAvoidanceSensor.ts rename to src/procedure/examples/input/uniqueDevice/infraredObstacleAvoidanceSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/knockSensor.ts b/src/procedure/examples/input/uniqueDevice/knockSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/knockSensor.ts rename to src/procedure/examples/input/uniqueDevice/knockSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/lineTracking.ts b/src/procedure/examples/input/uniqueDevice/lineTracking.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/lineTracking.ts rename to src/procedure/examples/input/uniqueDevice/lineTracking.ts diff --git a/src/procedure/factory/input/uniqueDevice/photoInterrupter.ts b/src/procedure/examples/input/uniqueDevice/photoInterrupter.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/photoInterrupter.ts rename to src/procedure/examples/input/uniqueDevice/photoInterrupter.ts diff --git a/src/procedure/factory/input/uniqueDevice/pirMotionSensor.ts b/src/procedure/examples/input/uniqueDevice/pirMotionSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/pirMotionSensor.ts rename to src/procedure/examples/input/uniqueDevice/pirMotionSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/pushButton.ts b/src/procedure/examples/input/uniqueDevice/pushButton.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/pushButton.ts rename to src/procedure/examples/input/uniqueDevice/pushButton.ts diff --git a/src/procedure/factory/input/uniqueDevice/readSensor.ts b/src/procedure/examples/input/uniqueDevice/readSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/readSensor.ts rename to src/procedure/examples/input/uniqueDevice/readSensor.ts diff --git a/src/procedure/factory/input/uniqueDevice/tiltSensor.ts b/src/procedure/examples/input/uniqueDevice/tiltSensor.ts similarity index 100% rename from src/procedure/factory/input/uniqueDevice/tiltSensor.ts rename to src/procedure/examples/input/uniqueDevice/tiltSensor.ts diff --git a/src/procedure/factory/output/outputPort.ts b/src/procedure/examples/output/outputPort.ts similarity index 100% rename from src/procedure/factory/output/outputPort.ts rename to src/procedure/examples/output/outputPort.ts diff --git a/src/procedure/factory/output/uniqueDevice/buzzer.ts b/src/procedure/examples/output/uniqueDevice/buzzer.ts similarity index 100% rename from src/procedure/factory/output/uniqueDevice/buzzer.ts rename to src/procedure/examples/output/uniqueDevice/buzzer.ts diff --git a/src/procedure/factory/output/uniqueDevice/led.ts b/src/procedure/examples/output/uniqueDevice/led.ts similarity index 100% rename from src/procedure/factory/output/uniqueDevice/led.ts rename to src/procedure/examples/output/uniqueDevice/led.ts diff --git a/src/procedure/factory/output/uniqueDevice/output.ts b/src/procedure/examples/output/uniqueDevice/output.ts similarity index 100% rename from src/procedure/factory/output/uniqueDevice/output.ts rename to src/procedure/examples/output/uniqueDevice/output.ts diff --git a/src/procedure/factory/output/uniqueDevice/rgbLed.ts b/src/procedure/examples/output/uniqueDevice/rgbLed.ts similarity index 100% rename from src/procedure/factory/output/uniqueDevice/rgbLed.ts rename to src/procedure/examples/output/uniqueDevice/rgbLed.ts diff --git a/src/procedure/factory/pwm/pwmPort.ts b/src/procedure/examples/pwm/pwmPort.ts similarity index 100% rename from src/procedure/factory/pwm/pwmPort.ts rename to src/procedure/examples/pwm/pwmPort.ts diff --git a/src/procedure/factory/pwm/uniqueDevice/passiveBuzzer.ts b/src/procedure/examples/pwm/uniqueDevice/passiveBuzzer.ts similarity index 100% rename from src/procedure/factory/pwm/uniqueDevice/passiveBuzzer.ts rename to src/procedure/examples/pwm/uniqueDevice/passiveBuzzer.ts diff --git a/src/procedure/factory/pwm/uniqueDevice/vibrationSensor.ts b/src/procedure/examples/pwm/uniqueDevice/vibrationSensor.ts similarity index 100% rename from src/procedure/factory/pwm/uniqueDevice/vibrationSensor.ts rename to src/procedure/examples/pwm/uniqueDevice/vibrationSensor.ts diff --git a/src/procedure/factory/servo/servoPort.ts b/src/procedure/examples/servo/servoPort.ts similarity index 100% rename from src/procedure/factory/servo/servoPort.ts rename to src/procedure/examples/servo/servoPort.ts diff --git a/src/procedure/factory/servo/uniqueDevice/rotationServo.ts b/src/procedure/examples/servo/uniqueDevice/rotationServo.ts similarity index 100% rename from src/procedure/factory/servo/uniqueDevice/rotationServo.ts rename to src/procedure/examples/servo/uniqueDevice/rotationServo.ts diff --git a/src/procedure/factory/servo/uniqueDevice/servo.ts b/src/procedure/examples/servo/uniqueDevice/servo.ts similarity index 100% rename from src/procedure/factory/servo/uniqueDevice/servo.ts rename to src/procedure/examples/servo/uniqueDevice/servo.ts diff --git a/src/procedure/helper/Analog/bufferAnalog.ts b/src/procedure/helper/Analog/bufferAnalog.ts index 990e8aa..a540b32 100644 --- a/src/procedure/helper/Analog/bufferAnalog.ts +++ b/src/procedure/helper/Analog/bufferAnalog.ts @@ -5,7 +5,6 @@ export const bufferAnalog = ( buffer: Buffer, ): Promise => { return new Promise((resolve, reject) => { - //console.log(resolve) port.write(buffer, (err) => { if (err) { reject(err) diff --git a/src/procedure/helper/Analog/setPinAnalog.ts b/src/procedure/helper/Analog/setPinAnalog.ts index e393365..478dee2 100644 --- a/src/procedure/helper/Analog/setPinAnalog.ts +++ b/src/procedure/helper/Analog/setPinAnalog.ts @@ -8,7 +8,6 @@ export const setPinAnalog = (pin: number, port: SerialPort): Promise => { const setPinModeOutput = Buffer.from([SET_PIN_MODE, pin, 2]) port.write(setPinModeOutput, (err) => { if (err) { - //console.log('Error on write: ', err.message); reject(err) } else { resolve() diff --git a/src/procedure/helper/Input/setInputState.ts b/src/procedure/helper/Input/setInputState.ts index 39aeb23..7635365 100644 --- a/src/procedure/helper/Input/setInputState.ts +++ b/src/procedure/helper/Input/setInputState.ts @@ -18,9 +18,9 @@ export const setInputState = ( bufferWrite(port, buffer2) return new Observable((observer) => { - port.on('data', (data: Buffer) => { - let lastState: undefined | boolean = undefined + let lastState: undefined | boolean = undefined + port.on('data', (data: Buffer) => { if (data.length === 0) return if (data[0] !== 0x90 && data[0] !== 0x91) return if (data[0] === 0x90 && pin >= 8) return diff --git a/src/procedure/helper/Output/setAnalogOutput.ts b/src/procedure/helper/Output/setAnalogOutput.ts index 8d61aec..2f65bc6 100644 --- a/src/procedure/helper/Output/setAnalogOutput.ts +++ b/src/procedure/helper/Output/setAnalogOutput.ts @@ -13,7 +13,6 @@ export const setAnalogOutput = async ( value & 0x7f, (value >> 7) & 0x7f, ]) - //console.log('analogWrite', buffer) await bufferWrite(port, buffer) } diff --git a/src/procedure/helper/Output/setPinOutput.ts b/src/procedure/helper/Output/setPinOutput.ts index 9cda205..0fbbac9 100644 --- a/src/procedure/helper/Output/setPinOutput.ts +++ b/src/procedure/helper/Output/setPinOutput.ts @@ -5,7 +5,6 @@ export const setPinOutput = (pin: number, port: SerialPort): Promise => { const setPinModeOutput = Buffer.from([0xf4, pin, 1]) port.write(setPinModeOutput, (err) => { if (err) { - //console.log('Error on write: ', err.message); reject(err) } else { resolve() diff --git a/src/procedure/helper/Servo/setPinToServo.ts b/src/procedure/helper/Servo/setPinToServo.ts index 68654ef..75ee452 100644 --- a/src/procedure/helper/Servo/setPinToServo.ts +++ b/src/procedure/helper/Servo/setPinToServo.ts @@ -10,6 +10,5 @@ export const setPinToServo = (pin: number, port: SerialPort): Promise => { PIN_MODE_SERVO, // Pin mode ] port.write(Buffer.from(data)) - ////console.log('setpintoservo,',data) return Promise.resolve() } diff --git a/src/procedure/helper/Utils/bufferWrite.ts b/src/procedure/helper/Utils/bufferWrite.ts index fd3e342..5815038 100644 --- a/src/procedure/helper/Utils/bufferWrite.ts +++ b/src/procedure/helper/Utils/bufferWrite.ts @@ -7,7 +7,6 @@ export const bufferWrite = ( return new Promise((resolve, reject) => { port.write(buffer, (err) => { if (err) { - //console.log('Error on write: ', err.message); reject(err) } else { resolve() diff --git a/src/procedure/test/Buzzer.ts b/src/procedure/test/Buzzer.ts deleted file mode 100644 index 284b15b..0000000 --- a/src/procedure/test/Buzzer.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { board } from '../utils/board' -import { attachBuzzer } from '../factory/output/uniqueDevice/buzzer' -import type { SerialPort } from 'serialport' - -board.on('ready', (port: SerialPort) => { - //console.log('Board is ready!') - const buzzer = attachBuzzer(port, 12) - buzzer.on() -}) diff --git a/src/procedure/test/CollisionSensor.ts b/src/procedure/test/CollisionSensor.ts deleted file mode 100644 index 9e77aae..0000000 --- a/src/procedure/test/CollisionSensor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { SerialPort } from 'serialport' -import { attachCollisionSensor } from '../factory/input/uniqueDevice/collisionSensor' -import { attachLed } from '../factory/output/uniqueDevice/led' -import { board } from '../utils/board' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', (port: SerialPort) => { - //console.log('Board is ready!') - const led1 = attachLed(port, 13) - - const collisionSensor = attachCollisionSensor(port, 12) - collisionSensor.read('on', () => { - led1.on() - }) -}) diff --git a/src/procedure/test/Complex.ts b/src/procedure/test/Complex.ts deleted file mode 100644 index b896c9d..0000000 --- a/src/procedure/test/Complex.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* -import { setup } from '../utils/setup' - -const main = async () => { - const port = await setup() - const servo1 = port.servo(8) - const led1 = port.led(12) - const sensor1 = port.pressureSensor('A0') - - sensor1.read('change', function () { - led1.on() - }) - await servo1.rotate(0) - await servo1.rotate(90) - await servo1.rotate(180) - await servo1.rotate(90) - await servo1.rotate(0) -} - -main() -*/ diff --git a/src/procedure/test/FlameSensor.ts b/src/procedure/test/FlameSensor.ts deleted file mode 100644 index c6e4630..0000000 --- a/src/procedure/test/FlameSensor.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// import { attachTiltSensor } from '../factory/input/uniqueDevice/tiltSensor' - -// board.connectManual('/dev/ttyUSB0') - -// board.on('ready', async (port: SerialPort) => { -// console.log('Board is ready!') -// const flameSensor = attachTiltSensor(port, 12) -// await flameSensor.on() -// }) diff --git a/src/procedure/test/HallEffectSensor.ts b/src/procedure/test/HallEffectSensor.ts deleted file mode 100644 index d21518e..0000000 --- a/src/procedure/test/HallEffectSensor.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { SerialPort } from 'serialport' -import { attachLed } from '../factory/output/uniqueDevice/led' -import { board } from '../utils/board' -import { attachHallEffectSensor } from '../factory/input/uniqueDevice/hallEffectSensor' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', (port: SerialPort) => { - //console.log('Board is ready!') - const led1 = attachLed(port, 13) - - const hallEffectSensor = attachHallEffectSensor(port, 12) - hallEffectSensor.read('on', () => { - led1.on() - }) -}) diff --git a/src/procedure/test/InfraredObstacleAvoidanceSensor.ts b/src/procedure/test/InfraredObstacleAvoidanceSensor.ts deleted file mode 100644 index 733be70..0000000 --- a/src/procedure/test/InfraredObstacleAvoidanceSensor.ts +++ /dev/null @@ -1,14 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// import { attachInfraredObstacleAvoidanceSensor } from '../factory/input/uniqueDevice/infraredObstacleAvoidanceSensor' - -// board.connectManual('/dev/ttyUSB0') - -// board.on('ready', async (port: SerialPort) => { -// console.log('Board is ready!') -// const infraredObstacleAvoidanceSensor = attachInfraredObstacleAvoidanceSensor( -// port, -// 12, -// ) -// await infraredObstacleAvoidanceSensor.on() -// }) diff --git a/src/procedure/test/KnockSensor.ts b/src/procedure/test/KnockSensor.ts deleted file mode 100644 index db82883..0000000 --- a/src/procedure/test/KnockSensor.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// // import type { SerialPort } from 'serialport' -// // import { attachKnockSensor } from '../factory/input/uniqueDevice/knockSensor' - -// // board.connectManual('/dev/ttyUSB0') - -// // board.on('ready', async (port: SerialPort) => { -// // console.log('Board is ready!') -// // const knockSensor = attachKnockSensor(port, 12) -// // await knockSensor.on() -// // }) diff --git a/src/procedure/test/Led.ts b/src/procedure/test/Led.ts deleted file mode 100644 index ec8600c..0000000 --- a/src/procedure/test/Led.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { board } from '../utils/board' -import { attachLed } from '../factory/output/uniqueDevice/led' -import type { SerialPort } from 'serialport' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', (port: SerialPort) => { - console.log('Board is ready!') - const led = attachLed(port, 13) - led.blink(500) -}) diff --git a/src/procedure/test/LineTracking.ts b/src/procedure/test/LineTracking.ts deleted file mode 100644 index 9c0b7f4..0000000 --- a/src/procedure/test/LineTracking.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// // import { attachLineTracking } from '../factory/input/uniqueDevice/lineTracking' - -// // board.connectManual('/dev/ttyUSB0') - -// // board.on('ready', (port: SerialPort) => { -// // console.log('Board is ready!') -// // const lineTracking = attachLineTracking(port, 12) -// // lineTracking.on() -// // }) diff --git a/src/procedure/test/Output.ts b/src/procedure/test/Output.ts deleted file mode 100644 index 2e8e61f..0000000 --- a/src/procedure/test/Output.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { attachOutput } from '../factory/output/uniqueDevice/output' -import { board } from '../utils/board' -import type { SerialPort } from 'serialport' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', (port: SerialPort) => { - //console.log('Board is ready!') - const led = attachOutput(port, 12) - led.on() -}) diff --git a/src/procedure/test/PIRMotionSensor.ts b/src/procedure/test/PIRMotionSensor.ts deleted file mode 100644 index 353f771..0000000 --- a/src/procedure/test/PIRMotionSensor.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// // import { attachPirMorionSensor } from '../factory/input/uniqueDevice/pirMotionSensor' - -// // board.connectManual('/dev/ttyUSB0') - -// // board.on('ready', async (port: SerialPort) => { -// // console.log('Board is ready!') -// // const pirMotionSensor = attachPirMorionSensor(port, 12) -// // await pirMotionSensor.on() -// // }) diff --git a/src/procedure/test/PassiveBuzzer.ts b/src/procedure/test/PassiveBuzzer.ts deleted file mode 100644 index c5635c7..0000000 --- a/src/procedure/test/PassiveBuzzer.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { board } from '../utils/board' -import type { SerialPort } from 'serialport' -import { delay } from '../utils/delay' -import { attachPassiveBuzzer } from '../factory/pwm/uniqueDevice/passiveBuzzer' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', async (port: SerialPort) => { - //console.log('Board is ready!') - const passiveBuzzer = attachPassiveBuzzer(port, 3) - passiveBuzzer.sound(500) - await delay(1000) -}) diff --git a/src/procedure/test/PhotoInterrupter.ts b/src/procedure/test/PhotoInterrupter.ts deleted file mode 100644 index 1ade992..0000000 --- a/src/procedure/test/PhotoInterrupter.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// import { attachPhotoInterrupter } from '../factory/input/uniqueDevice/photoInterrupter' - -// board.connectManual('/dev/ttyUSB0') - -// board.on('ready', async (port: SerialPort) => { -// console.log('Board is ready!') -// const photoInterrupter = attachPhotoInterrupter(port, 12) -// await photoInterrupter.on() -// }) diff --git a/src/procedure/test/RGBLed.ts b/src/procedure/test/RGBLed.ts deleted file mode 100644 index 330924e..0000000 --- a/src/procedure/test/RGBLed.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { board } from '../utils/board' -import { attachRgbLed } from '../factory/output/uniqueDevice/rgbLed' -import type { SerialPort } from 'serialport' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', async (port: SerialPort) => { - //console.log('Board is ready!') - //red, green, blue, vcc or gnd - const rgbLed = attachRgbLed(port, 9, 10, 11) - - await rgbLed.setColor(255, 0, 0) - - await new Promise((resolve) => setTimeout(resolve, 1000)) - - await rgbLed.setColor(0, 255, 0) - - await new Promise((resolve) => setTimeout(resolve, 1000)) - - await rgbLed.off() -}) diff --git a/src/procedure/test/ReadSensor.ts b/src/procedure/test/ReadSensor.ts deleted file mode 100644 index f619045..0000000 --- a/src/procedure/test/ReadSensor.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// // import { attachReadSensor } from '../factory/input/uniqueDevice/readSensor' - -// // board.connectManual('/dev/ttyUSB0') - -// // board.on('ready', async (port: SerialPort) => { -// // console.log('Board is ready!') -// // const readSensor = attachReadSensor(port, 12) -// // await readSensor.on() -// // }) diff --git a/src/procedure/test/Servo.ts b/src/procedure/test/Servo.ts deleted file mode 100644 index 2a0156c..0000000 --- a/src/procedure/test/Servo.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { board } from '../utils/board' -import { attachServo } from '../factory/servo/uniqueDevice/servo' -import type { SerialPort } from 'serialport' - -board.on('ready', async (port: SerialPort) => { - //console.log('Board is ready!') - const servo = attachServo(port, 8) - await servo.setAngle(50) - await servo.setAngle(150) - await servo.setAngle(50) -}) diff --git a/src/procedure/test/TempertureSensor.ts b/src/procedure/test/TempertureSensor.ts deleted file mode 100644 index cdd4b4e..0000000 --- a/src/procedure/test/TempertureSensor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { board } from '../utils/board' -import { attachPressureSensor } from '../factory/analog/uniqueDevice/pressureSensor' -import type { SerialPort } from 'serialport' -import { attachLed } from '../factory/output/uniqueDevice/led' - -board.on('ready', (port: SerialPort) => { - //console.log('Board is ready!') - const led1 = attachLed(port, 12) - const sensor1 = attachPressureSensor(port, 'A0') - - sensor1.read('on', () => { - led1.blink(500) - }) -}) diff --git a/src/procedure/test/TiltSensor.ts b/src/procedure/test/TiltSensor.ts deleted file mode 100644 index e508d8e..0000000 --- a/src/procedure/test/TiltSensor.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// import { attachTiltSensor } from '../factory/input/uniqueDevice/tiltSensor' - -// board.connectManual('/dev/ttyUSB0') - -// board.on('ready', async (port: SerialPort) => { -// console.log('Board is ready!') -// const tiltSensor = attachTiltSensor(port, 12) -// await tiltSensor.on() -// }) diff --git a/src/procedure/test/Vibration.ts b/src/procedure/test/Vibration.ts deleted file mode 100644 index 2312ccc..0000000 --- a/src/procedure/test/Vibration.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { board } from '../utils/board' -import type { SerialPort } from 'serialport' -import { delay } from '../utils/delay' -import { attachVibrationSensor } from '../factory/pwm/uniqueDevice/vibrationSensor' - -board.connectManual('/dev/ttyUSB0') - -board.on('ready', async (port: SerialPort) => { - //console.log('Board is ready!') - - const vibrationSensor = attachVibrationSensor(port, 3) - await vibrationSensor.write(200) - await delay(1000) -}) diff --git a/src/procedure/test/pressureSensor.ts b/src/procedure/test/pressureSensor.ts deleted file mode 100644 index cdd4b4e..0000000 --- a/src/procedure/test/pressureSensor.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { board } from '../utils/board' -import { attachPressureSensor } from '../factory/analog/uniqueDevice/pressureSensor' -import type { SerialPort } from 'serialport' -import { attachLed } from '../factory/output/uniqueDevice/led' - -board.on('ready', (port: SerialPort) => { - //console.log('Board is ready!') - const led1 = attachLed(port, 12) - const sensor1 = attachPressureSensor(port, 'A0') - - sensor1.read('on', () => { - led1.blink(500) - }) -}) diff --git a/src/procedure/test/pushButton.ts b/src/procedure/test/pushButton.ts deleted file mode 100644 index 05e7208..0000000 --- a/src/procedure/test/pushButton.ts +++ /dev/null @@ -1,11 +0,0 @@ -// import { board } from '../utils/board' -// import type { SerialPort } from 'serialport' -// import { attachPushButton } from '../factory/input/uniqueDevice/pushButton' - -// board.connectManual('/dev/ttyUSB0') - -// board.on('ready', async (port: SerialPort) => { -// console.log('Board is ready!') -// const pushButton = attachPushButton(port, 12) -// await pushButton.on() -// }) diff --git a/src/procedure/test/ultrasonicSensor.ts b/src/procedure/test/ultrasonicSensor.ts deleted file mode 100644 index b327d78..0000000 --- a/src/procedure/test/ultrasonicSensor.ts +++ /dev/null @@ -1,14 +0,0 @@ -// import { board } from '../utils/board' -// import { attachUltrasonicSensor } from '../factory/complex/ultrasonicSensor' -// import type { SerialPort } from '../index' - -// board.on('ready', async (port: SerialPort) => { -// console.log('Board is ready!') -// //const led1 = attachBuzzer(port, 12) -// const ultrasonicSensor = attachUltrasonicSensor(port, 3, 2) -// console.log( -// ultrasonicSensor.measure('on', () => { -// console.log('ooooooooooooooooooooooooooooooooooooooooooo') -// }), -// ) -// }) diff --git a/src/procedure/uniqueDevice/setAnalogState.ts b/src/procedure/uniqueDevice/setAnalogState.ts index 73a476d..7706e80 100644 --- a/src/procedure/uniqueDevice/setAnalogState.ts +++ b/src/procedure/uniqueDevice/setAnalogState.ts @@ -20,7 +20,6 @@ export const setAnalogState = (pin: number, port: SerialPort) => { const pinData = data[0] & 0x0f if (pin === pinData) { const value = data[1] | (data[2] << 7) - //console.log(value) // is 0 consecutive? if (value < 10) { onCount++ diff --git a/src/procedure/utils/board.ts b/src/procedure/utils/board.ts index 490e302..94d7b41 100644 --- a/src/procedure/utils/board.ts +++ b/src/procedure/utils/board.ts @@ -1,50 +1,61 @@ import { EventEmitter } from 'events' import { SerialPort } from 'serialport' -import { findArduinoPath } from './findArduinoPath' const boardEmitter = new EventEmitter() +let currentPort: SerialPort | null = null +let isPortActive = false -let isReadyEmitted = false - -let globalPort: SerialPort | null = null - -const connectAutomatic = async () => { - const arduinoPath = await findArduinoPath() - if (!arduinoPath) { - console.error('Could not find the path for the genuine Arduino.') - return - } - - const port = new SerialPort({ path: arduinoPath, baudRate: 57600 }) - - port.on('data', (/*data*/) => { - if (!isReadyEmitted) { - boardEmitter.emit('ready', port) - isReadyEmitted = true - } - }) -} +const MAX_RECENT_LISTENERS = 2 const connectManual = (arduinoPath: string) => { - if (globalPort?.isOpen) { + if (currentPort) { console.log('Port is already open.') return } - const port = new SerialPort({ path: arduinoPath, baudRate: 57600 }) - - port.on('data', (/*data*/) => { - if (!isReadyEmitted) { - boardEmitter.emit('ready', port) - isReadyEmitted = true + try { + const port = new SerialPort({ path: arduinoPath, baudRate: 57600 }) + currentPort = port + + const onData = (/*data*/) => { + if (port.listenerCount('data') > MAX_RECENT_LISTENERS) { + const allListeners = port.listeners('data') as (( + ...args: unknown[] + ) => void)[] + const oldListeners = allListeners.slice(0, -MAX_RECENT_LISTENERS) + + // biome-ignore lint/complexity/noForEach: + oldListeners.forEach((listener) => { + if (listener !== onData) { + port.removeListener('data', listener) + } + }) + } + + if (!isPortActive) { + console.log('Board is ready.') + boardEmitter.emit('ready', port) + isPortActive = true + } } - }) - globalPort = port + port.on('data', onData) + + port.on('close', () => { + currentPort = null + port.removeAllListeners() + isPortActive = false + }) + } catch (error) { + console.error('Failed to open port:', error) + currentPort = null + } } export const board = { on: boardEmitter.on.bind(boardEmitter), - connectAutomatic, + off: boardEmitter.off.bind(boardEmitter), connectManual, + getCurrentPort: () => currentPort, + isReady: () => isPortActive, } diff --git a/src/procedure/utils/findArduinoPath.ts b/src/procedure/utils/findArduinoPath.ts index 708be52..196b536 100644 --- a/src/procedure/utils/findArduinoPath.ts +++ b/src/procedure/utils/findArduinoPath.ts @@ -4,17 +4,13 @@ export const findArduinoPath = async (): Promise => { try { const ports = await SerialPort.list() for (const port of ports) { - // console.log(port) if (port.manufacturer?.includes('Arduino')) { - ////console.log(2) - // console.log('Arduino found at: ', port.path) return port.path } } //There is no arduino board return null } catch (error) { - // console.log(1) console.error( 'An error occurred while finding the Arduino port. Check your connection with your device:', error, diff --git a/src/procedure/utils/portClose.ts b/src/procedure/utils/portClose.ts index c7ca0d9..885465a 100644 --- a/src/procedure/utils/portClose.ts +++ b/src/procedure/utils/portClose.ts @@ -2,11 +2,9 @@ import type { SerialPort } from 'serialport' // This function closes the serial port. Console will be closed. export const portClose = (port: SerialPort): Promise => { - //console.log('Closing port'); return new Promise((resolve, reject) => { port.close((err) => { if (err) { - //console.log('Error closing port:', err); reject(err) } else { resolve() diff --git a/src/procedure/utils/setup.ts b/src/procedure/utils/setup.ts index ca78662..28c1b92 100644 --- a/src/procedure/utils/setup.ts +++ b/src/procedure/utils/setup.ts @@ -19,7 +19,6 @@ export const setup = async () => { let onDataReady: (() => void) | null = null const dataReady = new Promise((resolve) => { - //console.log(3) onDataReady = resolve }) @@ -30,7 +29,6 @@ export const setup = async () => { }) await dataReady - //console.log(2) return { servo: servoPort(port), led: outputPort(port), diff --git a/vite.config.ts b/vite.config.ts index 2a89e20..9ffcc67 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,3 @@ -// vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react'