diff --git a/package.json b/package.json index 0bb7a6d2a..7a2d327d1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "private": true, "scripts": { "docusaurus": "docusaurus", - "start": "concurrently --raw --kill-others 'docusaurus start' 'sleep 1s && ts-node updateSync/packageDocsSync/watch.ts --src packages --dest tdev-website/docs/packages'", + "start": "concurrently --raw --kill-others 'PACKAGE_SRC=packages PACKAGE_DEST=tdev-website/docs/packages docusaurus start' 'sleep 1s && ts-node updateSync/packageDocsSync/watch.ts --src packages --dest tdev-website/docs/packages'", "prebuild": "ts-node updateSync/packageDocsSync/preBuild.ts --src packages --dest tdev-website/docs/packages", "build": "docusaurus build", "swizzle": "docusaurus swizzle", diff --git a/packages/tdev/webserial/components/BinaryDecoder/Bit/index.tsx b/packages/tdev/webserial/components/BinaryDecoder/Bit/index.tsx new file mode 100644 index 000000000..efacab095 --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/Bit/index.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import Decoder from '../model/Decoder'; + +interface Props { + decoder: Decoder; + bitPos: number; +} + +const Bit = observer((props: Props) => { + const { decoder, bitPos } = props; + const value = decoder.buffer.length < bitPos ? undefined : decoder.buffer[bitPos] === '1' ? 1 : 0; + return
; +}); + +export default Bit; diff --git a/packages/tdev/webserial/components/BinaryDecoder/Bit/styles.module.scss b/packages/tdev/webserial/components/BinaryDecoder/Bit/styles.module.scss new file mode 100644 index 000000000..b9d0c6581 --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/Bit/styles.module.scss @@ -0,0 +1,8 @@ +.bit { + width: 5px; + background: black; + height: 5px; + &.active { + height: 15px; + } +} diff --git a/packages/tdev/webserial/components/BinaryDecoder/Byte/index.tsx b/packages/tdev/webserial/components/BinaryDecoder/Byte/index.tsx new file mode 100644 index 000000000..1915ca7e9 --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/Byte/index.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import Decoder from '../model/Decoder'; +import Bit from '../Bit'; + +interface Props { + decoder: Decoder; + offset: number; +} + +const BYTE_POS = [0, 1, 2, 3, 4, 5, 6, 7]; + +const Byte = observer((props: Props) => { + const { decoder, offset } = props; + if (offset >= decoder.size) { + return null; + } + return ( +
+ {BYTE_POS.map((pos) => ( + + ))} +
+ ); +}); + +export default Byte; diff --git a/packages/tdev/webserial/components/BinaryDecoder/Byte/styles.module.scss b/packages/tdev/webserial/components/BinaryDecoder/Byte/styles.module.scss new file mode 100644 index 000000000..30f420174 --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/Byte/styles.module.scss @@ -0,0 +1,4 @@ +.byte { + display: flex; + gap: 1px; +} diff --git a/packages/tdev/webserial/components/BinaryDecoder/index.tsx b/packages/tdev/webserial/components/BinaryDecoder/index.tsx new file mode 100644 index 000000000..7c2683ddb --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/index.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import { useDeviceId } from '@tdev/webserial/hooks/useDeviceId'; +import Decoder from './model/Decoder'; +import Icon from '@mdi/react'; +import { mdiCircleSmall, mdiLoading } from '@mdi/js'; +import Byte from './Byte'; + +interface Props {} + +const BinaryDecoder = observer((props: Props) => { + const subscriptionId = React.useId(); + const viewStore = useStore('viewStore'); + const webserialStore = viewStore.useStore('webserialStore'); + const deviceId = useDeviceId(); + const device = webserialStore.devices.get(deviceId); + const decoder = React.useMemo(() => { + if (device) { + return new Decoder(subscriptionId, device); + } + }, [device, subscriptionId]); + React.useEffect(() => { + return () => { + decoder?.cleanup(); + }; + }, [decoder, subscriptionId]); + + if (!decoder) { + return null; + } + + return ( +
+ +
+ {decoder.size} bytes + {[...Array(decoder.size)].map((_, i) => ( + + ))} +
+ {decoder.lines.map((line, i) => ( +
{line}
+ ))} +
+ ); +}); + +export default BinaryDecoder; diff --git a/packages/tdev/webserial/components/BinaryDecoder/model/Decoder.ts b/packages/tdev/webserial/components/BinaryDecoder/model/Decoder.ts new file mode 100644 index 000000000..b940b2d5f --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/model/Decoder.ts @@ -0,0 +1,68 @@ +import SerialDevice, { iSubscriber } from '@tdev/webserial/models/SerialDevice'; +import { action, computed, observable } from 'mobx'; + +class Decoder implements iSubscriber { + readonly id: string; + readonly device: SerialDevice; + + private bufferOffset = 0; + buffer = observable.array([], { deep: false }); + characters = observable.array([], { deep: false }); + + constructor(id: string, device: SerialDevice) { + this.id = id; + this.device = device; + this.device.subscribe(this); + } + + @action + onNewLines(lines: string[]) { + const bits = lines.map((l) => l.trim()).filter((l) => l === '0' || l === '1'); + this.buffer.push(...bits); + while (this.buffer.length - this.bufferOffset >= 8) { + const byte = this.buffer.slice(this.bufferOffset, this.bufferOffset + 8).join(''); + const charCode = parseInt(byte, 2); + if (!isNaN(charCode)) { + this.characters.push(String.fromCharCode(charCode)); + } + this.bufferOffset += 8; + } + } + + @action + reset() { + this.buffer.clear(); + this.characters.clear(); + this.bufferOffset = 0; + } + + @computed + get isProcessing(): boolean { + return this.buffer.length % 8 !== 0; + } + + /** + * Returns the number of complete bytes in the buffer + */ + @computed + get size(): number { + return Math.ceil(this.buffer.length / 8); + } + + @computed + get text(): string { + return this.characters.join(''); + } + + @computed + get lines(): string[] { + return this.text.split('\n'); + } + + @action + cleanup() { + this.device.unsubscribe(this.id); + } +} + +export default Decoder; diff --git a/packages/tdev/webserial/components/BinaryDecoder/styles.module.scss b/packages/tdev/webserial/components/BinaryDecoder/styles.module.scss new file mode 100644 index 000000000..9d061267e --- /dev/null +++ b/packages/tdev/webserial/components/BinaryDecoder/styles.module.scss @@ -0,0 +1,13 @@ +.binaryDecoder { + position: relative; + .indicator { + position: absolute; + top: 0.5em; + right: 0.5em; + } + .bytes { + display: flex; + flex-wrap: wrap; + gap: 0.5em; + } +} diff --git a/packages/tdev/webserial/components/index.tsx b/packages/tdev/webserial/components/index.tsx new file mode 100644 index 000000000..c8b5def09 --- /dev/null +++ b/packages/tdev/webserial/components/index.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import clsx from 'clsx'; +import styles from './styles.module.scss'; +import { observer } from 'mobx-react-lite'; +import { useStore } from '@tdev-hooks/useStore'; +import Logs from '@tdev-components/documents/CodeEditor/Editor/Footer/Logs'; +import { FullscreenContext } from '@tdev-hooks/useFullscreenTargetId'; +import Alert from '@tdev-components/shared/Alert'; +import Admonition from '@theme-original/Admonition'; +import CodeBlock from '@theme-original/CodeBlock'; +import { ConnectionState } from '../models/SerialDevice'; +import Badge from '@tdev-components/shared/Badge'; +import Card from '@tdev-components/shared/Card'; +import Button from '@tdev-components/shared/Button'; +import { mdiCloseNetwork, mdiConnection, mdiEjectCircle, mdiLoading, mdiSend } from '@mdi/js'; +import Icon from '@mdi/react'; +import TextInput from '@tdev-components/shared/TextInput'; +// @ts-ignore +import Details from '@theme/Details'; +import { DeviceContext } from '../hooks/useDeviceId'; + +interface Props { + /** Override default baud rate (default: 115200) */ + baudRate?: number; + deviceId?: string; + hideLogs?: boolean; + collapseLogs?: boolean; + showInput?: boolean; + inputPlaceholder?: string; + inputLabel?: string; + output?: React.ReactNode; + onReadyString?: string; +} + +const ConnectionStateMessage: Record = { + disconnected: 'Getrennt', + connecting: 'Verbinden…', + connected: 'Verbunden', + error: 'Fehler' +}; + +const ConnectionStateColor: Record = { + disconnected: 'gray', + connecting: 'orange', + connected: 'green', + error: 'red' +}; + +const ButtonIcon: Record = { + disconnected: mdiConnection, + connecting: mdiLoading, + connected: mdiEjectCircle, + error: mdiConnection +}; +const ButtonColor: Record = { + disconnected: 'blue', + connecting: 'orange', + connected: 'red', + error: 'blue' +}; +const ButtonText: Record = { + disconnected: 'Serielles Gerät verbinden', + connecting: 'Verbinden…', + connected: 'Verbindung trennen', + error: 'Erneut versuchen' +}; + +const SwitchCollapsed = observer( + ({ children, collapsed, title }: { children: React.ReactNode; collapsed?: boolean; title: string }) => { + if (collapsed) { + return
{children}
; + } + return <>{children}; + } +); + +const Webserial = observer((props: Props) => { + const defaultId = React.useId(); + const { baudRate, deviceId } = props; + const viewStore = useStore('viewStore'); + const webserialStore = viewStore.useStore('webserialStore'); + const device = webserialStore.useDevice(deviceId ?? defaultId, baudRate ? { baudRate } : {}, { + onReadyString: props.onReadyString + }); + + const handleConnect = async () => { + await device.connect(); + }; + + const handleDisconnect = async () => { + await webserialStore.disconnectDevice(deviceId ?? defaultId); + }; + + React.useEffect(() => { + return () => { + webserialStore.clearDevice(deviceId ?? defaultId); + }; + }, [deviceId]); + + if (!device) { + return null; + } + + return ( + + + {!webserialStore.isSupported && ( + + ⚠️ Die Web Serial API ist nicht unterstützt. Verwenden Sie Chrome oder Edge. + + )} + + {webserialStore.isSupported && ( +