├── 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 |
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 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/src/client/components/Informed/NumberInput.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'informed';
3 | import { NumberField } 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 |
14 | fieldApi.setValue(v, {})}
22 | />
23 |
24 | );
25 | };
26 |
27 | export default Input;
28 |
--------------------------------------------------------------------------------
/src/client/components/Informed/Input.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'informed';
3 | import { TextField } from '@adobe/react-spectrum';
4 |
5 | const Input = ({ hidden, ...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)}
21 | type={hidden ? 'hidden' : informed.type}
22 | isHidden={hidden}
23 | />
24 | );
25 | };
26 |
27 | export default Input;
28 |
--------------------------------------------------------------------------------
/src/client/hooks/usePost.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { useCallback, useState } from 'react';
3 |
4 | export const usetPost = ({ url, headers, onComplete }) => {
5 | const [res, setRes] = useState({ data: null, error: null, loading: false });
6 | // You POST method here
7 | const post = useCallback(
8 | ({ payload, url: newUrl }) => {
9 | setRes((prevState) => ({ ...prevState, loading: true }));
10 | axios
11 | .post(newUrl ?? url, payload)
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/Checkbox.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'informed';
3 | import { Checkbox as SpectrumCheckbox, TextField } from '@adobe/react-spectrum';
4 |
5 | const Checkbox = (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 Checkbox;
29 |
--------------------------------------------------------------------------------
/src/server/middleware/proxy.js:
--------------------------------------------------------------------------------
1 | import request from 'request';
2 | import logger from 'winston';
3 |
4 | /**
5 | * Helper function to handle errors when they occur
6 | * @param {*} res - response object
7 | * @param {*} e - the error to log
8 | * @param {*} path - path that was attepted to proxy to
9 | */
10 | const handleError = (res, e, path) => {
11 | console.error(`Unable to forward request to ${path}`, e);
12 | res.sendStatus(500);
13 | };
14 |
15 | /**
16 | * Function that generates a piece of proxy middleware
17 | *
18 | * Example usage: app.use('/foo', proxy('http://localhost:6900'))
19 | *
20 | * @param {*} to - where to proxy the requests to
21 | */
22 | const proxy = to => (req, res) => {
23 | const path = `${to}${req.originalUrl}`;
24 | logger.info(`Forwarding request to ${path}`);
25 | req
26 | .pipe(request(path))
27 | .on('error', e => handleError(res, e, path))
28 | .pipe(res);
29 | };
30 |
31 | export default proxy;
--------------------------------------------------------------------------------
/src/client/components/Informed/Select.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'informed';
3 | import { Item, Picker } 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 |
11 | const { required, options } = userProps;
12 | const { error, showError } = fieldState;
13 |
14 | return render(
15 | fieldApi.setValue(v, {})}
24 | >
25 | {options.map((op) => {
26 | return - {op.label}
;
27 | })}
28 |
29 | );
30 | };
31 |
32 | export default Input;
33 |
--------------------------------------------------------------------------------
/src/server/terminate.js:
--------------------------------------------------------------------------------
1 | import logger from 'winston';
2 |
3 | // copied from https://blog.heroku.com/best-practices-nodejs-errors
4 | const terminate = (server, options = { coredump: false, timeout: 500 }) => {
5 | // Exit function
6 | const exit = (code) => {
7 | if (options.coredump) {
8 | process.abort();
9 | } else {
10 | process.exit(code);
11 | }
12 | };
13 |
14 | return (code, reason) => (err) => {
15 | logger.info(`Terminating due to ${reason}`);
16 |
17 | if (err && err instanceof Error) {
18 | logger.error('Terminating due to error', err);
19 | }
20 |
21 | // Attempt a graceful shutdown
22 | server.close(() => {
23 | exit(code);
24 | });
25 | // If server hasn't finished in timeout, shut down process
26 | setTimeout(() => {
27 | process.exit(code);
28 | }, options.timeout).unref(); // Prevents the timeout from registering on event loop
29 | };
30 | };
31 |
32 | export default terminate;
33 |
--------------------------------------------------------------------------------
/example/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 | "favorites": {
9 | "joints": {
10 | "Home1": [30.47, 23.35, -90.53, -56.6, -37.4, 50.3],
11 | "Home2": [-30.47, 23.35, -90.53, 56.6, -37.4, -50.3],
12 | "PickUp": [0, -32.65, -70.53, 0, -76.82, 90]
13 | },
14 | "gripper": {
15 | "Open": {
16 | "width": 95,
17 | "force": 10,
18 | "vel": 0.05
19 | },
20 | "Hold": {
21 | "width": 76,
22 | "force": 17,
23 | "vel": 0.05
24 | },
25 | "Grasp": {
26 | "width": 50,
27 | "force": 10,
28 | "vel": 0.05
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example_py_7/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "j0": { "limNeg": -160, "limPos": 160, "homePos": 0 },
3 | "j1": { "limNeg": -130, "limPos": 130, "homePos": 0 },
4 | "j2": { "limNeg": -170, "limPos": 170, "homePos": 0 },
5 | "j3": { "limNeg": -107, "limPos": 154, "homePos": 0 },
6 | "j4": { "limNeg": -170, "limPos": 170, "homePos": 0 },
7 | "j5": { "limNeg": -80, "limPos": 260, "homePos": 0 },
8 | "j6": { "limNeg": -170, "limPos": 170, "homePos": 0 },
9 | "favorites": {
10 | "joints": {
11 | "HomeRight": [-44.467, -45.1, 18.142, 119.753, -54.867, -18.267, -6.997],
12 | "HomeLeft": [-150, 0.37, 1.93, 123.71, 107.81, 25.81, 52.41]
13 | },
14 | "gripper": {
15 | "Open": {
16 | "width": 95,
17 | "force": 10,
18 | "vel": 0.05
19 | },
20 | "Hold": {
21 | "width": 76,
22 | "force": 17,
23 | "vel": 0.05
24 | },
25 | "Grasp": {
26 | "width": 50,
27 | "force": 10,
28 | "vel": 0.05
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/client/components/Informed/RadioGroup.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'informed';
3 | import { Radio, RadioGroup } 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, options } = userProps;
11 | const { error, showError } = fieldState;
12 |
13 | return render(
14 | fieldApi.setValue(v)}
23 | >
24 | {options.map((option) => (
25 |
26 | {option.label}
27 |
28 | ))}
29 |
30 | );
31 | };
32 |
33 | export default Input;
34 |
--------------------------------------------------------------------------------
/src/client/hooks/useEffectOnce.js:
--------------------------------------------------------------------------------
1 | import { useState, useRef, useEffect } from 'react';
2 |
3 | export const useEffectOnce = (effect) => {
4 | const destroyFunc = useRef();
5 | const effectCalled = useRef(false);
6 | const renderAfterCalled = useRef(false);
7 | // eslint-disable-next-line no-unused-vars
8 | const [val, setVal] = useState(0);
9 |
10 | if (effectCalled.current) {
11 | renderAfterCalled.current = true;
12 | }
13 |
14 | useEffect(() => {
15 | // only execute the effect first time around
16 | if (!effectCalled.current) {
17 | destroyFunc.current = effect();
18 | effectCalled.current = true;
19 | }
20 |
21 | // this forces one render after the effect is run
22 | setVal((val) => val + 1);
23 |
24 | return () => {
25 | // if the comp didn't render since the useEffect was called,
26 | // we know it's the dummy React cycle
27 | if (!renderAfterCalled.current) {
28 | return;
29 | }
30 | if (destroyFunc.current) {
31 | destroyFunc.current();
32 | }
33 | };
34 | }, []);
35 | };
36 |
--------------------------------------------------------------------------------
/src/client/hooks/useOutsideAlerter.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import { useEffect } from 'react';
3 |
4 | /**
5 | * Hook that alerts clicks outside of the passed ref
6 | * https://stackoverflow.com/questions/32553158/detect-click-outside-react-component
7 | */
8 | function useOutsideAlerter(action, ref, elem) {
9 | useEffect(() => {
10 | /**
11 | * Alert if clicked on outside of element
12 | */
13 | function handleClickOutside(event) {
14 | const outsideOfTarket = ref.current && !ref.current.contains(event.target);
15 | const outsideOfTrigger = elem && !elem.contains(event.target);
16 | if (outsideOfTarket && (elem ? outsideOfTrigger : true)) {
17 | action();
18 | }
19 | }
20 |
21 | // Bind the event listener
22 | document.addEventListener('mousedown', handleClickOutside);
23 | return () => {
24 | // Unbind the event listener on clean up
25 | document.removeEventListener('mousedown', handleClickOutside);
26 | };
27 | }, [ref, elem]);
28 | }
29 |
30 | export default useOutsideAlerter;
31 |
--------------------------------------------------------------------------------
/src/client/components/3D/URDFRobot.jsx:
--------------------------------------------------------------------------------
1 | import { useThree } from '@react-three/fiber';
2 | import { useEffect } from 'react';
3 | import { LoadingManager } from 'three';
4 | import URDFLoader from 'urdf-loader';
5 |
6 | import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader';
7 |
8 | // New URDFRobot component
9 | export const URDFRobot = () => {
10 | const { scene } = useThree();
11 |
12 | useEffect(() => {
13 | const manager = new LoadingManager();
14 | const loader = new URDFLoader(manager);
15 |
16 | // Custom mesh loading function
17 | loader.loadMeshCb = (path, manager, done) => {
18 | if (path.endsWith('.obj')) {
19 | const objLoader = new OBJLoader(manager);
20 | objLoader.load(path, done);
21 | } else {
22 | // Handle other file types or fallback to default behavior
23 | // ...
24 | }
25 | };
26 |
27 | // loader.load('static/rizon/flexiv_rizon4_kinematics.urdf', (robot) => {
28 | loader.load('static/test.urdf', (robot) => {
29 | scene.add(robot);
30 | });
31 | }, [scene]);
32 |
33 | return null;
34 | };
35 |
--------------------------------------------------------------------------------
/src/client/components/Routes/Routes.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Routes as RouterRoutes, Route } from 'react-router-dom';
3 |
4 | // Components
5 | import { Cookbook } from '../Pages/Cookbook/Cookbook';
6 | import { Robot } from '../Pages/Robot/Robot';
7 | import { Motor } from '../Pages/Motor/Motor';
8 | import { NotFound } from '../Pages/NotFound/NotFound';
9 | import { Framer } from '../Pages/Framer/Framer';
10 | import { Builder } from '../Pages/Builder/Builder';
11 | import { Gamepad } from '../Pages/Gamepad/Gamepad';
12 |
13 | // Routes ------------------------------------------------------------
14 |
15 | export const Routes = () => {
16 | return (
17 |
18 | } />
19 | } />
20 | } />
21 | } />
22 | } />
23 | } />
24 | } />
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/client/hooks/useMedia.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | import * as media from '../utils/media';
4 |
5 | const useMedia = () => {
6 | const [isDesktopUp, setIsDesktopUp] = useState(media.isDesktopUp());
7 | const [isTabletLandscapeUp, setIsTabletLandscapeUp] = useState(media.isTabletLandscapeUp());
8 | const [isDesktopLargeUp, setIsDesktopLargeUp] = useState(media.isDesktopLargeUp());
9 | const [isDesktopBigUp, setIsDesktopBigUp] = useState(media.isDesktopBigUp());
10 |
11 | useEffect(() => {
12 | const handleResize = () => {
13 | setIsDesktopUp(media.isDesktopUp());
14 | setIsTabletLandscapeUp(media.isTabletLandscapeUp());
15 | setIsDesktopLargeUp(media.isDesktopLargeUp());
16 | setIsDesktopBigUp(media.isDesktopBigUp());
17 | };
18 |
19 | // For resizing header
20 | window.addEventListener('resize', handleResize);
21 |
22 | return () => {
23 | window.removeEventListener(handleResize);
24 | };
25 | }, []);
26 |
27 | return { isDesktopUp, isTabletLandscapeUp, isDesktopLargeUp, isDesktopBigUp };
28 | };
29 |
30 | export default useMedia;
31 |
--------------------------------------------------------------------------------
/robots/HalfBuilt.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "HalfBuilt",
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 | "r1": 90,
38 | "r2": 0,
39 | "r3": 0,
40 | "x": 0,
41 | "y": 0,
42 | "z": 15
43 | },
44 | {
45 | "r1": 0,
46 | "r2": 0,
47 | "r3": 90,
48 | "x": 0,
49 | "y": 20,
50 | "z": 0
51 | },
52 | {
53 | "r1": 0,
54 | "r2": 90,
55 | "r3": -90,
56 | "x": 15,
57 | "y": 0,
58 | "z": 0
59 | }
60 | ]
61 | }
62 |
--------------------------------------------------------------------------------
/src/client/tokens/media.json:
--------------------------------------------------------------------------------
1 | {
2 | "phone-only": "(max-width: 599px)",
3 | "tablet-portrait-only": "(min-width: 600px) and (max-width: 899px)",
4 | "tablet-portrait-up": "(min-width: 600px)",
5 | "tablet-landscape-only": "(min-width: 900px) and (max-width: 1199px)",
6 | "tablet-landscape-up": "(min-width: 900px)",
7 | "desktop-only": "(min-width: 1200px) and (max-width: 1799px)",
8 | "desktop-up": "(min-width: 1200px)",
9 | "desktop-big-up": "(min-width: 1600px)",
10 | "desktop-large-up": "(min-width: 1800px)",
11 | "density--2x": "only screen and (-o-min-device-pixel-ratio: 5/4), only screen and (-webkit-min-device-pixel-ratio: 1.25), only screen and (min-device-pixel-ratio: 1.25), only screen and (min-resolution: 1.25dppx)",
12 | "density--3x": "only screen and (-o-min-device-pixel-ratio: 9/4), only screen and (-webkit-min-device-pixel-ratio: 2.25), only screen and (min-device-pixel-ratio: 2.25), only screen and (min-resolution: 2.25dppx)",
13 | "density--4x": "only screen and (-o-min-device-pixel-ratio: 13/4), only screen and (-webkit-min-device-pixel-ratio: 3.25), only screen and (min-device-pixel-ratio: 3.25), only screen and (min-resolution: 3.25dppx)"
14 | }
15 |
--------------------------------------------------------------------------------
/src/client/components/Data/CameraData.jsx:
--------------------------------------------------------------------------------
1 | import { TableView } from '@adobe/react-spectrum';
2 | import { Cell, Column, Row, TableBody, TableHeader } from '@react-stately/table';
3 | import React from 'react';
4 | import useCamera from '../../hooks/useCamera';
5 |
6 | export const CameraData = () => {
7 | const data = useCamera();
8 |
9 | if (!data) return null;
10 |
11 | return (
12 |
13 |
14 |
15 | Id
16 | Type
17 | Conf
18 | X
19 | Y
20 | Z
21 |
22 |
23 | {data.map((obj) => {
24 | return (
25 |
26 | | {obj.id} |
27 | {obj.type} |
28 | {obj.confidence} |
29 | {obj.x} |
30 | {obj.y} |
31 | {obj.z} |
32 |
33 | );
34 | })}
35 |
36 |
37 |
38 | );
39 | };
40 |
--------------------------------------------------------------------------------
/src/client/utils/getEulers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * For X - Y - Z
3 | * X = roll
4 | * Y = pitch
5 | * Z = yaw
6 | *
7 | * Z
8 | * ^
9 | * |
10 | * |
11 | * |
12 | * |
13 | * + -----------> Y
14 | * /
15 | * /
16 | * /
17 | * X
18 | *
19 | */
20 |
21 | const orientations = {
22 | xyz: {
23 | x: [0, 90, 0],
24 | '-x': [0, -90, 0],
25 | y: [-90, 0, 0],
26 | '-y': [90, 0, 0],
27 | z: [0, 0, 0],
28 | '-z': [0, 180, 0],
29 | },
30 | zxz: {
31 | x: [90, 90, 90],
32 | '-x': [-270, -90, -90],
33 | y: [0, -90, 0],
34 | '-y': [-180, -90, 0],
35 | z: [0, 0, 0],
36 | // '-z': [90, 180, 0],
37 | '-z': [-90, -180, 0],
38 | // '-z': [180, 180, 90],
39 | },
40 | zyx: {
41 | z: [0, 0, 0],
42 | '-z': [0, 180, 0],
43 | y: [0, 0, -90],
44 | '-y': [0, 0, 90],
45 | x: [0, -90, 0],
46 | '-x': [0, 90, 0],
47 | },
48 | };
49 |
50 | export const getXYZ = (orientation) => {
51 | return orientations['xyz'][orientation];
52 | };
53 |
54 | export const getZXZ = (orientation) => {
55 | return orientations['zxz'][orientation];
56 | };
57 |
58 | export const getEulers = (orientation, type) => {
59 | return orientations[type][orientation];
60 | };
61 |
--------------------------------------------------------------------------------
/src/client/components/Informed/Listbox.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { useField } from 'informed';
3 | import { Item, ListBox } from '@adobe/react-spectrum';
4 |
5 | const ListBoxInput = (props) => {
6 | const { render, informed, fieldState, fieldApi, userProps, ref } = useField({
7 | type: 'text',
8 | ...props,
9 | });
10 |
11 | const { required, options } = userProps;
12 | const { error, showError } = fieldState;
13 |
14 | const items = useMemo(() => {
15 | return options.map((op) => {
16 | return { name: op.value };
17 | });
18 | });
19 |
20 | return render(
21 | {
32 | fieldApi.setValue(v.currentKey, {});
33 | }}
34 | >
35 | {/* {(item) => - {item.name}
} */}
36 | {options.map((op) => {
37 | return - {op.label}
;
38 | })}
39 |
40 | );
41 | };
42 |
43 | export default ListBoxInput;
44 |
--------------------------------------------------------------------------------
/src/client/components/App/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { defaultTheme, Provider } from '@adobe/react-spectrum';
3 | import { FormProvider } from 'informed';
4 | import { BrowserRouter as Router } from 'react-router-dom';
5 |
6 | // Hooks
7 | import useApp from '../../hooks/useApp';
8 | import { useGet } from '../../hooks/useGet';
9 |
10 | // Components
11 | import { Header } from '../Header/Header';
12 | import { Nav } from '../Nav/Nav';
13 | import { Data } from '../Data/Data';
14 |
15 | import { Routes } from '../Routes/Routes';
16 | import { Extra } from '../Extra/Extra';
17 |
18 | const App = () => {
19 | const { colorScheme, extraOpen } = useApp();
20 |
21 | const [{ loading, error }] = useGet({
22 | url: '/health',
23 | });
24 |
25 | if (loading) {
26 | return Loading...;
27 | }
28 |
29 | if (error) {
30 | return {error.message};
31 | }
32 |
33 | return (
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | };
51 |
52 | export default App;
53 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # --------- Phase 1 Setup --------
2 | # Use node base
3 | FROM node:20-alpine AS base
4 |
5 | # Install Python and other dependencies required by node-gyp
6 | RUN apk add --no-cache python3 make g++
7 |
8 | # Set environment variable for node-gyp to find Python
9 | ENV PYTHON=/usr/bin/python3
10 |
11 | # set working directory
12 | WORKDIR /app
13 |
14 | # copy install stuff
15 | COPY ["package-lock.json", "package.json", "./"]
16 |
17 | # install node packages
18 | RUN npm set progress=false && npm config set depth 0
19 | RUN npm install --omit=dev
20 |
21 | # --------- Phase 2 Build --------
22 |
23 | # Work off phase 1 container
24 | FROM base AS build
25 |
26 | WORKDIR /app
27 |
28 | # install ALL node_modules
29 | RUN npm i
30 |
31 | # Copy the rest of the files
32 | COPY ["babel.config.cjs", "./"]
33 | COPY ["index.html", "./"]
34 | COPY src ./src
35 | COPY robots ./robots
36 | COPY vite.config.js ./
37 |
38 | # Build
39 | RUN npm run build
40 |
41 | # --------- Phase 3 Runtime Image --------
42 |
43 | # Work off phase 1 container
44 | FROM base AS release
45 |
46 | # set working directory
47 | WORKDIR /app
48 |
49 | # copy app sources
50 | COPY --from=build /app/src/server ./server
51 | COPY --from=build /app/src/lib ./lib
52 | COPY --from=build /app/build ./client
53 | COPY --from=build /app/robots ./robots
54 |
55 | # Expose port
56 | EXPOSE 3000
57 |
58 | # Start the application
59 | CMD ["node", "server/index.js"]
60 |
--------------------------------------------------------------------------------
/src/client/components/Pages/Builder/Info.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Content, ContextualHelp, Flex, Heading, Text } from '@adobe/react-spectrum';
3 |
4 | export function Info() {
5 | let [state, setState] = useState(false);
6 |
7 | return (
8 |
9 | setState(isOpen)}>
10 | Frame Rules
11 |
12 |
13 |
14 | The Z axis must be the axis of rotation for a revolute joint, or direction of motion if
15 | you have prismatic joint.
16 |
17 |
18 |
19 |
20 | The X axis must be perpendicular both to its own Z axis and the Z axis of the frame
21 | before it
22 |
23 |
24 |
25 | All frames must follow the right hand rule
26 |
32 |
33 |
34 | Each X axis must intersect the Z axis of the frame before it
35 |
36 |
37 | Frame Rules
38 |
39 | );
40 | }
41 |
--------------------------------------------------------------------------------
/src/client/components/Shared/ResizablePopup.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 |
3 | export const ResizablePopup = ({ children }) => {
4 | const [height, setHeight] = useState(200);
5 | const [isResizing, setIsResizing] = useState(false);
6 |
7 | const handleMouseDown = () => {
8 | setIsResizing(true);
9 | document.body.style.cursor = 'ns-resize';
10 | };
11 |
12 | const handleMouseMove = (e) => {
13 | if (isResizing) {
14 | const newHeight = window.innerHeight - e.clientY;
15 | setHeight(newHeight);
16 | }
17 | };
18 |
19 | const handleMouseUp = () => {
20 | setIsResizing(false);
21 | document.body.style.cursor = 'default';
22 | };
23 |
24 | useEffect(() => {
25 | if (isResizing) {
26 | document.addEventListener('mousemove', handleMouseMove);
27 | document.addEventListener('mouseup', handleMouseUp);
28 | } else {
29 | document.removeEventListener('mousemove', handleMouseMove);
30 | document.removeEventListener('mouseup', handleMouseUp);
31 | }
32 | return () => {
33 | document.removeEventListener('mousemove', handleMouseMove);
34 | document.removeEventListener('mouseup', handleMouseUp);
35 | };
36 | }, [isResizing]);
37 |
38 | return (
39 |
40 |
41 | {children}
42 |
43 | );
44 | };
45 |
--------------------------------------------------------------------------------
/src/server/index.js:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 |
3 | import app from './app.js';
4 | import setup from './setup.js';
5 | import terminate from './terminate.js';
6 | import startWebsocket from './socketserver.js';
7 |
8 | const start = async () => {
9 | try {
10 | // Setup configuration ( database connections, etc )
11 | const configuration = await setup();
12 | // Build express app
13 | const application = app(configuration);
14 |
15 | // Create server
16 | const server = http.createServer(application);
17 |
18 | // Attach io to server
19 | configuration.io.attach(server);
20 |
21 | // Start server
22 | server.listen(configuration.PORT, () => {
23 | console.log('Server is now running on port', configuration.PORT);
24 | });
25 |
26 | // start websocket for metrics
27 | if (process.env.METRICS) {
28 | configuration.controller.websocket = startWebsocket();
29 | }
30 |
31 | // Add Terminate code
32 | const exitHandler = terminate(server, {
33 | coredump: false,
34 | timeout: 500,
35 | });
36 | process.on('uncaughtException', exitHandler(1, 'Unexpected Error'));
37 | process.on('unhandledRejection', exitHandler(1, 'Unhandled Promise'));
38 | process.on('SIGTERM', exitHandler(0, 'SIGTERM'));
39 | process.on('SIGINT', exitHandler(0, 'SIGINT'));
40 | } catch (e) {
41 | console.log('An error occurred when attempting to start application', e);
42 | }
43 | };
44 |
45 | start();
46 |
--------------------------------------------------------------------------------
/robots/UR3eHalf.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "UR3eHalf",
3 | "inverseType": "UR",
4 | "zeroPosition": [0, 0, 0],
5 | "units": "cm",
6 | "x0": 0,
7 | "y0": 0,
8 | "base": 12.6,
9 | "v0": 15.185,
10 | "v1": 24.355,
11 | "v2": 21.32,
12 | "v3": 13.105,
13 | "v4": 8.535,
14 | "v5": 9.21,
15 | "endEffector": 5,
16 | "x": 42,
17 | "y": 10,
18 | "z": 55,
19 | "r1": 90,
20 | "r2": 90,
21 | "r3": 90,
22 | "rangej0": [-180, 180],
23 | "rangej1": [-180, 180],
24 | "rangej2": [-180, 180],
25 | "rangej3": [-180, 180],
26 | "rangej4": [-180, 180],
27 | "rangej5": [-180, 180],
28 | "flip": false,
29 | "adjustments": {
30 | "t1": 90
31 | },
32 | "frames": [
33 | {
34 | "r1": 0,
35 | "r2": 0,
36 | "r3": 0,
37 | "x": 0,
38 | "y": 0,
39 | "z": 0,
40 | "moveFrame": false,
41 | "frameType": "rotary"
42 | },
43 | {
44 | "r1": 90,
45 | "r2": 0,
46 | "r3": 0,
47 | "x": 0,
48 | "y": 0,
49 | "z": 15.185,
50 | "moveFrame": false,
51 | "frameType": "rotary"
52 | },
53 | {
54 | "r1": 0,
55 | "r2": 0,
56 | "r3": 0,
57 | "x": -24.355,
58 | "y": 0,
59 | "z": 0,
60 | "moveFrame": false,
61 | "frameType": "rotary"
62 | },
63 | {
64 | "r1": 0,
65 | "r2": 0,
66 | "r3": 0,
67 | "x": -21.32,
68 | "y": 0,
69 | "z": 0,
70 | "moveFrame": false,
71 | "frameType": "rotary"
72 | }
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/src/client/components/Data/Data.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef } from 'react';
2 | import { Routes, Route, useLocation } from 'react-router-dom';
3 | import useApp from '../../hooks/useApp';
4 |
5 | // Hooks
6 | import useMedia from '../../hooks/useMedia';
7 | import useOutsideAlerter from '../../hooks/useOutsideAlerter';
8 |
9 | import { BuilderData } from './BuilderData';
10 | import { MotorData } from './MotorData';
11 | import { RobotData } from './RobotData';
12 | import useRobotMeta from '../../hooks/useRobotMeta';
13 |
14 | const DataBar = ({ children, wide }) => {
15 | const { dataOpen } = useApp();
16 |
17 | const className = `databar ${dataOpen ? 'databar-visible' : ''} ${wide ? 'databar-wide' : ''}`;
18 |
19 | return {children}
;
20 | };
21 |
22 | export const Data = () => {
23 |
24 | const { connected } = useRobotMeta();
25 |
26 | return (
27 |
28 |
32 |
33 |
34 | ) : null
35 | }
36 | />
37 |
41 |
42 |
43 | ) : null
44 | }
45 | />
46 |
50 |
51 |
52 | }
53 | />
54 |
55 |
56 | //
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/server/robot/client-messenger.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | import { Debug } from '../../lib/debug.js';
4 | const logger = Debug('robot:client-messenger' + '\t');
5 | export class ClientMessenger extends EventEmitter {
6 | constructor(io) {
7 | logger('client constructing ClientMessenger');
8 | super();
9 | // Create io with namespace client
10 | this.io = io.of('/client');
11 | // Initialize listeners
12 | this.io.on('connect', (socket) => this.connect(socket));
13 | }
14 |
15 | send(event, ...args) {
16 | // Send event via socket
17 | // logger(`client sending ${event}`, ...args);
18 | this.io.emit(event, ...args);
19 | }
20 |
21 | sendTo(id, event, ...args) {
22 | // Send event direct via socket
23 | // logger(`client sending ${event} to ${id}`, ...args);
24 | this.io.to(id).emit(event, ...args);
25 | }
26 |
27 | connect(socket) {
28 | // Get the key if there is one
29 | const key = socket.handshake.query.key;
30 | // Log connection
31 | logger(`client with key ${key} connected`);
32 | // Publish connection event
33 | this.emit('connect', key, socket);
34 |
35 | // Subscribe to disconnect event
36 | socket.on('disconnect', (...args) => {
37 | logger(`client disconnected`, key, args);
38 | this.emit('disconnect', key, socket, args);
39 | });
40 |
41 | // Subscribe to any events from client
42 | socket.onAny((eventName, ...args) => {
43 | logger(`client recived ${eventName} on key ${key}`, args);
44 | this.emit(eventName, key, ...args);
45 | });
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/client/utils/media.js:
--------------------------------------------------------------------------------
1 | import mediaQueries from '../tokens/media.json';
2 |
3 | /**
4 | * matchMediaQuery
5 | *
6 | * @param {*} query 'phone-only' | 'desktop-up', etc. See tokens/media.json for a complete list
7 | * @returns a MediaQueryList for the matching query
8 | */
9 | export const matchMediaQuery = (query) => window.matchMedia(mediaQueries[query]);
10 |
11 | /**
12 | * isMedia
13 | * - Util function for matching the current window's media query
14 | * @param query 'phone-only' | 'desktop-up', etc. See tokens/media.json for a complete list
15 | * @returns true | false if the window is of the query's dimensions
16 | */
17 | export const isMedia = (query) => matchMediaQuery(query).matches;
18 |
19 | /**
20 | * Query-specific utils
21 | * @returns true if query is matched
22 | */
23 | export const isPhoneOnly = () => isMedia('phone-only');
24 | export const isTabletPortraitOnly = () => isMedia('tablet-portrait-only');
25 | export const isTabletPortraitUp = () => isMedia('tablet-portrait-up');
26 | export const isTabletLandscapeOnly = () => isMedia('tablet-landscape-only');
27 | export const isTabletLandscapeUp = () => isMedia('tablet-landscape-up');
28 | export const isDesktopOnly = () => isMedia('desktop-only');
29 | export const isDesktopUp = () => isMedia('desktop-up');
30 | export const isDesktopBigUp = () => isMedia('desktop-big-up');
31 | export const isDesktopLargeUp = () => isMedia('desktop-large-up');
32 |
33 | /**
34 | * Density-specific utils
35 | */
36 | export const isDensity2x = () => isMedia('density--2x');
37 | export const isDensity3x = () => isMedia('density--3x');
38 | export const isDensity4x = () => isMedia('density--4x');
39 |
--------------------------------------------------------------------------------
/src/client/providers/GamepadProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useState } from 'react';
2 | import GamepadContext from '../context/GamepadContext';
3 |
4 | const GamepadProvider = ({ children }) => {
5 | const [connected, setConnected] = useState();
6 | const [gamepad, setGamepad] = useState({});
7 | const [buttons, setButtons] = useState([]);
8 | const [axes, setAxes] = useState([]);
9 |
10 | const handleConnect = useCallback((event) => {
11 | const gamepad = event.gamepad;
12 | console.log('Connect', gamepad);
13 |
14 | setConnected(true);
15 | setGamepad(gamepad);
16 | setButtons(gamepad.buttons);
17 | setAxes(gamepad.axes);
18 | }, []);
19 |
20 | const handleDisconnect = useCallback(() => {
21 | const gamepad = event.gamepad;
22 | console.log('Disconnect', gamepad);
23 |
24 | setConnected(false);
25 | }, []);
26 |
27 | useEffect(() => {
28 | window.addEventListener('gamepadconnected', handleConnect);
29 | window.addEventListener('gamepaddisconnected', handleDisconnect);
30 |
31 | return () => {
32 | if (handleConnect) {
33 | window.removeEventListener('gamepadconnected', handleConnect);
34 | window.removeEventListener('gamepaddisconnected', handleDisconnect);
35 | setGamepad({});
36 | setButtons([]);
37 | setAxes([]);
38 | }
39 | };
40 | }, []);
41 |
42 | const value = {
43 | buttons,
44 | axes,
45 | gamepad,
46 | connected,
47 | setButtons,
48 | setAxes,
49 | };
50 |
51 | return {children};
52 | };
53 |
54 | export default GamepadProvider;
55 |
--------------------------------------------------------------------------------
/src/server/robot/robot-messenger.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 |
3 | import { Debug } from '../../lib/debug.js';
4 | const logger = Debug('robot:robot-messenger' + '\t');
5 |
6 | export class RobotMessenger extends EventEmitter {
7 | constructor(io) {
8 | logger('robot constructing MotorMessenger');
9 | super();
10 | // Create io with namespace robot
11 | this.io = io.of('/robot');
12 | // Initialize listeners
13 | this.io.on('connection', (socket) => this.connect(socket));
14 | }
15 |
16 | send(event, ...args) {
17 | // Send event via socket
18 | logger(`sending ${event}`, ...args);
19 | this.io.emit(event, ...args);
20 | }
21 |
22 | sendTo(id, event, ...args) {
23 | // Send event direct via socket
24 | logger(`sending ${event} to ${id}`, ...args);
25 | this.io.to(id).emit(event, ...args);
26 | }
27 |
28 | connect(socket) {
29 | // Get the id of the robot from the socket handshake
30 | const id = socket.handshake.query.id;
31 | // Get the key if there is one
32 | const key = socket.handshake.query.key;
33 | // Log connection
34 | logger(`robot with key ${key} and id ${id} connected`);
35 | // Publish connection event
36 | this.emit('connect', key, id, socket);
37 |
38 | // Subscribe to disconnect event
39 | socket.on('disconnect', (...args) => {
40 | logger(`robot ${id} disconnected`, args);
41 | this.emit('disconnect', key, id, args);
42 | });
43 |
44 | // Subscribe to any events from robot
45 | socket.onAny((eventName, ...args) => {
46 | // logger(`robot recived ${eventName} from robot ${id}`, args);
47 | this.emit(eventName, key, id, ...args);
48 | });
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/rotateMatrix.js:
--------------------------------------------------------------------------------
1 | import { matrixDot } from './matrixDot';
2 |
3 | /**
4 | * Z0
5 | * ^
6 | * |
7 | * Z1 | Y1
8 | * \ | /
9 | * \ | /
10 | * + -----------> Y0
11 | * /
12 | * /
13 | * /
14 | * X0-X1
15 | *
16 | */
17 | // prettier-ignore
18 | export function rotateAroundXAxis(m, a) {
19 | //[x1, y1, z1]
20 | const rot = [
21 | [1, 0, 0 ], // x0
22 | [0, Math.cos(a), -Math.sin(a)], // y0
23 | [0, Math.sin(a), Math.cos(a) ], // z0
24 | ];
25 |
26 | return matrixDot(m, rot);
27 | }
28 |
29 | /**
30 | * Z0
31 | * ^
32 | * |
33 | * X1 | Z1
34 | * \ | /
35 | * \ | /
36 | * + -----------> Y0-Y1
37 | * /
38 | * /
39 | * /
40 | * X0
41 | *
42 | */
43 | // prettier-ignore
44 | export function rotateAroundYAxis(m, a) {
45 | //[x1, y1, z1]
46 | const rot = [
47 | [Math.cos(a), 0, Math.sin(a)], // x0
48 | [0, 1, 0 ], // y0
49 | [-Math.sin(a), 0, cos(a) ], // z0
50 | ];
51 |
52 | return matrixDot(m, rot);
53 | }
54 |
55 | /**
56 | * Z0-Z1
57 | * ^
58 | * |
59 | * | Y1
60 | * | /
61 | * | /
62 | * + -----------> Y0
63 | * / \
64 | * / \
65 | * / X1
66 | * X0
67 | *
68 | */
69 | // prettier-ignore
70 | export function rotateAroundZAxis(m, a) {
71 | //[x1, y1, z1]
72 | const rot = [
73 | [Math.cos(a), -Math.sin(a), 0], // x0
74 | [Math.sin(a), Math.cos(a), 0], // y0
75 | [0, 0, 1], // z0
76 | ];
77 |
78 | return matrixDot(m, rot);
79 | }
80 |
--------------------------------------------------------------------------------
/src/server/socketserver.js:
--------------------------------------------------------------------------------
1 | import stuff from 'websocket';
2 | import http from 'http';
3 |
4 | // https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
5 | export const uuidv4 = () => {
6 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
7 | var r = (Math.random() * 16) | 0,
8 | v = c == 'x' ? r : (r & 0x3) | 0x8;
9 | return v.toString(16);
10 | });
11 | };
12 |
13 | const WebSocketServer = stuff.server;
14 |
15 | const connections = {};
16 |
17 | const startWebsocket = () => {
18 | // Create server for websockets
19 | const webSocketServer = http.createServer();
20 |
21 | // Start server
22 | webSocketServer.listen(6900, () => {
23 | console.log('Websocket Server is now running on port', 6900);
24 | });
25 |
26 | // Add websocket for pub to metrics
27 | const wsServer = new WebSocketServer({
28 | httpServer: webSocketServer,
29 | autoAcceptConnections: true,
30 | });
31 |
32 | wsServer.on('connect', (socket) => {
33 | socket.id = uuidv4();
34 | connections[socket.id] = socket;
35 |
36 | console.log(`Client connected ID: ${socket.id}`);
37 |
38 | socket.on('close', () => {
39 | delete connections[socket.id];
40 | delete socket.id;
41 | });
42 | });
43 |
44 | // For testing
45 | // setInterval(() => {
46 | // Object.values(connections).forEach((connection) => {
47 | // console.log('SEND');
48 | // connection.send(JSON.stringify({ event: 'notify', payload: 'Hello specific client' }));
49 | // });
50 | // }, 1000);
51 |
52 | return {
53 | send: (payload) => {
54 | Object.values(connections).forEach((connection) => {
55 | connection.send(JSON.stringify(payload));
56 | });
57 | },
58 | };
59 | };
60 |
61 | export default startWebsocket;
62 |
--------------------------------------------------------------------------------
/src/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
21 | MyApp
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/lib/matrixDotString.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Use to build variable based matrix!
3 | *
4 | * @param {*} a
5 | * @param {*} b
6 | * @returns
7 | */
8 | export const matrixDotString = (a, b) => {
9 | let result = new Array(a.length).fill(0).map((row) => new Array(b[0].length).fill(0));
10 |
11 | return result.map((row, i) => {
12 | return row.map((val, j) => {
13 | const r = a[i].reduce((sum, elm, k) => {
14 | // If the result will be zero then do nothing
15 | if (elm === '0' || b[k][j] === '0' || elm === '-0' || b[k][j] === '-0') {
16 | return sum;
17 | }
18 |
19 | // If both are 1 then return 1
20 | if (elm === '1' && b[k][j] === '1') {
21 | return '1';
22 | }
23 |
24 | // If one of the operands is one then just return the other operand
25 | if (elm === '1' || b[k][j] === '1') {
26 | return elm === '1' ? b[k][j] : elm;
27 | }
28 |
29 | // If both are -1 then return 1
30 | if (elm === '-1' && b[k][j] === '-1') {
31 | return '1';
32 | }
33 |
34 | // If one of the operands is -1 then just return the other operand negated
35 | if (elm === '-1' || b[k][j] === '-1') {
36 | // We replace double neg with posative
37 | return elm === '-1' ? `-${b[k][j]}`.replace('--', '') : `-${elm}`.replace('--', '');
38 | }
39 |
40 | // First iteration
41 | if (sum != '') {
42 | return `${sum} + ${elm} * ${b[k][j]}`;
43 | } else {
44 | return `${elm} * ${b[k][j]}`;
45 | }
46 | }, '');
47 |
48 | // If we got nothing then its just a zero!
49 | if (r === '') {
50 | return '0';
51 | }
52 |
53 | // we got something so return that!
54 | return r;
55 | });
56 | });
57 | };
58 |
--------------------------------------------------------------------------------
/src/client/components/Nav/Nav.jsx:
--------------------------------------------------------------------------------
1 | import { Flex } from '@adobe/react-spectrum';
2 | import React, { useRef } from 'react';
3 | import { Routes, Route } from 'react-router-dom';
4 |
5 | // Hooks
6 | import useApp from '../../hooks/useApp';
7 | import useMedia from '../../hooks/useMedia';
8 |
9 | import useOutsideAlerter from '../../hooks/useOutsideAlerter';
10 | import { NavLink } from '../Shared/NavLink';
11 | import { BuilderNav } from './BuilderNav';
12 | import { CookbookNav } from './CookbookNav';
13 | import { FramerNav } from './FramerNav';
14 |
15 | import { MotorNav } from './MotorNav';
16 | import { RobotNav } from './RobotNav';
17 |
18 | export const Nav = () => {
19 | const { extraOpen, navOpen, closeNav } = useApp();
20 |
21 | const navRef = useRef();
22 |
23 | useOutsideAlerter(() => closeNav(), navRef);
24 |
25 | const { isDesktopUp } = useMedia();
26 |
27 | return (
28 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/src/client/components/Informed/InputSlider.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { useField } from 'informed';
3 | import { Flex, Slider, NumberField } from '@adobe/react-spectrum';
4 |
5 | const outside = (a, [l, h]) => {
6 | return a < l || a > h;
7 | };
8 |
9 | const Input = ({ displayError, ...props }) => {
10 | const validate = (value) => {
11 | return outside(value, [props.minValue, props.maxValue]) ? 'Too far!' : undefined;
12 | };
13 |
14 | const { render, informed, fieldState, fieldApi, userProps, ref } = useField({
15 | type: 'number',
16 | validate,
17 | ...props,
18 | });
19 | const { required, trackGradient } = userProps;
20 | const { error, showError } = fieldState;
21 |
22 | return render(
23 |
24 |
25 | fieldApi.setValue(v, {})}
33 | // type={props.type}
34 | step={props.step}
35 | type="number"
36 | />
37 | fieldApi.setValue(v, {})}
48 | />
49 |
50 | {showError ? {error} : null}
51 |
52 | );
53 | };
54 |
55 | export default Input;
56 |
--------------------------------------------------------------------------------
/example_py/debug.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 |
4 | def select_color(namespace, colors):
5 | hash_value = 0
6 |
7 | for char in namespace:
8 | hash_value = (hash_value << 5) - hash_value + ord(char)
9 | hash_value |= 0 # Convert to 32bit integer
10 |
11 | return colors[abs(hash_value) % len(colors)]
12 |
13 | def format_args(args, config):
14 | name = config['namespace']
15 | if config['use_colors']:
16 | color_code = f'\033[3{config["color"]};1m'
17 | prefix = f' {color_code}{name} \033[0m'
18 | args_list = list(args)
19 | args_list[0] = prefix + str(args_list[0]).replace('\n', f'\n{prefix}')
20 | return tuple(args_list)
21 | else:
22 | args_list = list(args)
23 | args_list[0] = f'{name} {args_list[0]}'
24 | return tuple(args_list)
25 |
26 | def create_logger(prefix=None, config=None):
27 | def logger(*args):
28 | if prefix:
29 | args = (prefix, *args)
30 |
31 | matches = [re.compile('^' + ns[:-1] + '.*$' if ns.endswith('*') else '^' + ns + '$')
32 | for ns in config['namespaces'].split(',')]
33 |
34 | match = any(regex.match(prefix) for regex in matches)
35 |
36 | conf = {
37 | 'color': select_color(prefix, config['colors']),
38 | 'namespace': prefix,
39 | 'use_colors': config['use_colors'],
40 | }
41 |
42 | if os.getenv('NODE_ENV') != 'production' and match:
43 | args = format_args(args, conf)
44 | print(*args)
45 |
46 | return logger
47 |
48 | def load_config():
49 | namespaces = os.getenv('DEBUG', '')
50 | colors = [1, 2, 3, 4, 5, 6]
51 | use_colors = True
52 |
53 | return {
54 | 'namespaces': namespaces,
55 | 'colors': colors,
56 | 'use_colors': use_colors,
57 | 'format_args': format_args,
58 | }
59 |
60 | def Debug(prefix):
61 | return create_logger(prefix, load_config())
--------------------------------------------------------------------------------
/example_py_7/debug.py:
--------------------------------------------------------------------------------
1 | import re
2 | import os
3 |
4 | def select_color(namespace, colors):
5 | hash_value = 0
6 |
7 | for char in namespace:
8 | hash_value = (hash_value << 5) - hash_value + ord(char)
9 | hash_value |= 0 # Convert to 32bit integer
10 |
11 | return colors[abs(hash_value) % len(colors)]
12 |
13 | def format_args(args, config):
14 | name = config['namespace']
15 | if config['use_colors']:
16 | color_code = f'\033[3{config["color"]};1m'
17 | prefix = f' {color_code}{name} \033[0m'
18 | args_list = list(args)
19 | args_list[0] = prefix + str(args_list[0]).replace('\n', f'\n{prefix}')
20 | return tuple(args_list)
21 | else:
22 | args_list = list(args)
23 | args_list[0] = f'{name} {args_list[0]}'
24 | return tuple(args_list)
25 |
26 | def create_logger(prefix=None, config=None):
27 | def logger(*args):
28 | if prefix:
29 | args = (prefix, *args)
30 |
31 | matches = [re.compile('^' + ns[:-1] + '.*$' if ns.endswith('*') else '^' + ns + '$')
32 | for ns in config['namespaces'].split(',')]
33 |
34 | match = any(regex.match(prefix) for regex in matches)
35 |
36 | conf = {
37 | 'color': select_color(prefix, config['colors']),
38 | 'namespace': prefix,
39 | 'use_colors': config['use_colors'],
40 | }
41 |
42 | if os.getenv('NODE_ENV') != 'production' and match:
43 | args = format_args(args, conf)
44 | print(*args)
45 |
46 | return logger
47 |
48 | def load_config():
49 | namespaces = os.getenv('DEBUG', '')
50 | colors = [1, 2, 3, 4, 5, 6]
51 | use_colors = True
52 |
53 | return {
54 | 'namespaces': namespaces,
55 | 'colors': colors,
56 | 'use_colors': use_colors,
57 | 'format_args': format_args,
58 | }
59 |
60 | def Debug(prefix):
61 | return create_logger(prefix, load_config())
--------------------------------------------------------------------------------
/src/lib/printRotationMatrix.test.js:
--------------------------------------------------------------------------------
1 | import { printZRotationMatrix } from './printRotationMatrix';
2 | import { rotateAroundZAxis } from './rotateMatrix';
3 | import { roundMatrix } from './roundMatrix';
4 | import { toRadians } from './toRadians';
5 | import { matrixEqual } from './matrixEqual';
6 |
7 | describe('printZRotationMatrix', () => {
8 | beforeEach(() => {
9 | jest.restoreAllMocks();
10 | });
11 |
12 | it('should generate the rotation matrix for z axis rotation around a and print JS matrix out', () => {
13 | const projection = [
14 | ['0', '-1', '0'],
15 | ['1', '0', '0'],
16 | ['0', '0', '1'],
17 | ];
18 |
19 | jest.spyOn(console, 'log').mockImplementation(() => {});
20 |
21 | printZRotationMatrix(projection, 'R1_2', 't2');
22 |
23 | // The first argument of the first call to the function was 'hello'
24 | expect(console.log.mock.calls[0][0]).toBe('const R1_2 = [');
25 | expect(console.log.mock.calls[1][0]).toBe(' [-Math.sin(t2),-Math.cos(t2),0],');
26 | expect(console.log.mock.calls[2][0]).toBe(' [Math.cos(t2),-Math.sin(t2),0],');
27 | expect(console.log.mock.calls[3][0]).toBe(' [0,0,1],');
28 | expect(console.log.mock.calls[4][0]).toBe(']');
29 | });
30 |
31 | it('result of executing the printed matrix should match actual matrix multiplication', () => {
32 | const r1_2_proj = [
33 | [0, -1, 0],
34 | [1, 0, 0],
35 | [0, 0, 1],
36 | ];
37 |
38 | const t2 = toRadians(90);
39 |
40 | // From above test
41 | const expected = [
42 | [-Math.sin(t2), -Math.cos(t2), 0],
43 | [Math.cos(t2), -Math.sin(t2), 0],
44 | [0, 0, 1],
45 | ];
46 |
47 | const actual = rotateAroundZAxis(r1_2_proj, t2);
48 |
49 | const roundedExpected = roundMatrix(expected);
50 | const roundedActual = roundMatrix(actual);
51 |
52 | expect(roundedActual).toEqual(roundedExpected);
53 |
54 | expect(matrixEqual(roundedActual, roundedExpected)).toEqual(true);
55 | });
56 | });
57 |
--------------------------------------------------------------------------------
/src/client/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { createRoot } from 'react-dom/client';
3 | import App from './components/App/App.jsx';
4 | import AppProvider from './providers/AppProvider.jsx';
5 | import SimulateProvider from './providers/SimulateProvider.jsx';
6 | import RobotProvider from './providers/RobotProvider.jsx';
7 | import ControlProvider from './providers/ControlProvider.jsx';
8 |
9 | import { Informed } from 'informed';
10 |
11 | /* ---- Include global variables first ---- */
12 | import '@spectrum-css/vars/dist/spectrum-global.css';
13 |
14 | /* ---- Include only the scales your application needs ---- */
15 | import '@spectrum-css/vars/dist/spectrum-large.css';
16 | import '@spectrum-css/vars/dist/spectrum-medium.css';
17 |
18 | /* ---- Include only the colorstops your application needs ---- */
19 | import '@spectrum-css/vars/dist/spectrum-light.css';
20 | import '@spectrum-css/vars/dist/spectrum-dark.css';
21 | import '@spectrum-css/vars/dist/spectrum-darkest.css';
22 |
23 | /* ---- Include index-vars.css for all components you need ---- */
24 | import '@spectrum-css/page/dist/index-vars.css';
25 | import '@spectrum-css/typography/dist/index-vars.css';
26 | import '@spectrum-css/sidenav/dist/index-vars.css';
27 |
28 | import './index.css';
29 | import GamepadProvider from './providers/GamepadProvider.jsx';
30 | import CameraProvider from './providers/CameraProvider.jsx';
31 |
32 | const rootElement = document.getElementById('root');
33 | const root = createRoot(rootElement); // createRoot(container!) if you use TypeScript
34 |
35 | root.render(
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | ,
51 | );
52 |
--------------------------------------------------------------------------------
/src/client/components/Header/Header.jsx:
--------------------------------------------------------------------------------
1 | import { ActionButton, Flex } from '@adobe/react-spectrum';
2 | import React from 'react';
3 |
4 | import ShowMenu from '@spectrum-icons/workflow/ShowMenu';
5 | import Table from '@spectrum-icons/workflow/Table';
6 |
7 | import useMedia from '../../hooks/useMedia';
8 | import useApp from '../../hooks/useApp';
9 | import { NavLink } from '../Shared/NavLink';
10 |
11 | export const Header = () => {
12 | // header contents modal open state when resize
13 | const { isDesktopUp, isDesktopBigUp } = useMedia();
14 | const { toggleNav, toggleData } = useApp();
15 |
16 | // For resizing header
17 | // window.addEventListener('resize', () => {
18 | // setModalOpen(false);
19 | // });
20 |
21 | return (
22 |
23 |
30 | {!isDesktopUp ? (
31 | toggleNav()}>
32 |
33 |
34 | ) : null}
35 | {!isDesktopBigUp ? (
36 | toggleData()}>
37 |
38 |
39 | ) : null}
40 | {isDesktopUp ? (
41 | <>
42 | Robot
43 | Motor
44 | Cookbook
45 | Framer
46 | Builder
47 | Gamepad
48 | >
49 | ) : null}
50 | {/*
51 |
52 | Kinematics
53 |
54 | */}
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/src/client/providers/ControlProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2 | import { FormProvider } from 'informed';
3 |
4 | // Hooks
5 | import { inverse as inverseBasic } from 'kinematics-js';
6 | import { inverse as inverseUR } from '../../lib/inverse_UR';
7 | // import { inverse } from '../../lib/inverse';
8 | import { toRadians } from '../../lib/toRadians';
9 | import { toDeg } from '../../lib/toDeg';
10 | import useApp from '../hooks/useApp';
11 |
12 | const ControlProvider = ({ children }) => {
13 | const { config } = useApp();
14 |
15 | const formApiRef = useRef();
16 |
17 | const initialValues = useMemo(() => {
18 | console.log('NEW INITIAL VALUES', config);
19 | // We give in degrees so turn into rads
20 | const ro1 = toRadians(config.r1);
21 | const ro2 = toRadians(config.r2);
22 | const ro3 = toRadians(config.r3);
23 |
24 | console.log('Initial getting angles for', [config.x, config.y, config.z, ro1, ro2, ro3]);
25 |
26 | const inverse = config.inverseType === 'UR' ? inverseUR : inverseBasic;
27 |
28 | const angles = inverse(config.x, config.y, config.z, ro1, ro2, ro3, {
29 | base: config.base,
30 | v1: config.v0,
31 | v2: config.v1,
32 | v3: config.v2,
33 | v4: config.v3,
34 | v5: config.v4,
35 | v6: config.v5 + config.endEffector,
36 | flip: config.flip,
37 | x0: config.x0,
38 | y0: config.y0,
39 | adjustments: config.adjustments,
40 | });
41 |
42 | return {
43 | ...config,
44 | j0: toDeg(angles[0]),
45 | j1: toDeg(angles[1]),
46 | j2: toDeg(angles[2]),
47 | j3: toDeg(angles[3]),
48 | j4: toDeg(angles[4]),
49 | j5: toDeg(angles[5]),
50 | };
51 | }, [config]);
52 |
53 | useEffect(() => {
54 | formApiRef.current.reset();
55 | }, [initialValues]);
56 |
57 | return (
58 |
59 | {children}
60 |
61 | );
62 | };
63 |
64 | export default ControlProvider;
65 |
--------------------------------------------------------------------------------
/src/server/app.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import cors from 'cors';
3 | import path, { dirname } from 'path';
4 | import bodyParser from 'body-parser';
5 |
6 | import health from './routes/health.js';
7 | import camera from './routes/camera.js';
8 | import fail from './routes/fail.js';
9 | import waypoints from './routes/waypoints.js';
10 | import recipes from './routes/recipes.js';
11 | import robots from './routes/robots.js';
12 | import errorHandler from './middleware/errorHandler.js';
13 | import proxy from './middleware/proxy.js';
14 |
15 | import { fileURLToPath } from 'url';
16 |
17 | const __filename = fileURLToPath(import.meta.url);
18 | const __dirname = dirname(__filename);
19 |
20 | const createApp = ({ corsConfig }) => {
21 | // Create Express application
22 | const app = express();
23 |
24 | // Health endpoint
25 | app.use(health);
26 |
27 | // Camera endpoint
28 | app.use(camera);
29 |
30 | // Fail endpoints
31 | app.use('/fail', fail);
32 |
33 | // Apply CORS to the endpoints
34 | app.use(cors(corsConfig));
35 |
36 | // Add body parser
37 | app.use(bodyParser.json());
38 |
39 | // Add error handler
40 | app.use(errorHandler);
41 |
42 | // Waypoint endpoints
43 | app.use('/waypoints', waypoints);
44 |
45 | // Recipe endpoints
46 | app.use('/recipes', recipes);
47 |
48 | // Robot endpoints
49 | app.use('/robots', robots);
50 |
51 | // Route for static content
52 | if (process.env.NODE_ENV === 'development') {
53 | // Route for static content
54 | app.use('/static', express.static(path.join(__dirname, './static')));
55 | // Route to dev server when developing
56 | app.use('/*', proxy('http://127.0.0.1:9001'));
57 | } else {
58 | // Routes for static content
59 | app.use('/static', express.static(path.join(__dirname, './static')));
60 | app.use(express.static(path.join(__dirname, '../client'), { redirect: false }));
61 |
62 | /* final catch-all route to index.html defined last */
63 | app.get('/*', (req, res) => {
64 | res.sendFile(path.join(__dirname, '../client/index.html'));
65 | });
66 | }
67 |
68 | return app;
69 | };
70 |
71 | export default createApp;
72 |
--------------------------------------------------------------------------------
/src/client/constants.js:
--------------------------------------------------------------------------------
1 | export const TYPE_MAPPING = {
2 | AR4: {
3 | position: 'encoderPosition',
4 | },
5 | Example: {
6 | position: 'currentPos',
7 | extWrenchInTcp: 'extWrenchInTcp',
8 | tcpPose: 'tcpPose',
9 | flangePose: 'flangePose',
10 | },
11 | Example7Axis: {
12 | position: 'currentPos',
13 | extWrenchInTcp: 'extWrenchInTcp',
14 | tcpPose: 'tcpPose',
15 | flangePose: 'flangePose',
16 | },
17 | IgusRebel: {
18 | position: 'currentPosition',
19 | },
20 | Rizon4: {
21 | position: 'angle',
22 | extWrenchInTcp: 'extWrenchInTcp',
23 | tcpPose: 'tcpPose',
24 | flangePose: 'flangePose',
25 | },
26 | Rizon4Test: {
27 | position: 'angle',
28 | extWrenchInTcp: 'extWrenchInTcp',
29 | tcpPose: 'tcpPose',
30 | flangePose: 'flangePose',
31 | },
32 | };
33 |
34 | // Example AR
35 | export const EXAMPLE_AR_JOINT_DATA = {
36 | id: 'j0',
37 | homing: false,
38 | home: false,
39 | homed: false,
40 | enabled: true,
41 | moving: true,
42 | ready: true,
43 | stepPosition: -7733,
44 | encoderPosition: 77309,
45 | error: 'Ahh!!!!',
46 | };
47 |
48 | // EXAMPLE IGUS
49 | export const EXAMPLE_IGUS_JOINT_DATA = {
50 | id: 'j0',
51 | canId: 16,
52 | homing: false,
53 | home: false,
54 | // TODO add to backend vvv
55 | ready: true,
56 | enabled: false,
57 | moving: false,
58 | // TODO add to backend ^^^
59 | currentPosition: 90.000235647645,
60 | currentTics: 8000,
61 | encoderPulsePosition: 90.000235647645,
62 | encoderPulseTics: 8000,
63 | jointPositionSetPoint: 90,
64 | jointPositionSetTics: 8000,
65 | goalPosition: 90,
66 | motorCurrent: 120,
67 | error: null,
68 | errorCode: null,
69 | errorCodeString: 'n/a',
70 | voltage: 0,
71 | tempMotor: 20,
72 | tempBoard: 30,
73 | direction: 'forwards',
74 | motorError: null,
75 | adcError: null,
76 | rebelError: null,
77 | controlError: null,
78 | sendInterval: 20,
79 | calculatedVelocity: 29,
80 | currentVelocity: 30,
81 | positionHistory: [
82 | { time: 1, pos: 10 },
83 | { time: 2, pos: 20 },
84 | { time: 3, pos: 30 },
85 | { time: 4, pos: 30 },
86 | { time: 5, pos: 10 },
87 | ],
88 | };
89 |
--------------------------------------------------------------------------------
/robots/UR3e.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "UR3e",
3 | "inverseType": "UR",
4 | "zeroPosition": [0, 0, 0],
5 | "units": "cm",
6 | "x0": 0,
7 | "y0": 0,
8 | "base": 12.6,
9 | "v0": 15.185,
10 | "v1": 24.355,
11 | "v2": 21.32,
12 | "v3": 13.105,
13 | "v4": 8.535,
14 | "v5": 9.21,
15 | "endEffector": 5,
16 | "x": 38,
17 | "y": -13,
18 | "z": 38,
19 | "r1": 90,
20 | "r2": 90,
21 | "r3": 90,
22 | "rangej0": [-180, 180],
23 | "rangej1": [-180, 180],
24 | "rangej2": [-180, 180],
25 | "rangej3": [-180, 180],
26 | "rangej4": [-180, 180],
27 | "rangej5": [-180, 180],
28 | "flip": false,
29 | "adjustments": {
30 | "t1": 90
31 | },
32 | "frames": [
33 | {
34 | "r1": 0,
35 | "r2": 0,
36 | "r3": 0,
37 | "x": 0,
38 | "y": 0,
39 | "z": 0,
40 | "moveFrame": false,
41 | "frameType": "rotary"
42 | },
43 | {
44 | "r1": 90,
45 | "r2": 0,
46 | "r3": 0,
47 | "x": 0,
48 | "y": 0,
49 | "z": 15.185,
50 | "moveFrame": false,
51 | "frameType": "rotary"
52 | },
53 | {
54 | "r1": 0,
55 | "r2": 0,
56 | "r3": 0,
57 | "x": -24.355,
58 | "y": 0,
59 | "z": 0,
60 | "moveFrame": false,
61 | "frameType": "rotary"
62 | },
63 | {
64 | "r1": 0,
65 | "r2": 0,
66 | "r3": 0,
67 | "x": -21.32,
68 | "y": 0,
69 | "z": 0,
70 | "moveFrame": false,
71 | "frameType": "rotary"
72 | },
73 | {
74 | "r1": 90,
75 | "r2": 0,
76 | "r3": 0,
77 | "x": 0,
78 | "y": 0,
79 | "z": 13.105,
80 | "moveFrame": false,
81 | "frameType": "rotary"
82 | },
83 | {
84 | "r1": -90,
85 | "r2": 0,
86 | "r3": 0,
87 | "x": 0,
88 | "y": 0,
89 | "z": 8.535,
90 | "moveFrame": false,
91 | "frameType": "rotary"
92 | },
93 | {
94 | "r1": 0,
95 | "r2": 0,
96 | "r3": 0,
97 | "x": 0,
98 | "y": 0,
99 | "z": 9.21,
100 | "moveFrame": false,
101 | "frameType": "stationary"
102 | }
103 | ]
104 | }
105 |
--------------------------------------------------------------------------------
/src/client/components/Data/JointsData/ExampleJointData.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 ExampleJointData = ({ motor }) => {
7 | return (
8 |
9 |
{motor.id}
10 |
11 |
12 | Name
13 | Status
14 |
15 |
16 |
17 | | Ready |
18 |
19 |
20 | |
21 |
22 |
23 | | Homing |
24 |
25 |
26 | |
27 |
28 |
29 | | Home |
30 |
31 |
32 | |
33 |
34 |
35 | | Homed |
36 |
37 |
38 | |
39 |
40 |
41 | | Enabled |
42 |
43 |
44 | |
45 |
46 |
47 | | Moving |
48 |
49 |
50 | |
51 |
52 |
53 | | Current Position |
54 |
55 | {motor.currentPos}
56 | |
57 |
58 |
59 | | Goal Position |
60 |
61 | {motor.goalPos}
62 | |
63 |
64 |
65 | | Error |
66 |
67 | {motor.error}
68 | |
69 |
70 |
71 |
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/robots/IgusRebelBackup.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "IgusRebel",
3 | "zeroPosition": [0, 0, 101.2],
4 | "units": "cm",
5 | "x0": 0,
6 | "y0": 0,
7 | "base": 12.6,
8 | "v0": 12.6,
9 | "v1": 23.7,
10 | "v2": 14.85,
11 | "v3": 14.85,
12 | "v4": 12.6,
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": [-180, 180],
22 | "rangej1": [-140, 80],
23 | "rangej2": [-140, 80],
24 | "rangej3": [-180, 180],
25 | "rangej4": [-95, 95],
26 | "rangej5": [-180, 180],
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": 0,
45 | "y": 0,
46 | "z": 12.6,
47 | "moveFrame": false
48 | },
49 | {
50 | "frameType": "rotary",
51 | "r1": 0,
52 | "r2": 0,
53 | "r3": 90,
54 | "x": 0,
55 | "y": 23.7,
56 | "z": 0,
57 | "moveFrame": false
58 | },
59 | {
60 | "frameType": "rotary",
61 | "r1": 0,
62 | "r2": 90,
63 | "r3": -90,
64 | "x": 14.85,
65 | "y": 0,
66 | "z": 0,
67 | "moveFrame": true,
68 | "moveBackBy": -14.85,
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": 14.85,
79 | "moveFrame": false
80 | },
81 | {
82 | "frameType": "rotary",
83 | "r1": -90,
84 | "r2": 0,
85 | "r3": 0,
86 | "x": 0,
87 | "y": 12.6,
88 | "z": 0,
89 | "moveFrame": true,
90 | "moveBackBy": -12.6,
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 | }
105 |
--------------------------------------------------------------------------------
/src/lib/forward.test.js:
--------------------------------------------------------------------------------
1 | import { forward } from './forward.js';
2 | import { toRadians } from './toRadians.js';
3 |
4 | const robotConfig = {
5 | a1: 2.5, // 2.5
6 | a2: 3, // 3
7 | a3: 2.5, // 2.5
8 | a4: 2.5, // 2.5
9 | a5: 2.5, // 2.5
10 | a6: 2, // 2
11 | };
12 |
13 | describe('forward', () => {
14 | /**
15 | * |
16 | * [ ]
17 | * |
18 | * ( )
19 | * |
20 | * [ ]
21 | * |
22 | * ( )
23 | * |
24 | * ( )
25 | * |
26 | * [ ]
27 | */
28 | it('should take forward of 0, 0, 0, 0, 0, 0', () => {
29 | expect(forward(0, 0, 0, 0, 0, 0, robotConfig)).toEqual([
30 | [1, 0, 0, 0],
31 | [0, 1, 0, 0],
32 | [0, 0, 1, 15],
33 | [0, 0, 0, 1],
34 | ]);
35 | });
36 |
37 | /**
38 | * [ ]
39 | * |
40 | * ( ) -- [ ] -- ( )
41 | * |
42 | * ( )
43 | * |
44 | * [ ]
45 | */
46 | it('should take forward of 0, 0, -90, 180, -90, 0', () => {
47 | expect(forward(0, 0, -Math.PI / 2, Math.PI, -Math.PI / 2, 0, robotConfig)).toEqual([
48 | [-1, 0, 0, 5],
49 | [0, -1, 0, 0],
50 | [0, 0, 1, 10],
51 | [0, 0, 0, 1],
52 | ]);
53 | });
54 |
55 | /**
56 | * [ ]
57 | * |
58 | * [ ] -- ( ) -- ( )
59 | * |
60 | * ( )
61 | * |
62 | * [ ]
63 | */
64 | it('should take forward of 180, 0, -90, 180, -90, 180', () => {
65 | // prettier-ignore
66 |
67 | expect(forward(Math.PI, 0, -Math.PI / 2, Math.PI, -Math.PI/2, -Math.PI, robotConfig)).toEqual([
68 | [-1, 0, 0, -5],
69 | [0, -1, 0, 0],
70 | [0, 0, 1, 10],
71 | [0, 0, 0, 1],
72 | ]);
73 | });
74 |
75 | /**
76 | *
77 | *
78 | * ( ) -- [ ] -- ( ) -- [ ]
79 | * |
80 | * ( )
81 | * |
82 | * [ ]
83 | */
84 | it('should take forward of 0, 0, -90, 0, 0, 0', () => {
85 | // expect(inverse(9.5, 0, 5.5, -d90, -d90, 0, robotConfig)).toEqual([0, 0, -Math.PI / 2, 0, 0, 0]);
86 |
87 | expect(forward(0, 0, -Math.PI / 2, 0, 0, 0, robotConfig)).toEqual([
88 | [0, 0, 1, 9.5],
89 | [0, 1, 0, 0],
90 | [-1, 0, 0, 5.5],
91 | [0, 0, 0, 1],
92 | ]);
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/src/lib/euler.js:
--------------------------------------------------------------------------------
1 | import { cleanMatrix } from './roundMatrix';
2 |
3 | /**
4 | * Z-X-Z
5 | *
6 | * @param {*} a
7 | * @param {*} b
8 | * @param {*} c
9 | * @returns
10 | */
11 | export const zxz = (a, b, c) => {
12 | const res = [
13 | [
14 | Math.cos(a) * Math.cos(c) + -Math.sin(a) * Math.cos(b) * Math.sin(c),
15 | Math.cos(a) * -Math.sin(c) + -Math.sin(a) * Math.cos(b) * Math.cos(c),
16 | -Math.sin(a) * -Math.sin(b),
17 | ],
18 | [
19 | Math.sin(a) * Math.cos(c) + Math.cos(a) * Math.cos(b) * Math.sin(c),
20 | Math.sin(a) * -Math.sin(c) + Math.cos(a) * Math.cos(b) * Math.cos(c),
21 | Math.cos(a) * -Math.sin(b),
22 | ],
23 | [Math.sin(b) * Math.sin(c), Math.sin(b) * Math.cos(c), Math.cos(b)],
24 | ];
25 |
26 | return cleanMatrix(res);
27 | };
28 |
29 | /**
30 | * X-Y-Z ( yaw + pitch + roll )
31 | *
32 | * yaw pitch and roll are the euler angles
33 | * yaw is the rotation around the x axis
34 | * pitch is the rotation around the y axis
35 | * roll is the rotation around the z axis
36 | * the output is the rotation matrix as a 3 x 3 matrix
37 | * clean the matrix by removing all negative zeros and rounding all numbers to 2 decimal places
38 | *
39 | */
40 | export const xyz = (yaw, pitch, roll) => {
41 | const c1 = Math.cos(yaw);
42 | const c2 = Math.cos(pitch);
43 | const c3 = Math.cos(roll);
44 | const s1 = Math.sin(yaw);
45 | const s2 = Math.sin(pitch);
46 | const s3 = Math.sin(roll);
47 | return cleanMatrix([
48 | [c1 * c2, c1 * s2 * s3 - c3 * s1, s1 * s3 + c1 * c3 * s2],
49 | [c2 * s1, c1 * c3 + s1 * s2 * s3, c3 * s1 * s2 - c1 * s3],
50 | [-s2, c2 * s3, c2 * c3],
51 | ]);
52 | };
53 |
54 | // (a, b, c) => {
55 | // const res = [
56 | // [
57 | // Math.cos(a) * Math.cos(b),
58 | // -Math.sin(a) * Math.cos(c) + Math.cos(a) * Math.sin(b) * Math.sin(c),
59 | // -Math.sin(a) * -Math.sin(c) + Math.cos(a) * Math.sin(b) * Math.cos(c),
60 | // ],
61 | // [
62 | // Math.sin(a) * Math.cos(b),
63 | // Math.cos(a) * Math.cos(c) + Math.sin(a) * Math.sin(b) * Math.sin(c),
64 | // Math.cos(a) * -Math.sin(c) + Math.sin(a) * Math.sin(b) * Math.cos(c),
65 | // ],
66 | // [-Math.sin(b), Math.cos(b) * Math.sin(c), Math.cos(b) * Math.cos(c)],
67 | // ];
68 |
69 | // return cleanMatrix(res);
70 | // };
71 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "robot-viewer",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "test": "jest",
9 | "start": "node build/index.js",
10 | "test:watch": "jest --watch",
11 | "test:watch:debug": "DEBUG='ik:.*' jest --watch",
12 | "build": "vite build",
13 | "start:dev": "concurrently --raw \"npm run client:local\" \" DEBUG='robot:.*' npm run server:local\"",
14 | "client:local": "vite",
15 | "server:local": "NODE_ENV=development nodemon -r dotenv/config src/server/index.js",
16 | "build:docker": "docker build -t robot-viewer .",
17 | "run:docker": "docker run --env-file=.env -p 3000:3000 robot-viewer"
18 | },
19 | "keywords": [],
20 | "author": "",
21 | "license": "ISC",
22 | "dependencies": {
23 | "axios": "^0.20.0",
24 | "body-parser": "^1.20.0",
25 | "cors": "^2.8.5",
26 | "d3": "^7.6.1",
27 | "dotenv": "^8.2.0",
28 | "express": "^4.17.1",
29 | "kinematics-js": "^1.0.5",
30 | "mathjs": "^12.4.2",
31 | "react-spring": "^9.7.0",
32 | "request": "^2.88.2",
33 | "socket.io": "^4.5.1",
34 | "websocket": "^1.0.34",
35 | "winston": "^3.3.3"
36 | },
37 | "devDependencies": {
38 | "@adobe/react-spectrum": "^3.24.1",
39 | "@babel/preset-env": "^7.23.3",
40 | "@react-spring/three": "^9.7.0",
41 | "@react-three/drei": "^9.17.1",
42 | "@react-three/fiber": "^8.2.0",
43 | "@spectrum-css/page": "^5.0.15",
44 | "@spectrum-css/sidenav": "^3.0.30",
45 | "@spectrum-css/typography": "^4.0.25",
46 | "@spectrum-css/vars": "^8.0.3",
47 | "@spectrum-icons/workflow": "^4.1.0",
48 | "@testing-library/jest-dom": "^5.11.10",
49 | "@testing-library/react": "^11.2.5",
50 | "@use-gesture/react": "^10.2.17",
51 | "@vitejs/plugin-react": "^4.2.1",
52 | "concurrently": "^5.3.0",
53 | "eslint": "^8.18.0",
54 | "eslint-plugin-prettier": "^4.0.0",
55 | "eslint-plugin-react": "^7.30.0",
56 | "informed": "^4.60.3",
57 | "jest": "^29.7.0",
58 | "msw": "^0.28.0",
59 | "nodemon": "^2.0.7",
60 | "react": "^18.2.0",
61 | "react-dom": "^18.2.0",
62 | "react-router-dom": "^6.3.0",
63 | "socket.io-client": "^4.5.1",
64 | "supertest": "^6.1.3",
65 | "three": "^0.142.0",
66 | "vite": "^5.0.12"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/robots/Example.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "Example",
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": 5,
14 | "endEffector": 5,
15 | "x": 42,
16 | "y": 10,
17 | "z": 55,
18 | "r1": 90,
19 | "r2": 90,
20 | "r3": 90,
21 | "rangej0": [-180, 180],
22 | "rangej1": [-140, 140],
23 | "rangej2": [-115, 115],
24 | "rangej3": [-180, 180],
25 | "rangej4": [-90, 90],
26 | "rangej5": [-180, 180],
27 | "flip": true,
28 | "features": {
29 | "motorZero": true,
30 | "motorReference": false,
31 | "forceTourqueZero": true
32 | },
33 | "frames": [
34 | {
35 | "frameType": "rotary",
36 | "r1": 0,
37 | "r2": 0,
38 | "r3": 0,
39 | "x": 0,
40 | "y": 0,
41 | "z": 0,
42 | "moveFrame": false
43 | },
44 | {
45 | "frameType": "rotary",
46 | "r1": 90,
47 | "r2": 0,
48 | "r3": 0,
49 | "x": 0,
50 | "y": 0,
51 | "z": 15,
52 | "moveFrame": false
53 | },
54 | {
55 | "frameType": "rotary",
56 | "r1": 0,
57 | "r2": 0,
58 | "r3": 90,
59 | "x": 0,
60 | "y": 20,
61 | "z": 0,
62 | "moveFrame": false
63 | },
64 | {
65 | "frameType": "rotary",
66 | "r1": 0,
67 | "r2": 90,
68 | "r3": -90,
69 | "x": 15,
70 | "y": 0,
71 | "z": 0,
72 | "moveFrame": true,
73 | "moveBackBy": -15,
74 | "moveBack": "x"
75 | },
76 | {
77 | "frameType": "rotary",
78 | "r1": 90,
79 | "r2": 0,
80 | "r3": 0,
81 | "x": 0,
82 | "y": 0,
83 | "z": 15,
84 | "moveFrame": false
85 | },
86 | {
87 | "frameType": "rotary",
88 | "r1": -90,
89 | "r2": 0,
90 | "r3": 0,
91 | "x": 0,
92 | "y": 15,
93 | "z": 0,
94 | "moveFrame": true,
95 | "moveBackBy": -15,
96 | "moveBack": "y"
97 | },
98 | {
99 | "frameType": "stationary",
100 | "r1": 0,
101 | "r2": 0,
102 | "r3": 0,
103 | "x": 0,
104 | "y": 0,
105 | "z": 5,
106 | "moveFrame": false
107 | }
108 | ]
109 | }
110 |
--------------------------------------------------------------------------------
/robots/Example7Axis.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "Example7Axis",
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": 0,
34 | "z": 21,
35 | "moveFrame": false
36 | },
37 | {
38 | "frameType": "rotary",
39 | "r1": -90,
40 | "r2": 0,
41 | "r3": 0,
42 | "x": 0,
43 | "y": 20.5,
44 | "z": 0,
45 | "moveFrame": true,
46 | "moveBackBy": -20.5,
47 | "moveBack": "y"
48 | },
49 | {
50 | "frameType": "rotary",
51 | "r1": 90,
52 | "r2": 180,
53 | "r3": 0,
54 | "x": 2,
55 | "y": 0,
56 | "z": 19,
57 | "moveFrame": false
58 | },
59 | {
60 | "frameType": "rotary",
61 | "r1": -90,
62 | "r2": 0,
63 | "r3": 0,
64 | "x": -2,
65 | "y": 19.5,
66 | "z": 0,
67 | "moveFrame": true,
68 | "moveBackBy": -19.5,
69 | "moveBack": "y"
70 | },
71 | {
72 | "frameType": "rotary",
73 | "r1": 90,
74 | "r2": 180,
75 | "r3": 0,
76 | "x": 0,
77 | "y": 0,
78 | "z": 19,
79 | "moveFrame": false
80 | },
81 | {
82 | "frameType": "rotary",
83 | "r1": 90,
84 | "r2": 90,
85 | "r3": -180,
86 | "x": 5.5,
87 | "y": 11,
88 | "z": 7,
89 | "moveFrame": true,
90 | "moveBackBy": -5.5,
91 | "moveBack": "x"
92 | },
93 | {
94 | "frameType": "stationary",
95 | "r1": 0,
96 | "r2": 0,
97 | "r3": 0,
98 | "x": 0,
99 | "y": 0,
100 | "z": 8.1,
101 | "moveFrame": false
102 | }
103 | ]
104 | }
105 |
--------------------------------------------------------------------------------
/src/client/components/Data/Visualizations/LineGraph.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef, useEffect } from 'react';
2 | import { line, scaleLinear, select } from 'd3';
3 | import * as d3 from 'd3';
4 | import './LineGraph.css';
5 |
6 | const LineGraph = ({ data, data2, xMin, xMax, yMin, yMax }) => {
7 | const ref = useRef();
8 |
9 | const layout = {
10 | width: 1000,
11 | height: 500,
12 | marginBottom: 480,
13 | marginLeft: 30,
14 | };
15 |
16 | const graphDetails = {
17 | xScale: scaleLinear().range([0, layout.width]),
18 | yScale: scaleLinear().range([layout.height, 0]),
19 | lineGenerator: line(),
20 | };
21 |
22 | useEffect(() => {
23 | const svgElement = d3.select(ref.current);
24 | svgElement.selectAll('g').remove();
25 |
26 | const bottomAxisGenerator = d3.axisBottom(graphDetails.xScale);
27 | const leftAxisGenerator = d3.axisLeft(graphDetails.yScale);
28 |
29 | bottomAxisGenerator.tickFormat((d, i) => new Date(d).toLocaleTimeString());
30 |
31 | svgElement
32 | .append('g')
33 | .attr('transform', `translate(${layout.marginLeft},${layout.marginBottom})`)
34 | .call(bottomAxisGenerator);
35 | svgElement
36 | .append('g')
37 | .attr('transform', `translate(${layout.marginLeft},10)`)
38 | .call(leftAxisGenerator);
39 | }, [data]);
40 |
41 | graphDetails.xScale.domain([xMin, xMax]);
42 | graphDetails.yScale.domain([yMin, yMax]);
43 |
44 | graphDetails.lineGenerator.x((d) => graphDetails.xScale(d['x']));
45 | graphDetails.lineGenerator.y((d) => graphDetails.yScale(d['y']));
46 |
47 | const lineData = graphDetails.lineGenerator(data);
48 | const lineData2 = data2 ? graphDetails.lineGenerator(data2) : null;
49 |
50 | return (
51 |
69 | );
70 | };
71 |
72 | export default LineGraph;
73 |
--------------------------------------------------------------------------------
/robots/Rizon4Test.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "Rizon4Test",
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 | "frameType": "rotary",
22 | "r1": 0,
23 | "r2": 0,
24 | "r3": 180,
25 | "x": 0,
26 | "y": 0,
27 | "z": 0,
28 | "moveFrame": false
29 | },
30 | {
31 | "frameType": "rotary",
32 | "r1": 90,
33 | "r2": 0,
34 | "r3": 0,
35 | "x": 0,
36 | "y": 0,
37 | "z": 21,
38 | "moveFrame": false
39 | },
40 | {
41 | "frameType": "rotary",
42 | "r1": -90,
43 | "r2": 0,
44 | "r3": 0,
45 | "x": 0,
46 | "y": 20.5,
47 | "z": 0,
48 | "moveFrame": true,
49 | "moveBackBy": -20.5,
50 | "moveBack": "y"
51 | },
52 | {
53 | "frameType": "rotary",
54 | "r1": 90,
55 | "r2": 180,
56 | "r3": 0,
57 | "x": 2,
58 | "y": 0,
59 | "z": 19,
60 | "moveFrame": false
61 | },
62 | {
63 | "frameType": "rotary",
64 | "r1": -90,
65 | "r2": 0,
66 | "r3": 0,
67 | "x": -2,
68 | "y": 19.5,
69 | "z": 0,
70 | "moveFrame": true,
71 | "moveBackBy": -19.5,
72 | "moveBack": "y"
73 | },
74 | {
75 | "frameType": "rotary",
76 | "r1": 90,
77 | "r2": 180,
78 | "r3": 0,
79 | "x": 0,
80 | "y": 0,
81 | "z": 19,
82 | "moveFrame": false
83 | },
84 | {
85 | "frameType": "rotary",
86 | "r1": 90,
87 | "r2": 90,
88 | "r3": -180,
89 | "x": 5.5,
90 | "y": 11,
91 | "z": 7,
92 | "moveFrame": true,
93 | "moveBackBy": -5.5,
94 | "moveBack": "x"
95 | },
96 | {
97 | "frameType": "stationary",
98 | "r1": 0,
99 | "r2": 0,
100 | "r3": 0,
101 | "x": 0,
102 | "y": 0,
103 | "z": 8.1,
104 | "moveFrame": false
105 | }
106 | ]
107 | }
108 |
--------------------------------------------------------------------------------
/src/client/components/Data/JointsData/JointsData.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useFieldState } from 'informed';
4 | import useRobotState from '../../../hooks/useRobotState';
5 | import { Flex } from '@adobe/react-spectrum';
6 | import { ARJointData } from './ARJointData';
7 | import { IgusRebelJointData } from './IgusRebelJointData';
8 | import { If } from '../../Shared/If';
9 | import { ExampleJointData } from './ExampleJointData';
10 | import { EXAMPLE_IGUS_JOINT_DATA } from '../../../constants';
11 | import { RizonJointData } from './RizonJointData';
12 |
13 | const JointData = ({ motor }) => {
14 | const { value: robotType } = useFieldState('robotType');
15 |
16 | if (robotType === 'AR4') {
17 | return ;
18 | }
19 |
20 | if (robotType === 'IgusRebel') {
21 | return ;
22 | }
23 |
24 | if (robotType === 'Example' || robotType === 'Example7Axis') {
25 | return ;
26 | }
27 |
28 | if (robotType === 'Rizon4' || robotType === 'Rizon4Test') {
29 | return ;
30 | }
31 |
32 | return null;
33 | };
34 |
35 | export const JointsData = () => {
36 | const { robotStates } = useRobotState();
37 |
38 | // Get value of robotId && motorId
39 | const { value: robotId } = useFieldState('robotId');
40 | const { value: motorId } = useFieldState('motorId');
41 |
42 | // Get the robot type
43 | const { value: robotType } = useFieldState('robotType');
44 |
45 | // Get the selected robot state
46 | const robotState = robotStates[robotId];
47 |
48 | if (!robotState) {
49 | return null;
50 | }
51 |
52 | console.log('RENDER JOINTS DATA');
53 |
54 | const motors = Object.values(robotState.motors);
55 |
56 | // FOR TESTING WITHOUT CONNECTION
57 | // const motors = [EXAMPLE_IGUS_JOINT_DATA];
58 | return (
59 |
66 | {motorId != 'na' && motorId != null && }
67 |
68 | <>
69 | {motors.map((motor, i) => (
70 |
71 | ))}
72 | >
73 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/client/components/Nav/FramerNav.jsx:
--------------------------------------------------------------------------------
1 | import React, { useMemo } from 'react';
2 | import { Flex } from '@adobe/react-spectrum';
3 |
4 | import Select from '../Informed/Select';
5 | import InputSlider from '../Informed/InputSlider';
6 | import Switch from '../Informed/Switch';
7 | import { useFieldState } from 'informed';
8 |
9 | export const FramerNav = () => {
10 | const { value: type } = useFieldState('eulerType');
11 |
12 | const [label1, label2, label3] = useMemo(() => {
13 | if (type) {
14 | // Example: 'xyz'.split('')
15 | // => [ 'x', 'y', 'z' ]
16 | return type.split('').map((l) => `Rotation ${l}`);
17 | }
18 | return ['Rotation1', 'Rotation2', 'Rotation3'];
19 | }, [type]);
20 |
21 | return (
22 | <>
23 |
24 | Frame Control
25 |
26 |
27 |
28 |
29 |
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 |
69 |
70 |
71 | >
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/src/server/routes/robots.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import logger from 'winston';
3 |
4 | // For reading and writing to config
5 | import path from 'path';
6 | import fs from 'fs';
7 |
8 | const router = express.Router();
9 |
10 | router.post('/save/:filename', (req, res) => {
11 | const { filename } = req.params;
12 |
13 | logger.info(`Saving robot to file ${filename}.json`, req.body);
14 |
15 | // Make robots dir if its not there
16 | if (!fs.existsSync('robots')) {
17 | logger.info(`Making robots directory`);
18 | fs.mkdirSync('robots');
19 | }
20 |
21 | // Write out the file
22 | try {
23 | // Get filepath
24 | const filepath = path.resolve(`robots/${filename}`);
25 | // Write out file
26 | fs.writeFileSync(`${filepath}.json`, JSON.stringify(req.body, null, 2));
27 | } catch (err) {
28 | console.error(err);
29 | }
30 |
31 | return res.sendStatus(200);
32 | });
33 |
34 | router.get('/load/:filename', (req, res) => {
35 | const { filename } = req.params;
36 |
37 | logger.info(`Loading robot from file ${filename}.json`);
38 |
39 | // Read in config file ( create if it does not exist yet )
40 | try {
41 | // Get filepath
42 | const filepath = path.resolve(`robots/${filename}`);
43 |
44 | // Read in config file
45 | const data = JSON.parse(fs.readFileSync(`${filepath}.json`, 'utf8'));
46 |
47 | logger.info('Successfully read in file', data);
48 |
49 | return res.send(data);
50 | } catch (err) {
51 | console.error(err);
52 | return res.sendStatus(400);
53 | }
54 | });
55 |
56 | router.get('/all', (req, res) => {
57 | logger.info(`Loading all robots from /robots`);
58 | const allRobots = {};
59 | const directory = path.resolve('robots/');
60 | try {
61 | fs.readdirSync(directory).forEach((filename) => {
62 | // get current file name
63 | const name = path.parse(filename).name;
64 | // get current file extension
65 | const ext = path.parse(filename).ext;
66 | // get current file path
67 | const filepath = path.resolve(directory, filename);
68 |
69 | // get information about the file
70 | const stat = fs.statSync(filepath);
71 | // check if the current path is a file or a folder
72 | const isFile = stat.isFile();
73 | // exclude folders
74 | if (isFile && ext === '.json') {
75 | const data = JSON.parse(fs.readFileSync(`${filepath}`, 'utf8'));
76 | allRobots[name] = data;
77 | }
78 | });
79 | return res.send(allRobots);
80 | } catch (err) {
81 | console.error(err);
82 | return res.sendStatus(400);
83 | }
84 | });
85 |
86 | export default router;
87 |
--------------------------------------------------------------------------------
/robots/IgusRebel.json:
--------------------------------------------------------------------------------
1 | {
2 | "robotType": "IgusRebel",
3 | "zeroPosition": [0, 0, 101.2],
4 | "units": "cm",
5 | "x0": 0,
6 | "y0": 0,
7 | "base": 12.6,
8 | "v0": 12.6,
9 | "v1": 23.7,
10 | "v2": 14.85,
11 | "v3": 14.85,
12 | "v4": 9,
13 | "v5": 3.6,
14 | "endEffector": 10,
15 | "x": 42,
16 | "y": 10,
17 | "z": 55,
18 | "r1": 90,
19 | "r2": 90,
20 | "r3": 90,
21 | "rangej0": [-180, 180],
22 | "rangej1": [-140, 80],
23 | "rangej2": [-140, 80],
24 | "rangej3": [-180, 180],
25 | "rangej4": [-95, 95],
26 | "rangej5": [-180, 180],
27 | "flip": true,
28 | "features": {
29 | "motorZero": true,
30 | "motorReference": true
31 | },
32 | "frames": [
33 | {
34 | "frameType": "rotary",
35 | "r1": 0,
36 | "r2": 0,
37 | "r3": 0,
38 | "x": 0,
39 | "y": 0,
40 | "z": 0,
41 | "moveFrame": false
42 | },
43 | {
44 | "frameType": "rotary",
45 | "r1": 90,
46 | "r2": 0,
47 | "r3": 0,
48 | "x": 0,
49 | "y": 0,
50 | "z": 12.6,
51 | "moveFrame": false
52 | },
53 | {
54 | "frameType": "rotary",
55 | "r1": 0,
56 | "r2": 0,
57 | "r3": 90,
58 | "x": 0,
59 | "y": 23.7,
60 | "z": 0,
61 | "moveFrame": false
62 | },
63 | {
64 | "frameType": "rotary",
65 | "r1": 0,
66 | "r2": 90,
67 | "r3": -90,
68 | "x": 14.85,
69 | "y": 0,
70 | "z": 0,
71 | "moveFrame": true,
72 | "moveBackBy": -14.85,
73 | "moveBack": "x"
74 | },
75 | {
76 | "frameType": "rotary",
77 | "r1": 90,
78 | "r2": 0,
79 | "r3": 0,
80 | "x": 0,
81 | "y": 0,
82 | "z": 14.85,
83 | "moveFrame": false
84 | },
85 | {
86 | "frameType": "rotary",
87 | "r1": -90,
88 | "r2": 0,
89 | "r3": 0,
90 | "x": 0,
91 | "y": 9,
92 | "z": 0,
93 | "moveFrame": true,
94 | "moveBackBy": -9,
95 | "moveBack": "y"
96 | },
97 | {
98 | "frameType": "stationary",
99 | "r1": 0,
100 | "r2": 0,
101 | "r3": 0,
102 | "x": 0,
103 | "y": 0,
104 | "z": 3.6,
105 | "moveFrame": false
106 | }
107 | ],
108 | "dhParameters": [
109 | { "theta": 0, "alpha": 90, "r": 0, "d": 25.2 },
110 | { "theta": 90, "alpha": 0, "r": 23.7, "d": 0 },
111 | { "theta": -90, "alpha": -90, "r": 0, "d": 0 },
112 | { "theta": 0, "alpha": 90, "r": 0, "d": 29.7 },
113 | { "theta": 0, "alpha": -90, "r": 0, "d": 0 },
114 | { "theta": 0, "alpha": 0, "r": 0, "d": 22.6 }
115 | ]
116 | }
117 |
--------------------------------------------------------------------------------
/src/server/routes/waypoints.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import logger from 'winston';
3 |
4 | // For reading and writing to config
5 | import path from 'path';
6 | import fs from 'fs';
7 |
8 | const router = express.Router();
9 |
10 | router.post('/save/:filename', (req, res) => {
11 | const { filename } = req.params;
12 |
13 | logger.info(`Saving waypoints to file ${filename}.json`, req.body);
14 |
15 | // Make waypoint dir if its not there
16 | if (!fs.existsSync('waypoints')) {
17 | logger.info(`Making waypoint directory`);
18 | fs.mkdirSync('waypoints');
19 | }
20 |
21 | // Write out the file
22 | try {
23 | // Get filepath
24 | const filepath = path.resolve(`waypoints/${filename}`);
25 | // Write out file
26 | fs.writeFileSync(`${filepath}.json`, JSON.stringify(req.body, null, 2));
27 | } catch (err) {
28 | console.error(err);
29 | }
30 |
31 | return res.sendStatus(200);
32 | });
33 |
34 | router.get('/load/:filename', (req, res) => {
35 | const { filename } = req.params;
36 |
37 | logger.info(`Loading waypoints from file ${filename}.json`);
38 |
39 | // Read in config file ( create if it does not exist yet )
40 | try {
41 | // Get filepath
42 | const filepath = path.resolve(`waypoints/${filename}`);
43 |
44 | // Read in config file
45 | const data = JSON.parse(fs.readFileSync(`${filepath}.json`, 'utf8'));
46 |
47 | logger.info('Successfully read in file', data);
48 |
49 | return res.send(data);
50 | } catch (err) {
51 | console.error(err);
52 | return res.sendStatus(400);
53 | }
54 | });
55 |
56 | router.get('/all', (req, res) => {
57 | logger.info(`Loading all waypoints from /waypoints`);
58 | const allWaypoints = {};
59 | const directory = path.resolve('waypoints/');
60 | try {
61 | fs.readdirSync(directory).forEach(filename => {
62 | // get current file name
63 | const name = path.parse(filename).name;
64 | // get current file extension
65 | const ext = path.parse(filename).ext;
66 | // get current file path
67 | const filepath = path.resolve(directory, filename);
68 |
69 | // get information about the file
70 | const stat = fs.statSync(filepath);
71 | // check if the current path is a file or a folder
72 | const isFile = stat.isFile();
73 | // exclude folders
74 | if (isFile && ext === '.json') {
75 | const data = JSON.parse(fs.readFileSync(`${filepath}`, 'utf8'));
76 | allWaypoints[name] = data;
77 | }
78 | });
79 | return res.send(allWaypoints);
80 | } catch (err) {
81 | console.error(err);
82 | return res.sendStatus(400);
83 | }
84 | });
85 |
86 | export default router;
87 |
--------------------------------------------------------------------------------
/src/server/routes/recipes.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import logger from 'winston';
3 |
4 | // For reading and writing to config
5 | import path from 'path';
6 | import fs from 'fs';
7 |
8 | const router = express.Router();
9 |
10 | router.post('/save/:filename', (req, res) => {
11 | const { filename } = req.params;
12 |
13 | logger.info(`Saving recipes to file ${filename}.json`, req.body);
14 |
15 | // Make recipe dir if its not there
16 | if (!fs.existsSync('recipes')) {
17 | logger.info(`Making recipe directory`);
18 | fs.mkdirSync('recipes');
19 | }
20 |
21 | // Write out the file
22 | try {
23 | // Get filepath
24 | const filepath = path.resolve(`recipes/${filename}`);
25 | // Write out file
26 | fs.writeFileSync(`${filepath}.json`, JSON.stringify(req.body, null, 2));
27 | } catch (err) {
28 | console.error(err);
29 | }
30 |
31 | return res.sendStatus(200);
32 | });
33 |
34 | router.get('/load/:filename', (req, res) => {
35 | const { filename } = req.params;
36 |
37 | logger.info(`Loading recipes from file ${filename}.json`);
38 |
39 | // Read in config file ( create if it does not exist yet )
40 | try {
41 | // Get filepath
42 | const filepath = path.resolve(`recipes/${filename}`);
43 |
44 | // Read in config file
45 | const data = JSON.parse(fs.readFileSync(`${filepath}.json`, 'utf8'));
46 |
47 | logger.info('Successfully read in file', data);
48 |
49 | return res.send(data);
50 | } catch (err) {
51 | console.error(err);
52 | return res.sendStatus(400);
53 | }
54 | });
55 |
56 | router.get('/all', (req, res) => {
57 | logger.info(`Loading all recipes from /recipes`);
58 | const allRecipes = {};
59 | // Make recipe dir if its not there
60 | if (!fs.existsSync('recipes')) {
61 | logger.info(`Making recipe directory`);
62 | fs.mkdirSync('recipes');
63 | }
64 | const directory = path.resolve('recipes/');
65 | try {
66 | fs.readdirSync(directory).forEach(filename => {
67 | // get current file name
68 | const name = path.parse(filename).name;
69 | // get current file extension
70 | const ext = path.parse(filename).ext;
71 | // get current file path
72 | const filepath = path.resolve(directory, filename);
73 |
74 | // get information about the file
75 | const stat = fs.statSync(filepath);
76 | // check if the current path is a file or a folder
77 | const isFile = stat.isFile();
78 | // exclude folders
79 | if (isFile && ext === '.json') {
80 | const data = JSON.parse(fs.readFileSync(`${filepath}`, 'utf8'));
81 | allRecipes[name] = data;
82 | }
83 | });
84 | return res.send(allRecipes);
85 | } catch (err) {
86 | console.error(err);
87 | return res.sendStatus(400);
88 | }
89 | });
90 |
91 | export default router;
92 |
--------------------------------------------------------------------------------
/src/lib/inverse.test.js:
--------------------------------------------------------------------------------
1 | import { inverse } from './inverse.js';
2 | import { toRadians } from './toRadians.js';
3 |
4 | const robotConfig = {
5 | a1: 2.5, // 2.5
6 | a2: 3, // 3
7 | a3: 2.5, // 2.5
8 | a4: 2.5, // 2.5
9 | a5: 2.5, // 2.5
10 | a6: 2, // 2
11 | };
12 |
13 | describe('inverse', () => {
14 | /**
15 | * |
16 | * [ ]
17 | * |
18 | * ( )
19 | * |
20 | * [ ]
21 | * |
22 | * ( )
23 | * |
24 | * ( )
25 | * |
26 | * [ ]
27 | */
28 | it('should take inverse of 0, 0, 15, 0, 0, 0', () => {
29 | expect(inverse(0, 0, 15, 0, 0, 0, robotConfig)).toEqual([0, 0, 0, 0, 0, 0]);
30 | });
31 |
32 | /**
33 | * [ ]
34 | * |
35 | * ( ) -- [ ] -- ( )
36 | * |
37 | * ( )
38 | * |
39 | * [ ]
40 | */
41 | it('should take inverse of 5, 0, 10, 0, 0, 0', () => {
42 | expect(inverse(5, 0, 10, 0, 0, 0, robotConfig)).toEqual([
43 | 0,
44 | 0,
45 | -Math.PI / 2,
46 | Math.PI,
47 | -Math.PI / 2,
48 | 0,
49 | ]);
50 | });
51 |
52 | /**
53 | * [ ]
54 | * |
55 | * [ ] -- ( ) -- ( )
56 | * |
57 | * ( )
58 | * |
59 | * [ ]
60 | */
61 | it('should take inverse of -5, 0, 10, 0, 0, 0', () => {
62 | // prettier-ignore
63 | expect(inverse(-5, 0, 10, 0, 0, 0, robotConfig)).toEqual([Math.PI, 0, -Math.PI / 2, Math.PI, -Math.PI/2, -Math.PI]);
64 | });
65 |
66 | /**
67 | *
68 | *
69 | * ( ) -- [ ] -- ( ) -- [ ]
70 | * |
71 | * ( )
72 | * |
73 | * [ ]
74 | */
75 | it('should take inverse of 9.5, 0, 5.5, -d90, -d90, 0', () => {
76 | const d90 = toRadians(90);
77 |
78 | expect(inverse(9.5, 0, 5.5, -d90, -d90, 0, robotConfig)).toEqual([0, 0, -Math.PI / 2, 0, 0, 0]);
79 | });
80 |
81 | /**
82 | * [ ]
83 | * |
84 | * ( ) -- [ ] -- ( )
85 | * |
86 | * ( )
87 | * |
88 | * [ ]
89 | */
90 | it('should take inverse of 5, 0, 10, 0, 0, 0 when flip is turned on', () => {
91 | expect(inverse(5, 0, 10, 0, 0, 0, { ...robotConfig, flip: true })).toEqual([
92 | 0,
93 | 0,
94 | -Math.PI / 2,
95 | 0,
96 | Math.PI / 2,
97 | 0,
98 | ]);
99 | });
100 |
101 | /**
102 | * [ ]
103 | * |
104 | * [ ] -- ( ) -- ( )
105 | * |
106 | * ( )
107 | * |
108 | * [ ]
109 | */
110 | it('should take inverse of -5, 0, 10, 0, 0, 0 when flip is turned on', () => {
111 | // prettier-ignore
112 | expect(inverse(-5, 0, 10, 0, 0, 0, {...robotConfig, flip: true})).toEqual([Math.PI, 0, -Math.PI/2, 0, Math.PI/2, 0]);
113 | });
114 | });
115 |
--------------------------------------------------------------------------------
/src/lib/euler.test.js:
--------------------------------------------------------------------------------
1 | import { xyz, zxz } from './euler';
2 | import { cleanAndRoundMatrix, roundMatrix } from './roundMatrix';
3 | import { toRadians } from './toRadians';
4 |
5 | describe('euler', () => {
6 | describe('ZXZ', () => {
7 | /**
8 | * Z0-Z1
9 | * ^
10 | * |
11 | * |
12 | * |
13 | * |
14 | * + -----------> Y0-Y1
15 | * /
16 | * /
17 | * /
18 | * X0-X1
19 | *
20 | */
21 | it('should rotate nothing', () => {
22 | const actual = zxz(0, 0, 0);
23 |
24 | const expected = [
25 | [1, 0, 0],
26 | [0, 1, 0],
27 | [0, 0, 1],
28 | ];
29 |
30 | expect(actual).toEqual(expected);
31 | });
32 |
33 | /**
34 | * Z0-Z1
35 | * ^
36 | * |
37 | * |
38 | * |
39 | * |
40 | * + -----------> Y0-Y1
41 | * /
42 | * /
43 | * /
44 | * X0-X1
45 | *
46 | */
47 | it('should rotate correctly for 90 -90 0', () => {
48 | const actual = zxz(toRadians(90), toRadians(-90), 0);
49 |
50 | const expected = [
51 | [0, 0, -1],
52 | [1, 0, 0],
53 | [0, -1, 0],
54 | ];
55 |
56 | const roundedExpected = cleanAndRoundMatrix(expected);
57 | const roundedActual = cleanAndRoundMatrix(actual);
58 |
59 | // console.table(roundedExpected);
60 | // console.table(roundedActual);
61 |
62 | expect(roundedActual).toEqual(roundedExpected);
63 | });
64 | });
65 |
66 | describe.skip('XYZ', () => {
67 | /**
68 | * X
69 | * ^
70 | * |
71 | * |
72 | * |
73 | * |
74 | * + -----------> Y
75 | * /
76 | * /
77 | * /
78 | * Z
79 | *
80 | */
81 | it('should rotate nothing', () => {
82 | const actual = xyz(0, 0, 0);
83 |
84 | const expected = [
85 | [1, 0, 0],
86 | [0, 1, 0],
87 | [0, 0, 1],
88 | ];
89 |
90 | expect(actual).toEqual(expected);
91 | });
92 |
93 | /**
94 | * X
95 | * ^
96 | * |
97 | * |
98 | * |
99 | * |
100 | * + -----------> Y
101 | * /
102 | * /
103 | * /
104 | * Z
105 | *
106 | */
107 | it('should rotate correctly for 90 90 0', () => {
108 | const actual = xyz(toRadians(90), toRadians(90), 0);
109 |
110 | const expected = [
111 | [0, 0, -1],
112 | [1, 0, 0],
113 | [0, -1, 0],
114 | ];
115 |
116 | const roundedExpected = cleanAndRoundMatrix(expected);
117 | const roundedActual = cleanAndRoundMatrix(actual);
118 |
119 | console.table(roundedExpected);
120 | console.table(roundedActual);
121 |
122 | expect(roundedActual).toEqual(roundedExpected);
123 | });
124 | });
125 | });
126 |
--------------------------------------------------------------------------------
/src/lib/newForward.js:
--------------------------------------------------------------------------------
1 | import { buildHomogeneousDenavitForTable } from './denavitHartenberg';
2 | import { toDeg } from './toDeg';
3 | import { toRadians } from './toRadians';
4 |
5 | // Example input
6 | // const exampleAngles = [45, 30, -15, 60, 0, -45];
7 | // const exampleConfig = {
8 | // dhParameters: [
9 | // { theta: 0, alpha: 90, r: 0, d: 25.2 },
10 | // { theta: 90, alpha: 0, r: 23.7, d: 0 },
11 | // { theta: -90, alpha: -90, r: 0, d: 0 },
12 | // { theta: 0, alpha: 90, r: 0, d: 29.7 },
13 | // { theta: 0, alpha: -90, r: 0, d: 0 },
14 | // { theta: 0, alpha: 0, r: 0, d: 22.6 },
15 | // ],
16 | // };
17 |
18 | /**
19 | * Calculate the forward kinematics of a robotic arm given joint angles and DH parameters.
20 | * @param {number[]} angles - Array of joint angles in degrees.
21 | * @param {Object} config - Configuration object containing DH parameters.
22 | * @param {Object[]} config.dhParameters - Array of DH parameters objects.
23 | * @param {number} config.dhParameters[].theta - Angle between the Z axes of consecutive links (in degrees).
24 | * @param {number} config.dhParameters[].alpha - Angle between the X axes of consecutive links (in degrees).
25 | * @param {number} config.dhParameters[].r - Distance along the X axis between consecutive origins.
26 | * @param {number} config.dhParameters[].d - Distance along the Z axis between consecutive origins.
27 | * @returns {Object} - Object containing position and orientation of the end-effector.
28 | */
29 | export const forwardKinematics = (angles, config) => {
30 | // Extract DH parameters from config and convert them to radians
31 | const dhParameters = config.dhParameters.map((param) => [
32 | toRadians(param.theta),
33 | toRadians(param.alpha),
34 | param.r,
35 | param.d,
36 | ]);
37 |
38 | // Convert angles to radians
39 | const anglesRad = angles.map((angle) => toRadians(angle));
40 |
41 | // Adjust DH parameters based on joint angles
42 | const adjustedDHParameters = dhParameters.map((param, i) => [
43 | param[0] + anglesRad[i], // Adjust theta based on joint angle
44 | param[1],
45 | param[2],
46 | param[3],
47 | ]);
48 |
49 | // Build the homogeneous Denavit-Hartenberg transformation matrix
50 | const res = buildHomogeneousDenavitForTable(adjustedDHParameters);
51 |
52 | // Extract position (x, y, z) from transformation matrix
53 | const position = [res.endMatrix[0][3], res.endMatrix[1][3], res.endMatrix[2][3]];
54 |
55 | // Extract rotation matrix elements
56 | const r11 = res.endMatrix[0][0];
57 | const r12 = res.endMatrix[0][1];
58 | const r13 = res.endMatrix[0][2];
59 | const r21 = res.endMatrix[1][0];
60 | const r22 = res.endMatrix[1][1];
61 | const r23 = res.endMatrix[1][2];
62 | const r31 = res.endMatrix[2][0];
63 | const r32 = res.endMatrix[2][1];
64 | const r33 = res.endMatrix[2][2];
65 |
66 | // Calculate orientation angles
67 | const orientation = [
68 | toDeg(Math.atan2(r32, r33)), // r1: rotation around z1 axis
69 | toDeg(Math.atan2(-r31, Math.sqrt(r32 ** 2 + r33 ** 2))), // r2: rotation around x axis
70 | toDeg(Math.atan2(r21, r11)), // r3: rotation around z2 axis
71 | ];
72 |
73 | // Return position and orientation
74 | return { position, orientation };
75 | };
76 |
--------------------------------------------------------------------------------
/src/lib/inverse1_3.test.js:
--------------------------------------------------------------------------------
1 | import { inverse1_3 } from './inverse1_3.js';
2 | import { roundArray } from './round.js';
3 | import { toRadians } from './toRadians.js';
4 |
5 | const robotConfig = {
6 | a1: 1,
7 | a2: 1,
8 | a3: 1,
9 | };
10 |
11 | describe('inverse', () => {
12 | /**
13 | * |
14 | * ( )
15 | * |
16 | * ( )
17 | * |
18 | * [ ]
19 | */
20 | it('should take inverse of 0, 0, 3', () => {
21 | expect(inverse1_3(0, 0, 3, robotConfig)).toEqual([0, 0, 0]);
22 | });
23 |
24 | /**
25 | *
26 | * ( ) --
27 | * |
28 | * ( )
29 | * |
30 | * [ ]
31 | */
32 | it('should take inverse of 1, 0, 2', () => {
33 | const inverse = inverse1_3(1, 0, 2, robotConfig);
34 | const expected = [0, 0, toRadians(-90)];
35 |
36 | const roundedInverse = roundArray(inverse);
37 | const roundedExpected = roundArray(expected);
38 |
39 | expect(roundedInverse).toEqual(roundedExpected);
40 | });
41 |
42 | /**
43 | *
44 | * ( ) -- ( ) --
45 | * |
46 | * [ ]
47 | */
48 | it('should take inverse of 2, 0, 1', () => {
49 | const inverse = inverse1_3(2, 0, 1, robotConfig);
50 | const expected = [0, toRadians(-90), 0];
51 |
52 | const roundedInverse = roundArray(inverse);
53 | const roundedExpected = roundArray(expected);
54 |
55 | expect(roundedInverse).toEqual(roundedExpected);
56 | });
57 |
58 | /**
59 | *
60 | * ( ) -- ( )
61 | * | |
62 | * [ ]
63 | *
64 | * Note: two solutions for this
65 | *
66 | * const expected = [Math.PI, toRadians(-90), toRadians(-90)];
67 | * const expected = [0, toRadians(90), toRadians(90)];
68 | *
69 | * The first one is what our inverse comes up with
70 | */
71 | it('should take inverse of -1, 0, 0', () => {
72 | const inverse = inverse1_3(-1, 0, 0, robotConfig);
73 | const expected = [Math.PI, toRadians(-90), toRadians(-90)];
74 |
75 | const roundedInverse = roundArray(inverse);
76 | const roundedExpected = roundArray(expected);
77 |
78 | expect(roundedInverse).toEqual(roundedExpected);
79 | });
80 |
81 | /**
82 | * |
83 | * ( ) -- ( )
84 | * |
85 | * [ ]
86 | *
87 | *
88 | * -- ( )
89 | * |
90 | * ( )
91 | * |
92 | * [ ]
93 | *
94 | *
95 | * Note: four solutions for this
96 | *
97 | * const expected = [Math.PI, toRadians(-90), toRadians(90)];
98 | * const expected = [0, toRadians(90), toRadians(-90)];
99 | * const expected = [0, 0, toRadians(90)];
100 | * const expected = [Math.PI, 0, toRadians(-90)];
101 | *
102 | * The first one is what our inverse comes up with
103 | */
104 | it('should take inverse of -1, 0, 2', () => {
105 | const inverse = inverse1_3(-1, 0, 2, robotConfig);
106 | const expected = [Math.PI, 0, toRadians(-90)];
107 |
108 | const roundedInverse = roundArray(inverse);
109 | const roundedExpected = roundArray(expected);
110 |
111 | expect(roundedInverse).toEqual(roundedExpected);
112 | });
113 | });
114 |
--------------------------------------------------------------------------------
/src/client/providers/AppProvider.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useRef, useState } from 'react';
2 | import AppContext from '../context/AppContext';
3 | import io from 'socket.io-client';
4 | import { useGet } from '../hooks/useGet';
5 |
6 | // import IgusRebel from '../../../robots/IgusRebel.json';
7 | import Example from '../../../robots/Example.json';
8 |
9 | /**
10 | * Provide any application specific data
11 | */
12 | const AppProvider = ({ children }) => {
13 | const [colorScheme, setColorScheme] = useState('dark');
14 | const [navOpen, setNavOpen] = useState(false);
15 | const [dataOpen, setDataOpen] = useState(false);
16 | const [extraOpen, setExtraOpen] = useState(false);
17 | const [orbitEnabled, setOrbitalEnabled] = useState(true);
18 |
19 | const setBall = useRef();
20 |
21 | // Define control
22 | const control = {
23 | setBall,
24 | };
25 |
26 | const [config, setConfig] = useState(Example);
27 |
28 | // Get robot types
29 | const [{ data: robotTypes, loading: getLoading, error: getError }, getRobotTypes] = useGet();
30 |
31 | useEffect(() => {
32 | getRobotTypes({ url: `/robots/all` });
33 | }, []);
34 |
35 | const selectRobot = useCallback(
36 | ({ value }) => {
37 | console.log('SELECTING ROBOT', value);
38 | console.log('TYPES', robotTypes[value]);
39 | setConfig(robotTypes[value]);
40 | },
41 | [robotTypes],
42 | );
43 |
44 | const toggleColorScheme = () => {
45 | setColorScheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
46 | document.getElementById('app-html').classList.toggle('spectrum--light');
47 | document.getElementById('app-html').classList.toggle('spectrum--darkest');
48 | };
49 |
50 | const toggleNav = () => {
51 | setNavOpen((prev) => !prev);
52 | };
53 |
54 | const closeNav = () => {
55 | setNavOpen(false);
56 | };
57 |
58 | const toggleOrbital = () => {
59 | setOrbitalEnabled((prev) => !prev);
60 | };
61 |
62 | const toggleExtra = () => {
63 | setExtraOpen((prev) => !prev);
64 | document.getElementById('app').classList.toggle('app-extra');
65 | };
66 |
67 | const toggleData = () => {
68 | setDataOpen((prev) => !prev);
69 | };
70 |
71 | const closeData = () => {
72 | setDataOpen(false);
73 | };
74 |
75 | const orbitControl = useRef();
76 | const cameraControl = useRef();
77 |
78 | // Socket Connection
79 | const key = new URLSearchParams(window.location.search).get('key');
80 | const socketRef = useRef();
81 |
82 | useState(() => {
83 | const socket = io(`/client?key=${key}`, {
84 | transports: ['websocket'],
85 | secure: true,
86 | });
87 | socketRef.current = socket;
88 | });
89 |
90 | const value = {
91 | colorScheme,
92 | setColorScheme,
93 | toggleColorScheme,
94 | navOpen,
95 | closeNav,
96 | toggleNav,
97 | config,
98 | setConfig,
99 | orbitEnabled,
100 | toggleOrbital,
101 | control,
102 | extraOpen,
103 | toggleExtra,
104 | toggleData,
105 | dataOpen,
106 | closeData,
107 | robotTypes,
108 | selectRobot,
109 | socket: socketRef.current,
110 | orbitControl,
111 | cameraControl,
112 | key,
113 | };
114 | return {children};
115 | };
116 |
117 | export default AppProvider;
118 |
--------------------------------------------------------------------------------
/src/client/components/Pages/Builder/Builder.jsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense, useEffect, useRef } from 'react';
2 | import { useFormApi, useFormState } from 'informed';
3 | import { OrbitControls, PerspectiveCamera } from '@react-three/drei';
4 | import Grid from '../../3D/Grid';
5 | import { Canvas } from '@react-three/fiber';
6 | import { ActionButton, Flex } from '@adobe/react-spectrum';
7 | import useApp from '../../../hooks/useApp';
8 | import { Joint } from '../../3D/Joint';
9 | import { If } from '../../Shared/If';
10 | import { Info } from './Info';
11 |
12 | const Control = ({ controlRef, virtualCam }) => {
13 | const reset = () => {
14 | virtualCam.current.position.set(70, 80, 70);
15 | controlRef.current.target.set(0, 20, 0);
16 | };
17 |
18 | return (
19 | <>
20 |
21 |
22 | Reset View
23 |
24 |
25 | >
26 | );
27 | };
28 |
29 | export const Builder = () => {
30 | const controlRef = useRef();
31 | const virtualCam = useRef();
32 |
33 | const { orbitEnabled } = useApp();
34 |
35 | const { values, errors } = useFormState();
36 | const formApi = useFormApi();
37 |
38 | const frames = values?.frames || [];
39 | const frameErrors = errors?.frames || [];
40 |
41 | const position = [values?.cameraX, values?.cameraY, values?.cameraZ];
42 | const cameraZoom = values?.cameraZoom;
43 |
44 | const { mainGrid, gridSize, base } = values;
45 |
46 | // This will zoom out and re pos the camera view when we add frames
47 | useEffect(() => {
48 | if (frames && controlRef.current) {
49 | // Set new camera target
50 | controlRef.current.target.set(0, frames.length * 15, 0);
51 | // Zoom out
52 | formApi.setValue('cameraZoom', 6 / frames.length);
53 | }
54 | }, [frames, frames?.length]);
55 |
56 | return (
57 |
58 |
66 | {/*
*/}
67 |
68 |
104 |
105 |
106 | );
107 | };
108 |
--------------------------------------------------------------------------------
/src/lib/math.js:
--------------------------------------------------------------------------------
1 | // mat_inv.js
2 | // matrix inverse using Crout's decomposition
3 | // https://jamesmccaffrey.wordpress.com/2020/04/24/matrix-inverse-with-javascript/
4 |
5 | function vecMake(n, val) {
6 | let result = [];
7 | for (let i = 0; i < n; ++i) {
8 | result[i] = val;
9 | }
10 | return result;
11 | }
12 |
13 | function matMake(rows, cols, val) {
14 | let result = [];
15 | for (let i = 0; i < rows; ++i) {
16 | result[i] = [];
17 | for (let j = 0; j < cols; ++j) {
18 | result[i][j] = val;
19 | }
20 | }
21 | return result;
22 | }
23 |
24 | export function inv(m) {
25 | // assumes determinant is not 0
26 | // that is, the matrix does have an inverse
27 | let n = m.length;
28 | let result = matMake(n, n, 0.0); // make a copy
29 | for (let i = 0; i < n; ++i) {
30 | for (let j = 0; j < n; ++j) {
31 | result[i][j] = m[i][j];
32 | }
33 | }
34 |
35 | let lum = matMake(n, n, 0.0); // combined lower & upper
36 | let perm = vecMake(n, 0.0); // out parameter
37 | matDecompose(m, lum, perm); // ignore return
38 |
39 | let b = vecMake(n, 0.0);
40 | for (let i = 0; i < n; ++i) {
41 | for (let j = 0; j < n; ++j) {
42 | if (i == perm[j]) b[j] = 1.0;
43 | else b[j] = 0.0;
44 | }
45 |
46 | let x = reduce(lum, b); //
47 | for (let j = 0; j < n; ++j) result[j][i] = x[j];
48 | }
49 | return result;
50 | }
51 |
52 | function matDecompose(m, lum, perm) {
53 | // Crout's LU decomposition for matrix determinant and inverse
54 | // stores combined lower & upper in lum[][]
55 | // stores row permuations into perm[]
56 | // returns +1 or -1 according to even or odd perms
57 | // lower gets dummy 1.0s on diagonal (0.0s above)
58 | // upper gets lum values on diagonal (0.0s below)
59 |
60 | let toggle = +1; // even (+1) or odd (-1) row permutatuions
61 | let n = m.length;
62 |
63 | // make a copy of m[][] into result lum[][]
64 | //lum = matMake(n, n, 0.0);
65 | for (let i = 0; i < n; ++i) {
66 | for (let j = 0; j < n; ++j) {
67 | lum[i][j] = m[i][j];
68 | }
69 | }
70 |
71 | // make perm[]
72 | //perm = vecMake(n, 0.0);
73 | for (let i = 0; i < n; ++i) perm[i] = i;
74 |
75 | for (let j = 0; j < n - 1; ++j) {
76 | // note n-1
77 | let max = Math.abs(lum[j][j]);
78 | let piv = j;
79 |
80 | for (let i = j + 1; i < n; ++i) {
81 | // pivot index
82 | let xij = Math.abs(lum[i][j]);
83 | if (xij > max) {
84 | max = xij;
85 | piv = i;
86 | }
87 | } // i
88 |
89 | if (piv != j) {
90 | let tmp = lum[piv]; // swap rows j, piv
91 | lum[piv] = lum[j];
92 | lum[j] = tmp;
93 |
94 | let t = perm[piv]; // swap perm elements
95 | perm[piv] = perm[j];
96 | perm[j] = t;
97 |
98 | toggle = -toggle;
99 | }
100 |
101 | let xjj = lum[j][j];
102 | if (xjj != 0.0) {
103 | // TODO: fix bad compare here
104 | for (let i = j + 1; i < n; ++i) {
105 | let xij = lum[i][j] / xjj;
106 | lum[i][j] = xij;
107 | for (let k = j + 1; k < n; ++k) {
108 | lum[i][k] -= xij * lum[j][k];
109 | }
110 | }
111 | }
112 | } // j
113 |
114 | return toggle; // for determinant
115 | } // matDecompose
116 |
117 | function reduce(lum, b) {
118 | // helper
119 | let n = lum.length;
120 | let x = vecMake(n, 0.0);
121 | for (let i = 0; i < n; ++i) {
122 | x[i] = b[i];
123 | }
124 |
125 | for (let i = 1; i < n; ++i) {
126 | let sum = x[i];
127 | for (let j = 0; j < i; ++j) {
128 | sum -= lum[i][j] * x[j];
129 | }
130 | x[i] = sum;
131 | }
132 |
133 | x[n - 1] /= lum[n - 1][n - 1];
134 | for (let i = n - 2; i >= 0; --i) {
135 | let sum = x[i];
136 | for (let j = i + 1; j < n; ++j) {
137 | sum -= lum[i][j] * x[j];
138 | }
139 | x[i] = sum / lum[i][i];
140 | }
141 |
142 | return x;
143 | }
144 |
--------------------------------------------------------------------------------
/src/client/components/Data/GeneralData.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { ActionButton, Flex } from '@adobe/react-spectrum';
4 | import Copy from '@spectrum-icons/workflow/Copy';
5 | import useRobotState from '../../hooks/useRobotState';
6 | import { useFieldState } from 'informed';
7 | import { TYPE_MAPPING } from '../../constants';
8 | import { round } from '../../../lib/round';
9 |
10 | export const Info = ({ label, data, copy }) => {
11 | return (
12 |
13 |
14 | {label}
15 |
16 |
17 | {data}
18 | {copy ? (
19 |
29 | ) : null}
30 |
31 |
32 | );
33 | };
34 |
35 | const Joints = ({ motors, robotType }) => {
36 | const copyText = motors
37 | .map((motor) => {
38 | const fieldName = TYPE_MAPPING[robotType].position;
39 | const motorPos = motor[fieldName];
40 | return round(motorPos, 1000);
41 | })
42 | .join(', ');
43 |
44 | const displayText = motors
45 | .map((motor) => {
46 | const fieldName = TYPE_MAPPING[robotType].position;
47 | const motorPos = motor[fieldName];
48 | return round(motorPos, 10);
49 | })
50 | .join('\u00A0\u00A0\u00A0\u00A0');
51 |
52 | return ;
53 | };
54 |
55 | const ToolCenterPointPosition = ({ robotState, robotType }) => {
56 | const fieldName = TYPE_MAPPING[robotType].tcpPose;
57 |
58 | // Array x, y, z, r1, r2, r3
59 | const tcpPos = robotState[fieldName];
60 |
61 | // Dont try to render if we dont have it
62 | if (!tcpPos) {
63 | return null;
64 | }
65 |
66 | const copyText = tcpPos.map((p) => round(p, 1000)).join(', ');
67 |
68 | const displayText = tcpPos.map((p) => round(p, 100)).join('\u00A0\u00A0\u00A0\u00A0');
69 |
70 | return ;
71 | };
72 |
73 | const TCPWrench = ({ robotState, robotType }) => {
74 | const fieldName = TYPE_MAPPING[robotType].extWrenchInTcp;
75 |
76 | // Array x, y, z, r1, r2, r3
77 | const extWrenchInTcp = robotState[fieldName];
78 |
79 | // Dont try to render if we dont have it
80 | if (!extWrenchInTcp) {
81 | return null;
82 | }
83 |
84 | const copyText = extWrenchInTcp.map((p) => round(p, 1000)).join(', ');
85 |
86 | const displayText = extWrenchInTcp.map((p) => round(p, 100)).join('\u00A0\u00A0\u00A0\u00A0');
87 |
88 | return ;
89 | };
90 |
91 | export const GeneralData = () => {
92 | const { robotStates } = useRobotState();
93 |
94 | // Get value of robotId && motorId
95 | const { value: robotId } = useFieldState('robotId');
96 |
97 | // Get the robot type
98 | const { value: robotType } = useFieldState('robotType');
99 |
100 | // Get the selected robot state
101 | const robotState = robotStates[robotId];
102 |
103 | if (!robotState) {
104 | return null;
105 | }
106 |
107 | // console.log('RENDER GENERAL DATA');
108 |
109 | const motors = Object.values(robotState.motors);
110 |
111 | return (
112 | <>
113 |
119 |
120 |
121 |
122 |
123 | >
124 | );
125 | };
126 |
--------------------------------------------------------------------------------
/src/server/setup.js:
--------------------------------------------------------------------------------
1 | import winston from 'winston';
2 | import { Controller } from './robot/controller.js';
3 | import { Server } from 'socket.io';
4 |
5 | /**------------------------------------------------------------------
6 | * Setup function for global variables
7 | */
8 | const setupGlobals = () => {
9 | global.myapp = {};
10 | };
11 |
12 | /**------------------------------------------------------------------
13 | * Setup logger
14 | */
15 | const setupLogger = () => {
16 | const { format } = winston;
17 | const { combine, timestamp, json, simple, metadata } = format;
18 |
19 | const myFormat = format.metadata({ fillExcept: ['message', 'level', 'timestamp'] });
20 |
21 | // Winston transports
22 | const transports = [
23 | new winston.transports.Console({
24 | level: process.env.LOG_LEVEL || 'info',
25 | }),
26 | // Add other transports here
27 | /*
28 | * See:
29 | * https://github.com/winstonjs/winston/blob/master/docs/transports.md
30 | */
31 | ];
32 |
33 | const formater = process.env.NODE_ENV === 'development' ? simple() : combine(timestamp(), json());
34 |
35 | winston.configure({
36 | transports,
37 | format: formater,
38 | // Prevent winston from exeting on uncaught error
39 | exitOnError: false,
40 | });
41 | };
42 |
43 | /**------------------------------------------------------------------
44 | * Setup io server
45 | */
46 | const setupIo = () => {
47 | const io = new Server({ pingInterval: 2000, pingTimeout: 5000 });
48 | return io;
49 | };
50 |
51 | /**------------------------------------------------------------------
52 | * Setup Controller
53 | */
54 | const setupController = (io) => {
55 | const controller = new Controller({ io });
56 | return controller;
57 | };
58 |
59 | /**------------------------------------------------------------------
60 | * Setup cors
61 | *
62 | * origin - ["http://example1.com", /\.example2\.com$/] will accept any request
63 | * from "http://example1.com" or from a subdomain of "example2.com".
64 | */
65 | const setupCors = () => {
66 | if (process.env.NODE_ENV === 'spec') {
67 | return {};
68 | }
69 | let whitelist = [];
70 | if (process.env.NODE_ENV === 'development') {
71 | whitelist = ['http://localhost:3000'];
72 | }
73 | if (process.env.NODE_ENV === 'dev') {
74 | whitelist = ['http://localhost:3000', 'https://dev.myapp.com'];
75 | }
76 | if (process.env.NODE_ENV === 'production') {
77 | whitelist = ['https://myapp.com', 'https://robot-viewer.com', 'https://www.joepuzzo.com'];
78 | }
79 | return {
80 | origin(origin, callback) {
81 | if (whitelist.indexOf(origin) !== -1 || !origin) {
82 | callback(null, true);
83 | } else {
84 | callback(new Error('Not allowed by CORS'));
85 | }
86 | },
87 | };
88 | };
89 |
90 | /**------------------------------------------------------------------
91 | * All setup function are called here, results can be added to the
92 | * configuration object
93 | */
94 |
95 | const setup = async () => {
96 | const configuration = {
97 | PORT: 3000,
98 | };
99 | // ---- Always add globals first ----
100 | setupGlobals();
101 |
102 | // ---- Do any async setup here ----
103 | // await setupDB();
104 |
105 | // ---- Now for the syncrounous stuff ;) ----
106 | setupLogger();
107 | configuration.corsConfig = setupCors();
108 | configuration.io = setupIo();
109 | configuration.controller = setupController(configuration.io);
110 |
111 | // ---- Put stuff onto global object ----
112 | global.myapp.configuration = configuration;
113 |
114 | // Give the configuration object to the caller ( will be used to build epxress app )
115 | return configuration;
116 | };
117 |
118 | const setupSpec = () => {
119 | const configuration = { PORT: 3000 };
120 | setupLogger();
121 | return configuration;
122 | };
123 |
124 | export default process.env.NODE_ENV === 'spec' ? setupSpec : setup;
125 |
--------------------------------------------------------------------------------
/src/client/components/Nav/MotorNav.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useMemo, useRef } from 'react';
2 | import { ActionButton, Flex } from '@adobe/react-spectrum';
3 | import Home from '@spectrum-icons/workflow/Home';
4 | import ChevronRight from '@spectrum-icons/workflow/ChevronRight';
5 | import useApp from '../../hooks/useApp';
6 | import Select from '../Informed/Select';
7 | import { Debug, useFieldState, useFormApi } from 'informed';
8 | import useRobotMeta from '../../hooks/useRobotMeta';
9 | import Switch from '../Informed/Switch';
10 | import useRobotController from '../../hooks/useRobotController';
11 | import { RobotType } from '../Shared/RobotType';
12 |
13 | export const MotorNav = () => {
14 | console.log('RENDER MOTOR NAV');
15 |
16 | // Get controls for nav
17 | const { extraOpen, toggleExtra } = useApp();
18 |
19 | // Get robot state
20 | const { robotOptions, robots, connected } = useRobotMeta();
21 |
22 | // Get robot control
23 | const { updateConfig } = useRobotController();
24 |
25 | // Get value of robotId
26 | const { value: robotId } = useFieldState('robotId');
27 |
28 | // Form api to manipulate form
29 | const formApi = useFormApi();
30 |
31 | // Ref to use in functions for if robot is connected
32 | const connectedRef = useRef();
33 | connectedRef.current = connected;
34 |
35 | // Build options for motor select
36 | const motorOptions = useMemo(() => {
37 | const selectedRobot = robots[robotId];
38 | if (selectedRobot && selectedRobot.motors) {
39 | return Object.values(selectedRobot.motors).map((motor) => {
40 | return {
41 | value: motor.id,
42 | label: `Motor-${motor.id}`,
43 | };
44 | });
45 | }
46 | return [];
47 | }, [robotId, robots]);
48 |
49 | const homeRobot = () => {};
50 |
51 | const onAccelChange = useCallback(({ value }) => {
52 | const motorId = formApi.getValue('motorId');
53 | // only send if we are connected and have selected motor
54 | if (connectedRef.current && motorId != 'na') {
55 | updateConfig(`${motorId}.accelEnabled`, value);
56 | }
57 | }, []);
58 |
59 | const onAdmittanceChange = useCallback(({ value }) => {
60 | const motorId = formApi.getValue('motorId');
61 | // only send if we are connected and have selected motor
62 | if (connectedRef.current && motorId != 'na') {
63 | updateConfig(`${motorId}.admittanceEnabled`, value);
64 | }
65 | }, []);
66 |
67 | onAdmittanceChange;
68 |
69 | return (
70 | <>
71 |
72 | Motor Control
73 | toggleExtra()}>
74 |
75 |
76 |
77 |
78 |
79 |
80 |
87 |
88 |
95 |
96 |
97 |
98 |
99 |
105 |
106 |
112 |
113 | {/* */}
114 |
115 |
116 |
117 |
118 | >
119 | );
120 | };
121 |
--------------------------------------------------------------------------------
/src/client/components/Pages/Cookbook/Recipe.jsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback, useEffect, useMemo, useRef } from 'react';
2 | import { ActionButton, Flex, ProgressCircle } from '@adobe/react-spectrum';
3 | import Select from '../../Informed/Select';
4 | import Switch from '../../Informed/Switch';
5 | import { ArrayField, Debug, Relevant, useArrayFieldState, useFormApi } from 'informed';
6 | import useSimulateController from '../../../hooks/useSimulateController';
7 | import useSimulateState from '../../../hooks/useSimulateState';
8 | import { usetPost } from '../../../hooks/usePost';
9 | import { useGet } from '../../../hooks/useGet';
10 |
11 | const ArrayButtons = ({ index, add, remove, isDisabled }) => {
12 | const { fields } = useArrayFieldState();
13 |
14 | if (index === fields.length - 1) {
15 | return (
16 | {
18 | add();
19 | }}
20 | type="button"
21 | minWidth={40}
22 | title="Add Action"
23 | aria-label="Add Action"
24 | isDisabled={isDisabled}
25 | >
26 | +
27 |
28 | );
29 | }
30 |
31 | return (
32 |
40 | -
41 |
42 | );
43 | };
44 |
45 | export const Recipe = ({ recipe, allActions, getRecipes }) => {
46 | const { play } = useSimulateController();
47 | const { simulating } = useSimulateState();
48 | const formApi = useFormApi();
49 |
50 | const arrayFieldApiRef = useRef();
51 |
52 | useEffect(() => {
53 | arrayFieldApiRef.current.reset();
54 | }, [recipe]);
55 |
56 | const [{ error: postError, loading: postLoading }, postRecipe] = usetPost({
57 | headers: { ContentType: 'application/json' },
58 | onComplete: () => {
59 | getRecipes({ url: `/recipes/all` });
60 | },
61 | });
62 |
63 | const [{ data, loading: getLoading, error: getError }, getWaypoints] = useGet();
64 |
65 | const loading = postLoading || getLoading;
66 | const error = postError || getError;
67 |
68 | const save = useCallback(() => {
69 | const { values } = formApi.getFormState();
70 |
71 | const { recipes, recipeName, selectedRecipe } = values;
72 |
73 | const name = selectedRecipe || recipeName;
74 |
75 | postRecipe({ payload: recipes, url: `/recipes/save/${name}` });
76 | }, []);
77 |
78 | const load = useCallback(() => {
79 | const { values } = formApi.getFormState();
80 |
81 | const { recipeName } = values;
82 |
83 | getWaypoints({ url: `/recipes/load/${recipeName}` });
84 | }, []);
85 |
86 | return (
87 |
88 |
89 |
90 | Run Recipe
91 |
92 |
93 | Save Recipe
94 |
95 |
96 |
97 |
102 | {({ add }) => {
103 | return (
104 |
105 |
106 | {({ remove, name, index }) => (
107 |
108 |
109 |
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 |
--------------------------------------------------------------------------------