Skip to content

Commit

Permalink
Add GPIO control buttons to turn on and off the lights (#172)
Browse files Browse the repository at this point in the history
* Start gpio storage

* Start Gpio config visual

* Add updatePin function

* Continue Gpio pin visual

* Add custom icon button

* Subscribe to topic state

* Fix topic names and message types

* Remove subscriber

* Update pin names

* Fix confusing type names
  • Loading branch information
GLDuval authored May 18, 2023
1 parent 8773491 commit 683e230
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/renderer/components/common/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const StyledButton = styled.button<StyledButtonProps>`
${({ theme }) => theme.colors.background},
${({ theme }) => theme.colors.darkerBackground}
);
border-radius: 4px;
&:hover {
background-image: linear-gradient(
Expand Down
43 changes: 43 additions & 0 deletions src/renderer/components/common/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { styled } from '@/renderer/globalStyles/styled';
import React, { ReactNode } from 'react';

interface IconButtonProps {
icon: ReactNode;
onClick: () => void;
title?: string;
style?: React.CSSProperties;
}

const IconContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
`;

const Button = styled.button`
background-color: ${({ theme }) => theme.colors.darkerBackground};
border-radius: 4px;
padding: 6px;
color: white;
border: none;
background: linear-gradient(#0000, rgb(0 0 0/30%)) top/100% 800%;
transition: background-position 0.2s ease-in-out;
&:hover {
background-position: bottom;
}
`;

const IconButton = ({ icon, onClick, title, style }: IconButtonProps) => {
return (
<Button style={style} onClick={onClick}>
<IconContainer>
{icon}
{title && <span style={{ marginLeft: '4px' }}>{title}</span>}
</IconContainer>
</Button>
);
};

export default IconButton;
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 @@ -19,6 +19,9 @@ export const ConfigMenu: FC = () => {
<li>
<StyledNavLink to="/config/armPresets">Arm Presets</StyledNavLink>
</li>
<li>
<StyledNavLink to="/config/gpioPins">GPIO Control</StyledNavLink>
</li>
<li>
<StyledNavLink to="/config/gamepad">Gamepad</StyledNavLink>
</li>
Expand Down
6 changes: 4 additions & 2 deletions src/renderer/components/pages/Config/ConfigPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { CameraConfig } from '@/renderer/components/pages/Config/pages/CameraCon
import { GamepadConfig } from '@/renderer/components/pages/Config/pages/GamepadConfig';
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';
import { LaunchConfig } from '@/renderer/components/pages/Config/pages/LaunchConfig/LaunchConfig';
import ArmPresetsConfig from '@/renderer/components/pages/Config/pages/ArmPresetsConfig/ArmPresetsConfig';
import { GpioPinsConfig } from '@/renderer/components/pages/Config/pages/GpioPinsConfig/GpioPinsConfig';

export const ConfigPage: FC = () => {
return (
Expand All @@ -22,6 +23,7 @@ export const ConfigPage: FC = () => {
<Route path="/camera" element={<CameraConfig />} />
<Route path="/graph" element={<GraphConfig />} />
<Route path="/armPresets" element={<ArmPresetsConfig />} />
<Route path="/gpioPins" element={<GpioPinsConfig />} />
<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,59 @@
import IconButton from '@/renderer/components/common/IconButton';
import { styled } from '@/renderer/globalStyles/styled';
import { GpioPinState, gpioPinsSlice } from '@/renderer/store/modules/gpioPins';
import { rosClient } from '@/renderer/utils/ros/rosClient';
import { TopicOptions } from '@/renderer/utils/ros/roslib-ts-client/@types';
import React, { useMemo } from 'react';
import { FaPowerOff } from 'react-icons/fa';
import { useDispatch } from 'react-redux';

interface GpioPinProps {
gpioPin: GpioPinState;
}

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

export const GpioPin = ({ gpioPin }: GpioPinProps) => {
const dispatch = useDispatch();

const topic: TopicOptions = useMemo(
() => ({
name: gpioPin.topicName,
messageType: 'std_msgs/UInt16',
}),
[gpioPin.topicName]
);

const togglePin = () => {
rosClient.publish(topic, { data: gpioPin.isOn ? 0 : 1 });

dispatch(gpioPinsSlice.actions.togglePin(gpioPin.id));
};

return (
<Card>
<h3 style={{ paddingBottom: '4px' }}>{gpioPin.name}</h3>
<IconButton
icon={<FaPowerOff />}
title={gpioPin.isOn ? 'Power Off' : 'Power On'}
onClick={togglePin}
style={
gpioPin.isOn
? { backgroundColor: 'red' }
: { backgroundColor: 'green' }
}
/>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';
import { SectionTitle } from '../../styles';
import { useSelector } from 'react-redux';
import { selectAllGpioPins } from '@/renderer/store/modules/gpioPins';
import { GpioPin } from './GpioPin';
import { styled } from '@/renderer/globalStyles/styled';

const Container = styled.div`
display: flex;
flex-direction: row;
align-items: flex-start;
`;

export const GpioPinsConfig = () => {
const gpioPins = useSelector(selectAllGpioPins);
return (
<>
<SectionTitle>GPIO Control</SectionTitle>
<Container>
{gpioPins.map((gpioPin) => (
<GpioPin key={gpioPin.id} gpioPin={gpioPin} />
))}
</Container>
</>
);
};
2 changes: 2 additions & 0 deletions src/renderer/store/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { initialState as inputState } from '@/renderer/store/modules/input';
import { initialState as debugTabState } from '@/renderer/store/modules/debugTab';
import { initialState as launchFilesState } from '@/renderer/store/modules/launchFiles';
import { initialState as armPresetsState } from '@/renderer/store/modules/armPresets';
import { initialState as gpioPinsState } from '@/renderer/store/modules/gpioPins';
import { log } from '@/renderer/logger';

export const defaultState: GlobalState = {
Expand All @@ -15,6 +16,7 @@ export const defaultState: GlobalState = {
debugTab: debugTabState,
launchFiles: launchFilesState,
armPresets: armPresetsState,
gpioPins: gpioPinsState,
};

// WARN
Expand Down
68 changes: 68 additions & 0 deletions src/renderer/store/modules/gpioPins.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { GlobalState } from '@/renderer/store/store';
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { nanoid } from 'nanoid';

export interface GpioPinState {
id: string;
name: string;
topicName: string;
isOn: boolean;
}

export const initialState: GpioPinState[] = [
{
id: nanoid(),
name: 'FRONT LED',
topicName: '/DOP1',
isOn: false,
},
{
id: nanoid(),
name: 'BACK LED',
topicName: '/DOP2',
isOn: false,
},
{
id: nanoid(),
name: 'ARM LED',
topicName: '/DOP3',
isOn: false,
},
];

export const gpioPinsSlice = createSlice({
name: 'gpioPins',
initialState,
reducers: {
togglePin: (state, { payload }: PayloadAction<string>) => {
const element = state.find((element) => element.id === payload);
if (element) {
element.isOn = !element.isOn;
}
},
addPin: (state, { payload }: PayloadAction<GpioPinState>) => {
state.push(payload);
},
updatePin: (state, { payload }: PayloadAction<GpioPinState>) => {
const element = state.find((element) => element.id === payload.id);
if (element) {
element.name = payload.name;
element.topicName = payload.topicName;
}
},
updateIsOn: (
state,
{ payload }: PayloadAction<{ id: string; isOn: boolean }>
) => {
const element = state.find((element) => element.id === payload.id);
if (element) {
element.isOn = payload.isOn;
}
},
removePin: (state, { payload }: PayloadAction<GpioPinState>) => {
state = state.filter((pin) => pin.id !== payload.id);
},
},
});

export const selectAllGpioPins = (state: GlobalState) => state.gpioPins;
2 changes: 2 additions & 0 deletions src/renderer/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { configureStore, AnyAction, combineReducers } from '@reduxjs/toolkit';
import { throttle } from 'lodash';
import { launchFilesSlice } from './modules/launchFiles';
import { armPresetsSlice } from './modules/armPresets';
import { gpioPinsSlice } from './modules/gpioPins';

const appReducer = combineReducers({
feed: feedSlice.reducer,
Expand All @@ -19,6 +20,7 @@ const appReducer = combineReducers({
debugTab: debugTabSlice.reducer,
launchFiles: launchFilesSlice.reducer,
armPresets: armPresetsSlice.reducer,
gpioPins: gpioPinsSlice.reducer,
});

export type GlobalState = ReturnType<typeof appReducer>;
Expand Down

0 comments on commit 683e230

Please sign in to comment.