Skip to content

Commit

Permalink
Arm presets config page (#163)
Browse files Browse the repository at this point in the history
  • Loading branch information
GLDuval committed Mar 6, 2023
1 parent af94bff commit ec02891
Show file tree
Hide file tree
Showing 11 changed files with 412 additions and 13 deletions.
16 changes: 11 additions & 5 deletions src/renderer/components/StatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { BiWifi, BiWifi0, BiWifi1, BiWifi2, BiWifiOff } from 'react-icons/bi';
import { useDispatch } from 'react-redux';
import { toast } from 'react-toastify';
import { ArmContext, armService } from '../state/arm';
import { selectSelectedPreset } from '../store/modules/armPresets';

export const StatusBar: FC = () => (
<StyledStatusBarWrapper>
Expand Down Expand Up @@ -73,6 +74,8 @@ const ModeInfo = () => {
const [arm] = useActor(armService);
const [control] = useActor(controlService);
const isReverse = useSelector(selectReverse);
const selectedArmPreset = useSelector(selectSelectedPreset);

if (control.matches('flipper')) {
return (
<div>
Expand All @@ -87,11 +90,14 @@ const ModeInfo = () => {
);
} else if (control.matches('arm')) {
return (
<div>
{arm.matches('joint')
? 'JOINT ' + String((arm.context as ArmContext).jointValue + 1)
: 'CARTESIAN'}
</div>
<>
<div>
{arm.matches('joint')
? 'JOINT ' + String((arm.context as ArmContext).jointValue + 1)
: 'CARTESIAN'}
</div>
<div>{selectedArmPreset.name.toUpperCase()}</div>
</>
);
} else {
return <div />;
Expand Down
3 changes: 3 additions & 0 deletions src/renderer/components/pages/Config/ConfigMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export const ConfigMenu: FC = () => {
<li>
<StyledNavLink to="/config/graph">Graph</StyledNavLink>
</li>
<li>
<StyledNavLink to="/config/armPresets">Arm Presets</StyledNavLink>
</li>
<li>
<StyledNavLink to="/config/gamepad">Gamepad</StyledNavLink>
</li>
Expand Down
2 changes: 2 additions & 0 deletions src/renderer/components/pages/Config/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GamepadConfig } from '@/renderer/components/pages/Config/pages/GamepadC
import { GraphConfig } from '@/renderer/components/pages/Config/pages/GraphConfig/GraphConfig';
import { styled } from '@/renderer/globalStyles/styled';
import { LaunchConfig } from './pages/LaunchConfig/LaunchConfig';
import ArmPresetsConfig from './pages/ArmPresetsConfig/ArmPresetsConfig';

export const ConfigPage: FC = () => {
return (
Expand All @@ -20,6 +21,7 @@ export const ConfigPage: FC = () => {
<Route path="/general" element={<GeneralConfig />} />
<Route path="/camera" element={<CameraConfig />} />
<Route path="/graph" element={<GraphConfig />} />
<Route path="/armPresets" element={<ArmPresetsConfig />} />
<Route path="/gamepad" element={<GamepadConfig />} />
<Route path="/launch" element={<LaunchConfig />} />
</Routes>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { LabeledInput } from '@/renderer/components/common/LabeledInput';
import React, { useCallback, useState } from 'react';

interface ArmJointInputProps {
onChange: (event: React.ChangeEvent<HTMLInputElement>, id: number) => void;
value: number;
id: number;
min: number;
max: number;
}

const ArmJointInput = ({
onChange,
value,
id,
min,
max,
}: ArmJointInputProps) => {
const [inputValue, setInputValue] = useState(value.toString());
const [error, setError] = useState(false);

const onInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const jointValue = parseInt(event.target.value);
setInputValue(event.target.value);
if (jointValue >= min && jointValue <= max) {
onChange(event, id);
setError(false);
} else {
setError(true);
}
},
[id, max, min, onChange]
);

return (
<div>
<LabeledInput
label={`Joint ${id + 1}`}
value={inputValue}
type="number"
onChange={onInputChange}
/>
{error && (
<div>
Value must be between {min} and {max}
</div>
)}
</div>
);
};

export default ArmJointInput;
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import React, { useCallback } from 'react';
import {
ArmPreset,
armPresetsSlice,
} from '@/renderer/store/modules/armPresets';
import { styled } from '@/renderer/globalStyles/styled';
import { LabeledInput } from '@/renderer/components/common/LabeledInput';
import { useDispatch } from 'react-redux';
import { FaTimes } from 'react-icons/fa';
import ArmJointInput from './ArmJointInput';

interface ArmPresetProps {
preset: ArmPreset;
}

//Temporary UI limit for each joint value.
interface JointBoundaries {
min: number;
max: number;
}

const jointBoundaries: JointBoundaries[] = [
{ min: 0, max: 360 }, //Joint 1
{ min: 100, max: 260 }, //Joint 2
{ min: 95, max: 260 }, //Joint 3
{ min: 0, max: 360 }, //Joint 4
{ min: 100, max: 250 }, //Joint 5
{ min: 0, max: 360 }, //Joint 6
];

const Card = styled.div`
background-color: ${({ theme }) => theme.colors.darkerBackground};
border-radius: 4px;
padding: 16px;
margin: 8px;
display: flex;
min-width: 300px;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5);
transition: box-shadow 0.2s ease-in-out;
`;

const HeaderRow = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
width: 100%;
`;

const DeleteButton = styled.a`
border: none;
border-radius: 4px;
padding: 4px 8px;
margin-top: 8px;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
&:hover {
color: ${({ theme }) => theme.colors.danger};
}
`;

const ArmPreset = ({ preset }: ArmPresetProps) => {
const dispatch = useDispatch();

const onJointChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>, id: number) => {
const jointValue = parseInt(event.target.value);
// Update the preset in the store with the new joint value.
dispatch(
armPresetsSlice.actions.updatePreset({
...preset,
positions: [
...preset.positions.slice(0, id),
jointValue,
...preset.positions.slice(id + 1),
],
})
);
},
[dispatch, preset]
);

const onNameChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
// Update the preset in the store with the new name.
dispatch(
armPresetsSlice.actions.updatePreset({
...preset,
name: event.target.value,
})
);
},
[dispatch, preset]
);

const onDelete = useCallback(
(id: string) => {
// Delete the preset from the store.
dispatch(armPresetsSlice.actions.removePreset(id));
},
[dispatch]
);

return (
<Card>
<HeaderRow>
<h3>{preset.name}</h3>
<DeleteButton onClick={() => onDelete(preset.id)}>
<FaTimes />
</DeleteButton>
</HeaderRow>
<LabeledInput
label="Preset Name"
value={preset.name}
type="text"
onChange={onNameChange}
/>
{preset.positions.map((joint, index) => (
<div key={index}>
<ArmJointInput
value={joint}
id={index}
onChange={onJointChange}
min={jointBoundaries[index].min}
max={jointBoundaries[index].max}
/>
</div>
))}
</Card>
);
};

export default ArmPreset;
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Button } from '@/renderer/components/common/Button';
import { Select } from '@/renderer/components/common/Select';
import { styled } from '@/renderer/globalStyles/styled';
import useArmJointPositions from '@/renderer/hooks/useArmJointPositions';
import {
armPresetsSlice,
selectAllPresets,
selectSelectedPreset,
} from '@/renderer/store/modules/armPresets';
import { round } from 'lodash';
import { nanoid } from 'nanoid';
import React, { useCallback } from 'react';
import { FaPlus } from 'react-icons/fa';
import { useDispatch, useSelector } from 'react-redux';
import { SectionTitle } from '../../styles';
import ArmPreset from './ArmPreset';

const PresetsContainer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: flex-start;
width: 100%;
flex-wrap: wrap;
`;

const ArmPresetsConfig = () => {
const armPresets = useSelector(selectAllPresets);
const selectedPreset = useSelector(selectSelectedPreset);
const dispatch = useDispatch();
const jointPositions = useArmJointPositions() ?? [];

const addPreset = useCallback(() => {
dispatch(
armPresetsSlice.actions.addPreset({
id: nanoid(),
name: 'New Preset',
positions: [180, 180, 180, 180, 180, 180],
})
);
}, [dispatch]);

const onPresetSelect = useCallback(
(e: React.ChangeEvent<HTMLSelectElement>) => {
dispatch(armPresetsSlice.actions.selectPreset(e.target.value));
},
[dispatch]
);

return (
armPresets &&
selectedPreset && (
<>
<SectionTitle>Selected Preset</SectionTitle>
<Select
options={armPresets.map((preset) => ({
key: preset.id,
content: preset.name,
value: preset.id,
}))}
value={selectedPreset.id}
onChange={onPresetSelect}
/>
{jointPositions.length > 0 && (
<>
<SectionTitle>Current positions</SectionTitle>
<div>
{jointPositions.map((position, index) => (
<div key={index}>
Joint {index + 1}: {round(position, 0)}
</div>
))}
</div>
</>
)}

<SectionTitle>Presets</SectionTitle>
<Button onClick={addPreset}>
<FaPlus />
<span style={{ marginLeft: '0.2rem' }}>Add Preset</span>
</Button>
<PresetsContainer>
{armPresets.map((preset) => (
<ArmPreset key={preset.id} preset={preset} />
))}
</PresetsContainer>
</>
)
);
};

export default ArmPresetsConfig;
27 changes: 27 additions & 0 deletions src/renderer/hooks/useArmJointPositions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useCallback, useState } from 'react';
import { TopicOptions } from '../utils/ros/roslib-ts-client/@types';
import { useRosSubscribeNoData } from './useRosSubscribe';

const jointPositionTopic: TopicOptions = {
name: 'ovis/arm/out/joint_position',
messageType: 'ovis_msgs/OvisJointPosition',
};

interface JointPositionMsg {
joint_positions: number[];
}

const useArmJointPositions = () => {
const [jointPositions, setJointPositions] = useState<number[]>();

useRosSubscribeNoData<JointPositionMsg>(
jointPositionTopic,
useCallback((message) => {
setJointPositions(message.joint_positions);
}, [])
);

return jointPositions;
};

export default useArmJointPositions;
Loading

0 comments on commit ec02891

Please sign in to comment.