├── .env ├── .github └── workflows │ └── build-and-deploy-to-github-pages.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc ├── .nvmrc ├── .prettierrc ├── README.md ├── components.json ├── docs ├── axidaw-web-arch.png ├── axidraw-pen-holder.svg └── screenshot-setup.png ├── eslint.config.js ├── index.html ├── package.json ├── patches └── @serialport__bindings-interface@1.2.2.patch ├── pnpm-lock.yaml ├── postcss.config.js ├── scripts └── create-cert.sh ├── server ├── cert │ └── .gitkeep ├── consts.ts ├── index.ts ├── package.json ├── serial-port.ts ├── utils.ts └── ws.ts ├── src ├── analystic.ts ├── assets │ └── svg │ │ ├── axidraw-first.svg │ │ ├── pen-holder.svg │ │ ├── test-arc.svg │ │ ├── test-bezier.svg │ │ ├── test-lines.svg │ │ ├── test-move.svg │ │ ├── test-shape.svg │ │ └── test-simple-path.svg ├── communication │ ├── device │ │ ├── consts.ts │ │ ├── device.ts │ │ ├── index.ts │ │ ├── usb.ts │ │ ├── utils.ts │ │ ├── virtual.ts │ │ ├── webscoket-type.ts │ │ └── websocket.ts │ └── ebb │ │ ├── command.ts │ │ ├── commands │ │ ├── a.ts │ │ ├── ac.ts │ │ ├── bl.ts │ │ ├── c.ts │ │ ├── ck.ts │ │ ├── cs.ts │ │ ├── cu.ts │ │ ├── em.ts │ │ ├── es.ts │ │ ├── hm.ts │ │ ├── i.ts │ │ ├── lm.ts │ │ ├── lt.ts │ │ ├── mr.ts │ │ ├── mw.ts │ │ ├── nd.ts │ │ ├── ni.ts │ │ ├── o.ts │ │ ├── pc.ts │ │ ├── pd.ts │ │ ├── pg.ts │ │ ├── pi.ts │ │ ├── po.ts │ │ ├── qb.ts │ │ ├── qc.ts │ │ ├── qe.ts │ │ ├── qg.ts │ │ ├── ql.ts │ │ ├── qm.ts │ │ ├── qn.ts │ │ ├── qp.ts │ │ ├── qr.ts │ │ ├── qs.ts │ │ ├── qt.ts │ │ ├── r.ts │ │ ├── rb.ts │ │ ├── s2.ts │ │ ├── sc.ts │ │ ├── se.ts │ │ ├── sl.ts │ │ ├── sm.ts │ │ ├── sn.ts │ │ ├── sp.ts │ │ ├── sr.ts │ │ ├── st.ts │ │ ├── t.ts │ │ ├── tp.ts │ │ ├── v.ts │ │ └── xm.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── messages │ │ ├── ebb.ts │ │ ├── error.ts │ │ └── ok.ts │ │ └── utils.ts ├── components │ ├── device-connector │ │ └── device-connector.tsx │ ├── footer │ │ ├── footer.module.css │ │ └── footer.tsx │ ├── loading │ │ ├── loading.module.css │ │ └── loading.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── form.module.css │ │ ├── sheet.module.css │ │ ├── toast.tsx │ │ └── toaster.tsx ├── containers │ ├── app │ │ └── app.tsx │ ├── composer │ │ ├── components │ │ │ └── midi-commander.tsx │ │ ├── composer.tsx │ │ ├── songs │ │ │ ├── index.ts │ │ │ ├── the-wheels-on-the-bus.ts │ │ │ └── twinkle-twinkle-little-star.ts │ │ └── utils.ts │ ├── debugger │ │ ├── components │ │ │ ├── batch-commander.tsx │ │ │ ├── fav-commander.tsx │ │ │ └── simple-commander.tsx │ │ ├── debugger.tsx │ │ └── utils.ts │ ├── plotter │ │ ├── components │ │ │ ├── panels │ │ │ │ ├── esimating.tsx │ │ │ │ ├── panel.module.css │ │ │ │ ├── panel.tsx │ │ │ │ ├── planning.module.css │ │ │ │ ├── planning.tsx │ │ │ │ ├── plotting.module.css │ │ │ │ ├── plotting.tsx │ │ │ │ ├── setup.module.css │ │ │ │ ├── setup.tsx │ │ │ │ └── simple-debugger.tsx │ │ │ └── workspace │ │ │ │ ├── debug.module.css │ │ │ │ ├── debug.tsx │ │ │ │ ├── gizmo.module.css │ │ │ │ ├── gizmo.tsx │ │ │ │ ├── page.module.css │ │ │ │ ├── page.tsx │ │ │ │ ├── planning.module.css │ │ │ │ ├── planning.tsx │ │ │ │ ├── setup.module.css │ │ │ │ ├── setup.tsx │ │ │ │ ├── shadow-def.tsx │ │ │ │ ├── workspace.module.css │ │ │ │ └── workspace.tsx │ │ ├── context.ts │ │ ├── plotter.module.css │ │ ├── plotter.tsx │ │ ├── presenters │ │ │ ├── page.ts │ │ │ ├── planning.ts │ │ │ └── work.ts │ │ └── utils.ts │ └── virtual │ │ ├── components │ │ ├── canvas.module.css │ │ ├── canvas.tsx │ │ ├── pen-holder.module.css │ │ └── pen-holder.tsx │ │ ├── plotter │ │ ├── command.ts │ │ ├── commands │ │ │ ├── em.ts │ │ │ ├── hm.ts │ │ │ ├── lm.ts │ │ │ ├── qb.ts │ │ │ ├── qs.ts │ │ │ ├── r.ts │ │ │ ├── sc.ts │ │ │ ├── sm.ts │ │ │ ├── sp.ts │ │ │ ├── sr.ts │ │ │ ├── tp.ts │ │ │ └── v.ts │ │ ├── index.ts │ │ └── utils.ts │ │ ├── utils.ts │ │ ├── virtual.module.css │ │ └── virtual.tsx ├── css │ ├── base.css │ ├── fonts.css │ ├── preflight.css │ └── typograph.css ├── hooks │ ├── device.ts │ └── use-toast.ts ├── index.css ├── lib │ └── utils.ts ├── main.tsx ├── math │ ├── ebb.ts │ ├── geom.ts │ └── svg.ts ├── plotter │ ├── cleaner.ts │ ├── consts.ts │ ├── estimator.ts │ ├── motion │ │ ├── const-acceleration.ts │ │ └── const-velocity.ts │ ├── planner.ts │ ├── plotter.ts │ ├── rtree │ │ ├── index.ts │ │ ├── tests │ │ │ └── rtree.test.ts │ │ └── utils.ts │ ├── svg │ │ ├── arc-to-lines.ts │ │ ├── bezier-to-lines.ts │ │ ├── element-to-path.ts │ │ ├── math.ts │ │ ├── path-to-lines.ts │ │ ├── path │ │ │ ├── cmd-a.ts │ │ │ ├── cmd-c.ts │ │ │ ├── cmd-h.ts │ │ │ ├── cmd-l.ts │ │ │ ├── cmd-m.ts │ │ │ ├── cmd-q.ts │ │ │ ├── cmd-s.ts │ │ │ ├── cmd-t.ts │ │ │ ├── cmd-v.ts │ │ │ ├── cmd-z.ts │ │ │ ├── index.ts │ │ │ ├── parser │ │ │ │ ├── index.ts │ │ │ │ ├── tests │ │ │ │ │ └── parser.test.ts │ │ │ │ └── utils.ts │ │ │ ├── steppers.ts │ │ │ ├── transformers.ts │ │ │ └── utils.ts │ │ ├── points-to-lines.ts │ │ ├── presentation.ts │ │ ├── svg-to-lines.ts │ │ └── utils.ts │ └── utils.ts ├── utils │ ├── dom-event.ts │ ├── file.ts │ ├── logger.ts │ ├── style.ts │ └── time.ts ├── vite-env.d.ts └── web-usb.d.ts ├── tailwind.config.js ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.env: -------------------------------------------------------------------------------- 1 | VITE_GA= 2 | -------------------------------------------------------------------------------- /.github/workflows/build-and-deploy-to-github-pages.yml: -------------------------------------------------------------------------------- 1 | name: Axidraw Web Deployment 2 | run-name: ${{ github.actor }} is deploying Axidraw Web to GitHub Pages 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | Build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: Setup Node.js 13 | uses: actions/setup-node@v4 14 | with: 15 | node-version-file: .nvmrc 16 | - name: Setup pnpm 17 | uses: pnpm/action-setup@v4 18 | with: 19 | run_install: true 20 | - name: Build 21 | run: pnpm build 22 | - name: Upload static files as artifact 23 | uses: actions/upload-pages-artifact@v3 24 | with: 25 | path: dist/ 26 | Deploy: 27 | runs-on: ubuntu-latest 28 | needs: Build 29 | permissions: 30 | pages: write 31 | id-token: write 32 | environment: 33 | name: github-pages 34 | url: ${{ steps.deployment.outputs.page_url }} 35 | steps: 36 | - name: Deploy to GitHub Pages 37 | id: deployment 38 | uses: actions/deploy-pages@v4 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Caches 27 | .eslintcache 28 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{ts,tsx}": "eslint --cache --fix" 3 | } -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 23.5.0 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "endOfLine": "lf", 8 | "printWidth": 80 9 | } 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/index.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /docs/axidaw-web-arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutoo/axidraw-web/fc50eb3fd597059f18dec7406ed8175b4c59a340/docs/axidaw-web-arch.png -------------------------------------------------------------------------------- /docs/screenshot-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mutoo/axidraw-web/fc50eb3fd597059f18dec7406ed8175b4c59a340/docs/screenshot-setup.png -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js'; 2 | import globals from 'globals'; 3 | import react from 'eslint-plugin-react'; 4 | import reactHooks from 'eslint-plugin-react-hooks'; 5 | import reactRefresh from 'eslint-plugin-react-refresh'; 6 | import tseslint from 'typescript-eslint'; 7 | import prettier from 'eslint-config-prettier'; 8 | import importPlugin from 'eslint-plugin-import'; 9 | 10 | export default tseslint.config( 11 | { ignores: ['dist'] }, 12 | { 13 | extends: [ 14 | js.configs.recommended, 15 | ...tseslint.configs.strictTypeChecked, 16 | importPlugin.flatConfigs.recommended, 17 | importPlugin.flatConfigs.typescript, 18 | prettier, 19 | ], 20 | files: ['**/*.{ts,tsx}'], 21 | languageOptions: { 22 | ecmaVersion: 2020, 23 | globals: globals.browser, 24 | parserOptions: { 25 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 26 | tsconfigRootDir: import.meta.dirname, 27 | }, 28 | }, 29 | settings: { 30 | react: { 31 | version: '18.3', 32 | }, 33 | 'import/resolver': { 34 | typescript: true, 35 | node: true, 36 | }, 37 | }, 38 | plugins: { 39 | react, 40 | 'react-hooks': reactHooks, 41 | 'react-refresh': reactRefresh, 42 | }, 43 | rules: { 44 | ...react.configs.recommended.rules, 45 | ...react.configs['jsx-runtime'].rules, 46 | ...reactHooks.configs.recommended.rules, 47 | 'react-refresh/only-export-components': [ 48 | 'warn', 49 | { allowConstantExport: true }, 50 | ], 51 | 'import/order': [ 52 | 'error', 53 | { 54 | pathGroups: [ 55 | { 56 | pattern: '@/**', 57 | group: 'external', 58 | position: 'after', 59 | }, 60 | ], 61 | pathGroupsExcludedImportTypes: ['builtin'], 62 | alphabetize: { 63 | order: 'asc', 64 | caseInsensitive: true, 65 | }, 66 | }, 67 | ], 68 | '@typescript-eslint/no-unused-vars': [ 69 | 'error', 70 | { 71 | args: 'all', 72 | argsIgnorePattern: '^_', 73 | caughtErrors: 'all', 74 | caughtErrorsIgnorePattern: '^_', 75 | destructuredArrayIgnorePattern: '^_', 76 | varsIgnorePattern: '^_', 77 | ignoreRestSiblings: true, 78 | }, 79 | ], 80 | '@typescript-eslint/restrict-template-expressions': [ 81 | 'error', 82 | { 83 | allowNumber: true, 84 | }, 85 | ], 86 | '@typescript-eslint/no-non-null-assertion': 'off', 87 | }, 88 | }, 89 | ); 90 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Axidraw Web 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axidraw-web", 3 | "private": true, 4 | "version": "0.2.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "test": "vitest", 9 | "build": "tsc -b && vite build", 10 | "lint": "eslint .", 11 | "server": "ts-node server", 12 | "preview": "vite preview", 13 | "prepare": "husky" 14 | }, 15 | "dependencies": { 16 | "@radix-ui/react-slot": "^1.1.1", 17 | "@radix-ui/react-toast": "^1.2.4", 18 | "@tailwindcss/forms": "^0.5.10", 19 | "class-variance-authority": "^0.7.1", 20 | "clsx": "^2.1.1", 21 | "events": "^3.3.0", 22 | "express": "5", 23 | "js-logger": "^1.6.1", 24 | "lucide-react": "^0.469.0", 25 | "mobx": "^6.13.5", 26 | "mobx-react-lite": "^4.1.0", 27 | "query-string": "^9.1.1", 28 | "react": "^18.3.1", 29 | "react-dom": "^18.3.1", 30 | "react-ga4": "^2.1.0", 31 | "react-router": "^7.1.1", 32 | "serialport": "^13.0.0", 33 | "tailwind-merge": "^2.6.0", 34 | "tailwindcss-animate": "^1.0.7", 35 | "ws": "^8.18.0" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.17.0", 39 | "@serialport/bindings-interface": "^1.2.2", 40 | "@types/express": "^5.0.0", 41 | "@types/node": "^22.10.5", 42 | "@types/react": "^18.3.18", 43 | "@types/react-dom": "^18.3.5", 44 | "@types/w3c-web-usb": "^1.0.10", 45 | "@types/ws": "^8.5.13", 46 | "@vitejs/plugin-react": "^4.3.4", 47 | "autoprefixer": "^10.4.20", 48 | "eslint": "^9.17.0", 49 | "eslint-config-prettier": "^9.1.0", 50 | "eslint-import-resolver-typescript": "^3.7.0", 51 | "eslint-plugin-import": "^2.31.0", 52 | "eslint-plugin-react": "^7.37.3", 53 | "eslint-plugin-react-hooks": "^5.0.0", 54 | "eslint-plugin-react-refresh": "^0.4.16", 55 | "globals": "^15.14.0", 56 | "husky": "^9.1.7", 57 | "lint-staged": "^15.3.0", 58 | "postcss": "^8.4.49", 59 | "postcss-import": "^16.1.0", 60 | "prettier": "^3.4.2", 61 | "tailwindcss": "^3.4.17", 62 | "ts-node": "^10.9.2", 63 | "typescript": "~5.6.2", 64 | "typescript-eslint": "^8.18.2", 65 | "vite": "^6.0.5", 66 | "vitest": "^2.1.8" 67 | }, 68 | "packageManager": "pnpm@9.15.3+sha512.1f79bc245a66eb0b07c5d4d83131240774642caaa86ef7d0434ab47c0d16f66b04e21e0c086eb61e62c77efc4d7f7ec071afad3796af64892fae66509173893a", 69 | "pnpm": { 70 | "patchedDependencies": { 71 | "@serialport/bindings-interface@1.2.2": "patches/@serialport__bindings-interface@1.2.2.patch" 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /patches/@serialport__bindings-interface@1.2.2.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 1f46c413862b1b4df9bc152a21f1c7405923074e..7694e1aed63b9f59ab7da4015e9eff44da17db04 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -6,6 +6,7 @@ 6 | "main": "./dist/index.js", 7 | "exports": { 8 | "require": "./dist/index.js", 9 | + "types": "./dist/index.d.ts", 10 | "default": "./dist/index-esm.mjs" 11 | }, 12 | "publishConfig": { 13 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /scripts/create-cert.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | CERT_DIR=`dirname $0`/../server/cert 4 | FILENAME_CA_CNF=ca.cnf 5 | FILENAME_CA_KEY=ca.key 6 | FILENAME_CA_CERT=ca.pem 7 | FILENAME_CERT_CNF=localhost.cnf 8 | FILENAME_CERT_KEY=localhost.key 9 | FILENAME_CERT_CSR=localhost.csr 10 | FILENAME_CERT_EXT=localhost.ext 11 | FILENAME_CERT_CERT=localhost.crt 12 | 13 | # create cert folder 14 | mkdir -p $CERT_DIR 15 | cd $CERT_DIR 16 | 17 | cat > $FILENAME_CA_CNF < $FILENAME_CERT_CNF < $FILENAME_CERT_EXT < { 32 | const addr = server.address(); 33 | if (!addr) { 34 | console.error('Server address is not available'); 35 | return; 36 | } 37 | if (typeof addr === 'string') { 38 | console.log(`Listening on ${addr}`); 39 | } else { 40 | console.log(`Listening on https://${addr.address}:${addr.port}`); 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axidraw-web-server", 3 | "private": true, 4 | "version": "0.2.0", 5 | "type": "commonjs" 6 | } 7 | -------------------------------------------------------------------------------- /server/serial-port.ts: -------------------------------------------------------------------------------- 1 | import { PortInfo } from '@serialport/bindings-interface'; 2 | import { SerialPort } from 'serialport'; 3 | import { delay } from './utils'; 4 | 5 | export const AXIDRAW_VENDOR_ID = '04d8'; 6 | export const AXIDRAW_PRODUCT_ID = 'fd92'; 7 | 8 | export const listDevices = async () => { 9 | const ports: PortInfo[] = await SerialPort.list(); 10 | return ports.filter( 11 | (port) => 12 | port.vendorId?.toLowerCase() === AXIDRAW_VENDOR_ID && 13 | port.productId?.toLowerCase() === AXIDRAW_PRODUCT_ID, 14 | ); 15 | }; 16 | 17 | export const waitForEBB = async (deviceId: string, retry = 10) => { 18 | let retried = 0; 19 | while (retried < retry) { 20 | const EBBs = await listDevices(); 21 | const device = EBBs.find((ebb) => ebb.path === deviceId); 22 | if (device) { 23 | return deviceId; 24 | } 25 | 26 | console.log('EBB not found, will retry in 3s...'); 27 | 28 | await delay(3000); 29 | retried += 1; 30 | } 31 | throw new Error('Device not available right now.'); 32 | }; 33 | 34 | export const connectToDevice = async ( 35 | deviceId: string, 36 | dataHandler: (resp: Buffer) => void, 37 | ) => { 38 | const path = await waitForEBB(deviceId, 10); 39 | return new Promise((resolve, reject) => { 40 | const port = new SerialPort({ path, baudRate: 9600 }); 41 | port.on('open', () => { 42 | console.log(`Connected to port: ${path}`); 43 | resolve(port); 44 | }); 45 | port.on('error', (err) => { 46 | console.log(`Can not connect to port: ${err}`); 47 | reject(err); 48 | }); 49 | port.on('data', dataHandler); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /server/utils.ts: -------------------------------------------------------------------------------- 1 | export const delay = (ms: number) => { 2 | return new Promise((resolve) => { 3 | setTimeout(resolve, ms); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /server/ws.ts: -------------------------------------------------------------------------------- 1 | import { Server } from 'https'; 2 | import { Express } from 'express'; 3 | import { SerialPort } from 'serialport'; 4 | import { WebSocket, WebSocketServer } from 'ws'; 5 | import { 6 | ServerMessage, 7 | ClientMessage, 8 | } from '../src/communication/device/webscoket-type'; 9 | import { 10 | WEBSOCKET_STATUS_AUTHORIZED, 11 | WEBSOCKET_STATUS_CONNECTED, 12 | WEBSOCKET_STATUS_DISCONNECTED, 13 | WEBSOCKET_STATUS_STANDBY, 14 | } from './consts'; 15 | import { connectToDevice, listDevices } from './serial-port'; 16 | 17 | const authCode = process.env.AXIDRAW_AUTH || 'axidraw-web'; 18 | 19 | export default function setupWebSocket(_app: Express, server: Server) { 20 | const wss = new WebSocketServer({ server, path: '/axidraw' }); 21 | 22 | wss.on('connection', (ws: WebSocket) => { 23 | let wsStatus = WEBSOCKET_STATUS_CONNECTED; 24 | let port: SerialPort | undefined; 25 | const wsSend = (data: ServerMessage) => { 26 | if (ws.readyState !== WebSocket.OPEN) { 27 | console.log('WebSocket is not open'); 28 | return; 29 | } 30 | ws.send(JSON.stringify({ status: wsStatus, ...data })); 31 | }; 32 | ws.on('message', (message: string) => { 33 | const data = JSON.parse(message) as ClientMessage; 34 | switch (data.type) { 35 | case 'auth': 36 | if (data.code !== authCode) { 37 | ws.close(3000, 'Forbidden'); 38 | } else { 39 | wsStatus = WEBSOCKET_STATUS_AUTHORIZED; 40 | void listDevices().then((EBBs) => { 41 | wsSend({ type: 'devices', devices: EBBs }); 42 | }); 43 | } 44 | break; 45 | case 'device_id': 46 | void (async () => { 47 | try { 48 | const { device } = data; 49 | port = await connectToDevice(device, (response: Buffer) => { 50 | console.log(`EBB: ${Array.from(response).join(' ')}`); 51 | wsSend({ type: 'ebb', response: Array.from(response) }); 52 | }); 53 | if (wsStatus === WEBSOCKET_STATUS_DISCONNECTED) { 54 | // websocket may disconnect during connecting to device 55 | return; 56 | } 57 | wsStatus = WEBSOCKET_STATUS_STANDBY; 58 | wsSend({ type: 'ready' }); 59 | } catch (e) { 60 | console.log('Failed to connect EBB'); 61 | ws.close(3001, String(e)); 62 | } 63 | })(); 64 | break; 65 | case 'command': 66 | if (port) { 67 | console.log(`Client: ${data.command}`); 68 | port.write(data.command); 69 | } else { 70 | ws.close(3002, 'Device is not connected'); 71 | } 72 | break; 73 | default: 74 | ws.close(3003, 'Unknown message.'); 75 | } 76 | }); 77 | ws.on('error', (e) => { 78 | console.error(e.toString()); 79 | ws.close(3005, `Internal Error: ${e.toString()}`); 80 | }); 81 | ws.on('close', () => { 82 | if (port) { 83 | console.log('Disconnect from client, close EBB'); 84 | port.write('R\r'); 85 | port.close(); 86 | } 87 | wsStatus = WEBSOCKET_STATUS_DISCONNECTED; 88 | }); 89 | }); 90 | } 91 | -------------------------------------------------------------------------------- /src/analystic.ts: -------------------------------------------------------------------------------- 1 | import ReactGA from 'react-ga4'; 2 | 3 | if (import.meta.env.VITE_GA) { 4 | ReactGA.initialize(import.meta.env.VITE_GA, { 5 | testMode: import.meta.env.MODE === 'development', 6 | }); 7 | } 8 | 9 | export const trackCategoryEvent = 10 | (category: string) => (action: string, label?: string) => { 11 | if (import.meta.env.VITE_GA) { 12 | ReactGA.event({ 13 | category, 14 | action, 15 | label, 16 | }); 17 | } else { 18 | console.log(`track event: ${category} ${action} ${label}`); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /src/assets/svg/test-arc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/svg/test-bezier.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/svg/test-lines.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 20 | 24 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/svg/test-move.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/svg/test-shape.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 13 | 14 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/assets/svg/test-simple-path.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/communication/device/consts.ts: -------------------------------------------------------------------------------- 1 | export const DEVICE_TYPE_USB = 'axidraw-web-device-type-usb'; 2 | export const DEVICE_TYPE_WEBSOCKET = 'axidraw-web-device-type-ws'; 3 | export const DEVICE_TYPE_VIRTUAL = 'axidraw-web-device-type-virtual'; 4 | 5 | export const DEVICE_EVENT_CONNECTED = 'axidraw-web-device-event-connected'; 6 | export const DEVICE_EVENT_DISCONNECTED = 7 | 'axidraw-web-device-event-disconnected'; 8 | 9 | export const WEBSOCKET_STATUS_DISCONNECTED = 10 | 'axidraw-web-ws-status-disconnected'; 11 | export const WEBSOCKET_STATUS_CONNECTED = 'axidraw-web-ws-status-connected'; 12 | export const WEBSOCKET_STATUS_AUTHORIZED = 'axidraw-web-ws-status-authorized'; 13 | export const WEBSOCKET_STATUS_STANDBY = 'axidraw-web-ws-status-standby'; 14 | 15 | export const WEBSOCKET_EVENT_CONNECTED = 'axidraw-web-ws-event-connected'; 16 | export const WEBSOCKET_EVENT_DISCONNECTED = 'axidraw-web-ws-event-disconnected'; 17 | export const WEBSOCKET_EVENT_MESSAGE = 'axidraw-web-ws-event-message'; 18 | 19 | export const VIRTUAL_STATUS_DISCONNECTED = 20 | 'axidraw-web-virtual-status-disconnected'; 21 | export const VIRTUAL_STATUS_CONNECTED = 'axidraw-web-virtual-status-connected'; 22 | 23 | export const VIRTUAL_EVENT_STARTED = 'axidraw-web-virtual-event-started'; 24 | export const VIRTUAL_EVENT_CONNECTED = 'axidraw-web-virtual-event-connected'; 25 | export const VIRTUAL_EVENT_DISCONNECTED = 26 | 'axidraw-web-virtual-event-disconnected'; 27 | export const VIRTUAL_EVENT_MESSAGE = 'axidraw-web-virtual-event-message'; 28 | -------------------------------------------------------------------------------- /src/communication/device/device.ts: -------------------------------------------------------------------------------- 1 | import { Command } from '../ebb/command'; 2 | 3 | export interface IDevice { 4 | /** 5 | * Check if the device is ready to send messages. 6 | */ 7 | isReady: boolean; 8 | /** 9 | * Check the status of the device. Throw an error if the device is not ready. 10 | */ 11 | checkStatus(): void; 12 | /** 13 | * Send a message to the device. 14 | */ 15 | send(message: string): Promise; 16 | /** 17 | * Disconnect the device. 18 | */ 19 | disconnect(): Promise; 20 | /** 21 | * Listen for the device disconnected event. 22 | */ 23 | onDisconnected(listener: (reason: string) => void): void; 24 | } 25 | 26 | export type DevicePicker = (devices: T[]) => Promise; 27 | 28 | export interface IDeviceConnector { 29 | connectDevice: (config: C) => Promise; 30 | disconnectDevice: () => Promise; 31 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 32 | executeCommand: ( 33 | cmd: Command, 34 | ...params: T 35 | ) => Promise; 36 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 37 | on(event: string, listener: (...args: any[]) => void): void; 38 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 39 | off(event: string, listener: (...args: any[]) => void): void; 40 | type: string; 41 | version: string; 42 | isConnected: boolean; 43 | } 44 | -------------------------------------------------------------------------------- /src/communication/device/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DEVICE_TYPE_USB, 3 | DEVICE_TYPE_VIRTUAL, 4 | DEVICE_TYPE_WEBSOCKET, 5 | } from './consts'; 6 | import { DevicePicker } from './device'; 7 | import createUSBDevice from './usb'; 8 | import createVirtualDevice from './virtual'; 9 | import { WSDevice } from './webscoket-type'; 10 | import createWSDevice from './websocket'; 11 | 12 | export default function createDevice( 13 | type: string, 14 | devicePicker: DevicePicker, 15 | ) { 16 | switch (type) { 17 | default: 18 | case DEVICE_TYPE_USB: 19 | return createUSBDevice({ 20 | devicePicker: devicePicker as DevicePicker, 21 | }); 22 | case DEVICE_TYPE_WEBSOCKET: 23 | return createWSDevice({ 24 | devicePicker: devicePicker as DevicePicker, 25 | }); 26 | case DEVICE_TYPE_VIRTUAL: 27 | return createVirtualDevice(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/communication/device/webscoket-type.ts: -------------------------------------------------------------------------------- 1 | export type WSDevice = { path: string }; 2 | 3 | export type ServerMessage = 4 | | { type: 'ready' } 5 | | { 6 | type: 'devices'; 7 | devices: WSDevice[]; 8 | } 9 | | { type: 'ebb'; response: ArrayLike }; 10 | 11 | export type ClientMessage = 12 | | { type: 'auth'; code: string } 13 | | { type: 'device_id'; device: string } 14 | | { type: 'command'; command: string }; 15 | -------------------------------------------------------------------------------- /src/communication/ebb/command.ts: -------------------------------------------------------------------------------- 1 | export type UnfinishedMessage = { 2 | consumed: number; 3 | }; 4 | 5 | export type FinishedMessage = { 6 | consumed: number; 7 | remain: string; 8 | result: T; 9 | }; 10 | 11 | export type CommandGenerator = Generator< 12 | string | UnfinishedMessage, 13 | FinishedMessage, 14 | number[] 15 | >; 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | export type Command = { 19 | cmd: string; 20 | title: string; 21 | create: (...params: T) => CommandGenerator; 22 | parseParams: (params: string) => T; 23 | version?: string; 24 | execution?: number; 25 | }; 26 | 27 | export type ExtractCommandParams = T extends { 28 | create: (...params: infer P) => CommandGenerator; 29 | } 30 | ? P 31 | : never; 32 | 33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 34 | export type CommandWithParams> = { 35 | cmd: T; 36 | title?: string; 37 | params: ExtractCommandParams; 38 | }; 39 | 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | export const createCommand = ( 42 | cmd: string, 43 | title: string, 44 | create: (...params: T) => CommandGenerator, 45 | parseParams: (params: string) => T, 46 | options: { version?: string; execution?: number } = {}, 47 | ): Command => { 48 | return { 49 | cmd, 50 | title, 51 | create, 52 | parseParams, 53 | ...options, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/a.ts: -------------------------------------------------------------------------------- 1 | import { CommandGenerator, createCommand } from '../command'; 2 | import { ENDING_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'A'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Get analog values', 10 | function* (): CommandGenerator<{ [port: number]: number }> { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "A,00:0713,02:0241,05:0089:09:1004\r\n" 13 | const parsed = yield* readUntil(ENDING_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => { 15 | const pairs = result.trim().substring(2).split(','); 16 | return pairs.reduce((memo, pair) => { 17 | const [port, value] = pair.split(':'); 18 | return { ...memo, [toInt(port)]: toInt(value) }; 19 | }, {}); 20 | }); 21 | }, 22 | noParameters, 23 | { 24 | version: '2.2.3', 25 | }, 26 | ); 27 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/ac.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'AC'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Analog configure', 9 | function* (channel: number, enable: number) { 10 | const dataIn = yield `${cmd},${[channel, enable].join(',')}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number, number] => { 14 | const [channel, enable] = params.split(',').map((p) => parseInt(p, 10)); 15 | return [channel, enable]; 16 | }, 17 | { 18 | version: '2.2.3', 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/bl.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { noParameters } from '../utils'; 4 | 5 | export const cmd = 'BL'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Enter bootloader', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | noParameters, 15 | { 16 | version: '1.9.5', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/c.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'C'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Configure pin direction', 9 | function* ( 10 | portA: number, 11 | portB: number, 12 | portC: number, 13 | portD: number, 14 | portE: number, 15 | ) { 16 | const dataIn = yield `${cmd},${[portA, portB, portC, portD, portE].join( 17 | ',', 18 | )}\r`; 19 | return yield* handleOKMessage(dataIn); 20 | }, 21 | (params: string): [number, number, number, number, number] => { 22 | const [portA, portB, portC, portD, portE] = params 23 | .split(',') 24 | .map((p) => parseInt(p, 10)); 25 | return [portA, portB, portC, portD, portE]; 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/ck.ts: -------------------------------------------------------------------------------- 1 | import { CommandGenerator, createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'CK'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Check input', 10 | function* ( 11 | v1: number, 12 | v2: number, 13 | v3: number, 14 | v4: number, 15 | v5: number, 16 | v6: number, 17 | v7: string, 18 | v8: string, 19 | ): CommandGenerator<(number | string)[]> { 20 | const dataIn = yield `${cmd},${[v1, v2, v3, v4, v5, v6, v7, v8].join( 21 | ',', 22 | )}\r`; 23 | // example response: "Param1=0\r\nParam2=0\r\nParam3=0\r\nParam4=0\r\nParam5=0\r\nParam6=0\r\nParam7=a\r\nParam8=A\r\nOK\r\n" 24 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 25 | return transformResult(parsed, (result) => { 26 | return result 27 | .substring(0, result.length - 6) // discard \r\nOK\r\n 28 | .split(/\s+/) 29 | .map((pair, idx) => { 30 | const value = pair.split('=')[1]; 31 | return idx < 6 ? toInt(value) : value; 32 | }); 33 | }); 34 | }, 35 | ( 36 | params: string, 37 | ): [number, number, number, number, number, number, string, string] => { 38 | const [v1, v2, v3, v4, v5, v6, v7, v8] = params.split(','); 39 | const [n1, n2, n3, n4, n5, n6] = [v1, v2, v3, v4, v5, v6].map((p) => 40 | parseInt(p, 10), 41 | ); 42 | return [n1, n2, n3, n4, n5, n6, v7, v8]; 43 | }, 44 | ); 45 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/cs.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { noParameters } from '../utils'; 4 | 5 | export const cmd = 'CS'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Clear step position', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | noParameters, 15 | { 16 | version: '2.4.3', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/cu.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'CU'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Configure user options', 9 | function* (paramNumber: number, paramValue: number) { 10 | const dataIn = yield `${cmd},${[paramNumber, paramValue].join(',')}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number, number] => { 14 | const [paramNumber, paramValue] = params 15 | .split(',') 16 | .map((p) => parseInt(p, 10)); 17 | return [paramNumber, paramValue]; 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/em.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | 5 | export const cmd = 'EM'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Enable Motors', 10 | function* (motor1: number, motor2: number) { 11 | const dataIn = yield `${cmd},${[motor1, motor2].join(',')}\r`; 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | (params: string): [number, number] => { 15 | const [motor1, motor2] = params.split(',').map((p) => parseInt(p, 10)); 16 | return [motor1, motor2]; 17 | }, 18 | { 19 | execution: EXECUTION_FIFO, 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/es.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { 4 | cmdWithOptionalParams, 5 | readUntil, 6 | toInt, 7 | transformResult, 8 | } from '../utils'; 9 | 10 | export const cmd = 'ES'; 11 | 12 | export default createCommand( 13 | cmd, 14 | 'E stop', 15 | function* (disableMotors?: number) { 16 | const dataIn = yield cmdWithOptionalParams(cmd, disableMotors); 17 | // example response: "0,0,0,0,0\n\rOK\r\n" 18 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 19 | return transformResult(parsed, (result) => { 20 | const values = result 21 | .substring(0, result.length - 6) 22 | .split(',') 23 | .map(toInt); 24 | return { 25 | interrupted: values[0], 26 | axis1: { 27 | steps: values[1], 28 | remain: values[3], 29 | }, 30 | axis2: { 31 | steps: values[2], 32 | remain: values[4], 33 | }, 34 | }; 35 | }); 36 | }, 37 | (params: string): [number | undefined] => { 38 | const [n] = params.split(',').map(toInt); 39 | return [n]; 40 | }, 41 | { 42 | version: '2.2.7', 43 | }, 44 | ); 45 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/hm.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'HM'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'Home or absolute move', 11 | function* (stepFrequency: number, axis1?: number, axis2?: number) { 12 | const dataIn = yield cmdWithOptionalParams( 13 | `${cmd},${stepFrequency}`, 14 | axis1, 15 | axis2, 16 | ); 17 | return yield* handleOKMessage(dataIn); 18 | }, 19 | (params: string): [number, number | undefined, number | undefined] => { 20 | const [stepFrequency, axis1, axis2] = params 21 | .split(',') 22 | .map((p) => parseInt(p, 10)); 23 | return [stepFrequency, axis1, axis2]; 24 | }, 25 | { 26 | execution: EXECUTION_FIFO, 27 | version: '2.6.2', 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/i.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'I'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Input (digital)', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "I,PortA,PortB,PortC,PortD,PortE\r\n" 13 | const parsed = yield* readUntil(ENDING_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => 15 | result.trim().substring(2).split(',').map(toInt), 16 | ); 17 | }, 18 | noParameters, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/lm.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'LM'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'Low-level move, step-limited', 11 | function* ( 12 | rate1: number, 13 | steps1: number, 14 | accel1: number, 15 | rate2: number, 16 | steps2: number, 17 | accel2: number, 18 | clear?: number, 19 | ) { 20 | const dataIn = yield cmdWithOptionalParams( 21 | `${cmd},${[rate1, steps1, accel1, rate2, steps2, accel2].join(',')}`, 22 | clear, 23 | ); 24 | return yield* handleOKMessage(dataIn); 25 | }, 26 | ( 27 | params: string, 28 | ): [number, number, number, number, number, number, number] => { 29 | const [rate1, steps1, accel1, rate2, steps2, accel2, clear] = params 30 | .split(',') 31 | .map((p) => parseInt(p, 10)); 32 | return [rate1, steps1, accel1, rate2, steps2, accel2, clear]; 33 | }, 34 | { 35 | execution: EXECUTION_FIFO, 36 | version: '2.7.0', 37 | }, 38 | ); 39 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/lt.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'LT'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'Low-level move, time-limited', 11 | function* ( 12 | intervals: number, 13 | rate1: number, 14 | accel1: number, 15 | rate2: number, 16 | accel2: number, 17 | clear?: number, 18 | ) { 19 | const dataIn = yield cmdWithOptionalParams( 20 | `${cmd},${[intervals, rate1, accel1, rate2, accel2].join(',')}`, 21 | clear, 22 | ); 23 | return yield* handleOKMessage(dataIn); 24 | }, 25 | (params: string): [number, number, number, number, number, number] => { 26 | const [intervals, rate1, accel1, rate2, accel2, clear] = params 27 | .split(',') 28 | .map((p) => parseInt(p, 10)); 29 | return [intervals, rate1, accel1, rate2, accel2, clear]; 30 | }, 31 | { 32 | execution: EXECUTION_FIFO, 33 | version: '2.7.0', 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/mr.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_CR_NL } from '../constants'; 3 | import { readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'MR'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Memory Read', 10 | function* (address: number) { 11 | const dataIn = yield `${cmd},${address.toFixed(0)}\r`; 12 | // example response: "MR,071\r\n" 13 | const parsed = yield* readUntil(ENDING_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result.substring(3))); 15 | }, 16 | (params: string): [number] => { 17 | return [toInt(params)]; 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/mw.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'MW'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Memory Write', 9 | function* (address: number, data: number) { 10 | const dataIn = yield `${cmd},${address.toFixed(0)},${data.toFixed(0)}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number, number] => { 14 | const [address, data] = params.split(',').map((p) => parseInt(p, 10)); 15 | return [address, data]; 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/nd.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { noParameters } from '../utils'; 4 | 5 | export const cmd = 'ND'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Node count decrement', 10 | function* () { 11 | const dataIn = yield `ND\r`; 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | noParameters, 15 | { 16 | version: '1.9.5', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/ni.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { noParameters } from '../utils'; 4 | 5 | export const cmd = 'NI'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Node count increment', 10 | function* () { 11 | const dataIn = yield `NI\r`; 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | noParameters, 15 | { 16 | version: '1.9.5', 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/o.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { cmdWithOptionalParams } from '../utils'; 4 | 5 | export const cmd = 'O'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Output (digital)', 10 | function* ( 11 | portA: number, 12 | portB: number, 13 | portC: number, 14 | portD: number, 15 | portE: number, 16 | ) { 17 | const dataIn = yield cmdWithOptionalParams( 18 | `${cmd},${portA.toFixed(0)}`, 19 | portB, 20 | portC, 21 | portD, 22 | portE, 23 | ); 24 | return yield* handleOKMessage(dataIn); 25 | }, 26 | (params: string): [number, number, number, number, number] => { 27 | const [portA, portB, portC, portD, portE] = params 28 | .split(',') 29 | .map((p) => parseInt(p, 10)); 30 | return [portA, portB, portC, portD, portE]; 31 | }, 32 | ); 33 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/pc.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { cmdWithOptionalParams } from '../utils'; 4 | 5 | export const cmd = 'PC'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Pulse configure', 10 | function* ( 11 | len0: number, 12 | period0: number, 13 | len1?: number, 14 | period1?: number, 15 | len2?: number, 16 | period2?: number, 17 | len3?: number, 18 | period3?: number, 19 | ) { 20 | const dataIn = yield cmdWithOptionalParams( 21 | `${cmd},${len0.toFixed(0)},${period0.toFixed(0)}`, 22 | len1, 23 | period1, 24 | len2, 25 | period2, 26 | len3, 27 | period3, 28 | ); 29 | return yield* handleOKMessage(dataIn); 30 | }, 31 | ( 32 | params: string, 33 | ): [number, number, number, number, number, number, number, number] => { 34 | const [len0, period0, len1, period1, len2, period2, len3, period3] = params 35 | .split(',') 36 | .map((p) => parseInt(p, 10)); 37 | return [len0, period0, len1, period1, len2, period2, len3, period3]; 38 | }, 39 | ); 40 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/pd.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'PD'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Pin direction', 9 | function* (port: string, pin: number, direction: number) { 10 | const dataIn = 11 | yield `${cmd},${port},${pin.toFixed(0)},${direction.toFixed(0)}\r`; 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | (params: string): [string, number, number] => { 15 | const [port, pin, direction] = params.split(','); 16 | return [port, parseInt(pin, 10), parseInt(direction, 10)]; 17 | }, 18 | ); 19 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/pg.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'PG'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Pulse go', 9 | function* (enable: number) { 10 | const dataIn = yield `${cmd},${enable.toFixed(0)}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number] => { 14 | const [enable] = params.split(',').map((p) => parseInt(p, 10)); 15 | return [enable]; 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/pi.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_CR_NL } from '../constants'; 3 | import { readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'PI'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Pin input', 10 | function* (port: string, pin: number) { 11 | const dataIn = yield `${cmd},${port},${pin.toFixed(0)}\r`; 12 | // example response: "PI,1\r\n" 13 | const parsed = yield* readUntil(ENDING_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result.substring(3))); 15 | }, 16 | (params: string): [string, number] => { 17 | const [port, pin] = params.split(','); 18 | return [port, toInt(pin)]; 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/po.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'PO'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Pin output', 9 | function* (port: string, pin: number, value: number) { 10 | const dataIn = yield `${cmd},${port},${pin},${value}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [string, number, number] => { 14 | const [port, pinStr, valueStr] = params.split(','); 15 | const pin = parseInt(pinStr, 10); 16 | const value = parseInt(valueStr, 10); 17 | return [port, pin, value]; 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qb.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QB'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query button', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "0\r\nOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result)); 15 | }, 16 | noParameters, 17 | { 18 | version: '1.9.2', 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qc.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QC'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query current', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "0394,0300\r\nOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => { 15 | const values = result 16 | .substring(0, result.length - 6) // discard \r\nOK\r\n 17 | .split(',') 18 | .map((v) => ((toInt(v) / 1023) * 3.3).toFixed(2)); 19 | return { 20 | ra0: { 21 | voltage: values[0], 22 | maxCurrent: (toInt(values[0]) / 1.76).toFixed(2), 23 | }, 24 | vPlus: { 25 | voltage: (toInt(values[1]) * 9.2 + 0.3).toFixed(2), 26 | }, 27 | }; 28 | }); 29 | }, 30 | noParameters, 31 | { 32 | version: '2.2.3', 33 | }, 34 | ); 35 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qe.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QE'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query motor enable', 10 | function* () { 11 | const dataIn = yield `QE\r`; 12 | // example response: "0,4\r\nOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => { 15 | return result 16 | .substring(0, result.length - 6) 17 | .split(',') 18 | .map(toInt); 19 | }); 20 | }, 21 | noParameters, 22 | { 23 | version: '2.8.0', 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qg.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QG'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query general', 10 | function* () { 11 | const dataIn = yield `QG\r`; 12 | // example response: "3E\r\n" 13 | const parsed = yield* readUntil(ENDING_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => { 15 | const results = toInt(result); 16 | return { 17 | fifo: (results & 1) > 0, 18 | mtr2: (results & 2) > 0, 19 | mtr1: (results & 4) > 0, 20 | cmd: (results & 8) > 0, 21 | pen: (results & 16) > 0, 22 | prg: (results & 32) > 0, 23 | rb2: (results & 64) > 0, 24 | rb5: (results & 128) > 0, 25 | }; 26 | }); 27 | }, 28 | noParameters, 29 | { 30 | version: '2.6.2', 31 | }, 32 | ); 33 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/ql.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QL'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query layer', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "4\r\nOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result)); 15 | }, 16 | noParameters, 17 | { 18 | version: '1.9.2', 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qm.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_NL_CR } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QM'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query motors', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "QM,0,0,0,0\n\r" 13 | // N.B. QM is the only command returning \n\r 14 | // https://github.com/evil-mad/EggBot/issues/159 15 | const parsed = yield* readUntil(ENDING_NL_CR, dataIn); 16 | return transformResult(parsed, (result) => { 17 | const results = result.trim().substring(3).split(',').map(toInt); 18 | return { 19 | command: results[0], 20 | motor1: results[1], 21 | motor2: results[2], 22 | fifo: results[3], 23 | }; 24 | }); 25 | }, 26 | noParameters, 27 | { 28 | version: '2.4.4', 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qn.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QN'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query node count', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "256\r\nOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result)); 15 | }, 16 | noParameters, 17 | { 18 | version: '1.9.2', 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qp.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QP'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query pen', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "1\n\rOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result)); 15 | }, 16 | noParameters, 17 | { 18 | version: '1.9.0', 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qr.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QR'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query RC Servo power state', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "1\n\rOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => toInt(result)); 15 | }, 16 | noParameters, 17 | { 18 | version: '2.6.0', 19 | }, 20 | ); 21 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qs.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, toInt, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QS'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query step position', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "1024,-512\n\rOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => { 15 | const positions = result 16 | .substring(0, result.length - 6) 17 | .split(',') 18 | .map(toInt); 19 | return { 20 | a1: positions[0], 21 | a2: positions[1], 22 | }; 23 | }); 24 | }, 25 | noParameters, 26 | { 27 | version: '2.4.3', 28 | }, 29 | ); 30 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/qt.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, transformResult } from '../utils'; 4 | 5 | export const cmd = 'QT'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Query EBB nickname tag', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "East EBB\r\nOK\r\n" 13 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => 15 | result.substring(0, result.length - 6), 16 | ); 17 | }, 18 | noParameters, 19 | { 20 | version: '2.5.4', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/r.ts: -------------------------------------------------------------------------------- 1 | import { CommandGenerator, createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { noParameters } from '../utils'; 4 | 5 | export const cmd = 'R'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Reset', 10 | function* (): CommandGenerator { 11 | let dataIn = yield `${cmd}\r`; 12 | const parsed = yield* handleOKMessage(dataIn); 13 | // the R command would receive OK twice 14 | dataIn = yield parsed; 15 | return yield* handleOKMessage(dataIn); 16 | }, 17 | noParameters, 18 | ); 19 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/rb.ts: -------------------------------------------------------------------------------- 1 | import { CommandGenerator, createCommand } from '../command'; 2 | import { decode, noParameters } from '../utils'; 3 | 4 | export const cmd = 'RB'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Reboot', 9 | function* (): CommandGenerator { 10 | const dataIn = yield 'RB\r'; 11 | // no data returned 12 | return { 13 | consumed: 0, 14 | remain: decode(dataIn), 15 | result: null, 16 | }; 17 | }, 18 | noParameters, 19 | { 20 | version: '2.5.4', 21 | }, 22 | ); 23 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/s2.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'S2'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'General RC Servo output', 11 | function* ( 12 | position: number, 13 | outputPin: number, 14 | rate?: number, 15 | delay?: number, 16 | ) { 17 | const dataIn = yield cmdWithOptionalParams( 18 | `${cmd},${position.toFixed(0)},${outputPin.toFixed(0)}`, 19 | rate, 20 | delay, 21 | ); 22 | return yield* handleOKMessage(dataIn); 23 | }, 24 | ( 25 | params: string, 26 | ): [number, number, number | undefined, number | undefined] => { 27 | const [position, outputPin, rate, delay] = params.split(',').map(Number); 28 | return [position, outputPin, rate, delay]; 29 | }, 30 | { 31 | execution: EXECUTION_FIFO, 32 | version: '2.2.0', 33 | }, 34 | ); 35 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/sc.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'SC'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Stepper and Servo mode configure', 9 | function* (value1: number, value2: number) { 10 | const dataIn = yield `${cmd},${value1.toFixed(0)},${value2.toFixed(0)}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number, number] => { 14 | const [value1, value2] = params.split(',').map((p) => parseInt(p, 10)); 15 | return [value1, value2]; 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/se.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'SE'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'Set engraver', 11 | function* (state: number, power?: number, useMotionQueue?: number) { 12 | const dataIn = yield cmdWithOptionalParams( 13 | `${cmd},${state.toFixed(0)}`, 14 | power, 15 | useMotionQueue, 16 | ); 17 | return yield* handleOKMessage(dataIn); 18 | }, 19 | (params: string): [number, number | undefined, number | undefined] => { 20 | const [state, power, useMotionQueue] = params.split(',').map(Number); 21 | return [state, power, useMotionQueue]; 22 | }, 23 | { 24 | // FIXME: this command only use FIFO queue when useMotionQueue is set to 1 25 | execution: EXECUTION_FIFO, 26 | version: '2.1.0', 27 | }, 28 | ); 29 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/sl.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'SL'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Set layer', 9 | function* (layer: number) { 10 | const dataIn = yield `${cmd},${layer.toFixed(0)}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number] => { 14 | return [parseInt(params, 10)]; 15 | }, 16 | { 17 | version: '1.9.2', 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/sm.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'SM'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'Stepper move', 11 | function* (duration: number, axis1: number, axis2?: number) { 12 | const dataIn = yield cmdWithOptionalParams( 13 | `${cmd},${duration.toFixed(0)},${axis1.toFixed(0)}`, 14 | axis2, 15 | ); 16 | return yield* handleOKMessage(dataIn); 17 | }, 18 | (params: string): [number, number, number | undefined] => { 19 | const [duration, axis1, axis2] = params.split(',').map(Number); 20 | return [duration, axis1, axis2]; 21 | }, 22 | { 23 | execution: EXECUTION_FIFO, 24 | }, 25 | ); 26 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/sn.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'SN'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Set node count', 9 | function* (count: number) { 10 | const dataIn = yield `${cmd},${count.toFixed(0)}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number] => { 14 | return [parseInt(params, 10)]; 15 | }, 16 | { 17 | version: '1.9.5', 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/sp.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | import { cmdWithOptionalParams } from '../utils'; 5 | 6 | export const cmd = 'SP'; 7 | 8 | export default createCommand( 9 | cmd, 10 | 'Set pen state', 11 | function* (value: number, duration?: number, portBPin?: number) { 12 | const dataIn = yield cmdWithOptionalParams( 13 | `${cmd},${value.toFixed(0)}`, 14 | duration, 15 | portBPin, 16 | ); 17 | return yield* handleOKMessage(dataIn); 18 | }, 19 | (params: string): [number, number | undefined, number | undefined] => { 20 | const [value, duration, portBPin] = params.split(',').map(Number); 21 | return [value, duration, portBPin]; 22 | }, 23 | { 24 | execution: EXECUTION_FIFO, 25 | }, 26 | ); 27 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/sr.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { cmdWithOptionalParams } from '../utils'; 4 | 5 | export const cmd = 'SR'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Set RC Servo power timeout', 10 | function* (timeout: number, state?: number) { 11 | const dataIn = yield cmdWithOptionalParams( 12 | `${cmd},${timeout.toFixed(0)}`, 13 | state, 14 | ); 15 | return yield* handleOKMessage(dataIn); 16 | }, 17 | (params: string): [number, number | undefined] => { 18 | const [timeout, state] = params.split(',').map(Number); 19 | return [timeout, state]; 20 | }, 21 | { 22 | version: '2.6.0', 23 | }, 24 | ); 25 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/st.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'ST'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Set EBB nickname tag', 9 | function* (tag: string) { 10 | const dataIn = yield `${cmd},${tag}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [string] => { 14 | return [params]; 15 | }, 16 | { 17 | version: '2.5.5', 18 | }, 19 | ); 20 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/t.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | 4 | export const cmd = 'T'; 5 | 6 | export default createCommand( 7 | cmd, 8 | 'Timed analog/digital read', 9 | function* (duration: number, mode: number) { 10 | const dataIn = yield `${cmd},${duration.toFixed(0)},${mode.toFixed(0)}\r`; 11 | return yield* handleOKMessage(dataIn); 12 | }, 13 | (params: string): [number, number] => { 14 | const [duration, mode] = params.split(',').map(Number); 15 | return [duration, mode]; 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/tp.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import handleOKMessage from '../messages/ok'; 3 | import { cmdWithOptionalParams } from '../utils'; 4 | 5 | export const cmd = 'TP'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Toggle pen', 10 | function* (duration?: number) { 11 | const dataIn = yield cmdWithOptionalParams(cmd, duration); 12 | return yield* handleOKMessage(dataIn); 13 | }, 14 | (params: string): [number | undefined] => { 15 | const [duration] = params.split(',').map(Number); 16 | return [duration]; 17 | }, 18 | { 19 | version: '1.9.0', 20 | }, 21 | ); 22 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/v.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { ENDING_CR_NL } from '../constants'; 3 | import { noParameters, readUntil, transformResult } from '../utils'; 4 | 5 | export const cmd = 'V'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Version', 10 | function* () { 11 | const dataIn = yield `${cmd}\r`; 12 | // example response: "EBBv13_and_above EB Firmware Version 2.7.0\r\n" 13 | const parsed = yield* readUntil(ENDING_CR_NL, dataIn); 14 | return transformResult(parsed, (result) => result.trim()); 15 | }, 16 | noParameters, 17 | ); 18 | -------------------------------------------------------------------------------- /src/communication/ebb/commands/xm.ts: -------------------------------------------------------------------------------- 1 | import { createCommand } from '../command'; 2 | import { EXECUTION_FIFO } from '../constants'; 3 | import handleOKMessage from '../messages/ok'; 4 | 5 | export const cmd = 'XM'; 6 | 7 | export default createCommand( 8 | cmd, 9 | 'Stepper move, mixed-axis geometries', 10 | function* (duration: number, xSteps: number, ySteps: number) { 11 | const dataIn = 12 | yield `${cmd},${duration.toFixed(0)},${xSteps.toFixed(0)},${ySteps.toFixed(0)}\r`; 13 | return yield* handleOKMessage(dataIn); 14 | }, 15 | (params: string): [number, number, number] => { 16 | const [duration, xSteps, ySteps] = params.split(',').map(Number); 17 | return [duration, xSteps, ySteps]; 18 | }, 19 | { 20 | execution: EXECUTION_FIFO, 21 | version: '2.3.0', 22 | }, 23 | ); 24 | -------------------------------------------------------------------------------- /src/communication/ebb/constants.ts: -------------------------------------------------------------------------------- 1 | export const ENDING_CR = '\r'; 2 | export const ENDING_CR_NL = '\r\n'; 3 | export const ENDING_NL_CR = '\n\r'; 4 | export const ENDING_OK_CR_NL = 'OK\r\n'; 5 | 6 | export const EXECUTION_IMMEDIATE = 0; 7 | export const EXECUTION_FIFO = 1; 8 | 9 | export const HIGH_DPI_AA = 2870; 10 | export const HIGH_DPI_XY = 2029; // HIGH_DPI_AA / Sqrt(2) 11 | -------------------------------------------------------------------------------- /src/communication/ebb/index.ts: -------------------------------------------------------------------------------- 1 | export { default as a } from './commands/a'; 2 | export { default as ac } from './commands/ac'; 3 | export { default as bl } from './commands/bl'; 4 | export { default as c } from './commands/c'; 5 | export { default as ck } from './commands/ck'; 6 | export { default as cs } from './commands/cs'; 7 | export { default as cu } from './commands/cu'; 8 | export { default as em } from './commands/em'; 9 | export { default as es } from './commands/es'; 10 | export { default as hm } from './commands/hm'; 11 | export { default as i } from './commands/i'; 12 | export { default as lm } from './commands/lm'; 13 | export { default as lt } from './commands/lt'; 14 | export { default as mr } from './commands/mr'; 15 | export { default as mw } from './commands/mw'; 16 | export { default as nd } from './commands/nd'; 17 | export { default as ni } from './commands/ni'; 18 | export { default as o } from './commands/o'; 19 | export { default as pc } from './commands/pc'; 20 | export { default as pd } from './commands/pd'; 21 | export { default as pg } from './commands/pg'; 22 | export { default as pi } from './commands/pi'; 23 | export { default as po } from './commands/po'; 24 | export { default as qb } from './commands/qb'; 25 | export { default as qc } from './commands/qc'; 26 | export { default as qe } from './commands/qe'; 27 | export { default as qg } from './commands/qg'; 28 | export { default as ql } from './commands/ql'; 29 | export { default as qm } from './commands/qm'; 30 | export { default as qn } from './commands/qn'; 31 | export { default as qp } from './commands/qp'; 32 | export { default as qr } from './commands/qr'; 33 | export { default as qs } from './commands/qs'; 34 | export { default as qt } from './commands/qt'; 35 | export { default as r } from './commands/r'; 36 | export { default as rb } from './commands/rb'; 37 | export { default as s2 } from './commands/s2'; 38 | export { default as sc } from './commands/sc'; 39 | export { default as se } from './commands/se'; 40 | export { default as sl } from './commands/sl'; 41 | export { default as sm } from './commands/sm'; 42 | export { default as sn } from './commands/sn'; 43 | export { default as sp } from './commands/sp'; 44 | export { default as sr } from './commands/sr'; 45 | export { default as st } from './commands/st'; 46 | export { default as t } from './commands/t'; 47 | export { default as tp } from './commands/tp'; 48 | export { default as v } from './commands/v'; 49 | export { default as xm } from './commands/xm'; 50 | -------------------------------------------------------------------------------- /src/communication/ebb/messages/ebb.ts: -------------------------------------------------------------------------------- 1 | import { PendingCommand } from '@/communication/device/utils'; 2 | import { decode, logger } from '../utils'; 3 | import handleErrorMessage from './error'; 4 | 5 | export default function* handleEBBMessages( 6 | commandQueue: PendingCommand[], 7 | ): Generator { 8 | let buffer: number[] = []; 9 | let errorHandler = null; 10 | for (;;) { 11 | const dataIn = yield; 12 | buffer.push(...new Uint8Array(dataIn)); 13 | // data arrived 14 | while (buffer.length) { 15 | if (commandQueue.length) { 16 | if (buffer[0] === '!'.charCodeAt(0)) { 17 | if (!errorHandler) { 18 | errorHandler = handleErrorMessage(); 19 | errorHandler.next(); // set ready 20 | } 21 | } 22 | const cmd = commandQueue[0]; 23 | const handler = (errorHandler || cmd.parser) as unknown as Generator< 24 | { consumed: number }, 25 | { result: unknown; consumed: number }, 26 | number[] 27 | >; 28 | // pass the current buffer to cmd, 29 | // and let it consume what it needs. 30 | const cmdStatus = handler.next(buffer); 31 | const { consumed } = cmdStatus.value; 32 | if (consumed) { 33 | buffer = buffer.slice(consumed); 34 | } 35 | if (cmdStatus.done) { 36 | const { result } = cmdStatus.value; 37 | if (handler === errorHandler) { 38 | errorHandler = null; 39 | cmd.reject(result as string); 40 | } else { 41 | logger.debug(`Received message: ${JSON.stringify(result)}`); 42 | cmd.resolve(result); 43 | } 44 | commandQueue.shift(); 45 | } 46 | } else { 47 | const garbage = decode(buffer); 48 | logger.debug(`Discard garbage message: ${garbage}`); 49 | buffer.length = 0; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/communication/ebb/messages/error.ts: -------------------------------------------------------------------------------- 1 | import { CommandGenerator } from '../command'; 2 | import { ENDING_CR } from '../constants'; 3 | import { readUntil } from '../utils'; 4 | 5 | export default function* handleErrorMessage(): CommandGenerator { 6 | let parsed: { consumed: number; remain: string; result?: string } = { 7 | consumed: 0, 8 | remain: '', 9 | }; 10 | let result = ''; 11 | do { 12 | const dataIn: number[] = yield parsed; 13 | // The error message may ends with \r\n or \n\r 14 | parsed = yield* readUntil(ENDING_CR, dataIn); 15 | result += (parsed as { result: string }).result; 16 | } while ((parsed as { remain: string }).remain[0] === '!'); 17 | return { 18 | ...(parsed as { consumed: number; remain: string }), 19 | result, 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /src/communication/ebb/messages/ok.ts: -------------------------------------------------------------------------------- 1 | import { CommandGenerator } from '../command'; 2 | import { ENDING_OK_CR_NL } from '../constants'; 3 | import { readUntil, transformResult } from '../utils'; 4 | 5 | export default function* handleOKMessage( 6 | dataIn: number[], 7 | ): CommandGenerator { 8 | const parsed = yield* readUntil(ENDING_OK_CR_NL, dataIn); 9 | return transformResult(parsed, (result) => result.trim()); 10 | } 11 | -------------------------------------------------------------------------------- /src/communication/ebb/utils.ts: -------------------------------------------------------------------------------- 1 | import Logger from 'js-logger'; 2 | 3 | export const logger = Logger.get('ebb'); 4 | 5 | export const toInt = (i: string) => parseInt(i, 10); 6 | 7 | const decoder = new TextDecoder(); 8 | export const decode = (buffer: number[]) => 9 | decoder.decode(Uint8Array.from(buffer)); 10 | 11 | const encoder = new TextEncoder(); 12 | export const encode = (msg: string) => encoder.encode(msg); 13 | 14 | export const cmdWithOptionalParams = ( 15 | cmd: string, 16 | ...optional: (string | number | undefined)[] 17 | ) => { 18 | let cmdWithParams = cmd; 19 | for (const param of optional) { 20 | // ignore all following commands if it's undefined 21 | if (param === undefined) break; 22 | cmdWithParams += `,${param}`; 23 | } 24 | return `${cmdWithParams}\r`; 25 | }; 26 | 27 | export const readUntil = function* ( 28 | ending: string, 29 | dataIn: number[], 30 | ): Generator< 31 | { consumed: number }, 32 | { result: string; consumed: number; remain: string }, 33 | number[] 34 | > { 35 | let buffer = ''; 36 | let foundEnding = -1; 37 | let consumed = 0; 38 | let nextDataIn = dataIn; 39 | do { 40 | const fragment = decode(nextDataIn); 41 | buffer += fragment; 42 | foundEnding = buffer.indexOf(ending); 43 | if (foundEnding !== -1) { 44 | foundEnding += ending.length; 45 | break; 46 | } 47 | nextDataIn = yield { 48 | consumed: fragment.length, 49 | }; 50 | consumed += fragment.length; 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition 53 | } while (foundEnding === -1); 54 | return { 55 | // discard any space at the beginning 56 | result: buffer.substring(0, foundEnding).trimStart(), 57 | consumed: foundEnding - consumed, 58 | remain: buffer.substring(foundEnding), 59 | }; 60 | }; 61 | 62 | export const transformResult = ( 63 | parsed: { result: string; consumed: number; remain: string }, 64 | fn: (result: string) => T, 65 | ): { result: T; consumed: number; remain: string } => { 66 | return { 67 | ...parsed, 68 | result: fn(parsed.result), 69 | }; 70 | }; 71 | 72 | export const checkVersion = ( 73 | deviceVersion: string, 74 | cmdVersion: string, 75 | ): boolean => { 76 | const [dMajor, dMinor, dPatch] = deviceVersion.split('.'); 77 | const [cMajor, cMinor, cPatch] = cmdVersion.split('.'); 78 | if (cMajor < dMajor) return true; 79 | if (dMajor < cMajor) return false; 80 | // cMajor === dMajor 81 | if (cMinor < dMinor) return true; 82 | if (dMinor < cMinor) return false; 83 | // dMinor === dMinor 84 | return cPatch <= dPatch; 85 | }; 86 | 87 | export const noParameters = (_: string): [] => []; 88 | -------------------------------------------------------------------------------- /src/components/footer/footer.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply text-sm text-center mb-8; 3 | 4 | color: hsl(0 0% 63.9%); 5 | 6 | p { 7 | @apply leading-6; 8 | } 9 | 10 | a { 11 | @apply underline; 12 | } 13 | 14 | svg { 15 | @apply fill-current; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/footer/footer.tsx: -------------------------------------------------------------------------------- 1 | import { version } from '../../../package.json'; 2 | import styles from './footer.module.css'; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |

8 | AxiDraw Web © 2021-2025{' '} 9 | 10 | mutoo.im 11 | 12 |

13 |

14 | Build: {version}-{import.meta.env.MODE} | Report Issues:{' '} 15 | 20 | 27 | 28 | 29 | 30 |

31 |
32 | ); 33 | }; 34 | 35 | export default Footer; 36 | -------------------------------------------------------------------------------- /src/components/loading/loading.module.css: -------------------------------------------------------------------------------- 1 | .loading { 2 | @apply block p-3; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/loading/loading.tsx: -------------------------------------------------------------------------------- 1 | import styles from './loading.module.css'; 2 | 3 | const Loading = () => { 4 | return
Loading
; 5 | }; 6 | 7 | export default Loading; 8 | -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { cva, type VariantProps } from "class-variance-authority" 2 | import * as React from "react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | import * as React from "react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /src/components/ui/form.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply grid grid-cols-1 gap-4 lg:gap-6; 3 | } 4 | 5 | .checkboxLabel, 6 | .radioLabel { 7 | @apply inline-flex items-center; 8 | 9 | span { 10 | @apply ml-2; 11 | } 12 | 13 | input[disabled] { 14 | background-color: var(--muted); 15 | } 16 | } 17 | 18 | .inputLabel { 19 | @apply inline-flex flex-col items-stretch; 20 | 21 | span { 22 | @apply mb-2; 23 | } 24 | 25 | input[disabled], 26 | select[disabled], 27 | textarea[disabled] { 28 | background-color: var(--muted); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/ui/sheet.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply p-8 mx-auto mb-8 bg-white shadow-xl md:mt-8; 3 | @apply grid grid-cols-1 gap-6; 4 | 5 | @screen md { 6 | max-width: 600px; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Toast, 3 | ToastClose, 4 | ToastDescription, 5 | ToastProvider, 6 | ToastTitle, 7 | ToastViewport, 8 | } from "@/components/ui/toast" 9 | import { useToast } from "@/hooks/use-toast" 10 | 11 | export function Toaster() { 12 | const { toasts } = useToast() 13 | 14 | return ( 15 | 16 | {toasts.map(function ({ id, title, description, action, ...props }) { 17 | return ( 18 | 19 |
20 | {title && {title}} 21 | {description && ( 22 | {description} 23 | )} 24 |
25 | {action} 26 | 27 |
28 | ) 29 | })} 30 | 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/containers/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense, lazy } from 'react'; 2 | import { Route, Routes } from 'react-router'; 3 | import Loading from '@/components/loading/loading'; 4 | import { Toaster } from '@/components/ui/toaster'; 5 | 6 | const Plotter = lazy(() => import('../plotter/plotter')); 7 | const VirtualPlotter = lazy(() => import('../virtual/virtual')); 8 | const Debugger = lazy(() => import('../debugger/debugger')); 9 | const Composer = lazy(() => import('../composer/composer')); 10 | 11 | const App = () => { 12 | return ( 13 | <> 14 | }> 15 | 16 | } /> 17 | } /> 18 | } /> 19 | } /> 20 | 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default App; 28 | -------------------------------------------------------------------------------- /src/containers/composer/composer.tsx: -------------------------------------------------------------------------------- 1 | import classnames from 'clsx'; 2 | import { useState } from 'react'; 3 | import { IDeviceConnector } from '@/communication/device/device'; 4 | import DeviceConnector from '@/components/device-connector/device-connector'; 5 | import Footer from '@/components/footer/footer'; 6 | import formStyles from '@/components/ui/form.module.css'; 7 | import sheetsStyles from '@/components/ui/sheet.module.css'; 8 | import MidiCommander from './components/midi-commander'; 9 | 10 | const Composer = () => { 11 | const [device, setDevice] = useState | null>(null); 12 | 13 | return ( 14 | <> 15 |
16 | { setDevice(null); }} 19 | /> 20 | {device && } 21 |
22 |