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 @@
-
+
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'