├── media.css ├── src ├── server │ ├── static │ │ ├── foo.js │ │ ├── favicon.ico │ │ ├── hand-axis.png │ │ ├── favicon-doge.ico │ │ ├── KinematicsDiagram.jpg │ │ └── KinematicsDiagram.pdf │ ├── middleware │ │ ├── errorHandler.js │ │ └── proxy.js │ ├── routes │ │ ├── health.js │ │ ├── camera.js │ │ ├── fail.js │ │ ├── robots.js │ │ ├── waypoints.js │ │ └── recipes.js │ ├── robot │ │ ├── camera-messenger.js │ │ ├── client-messenger.js │ │ └── robot-messenger.js │ ├── terminate.js │ ├── index.js │ ├── socketserver.js │ ├── app.js │ └── setup.js ├── client │ ├── context │ │ ├── AppContext.js │ │ ├── ArmContext.js │ │ ├── CameraContext.js │ │ ├── GamepadContext.js │ │ ├── SimulateContext.js │ │ └── RobotContext.js │ ├── hooks │ │ ├── useAuth.js │ │ ├── useApp.js │ │ ├── useCamera.js │ │ ├── useGamepad.js │ │ ├── useRobotMeta.js │ │ ├── useRobotState.js │ │ ├── useSimulateState.js │ │ ├── useRobotController.js │ │ ├── useRobotKinematics.js │ │ ├── useOverflowHidden.js │ │ ├── useSimulateController.js │ │ ├── useStateWithGetter.js │ │ ├── useGet.js │ │ ├── usePost.js │ │ ├── useEffectOnce.js │ │ ├── useOutsideAlerter.js │ │ └── useMedia.js │ ├── components │ │ ├── Shared │ │ │ ├── If.jsx │ │ │ ├── Status.jsx │ │ │ ├── NavLink.jsx │ │ │ ├── RobotType.jsx │ │ │ └── ResizablePopup.jsx │ │ ├── Data │ │ │ ├── MotorData.jsx │ │ │ ├── Visualizations │ │ │ │ ├── LineGraph.css │ │ │ │ └── LineGraph.jsx │ │ │ ├── RobotData.jsx │ │ │ ├── JointsData │ │ │ │ ├── RizonJointData.jsx │ │ │ │ ├── ExampleJointData.jsx │ │ │ │ ├── JointsData.jsx │ │ │ │ └── ARJointData.jsx │ │ │ ├── CameraData.jsx │ │ │ ├── Data.jsx │ │ │ └── GeneralData.jsx │ │ ├── Pages │ │ │ ├── NotFound │ │ │ │ ├── NotFound.jsx │ │ │ │ └── NotFound.css │ │ │ ├── NotAuthorized │ │ │ │ ├── NotAuthorized.jsx │ │ │ │ └── NotAuthorized.css │ │ │ ├── Gamepad │ │ │ │ └── gamepad.css │ │ │ ├── Builder │ │ │ │ ├── Info.jsx │ │ │ │ └── Builder.jsx │ │ │ └── Cookbook │ │ │ │ └── Recipe.jsx │ │ ├── Extra │ │ │ ├── Extra.jsx │ │ │ └── RobotExtra.jsx │ │ ├── Informed │ │ │ ├── Form.jsx │ │ │ ├── Slider.jsx │ │ │ ├── Switch.jsx │ │ │ ├── NumberInput.jsx │ │ │ ├── Input.jsx │ │ │ ├── Checkbox.jsx │ │ │ ├── Select.jsx │ │ │ ├── RadioGroup.jsx │ │ │ ├── Listbox.jsx │ │ │ └── InputSlider.jsx │ │ ├── Footer │ │ │ └── Footer.jsx │ │ ├── 3D │ │ │ └── URDFRobot.jsx │ │ ├── Routes │ │ │ └── Routes.jsx │ │ ├── App │ │ │ └── App.jsx │ │ ├── Nav │ │ │ ├── Nav.jsx │ │ │ ├── FramerNav.jsx │ │ │ ├── CookbookNav.jsx │ │ │ └── MotorNav.jsx │ │ └── Header │ │ │ └── Header.jsx │ ├── utils │ │ ├── debounce.js │ │ ├── getEulers.js │ │ └── media.js │ ├── providers │ │ ├── CameraProvider.jsx │ │ ├── GamepadProvider.jsx │ │ ├── ControlProvider.jsx │ │ └── AppProvider.jsx │ ├── tokens │ │ └── media.json │ ├── public │ │ └── index.html │ ├── index.jsx │ └── constants.js └── lib │ ├── toDeg.js │ ├── toRadians.js │ ├── printMatrixJs.js │ ├── matrixSubset.js │ ├── matrixDot.js │ ├── printRotationMatrix.js │ ├── matrixEqual.js │ ├── round.js │ ├── roundMatrix.js │ ├── forward.js │ ├── rotateMatrix.js │ ├── matrixDotString.js │ ├── printRotationMatrix.test.js │ ├── forward.test.js │ ├── euler.js │ ├── inverse.test.js │ ├── euler.test.js │ ├── newForward.js │ ├── inverse1_3.test.js │ ├── math.js │ └── denavitHartenberg.test.js ├── .npmrc ├── mocks ├── file-mock.js └── style-mock.js ├── jest.setup.js ├── .prettierrc.cjs ├── example_py ├── requirements.txt ├── config.json ├── main.py ├── test_debug.py ├── debug.py └── motor.py ├── example_py_7 ├── requirements.txt ├── main.py ├── test_debug.py ├── config.json ├── debug.py └── motor.py ├── babel.config.cjs ├── .gitignore ├── index.html ├── .eslintrc.cjs ├── .dockerignore ├── jest.config.cjs ├── vite.config.js ├── robots ├── SingleFrame.json ├── HalfBuilt.json ├── UR3eHalf.json ├── UR3e.json ├── IgusRebelBackup.json ├── Example.json ├── Example7Axis.json ├── Rizon4Test.json ├── Rizon4Test2.json ├── Rizon4.json ├── AR4.json └── IgusRebel.json ├── example ├── index.js └── config.json ├── Dockerfile └── package.json /media.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/server/static/foo.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /mocks/file-mock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /mocks/style-mock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; 2 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | }; 5 | -------------------------------------------------------------------------------- /example_py/requirements.txt: -------------------------------------------------------------------------------- 1 | python-socketio>=5.0.0 2 | colorama 3 | pyee 4 | requests 5 | websocket-client 6 | -------------------------------------------------------------------------------- /example_py_7/requirements.txt: -------------------------------------------------------------------------------- 1 | python-socketio>=5.0.0 2 | colorama 3 | pyee 4 | requests 5 | websocket-client 6 | -------------------------------------------------------------------------------- /src/server/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepuzzo/robot-viewer/HEAD/src/server/static/favicon.ico -------------------------------------------------------------------------------- /src/server/static/hand-axis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepuzzo/robot-viewer/HEAD/src/server/static/hand-axis.png -------------------------------------------------------------------------------- /src/server/static/favicon-doge.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepuzzo/robot-viewer/HEAD/src/server/static/favicon-doge.ico -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }] 4 | ] 5 | }; -------------------------------------------------------------------------------- /src/server/static/KinematicsDiagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepuzzo/robot-viewer/HEAD/src/server/static/KinematicsDiagram.jpg -------------------------------------------------------------------------------- /src/server/static/KinematicsDiagram.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joepuzzo/robot-viewer/HEAD/src/server/static/KinematicsDiagram.pdf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | build 4 | coverage 5 | .vscode 6 | waypoints 7 | recipes 8 | .DS_Store 9 | venv 10 | **/__pycache__/ -------------------------------------------------------------------------------- /src/client/context/AppContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const AppContext = React.createContext(); 3 | 4 | export default AppContext; 5 | -------------------------------------------------------------------------------- /src/client/context/ArmContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const ArmContext = React.createContext(); 3 | 4 | export default ArmContext; 5 | -------------------------------------------------------------------------------- /src/client/context/CameraContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const CameraContext = React.createContext(); 3 | 4 | export default CameraContext; 5 | -------------------------------------------------------------------------------- /src/client/context/GamepadContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | const GamepadContext = React.createContext(); 3 | 4 | export default GamepadContext; 5 | -------------------------------------------------------------------------------- /src/client/hooks/useAuth.js: -------------------------------------------------------------------------------- 1 | const useAuth = () => { 2 | return { user: { name: 'Joe', permissions: ['USER'] } }; 3 | }; 4 | 5 | export default useAuth; 6 | -------------------------------------------------------------------------------- /src/client/components/Shared/If.jsx: -------------------------------------------------------------------------------- 1 | export const If = ({ condition, otherwise, children }) => { 2 | return condition ? children : otherwise || null; 3 | }; 4 | -------------------------------------------------------------------------------- /src/client/context/SimulateContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export const SimulateStateContext = React.createContext(); 3 | export const SimulateControllerContext = React.createContext(); 4 | -------------------------------------------------------------------------------- /src/lib/toDeg.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts radians to degrees 3 | * 4 | * @param {*} rad 5 | * @returns 6 | */ 7 | export const toDeg = (rad) => { 8 | return 180 * (rad / Math.PI); 9 | }; 10 | -------------------------------------------------------------------------------- /src/client/components/Data/MotorData.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { JointsData } from './JointsData/JointsData'; 3 | 4 | export const MotorData = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /src/lib/toRadians.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts deg to radians 3 | * 4 | * @param {*} deg 5 | * @returns 6 | */ 7 | export const toRadians = (deg) => { 8 | return (deg / 180) * Math.PI; 9 | }; 10 | -------------------------------------------------------------------------------- /src/client/components/Data/Visualizations/LineGraph.css: -------------------------------------------------------------------------------- 1 | .lineGraph { 2 | border: 1px solid #dad8d2; 3 | } 4 | 5 | .graphData { 6 | fill: none; 7 | stroke: #00b7c6; 8 | } 9 | 10 | .graphData2 { 11 | fill: none; 12 | stroke: #c6b900; 13 | } 14 | -------------------------------------------------------------------------------- /src/client/hooks/useApp.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import AppContext from '../context/AppContext.js'; 3 | 4 | function useApp() { 5 | const appContext = useContext(AppContext); 6 | return appContext; 7 | } 8 | 9 | export default useApp; 10 | -------------------------------------------------------------------------------- /src/client/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export function debounce(func, timeout = 300) { 2 | let timer; 3 | return (...args) => { 4 | clearTimeout(timer); 5 | timer = setTimeout(() => { 6 | func.apply(this, args); 7 | }, timeout); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/client/hooks/useCamera.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import CameraContext from '../context/CameraContext'; 3 | 4 | function useCamera() { 5 | const context = useContext(CameraContext); 6 | return context; 7 | } 8 | 9 | export default useCamera; 10 | -------------------------------------------------------------------------------- /src/server/middleware/errorHandler.js: -------------------------------------------------------------------------------- 1 | import logger from 'winston'; 2 | 3 | const errorHandler = (err, req, res, next) => { 4 | logger.error('Unexpected Error', err); 5 | // TODO evetually route the user to an error page 6 | res.sendStatus(500); 7 | }; 8 | 9 | export default errorHandler; -------------------------------------------------------------------------------- /src/client/hooks/useGamepad.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import GamepadContext from '../context/GamepadContext'; 3 | 4 | function useGamepad() { 5 | const gamepadContext = useContext(GamepadContext); 6 | return gamepadContext; 7 | } 8 | 9 | export default useGamepad; 10 | -------------------------------------------------------------------------------- /src/client/hooks/useRobotMeta.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { RobotMetaContext } from '../context/RobotContext'; 3 | 4 | function useRobotMeta() { 5 | const robotMeta = useContext(RobotMetaContext); 6 | return robotMeta; 7 | } 8 | 9 | export default useRobotMeta; 10 | -------------------------------------------------------------------------------- /src/client/hooks/useRobotState.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { RobotStateContext } from '../context/RobotContext'; 3 | 4 | function useRobotState() { 5 | const robotState = useContext(RobotStateContext); 6 | return robotState; 7 | } 8 | 9 | export default useRobotState; 10 | -------------------------------------------------------------------------------- /src/client/context/RobotContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | export const RobotStateContext = React.createContext(); 3 | export const RobotMetaContext = React.createContext(); 4 | export const RobotControllerContext = React.createContext(); 5 | export const RobotKinimaticsContext = React.createContext(); 6 | -------------------------------------------------------------------------------- /src/client/components/Pages/NotFound/NotFound.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import NotFoundSVG from '../notfound.svg'; 3 | 4 | export const NotFound = () => { 5 | return ( 6 |
7 | {/* */} 8 |

Not Found

9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/client/hooks/useSimulateState.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { SimulateStateContext } from '../context/SimulateContext'; 3 | 4 | function useSimulateState() { 5 | const simulateState = useContext(SimulateStateContext); 6 | return simulateState; 7 | } 8 | 9 | export default useSimulateState; 10 | -------------------------------------------------------------------------------- /src/lib/printMatrixJs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Prints out the JS function for this matrix 3 | * @param {*} m - the matrix 4 | * @param {*} c - the variable name 5 | */ 6 | export const printMatrixJS = (m, c) => { 7 | console.log(`const ${c} = [`); 8 | m.forEach((a) => console.log(` [${a}],`)); 9 | console.log(']'); 10 | }; 11 | -------------------------------------------------------------------------------- /src/client/hooks/useRobotController.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { RobotControllerContext } from '../context/RobotContext'; 3 | 4 | function useRobotController() { 5 | const robotController = useContext(RobotControllerContext); 6 | return robotController; 7 | } 8 | 9 | export default useRobotController; 10 | -------------------------------------------------------------------------------- /src/client/hooks/useRobotKinematics.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { RobotKinimaticsContext } from '../context/RobotContext'; 3 | 4 | function useRobotKinematics() { 5 | const robotKinematics = useContext(RobotKinimaticsContext); 6 | return robotKinematics; 7 | } 8 | 9 | export default useRobotKinematics; 10 | -------------------------------------------------------------------------------- /src/client/components/Pages/NotAuthorized/NotAuthorized.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // import Unauthorized from '../unauthorized.svg'; 3 | 4 | export const NotAuthorized = () => { 5 | return ( 6 |
7 |

Not NotAuthorized

8 | {/* */} 9 |
10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/client/hooks/useOverflowHidden.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | export const useOverFlowHidden = () => { 4 | useEffect(() => { 5 | document.body.style.overflow = 'hidden'; 6 | 7 | return () => { 8 | document.body.style.overflow = 'auto'; // cleanup or run on page unmount 9 | }; 10 | }, []); 11 | }; 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Robot Viewer 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/client/hooks/useSimulateController.js: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { SimulateControllerContext } from '../context/SimulateContext'; 3 | 4 | function useSimulateController() { 5 | const simulateController = useContext(SimulateControllerContext); 6 | return simulateController; 7 | } 8 | 9 | export default useSimulateController; 10 | -------------------------------------------------------------------------------- /src/client/components/Extra/Extra.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Routes, Route } from 'react-router-dom'; 3 | 4 | import { RobotExtra } from './RobotExtra'; 5 | 6 | export const Extra = () => { 7 | return ( 8 | 9 | } /> 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /example_py/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "j0": { "limNeg": -180, "limPos": 180, "homePos": 0 }, 3 | "j1": { "limNeg": -140, "limPos": 80, "homePos": 0 }, 4 | "j2": { "limNeg": -140, "limPos": 80, "homePos": 0 }, 5 | "j3": { "limNeg": -180, "limPos": 180, "homePos": 0 }, 6 | "j4": { "limNeg": -95, "limPos": 95, "homePos": 0 }, 7 | "j5": { "limNeg": -180, "limPos": 180, "homePos": 0 } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:prettier/recommended'], 3 | ignorePatterns: ['*rc.*js', '*.config.*js'], 4 | parser: undefined, 5 | parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' }, 6 | plugins: ['import', 'react', 'prettier'], 7 | root: true, 8 | rules: { 'import/extensions': [2, 'ignorePackages'] }, 9 | }; 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | ** 3 | 4 | # Allow files and directories 5 | !/src/** 6 | !/robots/** 7 | !/public/** 8 | !/build-utils/** 9 | !/package.json 10 | !/package-lock.json 11 | !/.npmrc 12 | !/babel.config.cjs 13 | !/vite.config.js 14 | !/index.html 15 | 16 | # Ignore unnecessary files inside allowed directories 17 | # This should go after the allowed directories 18 | **/*~ 19 | **/*.log 20 | **/.DS_Store -------------------------------------------------------------------------------- /src/lib/matrixSubset.js: -------------------------------------------------------------------------------- 1 | /** 2 | * matrixSubset 3 | * 4 | * @param {*} m - the matrix 5 | * @param {*} cols - the number of columns we want 6 | * @param {*} rows - the number of rows we want 7 | * @returns a subset of the original matrix 8 | */ 9 | export const matrixSubset = (m, cols, rows) => { 10 | const subset = []; 11 | 12 | for (let i = 0; i < rows; i++) { 13 | subset[i] = m[i].slice(0, cols); 14 | } 15 | 16 | return subset; 17 | }; 18 | -------------------------------------------------------------------------------- /src/client/components/Data/RobotData.jsx: -------------------------------------------------------------------------------- 1 | import { Flex } from '@adobe/react-spectrum'; 2 | import React from 'react'; 3 | import { JointsData } from './JointsData/JointsData'; 4 | import { CameraData } from './CameraData'; 5 | import { GeneralData } from './GeneralData'; 6 | 7 | export const RobotData = () => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/client/components/Shared/Status.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StatusLight } from '@adobe/react-spectrum'; 3 | 4 | export const Status = ({ status }) => { 5 | if (status) { 6 | return ( 7 | 8 | Yes 9 | 10 | ); 11 | } else { 12 | return ( 13 | 14 | No 15 | 16 | ); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /src/lib/matrixDot.js: -------------------------------------------------------------------------------- 1 | import { round } from './round'; 2 | 3 | /** 4 | * Takes the dot product of two matricies 5 | * 6 | * @param {*} a 7 | * @param {*} b 8 | * @returns 9 | */ 10 | export function matrixDot(a, b) { 11 | var result = new Array(a.length).fill(0).map((row) => new Array(b[0].length).fill(0)); 12 | 13 | return result.map((row, i) => { 14 | return row.map((val, j) => { 15 | return round(a[i].reduce((sum, elm, k) => sum + elm * b[k][j], 0)); 16 | }); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /src/client/components/Extra/RobotExtra.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import useApp from '../../hooks/useApp'; 3 | import { Cookbook } from '../Pages/Cookbook/Cookbook'; 4 | import { ResizablePopup } from '../Shared/ResizablePopup'; 5 | 6 | export const RobotExtra = () => { 7 | const { extraOpen } = useApp(); 8 | return ( 9 |
10 | 11 | 12 | 13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /src/lib/printRotationMatrix.js: -------------------------------------------------------------------------------- 1 | import { printMatrixJS } from './printMatrixJs'; 2 | import { matrixDotString } from './matrixDotString'; 3 | 4 | export const printZRotationMatrix = (projection, name, angle) => { 5 | const z_matrix_rotation = [ 6 | [`Math.cos(${angle})`, `-Math.sin(${angle})`, '0'], 7 | [`Math.sin(${angle})`, `Math.cos(${angle})`, '0'], 8 | ['0', '0', '1'], 9 | ]; 10 | 11 | const result = matrixDotString(z_matrix_rotation, projection); 12 | 13 | printMatrixJS(result, name); 14 | }; 15 | -------------------------------------------------------------------------------- /src/lib/matrixEqual.js: -------------------------------------------------------------------------------- 1 | export const matrixEqual = (m1, m2) => { 2 | // If sizes are not same return early 3 | if (m1.length != m2.length || m1[0].length != m2[0].length) { 4 | return false; 5 | } 6 | 7 | // return the second something is not equal 8 | for (let i = 0; i < m1.length; i++) { 9 | for (let j = 0; j < m1.length; j++) { 10 | if (m1[i][j] != m2[i][j]) { 11 | return false; 12 | } 13 | } 14 | } 15 | 16 | // If we got here we are equal! so return true!! 17 | return true; 18 | }; 19 | -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | collectCoverage: false, 3 | verbose: true, 4 | testEnvironment: 'node', 5 | testEnvironmentOptions: { 6 | url: 'http://localhost/' 7 | }, 8 | moduleNameMapper: { 9 | '\\.(css)$': '/mocks/style-mock.js', 10 | '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 11 | '/mocks/file-mock.js', 12 | }, 13 | setupFilesAfterEnv: ['./jest.setup.js'], 14 | transform: { 15 | '^.+\\.js$': 'babel-jest' 16 | } 17 | }; 18 | 19 | module.exports = config; 20 | -------------------------------------------------------------------------------- /src/client/components/Informed/Form.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm } from 'informed'; 3 | import { Form as AdobeForm } from '@adobe/react-spectrum'; 4 | 5 | const Form = ({ children, ...rest }) => { 6 | const { formController, render, userProps } = useForm(rest); 7 | 8 | return render( 9 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export default Form; 21 | -------------------------------------------------------------------------------- /src/lib/round.js: -------------------------------------------------------------------------------- 1 | export const round = (n, to = 1000000) => Math.round(n * to) / to; 2 | 3 | export const roundOne = (n) => { 4 | let r = n; 5 | if (r > 1) { 6 | return 1; 7 | } 8 | 9 | if (r < -1) { 10 | return -1; 11 | } else return r; 12 | }; 13 | 14 | /** 15 | * Rounds the array and removes negative zeros 16 | * 17 | * @param {*} arr 18 | * @returns 19 | */ 20 | export const roundArray = (arr) => { 21 | return arr.map((n) => { 22 | let rounded = round(n); 23 | rounded = Object.is(rounded, -0) ? 0 : rounded; 24 | return rounded; 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | server: { 8 | port: 9001, 9 | host: '0.0.0.0', 10 | }, 11 | build: { 12 | outDir: 'build', 13 | emptyOutDir: true, 14 | }, 15 | resolve: { 16 | alias: { 17 | '@': path.resolve(__dirname, './src'), 18 | 'Components': path.resolve(__dirname, './src/client/components/'), 19 | 'Utils': path.resolve(__dirname, './src/client/utils/utils'), 20 | 'Hooks': path.resolve(__dirname, './src/client/hooks/hooks'), 21 | }, 22 | }, 23 | }); -------------------------------------------------------------------------------- /src/server/routes/health.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import logger from 'winston'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/health', (req, res) => { 7 | const status = { status: 'UP' }; 8 | // Note: we specifically DONT log here because F5 pings health too much 9 | return res.send(status); 10 | }); 11 | 12 | router.get('/readiness', (req, res) => { 13 | const status = { status: 'UP' }; 14 | logger.info('readiness', status); 15 | return res.send(status); 16 | }); 17 | 18 | router.get('/liveness', (req, res) => { 19 | const status = { status: 'UP' }; 20 | logger.info('liveness', status); 21 | return res.send(status); 22 | }); 23 | 24 | export default router; -------------------------------------------------------------------------------- /src/lib/roundMatrix.js: -------------------------------------------------------------------------------- 1 | import { round as defaultRound } from './round.js'; 2 | 3 | export const roundMatrix = (m, round = defaultRound) => { 4 | const rounded = m.map((row) => row.map((col) => round(col))); 5 | return rounded; 6 | }; 7 | 8 | export const cleanMatrix = (m) => { 9 | const rounded = m.map((row) => row.map((col) => (Object.is(col, -0) ? 0 : col))); 10 | return rounded; 11 | }; 12 | 13 | export const cleanAndRoundMatrix = (m, round = defaultRound) => { 14 | const rounded = m.map((row) => 15 | row.map((n) => { 16 | let rounded = round(n); 17 | rounded = Object.is(rounded, -0) ? 0 : rounded; 18 | return rounded; 19 | }) 20 | ); 21 | return rounded; 22 | }; 23 | -------------------------------------------------------------------------------- /src/client/providers/CameraProvider.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import CameraContext from '../context/CameraContext'; 3 | import useApp from '../hooks/useApp'; 4 | 5 | const CameraProvider = ({ children }) => { 6 | const { socket } = useApp(); 7 | 8 | const [data, setData] = useState(); 9 | 10 | useEffect(() => { 11 | const dataHandler = (d) => { 12 | setData(d); 13 | }; 14 | 15 | socket.on('camera', dataHandler); 16 | return () => { 17 | socket.removeListener('camera', dataHandler); 18 | }; 19 | }, []); 20 | 21 | return {children}; 22 | }; 23 | 24 | export default CameraProvider; 25 | -------------------------------------------------------------------------------- /src/lib/forward.js: -------------------------------------------------------------------------------- 1 | import { buildHomogeneousDenavitForTable } from './denavitHartenberg'; 2 | import { toRadians } from './toRadians'; 3 | 4 | export const forward = (t1, t2, t3, t4, t5, t6, robotConfig) => { 5 | const { a1, a2, a3, a4, a5, a6, x0 = 0 } = robotConfig; 6 | 7 | // 90 in radians 8 | const d90 = toRadians(90); 9 | 10 | // prettier-ignore 11 | const PT = [ 12 | [ t1, d90, x0, a1 ], 13 | [ t2+d90, 0, a2, 0 ], 14 | [ t3-d90, -d90, 0, 0 ], 15 | [ t4, d90, 0, a3 + a4 ], 16 | [ t5, -d90, 0, 0 ], 17 | [ t6, 0, 0, a5+ a6 ] 18 | ]; 19 | 20 | const res = buildHomogeneousDenavitForTable(PT); 21 | 22 | // console.table(res.endMatrix); 23 | return res.endMatrix; 24 | }; 25 | -------------------------------------------------------------------------------- /src/server/routes/camera.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | 3 | const router = express.Router(); 4 | 5 | router.get('/camera', (req, res) => { 6 | const data = [ 7 | { id: '7', type: 'Cup', confidence: 50, x: 30, y: 30, z: 30 }, 8 | { id: '8', type: 'Cup', confidence: 50, x: -30, y: -30, z: 30 }, 9 | ]; 10 | 11 | // Random data 12 | const updatedData = data.map((item) => { 13 | return { 14 | ...item, 15 | x: Math.random() * 100 - 50, // generates random number between -60 and 60 16 | y: Math.random() * 100 - 50, // generates random number between -60 and 60 17 | z: 10, 18 | }; 19 | }); 20 | return res.send(updatedData); 21 | }); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /src/client/components/Pages/NotFound/NotFound.css: -------------------------------------------------------------------------------- 1 | /* -- Site 404 Page 2 | ----------------------------------------------------------------------------- */ 3 | .error-container { 4 | display: flex; 5 | block-size: 100%; 6 | 7 | padding-inline-end: 0; 8 | } 9 | 10 | .error-text, 11 | .error-image { 12 | display: flex; 13 | flex: 1; 14 | flex-direction: column; 15 | justify-content: center; 16 | margin-block-end: 57px; 17 | } 18 | 19 | .error-text { 20 | margin-inline-start: 8%; 21 | margin-inline-end: 4%; 22 | } 23 | 24 | .error-code { 25 | font-size: 130px; 26 | line-height: 1; 27 | } 28 | 29 | .error-image { 30 | align-items: center; 31 | block-size: 100%; 32 | inline-size: 100%; 33 | } 34 | -------------------------------------------------------------------------------- /robots/SingleFrame.json: -------------------------------------------------------------------------------- 1 | { 2 | "robotType": "SingleFrame", 3 | "zeroPosition": [0, 0, 100], 4 | "units": "cm", 5 | "base": 10, 6 | "x0": 0, 7 | "y0": 0, 8 | "v0": 15, 9 | "v1": 20, 10 | "v2": 15, 11 | "v3": 15, 12 | "v4": 15, 13 | "v5": 10, 14 | "x": 42, 15 | "y": 10, 16 | "z": 55, 17 | "r1": 90, 18 | "r2": 90, 19 | "r3": 90, 20 | "rangej0": [-180, 180], 21 | "rangej1": [-140, 140], 22 | "rangej2": [-115, 115], 23 | "rangej3": [-180, 180], 24 | "rangej4": [-90, 90], 25 | "rangej5": [-180, 180], 26 | "flip": true, 27 | "frames": [ 28 | { 29 | "r1": 0, 30 | "r2": 0, 31 | "r3": 0, 32 | "x": 0, 33 | "y": 0, 34 | "z": 0 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/client/components/Pages/NotAuthorized/NotAuthorized.css: -------------------------------------------------------------------------------- 1 | /* -- Site 404 Page 2 | ----------------------------------------------------------------------------- */ 3 | .error-container { 4 | display: flex; 5 | block-size: 100%; 6 | 7 | padding-inline-end: 0; 8 | } 9 | 10 | .error-text, 11 | .error-image { 12 | display: flex; 13 | flex: 1; 14 | flex-direction: column; 15 | justify-content: center; 16 | margin-block-end: 57px; 17 | } 18 | 19 | .error-text { 20 | margin-inline-start: 8%; 21 | margin-inline-end: 4%; 22 | } 23 | 24 | .error-code { 25 | font-size: 130px; 26 | line-height: 1; 27 | } 28 | 29 | .error-image { 30 | align-items: center; 31 | block-size: 100%; 32 | inline-size: 100%; 33 | } 34 | -------------------------------------------------------------------------------- /src/client/components/Footer/Footer.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@adobe/react-spectrum'; 2 | import React from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | 5 | const NavLink = ({ children, href, ...rest }) => { 6 | const navigate = useNavigate(); 7 | 8 | const onClick = (e) => { 9 | navigate(href); 10 | }; 11 | 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export const Footer = () => ( 20 |
21 | App © {new Date().getFullYear()} 22 | / 23 | Home 24 | / 25 | Stock 404 Page 26 |
27 | ); 28 | -------------------------------------------------------------------------------- /example_py/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from server import start_server 5 | 6 | # Define default config 7 | config = { 8 | 'port': 80, # client port to connect to 9 | 'host': 'localhost' # client url to connect to 10 | } 11 | 12 | # Process the arguments 13 | args = sys.argv[1:] 14 | for i, val in enumerate(args): 15 | if val in ('-p', '--port'): 16 | config['port'] = int(args[i + 1]) 17 | elif val in ('-h', '--host'): 18 | config['host'] = args[i + 1] 19 | elif val == '--url': 20 | config['url'] = args[i + 1] 21 | elif val == '--id': 22 | config['id'] = args[i + 1] 23 | elif val == '--key': 24 | config['key'] = args[i + 1] 25 | 26 | # Start the server 27 | start_server(config) 28 | -------------------------------------------------------------------------------- /example_py_7/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from server import start_server 5 | 6 | # Define default config 7 | config = { 8 | 'port': 80, # client port to connect to 9 | 'host': 'localhost' # client url to connect to 10 | } 11 | 12 | # Process the arguments 13 | args = sys.argv[1:] 14 | for i, val in enumerate(args): 15 | if val in ('-p', '--port'): 16 | config['port'] = int(args[i + 1]) 17 | elif val in ('-h', '--host'): 18 | config['host'] = args[i + 1] 19 | elif val == '--url': 20 | config['url'] = args[i + 1] 21 | elif val == '--id': 22 | config['id'] = args[i + 1] 23 | elif val == '--key': 24 | config['key'] = args[i + 1] 25 | 26 | # Start the server 27 | start_server(config) 28 | -------------------------------------------------------------------------------- /example_py/test_debug.py: -------------------------------------------------------------------------------- 1 | from debug import Debug 2 | 3 | # Create the logger instance 4 | logger = Debug('mock:test\t') 5 | 6 | # Test data 7 | test = [1, 2, 3] 8 | 9 | my_object = { 10 | "name": "Grok", 11 | "species": "AI", 12 | "humor_level": 11, 13 | "rebellious_streak": True 14 | } 15 | 16 | foo = 1 17 | 18 | # Logging various messages 19 | logger("Hello World") 20 | logger(f"Hello World {foo}") 21 | logger("Hello Array", test) 22 | logger("Hello Object", my_object) 23 | 24 | # If you want to run this file as a standalone script 25 | if __name__ == '__main__': 26 | # Additional test messages can be added here 27 | logger("Test message 1") 28 | logger("Test message 2", [4, 5, 6]) 29 | logger("Test message 3", {"key": "value", "number": 42}) 30 | -------------------------------------------------------------------------------- /example_py_7/test_debug.py: -------------------------------------------------------------------------------- 1 | from debug import Debug 2 | 3 | # Create the logger instance 4 | logger = Debug('mock:test\t') 5 | 6 | # Test data 7 | test = [1, 2, 3] 8 | 9 | my_object = { 10 | "name": "Grok", 11 | "species": "AI", 12 | "humor_level": 11, 13 | "rebellious_streak": True 14 | } 15 | 16 | foo = 1 17 | 18 | # Logging various messages 19 | logger("Hello World") 20 | logger(f"Hello World {foo}") 21 | logger("Hello Array", test) 22 | logger("Hello Object", my_object) 23 | 24 | # If you want to run this file as a standalone script 25 | if __name__ == '__main__': 26 | # Additional test messages can be added here 27 | logger("Test message 1") 28 | logger("Test message 2", [4, 5, 6]) 29 | logger("Test message 3", {"key": "value", "number": 42}) 30 | -------------------------------------------------------------------------------- /src/client/components/Informed/Slider.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useField } from 'informed'; 3 | import { Slider, TextField } from '@adobe/react-spectrum'; 4 | 5 | const Input = (props) => { 6 | const { render, informed, fieldState, fieldApi, userProps, ref } = useField({ 7 | type: 'number', 8 | ...props, 9 | }); 10 | const { required } = userProps; 11 | const { error, showError } = fieldState; 12 | return render( 13 | fieldApi.setValue(v)} 21 | /> 22 | ); 23 | }; 24 | 25 | export default Input; 26 | -------------------------------------------------------------------------------- /src/client/components/Shared/NavLink.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useLocation, useNavigate } from 'react-router-dom'; 3 | import useApp from '../../hooks/useApp'; 4 | 5 | export const NavLink = ({ children, href, ...rest }) => { 6 | const navigate = useNavigate(); 7 | const { closeData } = useApp(); 8 | 9 | const onClick = (e) => { 10 | e.preventDefault(); 11 | closeData(); 12 | navigate(href); 13 | }; 14 | 15 | let location = useLocation(); 16 | const isSelected = href === location.pathname; 17 | 18 | return ( 19 |
  • 20 | 21 | {children} 22 | 23 |
  • 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /src/client/hooks/useStateWithGetter.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { useEffectOnce } from './useEffectOnce'; 3 | 4 | // TODO figure out if this is bad? 5 | // https://github.com/facebook/react/issues/14543 6 | function useStateWithGetter(initial) { 7 | const ref = useRef(); 8 | const mounted = useRef(true); 9 | const [state, setState] = useState(initial); 10 | ref.current = state; 11 | const set = (value) => { 12 | ref.current = value; 13 | if (mounted.current) setState(value); 14 | }; 15 | const get = () => { 16 | return ref.current; 17 | }; 18 | useEffectOnce(() => { 19 | return () => { 20 | mounted.current = false; 21 | }; 22 | }, []); 23 | return [state, set, get]; 24 | } 25 | 26 | export { useStateWithGetter }; 27 | -------------------------------------------------------------------------------- /src/client/components/Data/JointsData/RizonJointData.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TableView, TableHeader, TableBody, Column, Row, Cell } from '@adobe/react-spectrum'; 4 | import { Status } from '../../Shared/Status'; 5 | 6 | export const RizonJointData = ({ motor }) => { 7 | return ( 8 |
    9 |

    {motor.id}

    10 | 11 | 12 | Name 13 | Status 14 | 15 | 16 | 17 | Current Position 18 | 19 | {motor.angle} 20 | 21 | 22 | 23 | 24 |
    25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/client/components/Pages/Gamepad/gamepad.css: -------------------------------------------------------------------------------- 1 | /* button grid */ 2 | #buttons { 3 | display: grid; 4 | grid-template-columns: repeat(8, minmax(min-content, max-content)); 5 | grid-gap: 15px; 6 | justify-content: center; 7 | justify-items: start; 8 | } 9 | 10 | .button { 11 | background-color: rgb(223, 222, 222); 12 | height: 50px; 13 | min-width: 50px; 14 | display: flex; 15 | } 16 | 17 | .button-text-area { 18 | padding-left: 10px; 19 | padding-right: 10px; 20 | } 21 | 22 | .button-name { 23 | color: grey; 24 | } 25 | 26 | /* controller */ 27 | 28 | .selected-button { 29 | fill: rgb(231, 24, 24); 30 | } 31 | 32 | /* axes */ 33 | 34 | .axis-name { 35 | color: grey; 36 | font-weight: bolder; 37 | } 38 | 39 | .axis-value { 40 | color: rgb(126, 54, 54); 41 | } 42 | 43 | a { 44 | color: red; 45 | } 46 | -------------------------------------------------------------------------------- /src/server/routes/fail.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import fs from 'fs'; 3 | import logger from 'winston'; 4 | const router = express.Router(); 5 | 6 | // This will crash app completley 7 | router.get('/crash', (req, res) => { 8 | fs.readFile('somefile.txt', function(err, data) { 9 | if (err) throw err; 10 | console.log(data); 11 | }); 12 | }); 13 | 14 | // This will just throw normal error 15 | router.get('/throw', () => { 16 | throw new Error('Ahhhh!!!!!'); 17 | }); 18 | 19 | // This will just throw, catch, and log an error 20 | router.get('/catch', (req, res) => { 21 | try{ 22 | throw new Error('Ahhhh!!!!!'); 23 | } catch(e) { 24 | logger.error('Recieved and error when trying to do something', e ); 25 | res.sendStatus(500); 26 | } 27 | }); 28 | 29 | export default router; -------------------------------------------------------------------------------- /src/client/hooks/useGet.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { useCallback, useState } from 'react'; 3 | 4 | export const useGet = ({ url, headers, onComplete } = {}) => { 5 | const [res, setRes] = useState({ data: null, error: null, loading: false }); 6 | // You GET method here 7 | const post = useCallback( 8 | ({ url: newUrl }) => { 9 | setRes((prevState) => ({ ...prevState, loading: true })); 10 | axios 11 | .get(newUrl ?? url) 12 | .then((res) => { 13 | setRes({ data: res.data, loading: false, error: null }); 14 | if (onComplete) { 15 | onComplete(res.data); 16 | } 17 | }) 18 | .catch((error) => { 19 | setRes({ data: null, loading: false, error }); 20 | }); 21 | }, 22 | [url, headers] 23 | ); 24 | return [res, post]; 25 | }; 26 | -------------------------------------------------------------------------------- /src/client/components/Informed/Switch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useField } from 'informed'; 3 | import { Switch } from '@adobe/react-spectrum'; 4 | 5 | const Input = (props) => { 6 | const { render, informed, fieldState, fieldApi, userProps, ref } = useField({ 7 | type: 'text', 8 | ...props, 9 | }); 10 | const { required } = userProps; 11 | const { error, showError } = fieldState; 12 | return render( 13 | fieldApi.setValue(v, {})} 22 | > 23 | {props.label} 24 | 25 | ); 26 | }; 27 | 28 | export default Input; 29 | -------------------------------------------------------------------------------- /src/server/robot/camera-messenger.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events'; 2 | import axios from 'axios'; 3 | 4 | import { Debug } from '../../lib/debug.js'; 5 | const logger = Debug('robot:camera-messenger' + '\t'); 6 | 7 | export class CameraMessenger extends EventEmitter { 8 | constructor(io) { 9 | logger('robot constructing CameraMessenger'); 10 | super(); 11 | // Start polling for camera data 12 | } 13 | 14 | start() { 15 | setInterval(() => { 16 | this.fetchCameraData(); 17 | }, 3000); 18 | } 19 | 20 | async fetchCameraData() { 21 | try { 22 | const response = await axios.get('http://localhost:3000/camera'); 23 | const data = response.data; 24 | // logger('Data', data); 25 | this.emit('data', data); 26 | } catch (error) { 27 | logger('Error', err); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { startServer } from './server.js'; 4 | 5 | // Define default config 6 | const config = { 7 | port: 80, // client port to connect to 8 | host: 'localhost', // client url to connect to 9 | }; 10 | 11 | // Process the arguments 12 | process.argv.forEach(function (val, i, arr) { 13 | switch (val) { 14 | case '-p': 15 | case '--port': 16 | config.port = arr[i + 1]; 17 | break; 18 | case '-h': 19 | case '--host': 20 | config.host = arr[i + 1]; 21 | break; 22 | case '--url': 23 | config.url = arr[i + 1]; 24 | break; 25 | case '--id': 26 | config.id = arr[i + 1]; 27 | break; 28 | case '--key': 29 | config.key = arr[i + 1]; 30 | break; 31 | default: 32 | } 33 | }); 34 | 35 | // Start the server 36 | startServer(config); 37 | -------------------------------------------------------------------------------- /src/client/components/Shared/RobotType.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react'; 2 | 3 | import useApp from '../../hooks/useApp'; 4 | import Select from '../Informed/Select'; 5 | 6 | export const RobotType = ({ filter = () => true }) => { 7 | const { robotTypes, selectRobot } = useApp(); 8 | 9 | const robotTypeOptions = useMemo(() => { 10 | if (robotTypes) { 11 | return Object.entries(robotTypes) 12 | .filter(([key, robot]) => filter(robot)) 13 | .map(([key, robot]) => { 14 | return { value: key, label: key }; 15 | }); 16 | } 17 | return []; 18 | }, [robotTypes]); 19 | 20 | return ( 21 | 39 | 48 | 57 | 66 |
    67 | 68 |
    69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /src/client/components/Data/JointsData/ARJointData.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { TableView, TableHeader, TableBody, Column, Row, Cell } from '@adobe/react-spectrum'; 4 | import { Status } from '../../Shared/Status'; 5 | 6 | // Example: 7 | // const exampleAR = { 8 | // homing: false, 9 | // home: false, 10 | // homed: false, 11 | // enabled: true, 12 | // moving: true, 13 | // ready: true, 14 | // stepPosition: -7733, 15 | // encoderPosition: 77309, 16 | // error: 'Ahh!!!!', 17 | // }; 18 | export const ARJointData = ({ motor }) => { 19 | return ( 20 |
    21 |

    {motor.id}

    22 | 23 | 24 | Name 25 | Status 26 | 27 | 28 | 29 | Ready 30 | 31 | 32 | 33 | 34 | 35 | Homing 36 | 37 | 38 | 39 | 40 | 41 | Home 42 | 43 | 44 | 45 | 46 | 47 | Homed 48 | 49 | 50 | 51 | 52 | 53 | Enabled 54 | 55 | 56 | 57 | 58 | 59 | Moving 60 | 61 | 62 | 63 | 64 | 65 | Step Position 66 | 67 | {motor.stepPosition} 68 | 69 | 70 | 71 | Encoder Position 72 | 73 | {motor.encoderPosition} 74 | 75 | 76 | 77 | Error 78 | 79 | {motor.error} 80 | 81 | 82 | 83 | 84 |
    85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /robots/Rizon4Test2.json: -------------------------------------------------------------------------------- 1 | { 2 | "robotType": "Rizon4Test2", 3 | "zeroPosition": [0, 0, 0], 4 | "units": "cm", 5 | "x0": 0, 6 | "y0": 0, 7 | "base": 15.5, 8 | "endEffector": 0, 9 | "rangej0": [-160.0, 160.0], 10 | "rangej1": [-130.0, 130.0], 11 | "rangej2": [-170.0, 170.0], 12 | "rangej3": [-107.0, 154.0], 13 | "rangej4": [-170.0, 170.0], 14 | "rangej5": [-80.0, 260.0], 15 | "rangej6": [-170.0, 170.0], 16 | "frames": [ 17 | { 18 | "frameType": "rotary", 19 | "r1": 0, 20 | "r2": 0, 21 | "r3": 180, 22 | "x": 0, 23 | "y": 0, 24 | "z": 0, 25 | "moveFrame": false 26 | }, 27 | { 28 | "frameType": "rotary", 29 | "r1": 90, 30 | "r2": 0, 31 | "r3": 0, 32 | "x": 0, 33 | "y": -3, 34 | "z": 21, 35 | "moveFrame": true, 36 | "moveBackBy": 3, 37 | "moveBack": "y" 38 | }, 39 | { 40 | "frameType": "rotary", 41 | "r1": -90, 42 | "r2": 0, 43 | "r3": 0, 44 | "x": 0, 45 | "y": 20.5, 46 | "z": 3.5, 47 | "moveFrame": true, 48 | "moveBackBy": -20.5, 49 | "moveBack": "y" 50 | }, 51 | { 52 | "frameType": "rotary", 53 | "r1": 90, 54 | "r2": 180, 55 | "r3": 0, 56 | "x": 2, 57 | "y": 3, 58 | "z": 19, 59 | "moveFrame": true, 60 | "moveBackBy": -3, 61 | "moveBack": "y" 62 | }, 63 | { 64 | "frameType": "rotary", 65 | "r1": -90, 66 | "r2": 0, 67 | "r3": 0, 68 | "x": -2, 69 | "y": 19.5, 70 | "z": 2.5, 71 | "moveFrame": true, 72 | "moveBackBy": -19.5, 73 | "moveBack": "y" 74 | }, 75 | { 76 | "frameType": "rotary", 77 | "r1": 90, 78 | "r2": 180, 79 | "r3": 0, 80 | "x": 0, 81 | "y": 3, 82 | "z": 19, 83 | "moveFrame": true, 84 | "moveBackBy": -3, 85 | "moveBack": "y" 86 | }, 87 | { 88 | "frameType": "rotary", 89 | "r1": 90, 90 | "r2": 90, 91 | "r3": -180, 92 | "x": 5.5, 93 | "y": 11, 94 | "z": 7, 95 | "moveFrame": true, 96 | "moveBackBy": -5.5, 97 | "moveBack": "x" 98 | }, 99 | { 100 | "frameType": "stationary", 101 | "r1": 0, 102 | "r2": 0, 103 | "r3": 0, 104 | "x": 0, 105 | "y": 0, 106 | "z": 8.1, 107 | "moveFrame": false 108 | } 109 | ] 110 | } 111 | -------------------------------------------------------------------------------- /robots/Rizon4.json: -------------------------------------------------------------------------------- 1 | { 2 | "robotType": "Rizon4", 3 | "zeroPosition": [0, 0, 0], 4 | "units": "cm", 5 | "x0": 0, 6 | "y0": 0, 7 | "base": 15.5, 8 | "endEffector": 0, 9 | "rangej0": [-160.0, 160.0], 10 | "rangej1": [-130.0, 130.0], 11 | "rangej2": [-170.0, 170.0], 12 | "rangej3": [-107.0, 154.0], 13 | "rangej4": [-170.0, 170.0], 14 | "rangej5": [-80.0, 260.0], 15 | "rangej6": [-170.0, 170.0], 16 | "features": { 17 | "forceTourqueZero": true 18 | }, 19 | "frames": [ 20 | { 21 | "r1": 0, 22 | "r2": 0, 23 | "r3": 180, 24 | "x": 0, 25 | "y": 0, 26 | "z": 0, 27 | "moveFrame": false, 28 | "frameType": "rotary" 29 | }, 30 | { 31 | "r1": 90, 32 | "r2": 0, 33 | "r3": 0, 34 | "x": 0, 35 | "y": -3, 36 | "z": 21, 37 | "moveFrame": true, 38 | "moveBackBy": 3, 39 | "moveBack": "y", 40 | "frameType": "rotary" 41 | }, 42 | { 43 | "r1": -90, 44 | "r2": 0, 45 | "r3": 0, 46 | "x": 0, 47 | "y": 20.5, 48 | "z": 3.5, 49 | "moveFrame": true, 50 | "moveBackBy": -20.5, 51 | "moveBack": "y", 52 | "frameType": "rotary" 53 | }, 54 | { 55 | "r1": 90, 56 | "r2": 180, 57 | "r3": 0, 58 | "x": 2, 59 | "y": 3, 60 | "z": 19, 61 | "moveFrame": true, 62 | "moveBackBy": -3, 63 | "moveBack": "y", 64 | "frameType": "rotary" 65 | }, 66 | { 67 | "r1": -90, 68 | "r2": 0, 69 | "r3": 0, 70 | "x": -2, 71 | "y": 19.5, 72 | "z": -2.5, 73 | "moveFrame": true, 74 | "moveBackBy": -19.5, 75 | "moveBack": "y", 76 | "frameType": "rotary" 77 | }, 78 | { 79 | "r1": 90, 80 | "r2": 180, 81 | "r3": 0, 82 | "x": 0, 83 | "y": -3, 84 | "z": 19, 85 | "moveFrame": true, 86 | "moveBackBy": 3, 87 | "moveBack": "y", 88 | "frameType": "rotary" 89 | }, 90 | { 91 | "r1": 0, 92 | "r2": 90, 93 | "r3": 90, 94 | "x": 5.5, 95 | "y": 11, 96 | "z": 7, 97 | "moveFrame": true, 98 | "moveBackBy": -5.5, 99 | "moveBack": "x", 100 | "frameType": "rotary" 101 | }, 102 | { 103 | "r1": 0, 104 | "r2": 0, 105 | "r3": -180, 106 | "x": 0, 107 | "y": 0, 108 | "z": 8.1, 109 | "moveFrame": false, 110 | "frameType": "stationary" 111 | } 112 | ] 113 | } 114 | -------------------------------------------------------------------------------- /robots/AR4.json: -------------------------------------------------------------------------------- 1 | { 2 | "robotType": "AR4", 3 | "zeroPosition": [6.42, 0, 83.365], 4 | "units": "cm", 5 | "y0": 0, 6 | "x0": 6.42, 7 | "base": 3.11, 8 | "v0": 13.867, 9 | "v1": 30.5, 10 | "v2": 3.5, 11 | "v3": 18.763, 12 | "v4": 3.625, 13 | "v5": 5, 14 | "endEffector": 5, 15 | "x": 42, 16 | "y": 10, 17 | "z": 55, 18 | "r1": 90, 19 | "r2": 90, 20 | "r3": 90, 21 | "rangej0": [-170, 170], 22 | "rangej1": [-90, 42], 23 | "rangej2": [-141, 20], 24 | "rangej3": [-165, 165], 25 | "rangej4": [-100, 100], 26 | "rangej5": [-155, 155], 27 | "flip": true, 28 | "frames": [ 29 | { 30 | "frameType": "rotary", 31 | "r1": 0, 32 | "r2": 0, 33 | "r3": 0, 34 | "x": 0, 35 | "y": 0, 36 | "z": 0, 37 | "moveFrame": false 38 | }, 39 | { 40 | "frameType": "rotary", 41 | "r1": 90, 42 | "r2": 0, 43 | "r3": 0, 44 | "x": 6.42, 45 | "y": 0, 46 | "z": 13.867, 47 | "moveFrame": false 48 | }, 49 | { 50 | "frameType": "rotary", 51 | "r1": 0, 52 | "r2": 0, 53 | "r3": 90, 54 | "x": 0, 55 | "y": 30.5, 56 | "z": 0, 57 | "moveFrame": false 58 | }, 59 | { 60 | "frameType": "rotary", 61 | "r1": 0, 62 | "r2": 90, 63 | "r3": -90, 64 | "x": 3.5, 65 | "y": 0, 66 | "z": 0, 67 | "moveFrame": true, 68 | "moveBackBy": -3.5, 69 | "moveBack": "x" 70 | }, 71 | { 72 | "frameType": "rotary", 73 | "r1": 90, 74 | "r2": 0, 75 | "r3": 0, 76 | "x": 0, 77 | "y": 0, 78 | "z": 18.763, 79 | "moveFrame": false 80 | }, 81 | { 82 | "frameType": "rotary", 83 | "r1": -90, 84 | "r2": 0, 85 | "r3": 0, 86 | "x": 0, 87 | "y": 3.625, 88 | "z": 0, 89 | "moveFrame": true, 90 | "moveBackBy": -3.625, 91 | "moveBack": "y" 92 | }, 93 | { 94 | "frameType": "stationary", 95 | "r1": 0, 96 | "r2": 0, 97 | "r3": 0, 98 | "x": 0, 99 | "y": 0, 100 | "z": 5, 101 | "moveFrame": false 102 | } 103 | ], 104 | "dhParameters": [ 105 | { "theta": 0, "alpha": 90, "r": 6.42, "d": 16.977 }, 106 | { "theta": 90, "alpha": 0, "r": 30.5, "d": 0 }, 107 | { "theta": -90, "alpha": -90, "r": 0, "d": 0 }, 108 | { "theta": 0, "alpha": 90, "r": 0, "d": 22.263 }, 109 | { "theta": 0, "alpha": -90, "r": 0, "d": 0 }, 110 | { "theta": 0, "alpha": 0, "r": 0, "d": 13.625 } 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /src/client/components/Nav/CookbookNav.jsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useRef } from 'react'; 2 | import { ActionButton, Flex } from '@adobe/react-spectrum'; 3 | import ChevronRight from '@spectrum-icons/workflow/ChevronRight'; 4 | import useApp from '../../hooks/useApp'; 5 | import Select from '../Informed/Select'; 6 | import { Debug, useFieldState, useFormApi } from 'informed'; 7 | import useRobotMeta from '../../hooks/useRobotMeta'; 8 | import Switch from '../Informed/Switch'; 9 | import useRobotController from '../../hooks/useRobotController'; 10 | 11 | export const CookbookNav = () => { 12 | console.log('RENDER CookBook NAV'); 13 | 14 | // Get controls for nav 15 | const { extraOpen, toggleExtra } = useApp(); 16 | 17 | // Get robot state 18 | const { robotOptions, robots, connected } = useRobotMeta(); 19 | 20 | // Get robot control 21 | const { updateConfig } = useRobotController(); 22 | 23 | // Get value of robotId 24 | const { value: robotId } = useFieldState('robotId'); 25 | 26 | // Form api to manipulate form 27 | const formApi = useFormApi(); 28 | 29 | // Ref to use in functions for if robot is connected 30 | const connectedRef = useRef(); 31 | connectedRef.current = connected; 32 | 33 | const onAccelChange = useCallback(({ value }) => { 34 | const motorId = formApi.getValue('motorId'); 35 | // only send if we are connected and have selected motor 36 | if (connectedRef.current && motorId != 'na') { 37 | updateConfig(`${motorId}.accelEnabled`, value); 38 | } 39 | }, []); 40 | 41 | return ( 42 | <> 43 | 44 |

    Cookbook

    45 | toggleExtra()}> 46 | 47 | 48 |
    49 | 50 |
    51 |
      52 | 87 |
      88 | { 116 | return { label: x, value: x }; 117 | })} 118 | /> 119 | 120 | 121 |
    122 | )} 123 | 124 |
    125 | ); 126 | }} 127 | 128 | 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /src/lib/denavitHartenberg.test.js: -------------------------------------------------------------------------------- 1 | import { 2 | buildHomogeneousDenavitForTable, 3 | buildHomogeneousDenavitStringForTable, 4 | } from './denavitHartenberg'; 5 | import { toRadians } from './toRadians'; 6 | 7 | const D_90 = toRadians(90); 8 | 9 | let t1 = 0; // Theta 1 angle in degrees 10 | let t2 = 90; // Theta 2 angle in degrees 11 | let t3 = 0; // Theta 3 angle in degrees 12 | let t4 = 0; // Theta 4 angle in degrees 13 | let t5 = 0; // Theta 5 angle in degrees 14 | let t6 = 0; // Theta 6 angle in degrees 15 | 16 | t1 = toRadians(t1); 17 | t2 = toRadians(t2); 18 | t3 = toRadians(t3); 19 | t4 = toRadians(t4); 20 | t5 = toRadians(t5); 21 | t6 = toRadians(t6); 22 | 23 | describe('denavitHartenberg', () => { 24 | describe('buildHomogeneousDenavitForTable', () => { 25 | // prettier-ignore 26 | // Theta | Alpha | r | d 27 | const PT = [ 28 | [ t1, D_90, 0, 1 ], 29 | [ t2 + D_90, 0, 1, 0 ], 30 | [ t3 - D_90, -D_90, 0, 0 ], 31 | [ t4, D_90, 0, 2 ], 32 | [ t5, -D_90, 0, 0 ], 33 | [ t6, 0, 0, 2 ] 34 | ]; 35 | 36 | it('should build Homogeneous transformation matrix from given P Table', () => { 37 | const { matriceis, endMatrix } = buildHomogeneousDenavitForTable(PT); 38 | 39 | // prettier-ignore 40 | const expectedEnd = [ 41 | [0, 0, -1, -5], 42 | [0, 1, 0, 0], 43 | [1, 0, 0, 1], 44 | [0, 0, 0, 1], 45 | ]; 46 | 47 | expect(endMatrix).toEqual(expectedEnd); 48 | }); 49 | }); 50 | 51 | describe('buildHomogeneousDenavitStringForTable', () => { 52 | it('should build rotation transformation matrix from given P Table', () => { 53 | // Note: this is the H3_6 P table for my kinematics 54 | const PT = [ 55 | ['t4', 'd90', '0', 'v2 + v3'], 56 | ['t5', '-d90', '0', '0'], 57 | ['t6', '0', '0', 'v4 + v5'], 58 | ]; 59 | 60 | const { endRotation } = buildHomogeneousDenavitStringForTable(PT); 61 | 62 | const expectedEndRotation = [ 63 | [ 64 | 'Math.cos(t4) * Math.cos(t5) * Math.cos(t6) + -Math.sin(t4) * Math.sin(t6)', 65 | 'Math.cos(t4) * Math.cos(t5) * -Math.sin(t6) + -Math.sin(t4) * Math.cos(t6)', 66 | 'Math.cos(t4) * -Math.sin(t5)', 67 | ], 68 | [ 69 | 'Math.sin(t4) * Math.cos(t5) * Math.cos(t6) + Math.cos(t4) * Math.sin(t6)', 70 | 'Math.sin(t4) * Math.cos(t5) * -Math.sin(t6) + Math.cos(t4) * Math.cos(t6)', 71 | 'Math.sin(t4) * -Math.sin(t5)', 72 | ], 73 | ['Math.sin(t5) * Math.cos(t6)', 'Math.sin(t5) * -Math.sin(t6)', 'Math.cos(t5)'], 74 | ]; 75 | 76 | expect(endRotation).toEqual(expectedEndRotation); 77 | }); 78 | 79 | it('should build rotation transformation matrix from given P Table when default type is passed', () => { 80 | // Note: this is the H3_6 P table for my kinematics 81 | const PT = [ 82 | ['t4', 'd90', '0', 'v2 + v3'], 83 | ['t5', '-d90', '0', '0'], 84 | ['t6', '0', '0', 'v4 + v5'], 85 | ]; 86 | 87 | const { endRotation } = buildHomogeneousDenavitStringForTable(PT, 'default'); 88 | 89 | const expectedEndRotation = [ 90 | [ 91 | 'cos(t4) * cos(t5) * cos(t6) + -sin(t4) * sin(t6)', 92 | 'cos(t4) * cos(t5) * -sin(t6) + -sin(t4) * cos(t6)', 93 | 'cos(t4) * -sin(t5)', 94 | ], 95 | [ 96 | 'sin(t4) * cos(t5) * cos(t6) + cos(t4) * sin(t6)', 97 | 'sin(t4) * cos(t5) * -sin(t6) + cos(t4) * cos(t6)', 98 | 'sin(t4) * -sin(t5)', 99 | ], 100 | ['sin(t5) * cos(t6)', 'sin(t5) * -sin(t6)', 'cos(t5)'], 101 | ]; 102 | 103 | expect(endRotation).toEqual(expectedEndRotation); 104 | }); 105 | 106 | it('should build rotation transformation matrix from given P Table', () => { 107 | // prettier-ignore 108 | // Note: this is the H3_6 P table for the UR robot 109 | const PT = [ 110 | ['t4', 'd90', '0', 'v3'], 111 | ['t5', '-d90', '0', 'v4'], 112 | ['t6', '0', '0', 'v5'], 113 | ]; 114 | 115 | const { endRotation } = buildHomogeneousDenavitStringForTable(PT); 116 | 117 | console.log(endRotation); 118 | 119 | const expectedEndRotation = [ 120 | [ 121 | 'Math.cos(t4) * Math.cos(t5) * Math.cos(t6) + -Math.sin(t4) * Math.sin(t6)', 122 | 'Math.cos(t4) * Math.cos(t5) * -Math.sin(t6) + -Math.sin(t4) * Math.cos(t6)', 123 | 'Math.cos(t4) * -Math.sin(t5)', 124 | ], 125 | [ 126 | 'Math.sin(t4) * Math.cos(t5) * Math.cos(t6) + Math.cos(t4) * Math.sin(t6)', 127 | 'Math.sin(t4) * Math.cos(t5) * -Math.sin(t6) + Math.cos(t4) * Math.cos(t6)', 128 | 'Math.sin(t4) * -Math.sin(t5)', 129 | ], 130 | ['Math.sin(t5) * Math.cos(t6)', 'Math.sin(t5) * -Math.sin(t6)', 'Math.cos(t5)'], 131 | ]; 132 | 133 | expect(endRotation).toEqual(expectedEndRotation); 134 | }); 135 | }); 136 | }); 137 | -------------------------------------------------------------------------------- /example_py/motor.py: -------------------------------------------------------------------------------- 1 | from pyee import EventEmitter 2 | from debug import Debug 3 | import threading 4 | 5 | logger = Debug('mock:motor\t') 6 | 7 | class Motor(EventEmitter): 8 | def __init__(self, id, homePos, limNeg=None, limPos=None, maxSpeed=40, maxAccel=20): 9 | super().__init__() 10 | self.id = id 11 | self.homePos = homePos 12 | self.limNeg = limNeg 13 | self.limPos = limPos 14 | self.maxSpeed = maxSpeed 15 | self.maxAccel = maxAccel 16 | self.currentPos = homePos 17 | self.goalPos = homePos 18 | self.moving = False 19 | self.homing = False 20 | self.enabled = False 21 | self.error = None 22 | logger(f'Motor {self.id} created with homePos {self.homePos}') 23 | 24 | @property 25 | def state(self): 26 | return { 27 | 'id': self.id, 28 | 'homing': self.homing, 29 | 'home': self.homePos, 30 | 'enabled': self.enabled, 31 | 'error': self.error, 32 | 'moving': self.moving, 33 | 'currentPos': self.currentPos, 34 | 'goalPos': self.goalPos, 35 | } 36 | 37 | def validate(self, enabled=False, cleared=False, log=''): 38 | if enabled and not self.enabled: 39 | message = f'Please enable before {log}' 40 | logger(message) 41 | self.error = 'DISABLED' 42 | self.emit('meta') 43 | return False 44 | if cleared and self.error: 45 | message = f'Please clear error before {log}' 46 | logger(message) 47 | self.error = 'CLEAR_ERROR' 48 | self.emit('meta') 49 | return False 50 | return True 51 | 52 | def set_position(self, position, spd=None, acc=None): 53 | speed = spd or self.maxSpeed 54 | acceleration = acc or self.maxAccel 55 | 56 | if not self.validate(enabled=True, cleared=True, log='attempting to set position'): 57 | return 58 | 59 | logger(f'Motor {self.id} setPosition called with position {position}, speed {speed}, acceleration {acceleration}') 60 | self.goalPos = position 61 | self.moving = True 62 | 63 | distance = abs(self.goalPos - self.currentPos) 64 | direction = 1 if self.goalPos > self.currentPos else -1 65 | interval = 0.1 # Update every 100 ms 66 | step = (speed * interval) * direction # Degrees per interval 67 | 68 | logger(f'Motor {self.id} will be moving in steps of {step}') 69 | 70 | def move(): 71 | if self.moving: 72 | if abs(self.goalPos - self.currentPos) < abs(step): 73 | self.currentPos = self.goalPos 74 | self.moving = False 75 | logger(f'Motor {self.id} reached goal position {self.goalPos}') 76 | self.emit('moved', self.id) 77 | self.emit('pulse', self.id, self.currentPos) 78 | if self.homing: 79 | self.homing = False 80 | logger(f'Motor {self.id} finished homing') 81 | self.emit('home', self.id) 82 | elif not self.enabled: 83 | logger(f'Motor {self.id} was moving to {self.goalPos} but was stopped at {self.currentPos}') 84 | self.goalPos = self.currentPos 85 | self.moving = False 86 | self.homing = False 87 | self.emit('moved', self.id) 88 | self.emit('pulse', self.id, self.currentPos) 89 | else: 90 | self.currentPos += step 91 | self.emit('pulse', self.id, self.currentPos) 92 | threading.Timer(interval, move).start() 93 | 94 | move() 95 | 96 | def go_home(self): 97 | logger(f'Motor {self.id} going home to {self.homePos}') 98 | if not self.validate(enabled=True, cleared=True, log='attempting to home'): 99 | return 100 | self.homing = True 101 | self.set_position(self.homePos) 102 | 103 | def reset_errors(self): 104 | logger(f'Motor {self.id} resetting errors') 105 | self.error = None 106 | self.emit('reset') 107 | 108 | def enable(self): 109 | logger(f'Motor {self.id} enabled') 110 | self.enabled = True 111 | self.emit('enabled') 112 | 113 | def disable(self): 114 | logger(f'Motor {self.id} disabled') 115 | self.enabled = False 116 | self.emit('disabled') 117 | 118 | def freeze(self): 119 | logger(f'Motor {self.id} freeze called') 120 | self.moving = False 121 | self.emit('freeze') 122 | 123 | def center(self): 124 | logger(f'Motor {self.id} centering') 125 | self.set_position(0) 126 | 127 | def zero(self): 128 | logger(f'Motor {self.id} zeroing position') 129 | self.currentPos = 0 130 | self.goalPos = 0 131 | self.emit('reset') 132 | 133 | def start_homing(self): 134 | logger(f'Motor {self.id} starting homing') 135 | self.emit('homing') 136 | self.go_home() 137 | -------------------------------------------------------------------------------- /example_py_7/motor.py: -------------------------------------------------------------------------------- 1 | from pyee import EventEmitter 2 | from debug import Debug 3 | import threading 4 | 5 | logger = Debug('mock:motor\t') 6 | 7 | class Motor(EventEmitter): 8 | def __init__(self, id, homePos, limNeg=None, limPos=None, maxSpeed=40, maxAccel=20): 9 | super().__init__() 10 | self.id = id 11 | self.homePos = homePos 12 | self.limNeg = limNeg 13 | self.limPos = limPos 14 | self.maxSpeed = maxSpeed 15 | self.maxAccel = maxAccel 16 | self.currentPos = homePos 17 | self.goalPos = homePos 18 | self.moving = False 19 | self.homing = False 20 | self.enabled = False 21 | self.error = None 22 | logger(f'Motor {self.id} created with homePos {self.homePos}') 23 | 24 | @property 25 | def state(self): 26 | return { 27 | 'id': self.id, 28 | 'homing': self.homing, 29 | 'home': self.homePos, 30 | 'enabled': self.enabled, 31 | 'error': self.error, 32 | 'moving': self.moving, 33 | 'currentPos': self.currentPos, 34 | 'goalPos': self.goalPos, 35 | } 36 | 37 | def validate(self, enabled=False, cleared=False, log=''): 38 | if enabled and not self.enabled: 39 | message = f'Please enable before {log}' 40 | logger(message) 41 | self.error = 'DISABLED' 42 | self.emit('meta') 43 | return False 44 | if cleared and self.error: 45 | message = f'Please clear error before {log}' 46 | logger(message) 47 | self.error = 'CLEAR_ERROR' 48 | self.emit('meta') 49 | return False 50 | return True 51 | 52 | def set_position(self, position, spd=None, acc=None): 53 | speed = spd or self.maxSpeed 54 | acceleration = acc or self.maxAccel 55 | 56 | if not self.validate(enabled=True, cleared=True, log='attempting to set position'): 57 | return 58 | 59 | logger(f'Motor {self.id} setPosition called with position {position}, speed {speed}, acceleration {acceleration}') 60 | self.goalPos = position 61 | self.moving = True 62 | 63 | distance = abs(self.goalPos - self.currentPos) 64 | direction = 1 if self.goalPos > self.currentPos else -1 65 | interval = 0.1 # Update every 100 ms 66 | step = (speed * interval) * direction # Degrees per interval 67 | 68 | logger(f'Motor {self.id} will be moving in steps of {step}') 69 | 70 | def move(): 71 | if self.moving: 72 | if abs(self.goalPos - self.currentPos) < abs(step): 73 | self.currentPos = self.goalPos 74 | self.moving = False 75 | logger(f'Motor {self.id} reached goal position {self.goalPos}') 76 | self.emit('moved', self.id) 77 | self.emit('pulse', self.id, self.currentPos) 78 | if self.homing: 79 | self.homing = False 80 | logger(f'Motor {self.id} finished homing') 81 | self.emit('home', self.id) 82 | elif not self.enabled: 83 | logger(f'Motor {self.id} was moving to {self.goalPos} but was stopped at {self.currentPos}') 84 | self.goalPos = self.currentPos 85 | self.moving = False 86 | self.homing = False 87 | self.emit('moved', self.id) 88 | self.emit('pulse', self.id, self.currentPos) 89 | else: 90 | self.currentPos += step 91 | self.emit('pulse', self.id, self.currentPos) 92 | threading.Timer(interval, move).start() 93 | 94 | move() 95 | 96 | def go_home(self): 97 | logger(f'Motor {self.id} going home to {self.homePos}') 98 | if not self.validate(enabled=True, cleared=True, log='attempting to home'): 99 | return 100 | self.homing = True 101 | self.set_position(self.homePos) 102 | 103 | def reset_errors(self): 104 | logger(f'Motor {self.id} resetting errors') 105 | self.error = None 106 | self.emit('reset') 107 | 108 | def enable(self): 109 | logger(f'Motor {self.id} enabled') 110 | self.enabled = True 111 | self.emit('enabled') 112 | 113 | def disable(self): 114 | logger(f'Motor {self.id} disabled') 115 | self.enabled = False 116 | self.emit('disabled') 117 | 118 | def freeze(self): 119 | logger(f'Motor {self.id} freeze called') 120 | self.moving = False 121 | self.emit('freeze') 122 | 123 | def center(self): 124 | logger(f'Motor {self.id} centering') 125 | self.set_position(0) 126 | 127 | def zero(self): 128 | logger(f'Motor {self.id} zeroing position') 129 | self.currentPos = 0 130 | self.goalPos = 0 131 | self.emit('reset') 132 | 133 | def start_homing(self): 134 | logger(f'Motor {self.id} starting homing') 135 | self.emit('homing') 136 | self.go_home() 137 | --------------------------------------------------------------------------------