(({ server, onDeviceClick }) => {
56 | // to properly update on location change.
57 | useLocation()
58 |
59 | const sortedDevices = useMemo(() => {
60 | if (server.connectionStatus === ServerConnectionStatus.Connected) {
61 | return sortBy(Object.values(server.devices), d => d.configuration.name)
62 | }
63 | }, [server])
64 |
65 | return (
66 |
67 |
68 |
69 | {server.address}
70 |
71 |
72 | {server.connectionStatus === ServerConnectionStatus.Connected ? (
73 | sortedDevices && sortedDevices.length === 0 ? (
74 | No devices connected to server!
75 | ) : (
76 | sortedDevices &&
77 | sortedDevices.map(device => (
78 |
83 | {device.configuration.name}
84 |
85 | ))
86 | )
87 | ) : (
88 | Not connected to server!
89 | )}
90 |
91 | )
92 | })
93 |
94 | export default MenuServer
95 |
--------------------------------------------------------------------------------
/client/src/views/DeviceView/calibration/CalibrationSlider.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { faCheck } from '@fortawesome/free-solid-svg-icons'
4 |
5 | import scale from '../../../utils/scale'
6 | import IconButton from '../../../components/IconButton'
7 | import { basicText } from '../../../components/Typography'
8 |
9 | interface Props {
10 | onCalibrate: (calibrationBuffer: number) => void
11 | }
12 |
13 | const Container = styled.div`
14 | align-items: center;
15 | border-radius: 999px;
16 | display: flex;
17 | width: 100%;
18 | `
19 |
20 | const Slider = styled.input`
21 | flex: 1;
22 | height: ${scale(3)};
23 | appearance: none;
24 | width: 100%;
25 | border: none;
26 | padding: 0 ${scale(1)};
27 | outline: none;
28 |
29 | ::-webkit-slider-runnable-track {
30 | appearance: none;
31 | width: 100%;
32 | height: ${scale(1)};
33 | background-color: rgba(255, 255, 255, 0.33);
34 | border-radius: 999px;
35 | }
36 |
37 | ::-webkit-slider-thumb {
38 | appearance: none;
39 | height: ${scale(3)};
40 | width: ${scale(3)};
41 | margin-top: ${scale(-1)};
42 | border-radius: 100%;
43 | background-color: white;
44 | border: none;
45 | box-shadow: 0 0 4px rgba(0, 0, 0, 0.25);
46 | }
47 | `
48 |
49 | const CalibrationBuffer = styled.div`
50 | ${basicText};
51 | font-weight: bold;
52 | color: white;
53 | width: ${scale(4.5)};
54 | margin-left: ${scale(0.5)};
55 | line-height: 1;
56 | text-align: center;
57 | `
58 |
59 | const CheckButton = styled(IconButton).attrs({
60 | size: scale(2),
61 | icon: faCheck,
62 | color: 'white'
63 | })`
64 | border: ${scale(0.25)} solid white;
65 | border-radius: 100%;
66 | padding: ${scale(0.75)};
67 | margin: 0 ${scale(1)};
68 | `
69 |
70 | const CalibrationSlider = React.memo(({ onCalibrate }) => {
71 | const [sliderValue, setSliderValue] = React.useState(0.05)
72 |
73 | const handleSliderValueChange = React.useCallback(
74 | (e: React.ChangeEvent) => {
75 | setSliderValue(parseFloat(e.target.value))
76 | },
77 | []
78 | )
79 |
80 | const handleCheckClick = React.useCallback(() => {
81 | onCalibrate(sliderValue)
82 | }, [onCalibrate, sliderValue])
83 |
84 | return (
85 |
86 | {(sliderValue * 100).toFixed()}%
87 |
95 |
96 |
97 | )
98 | })
99 |
100 | export default CalibrationSlider
101 |
--------------------------------------------------------------------------------
/firmware/teensy2/AnalogDancePad.h:
--------------------------------------------------------------------------------
1 | /*
2 | Based on LUFA Library example code (www.lufa-lib.org):
3 |
4 | Copyright 2017 Dean Camera (dean [at] fourwalledcubicle [dot] com)
5 |
6 | See other copyrights in LICENSE file on repository root.
7 |
8 | Permission to use, copy, modify, distribute, and sell this
9 | software and its documentation for any purpose is hereby granted
10 | without fee, provided that the above copyright notice appear in
11 | all copies and that both that the copyright notice and this
12 | permission notice and warranty disclaimer appear in supporting
13 | documentation, and that the name of the author not be used in
14 | advertising or publicity pertaining to distribution of the
15 | software without specific, written prior permission.
16 |
17 | The author disclaims all warranties with regard to this
18 | software, including all implied warranties of merchantability
19 | and fitness. In no event shall the author be liable for any
20 | special, indirect or consequential damages or any damages
21 | whatsoever resulting from loss of use, data or profits, whether
22 | in an action of contract, negligence or other tortious action,
23 | arising out of or in connection with the use or performance of
24 | this software.
25 | */
26 |
27 | #ifndef _ANALOG_DANCE_PAD_H_
28 | #define _ANALOG_DANCE_PAD_H_
29 | #include
30 | #include
31 | #include
32 | #include
33 | #include
34 |
35 | #include "Descriptors.h"
36 |
37 | #include
38 | #include
39 | #include
40 |
41 | void SetupHardware(void);
42 |
43 | void EVENT_USB_Device_Connect(void);
44 | void EVENT_USB_Device_Disconnect(void);
45 | void EVENT_USB_Device_ConfigurationChanged(void);
46 | void EVENT_USB_Device_ControlRequest(void);
47 | void EVENT_USB_Device_StartOfFrame(void);
48 |
49 | bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
50 | uint8_t* const ReportID,
51 | const uint8_t ReportType,
52 | void* ReportData,
53 | uint16_t* const ReportSize);
54 | void CALLBACK_HID_Device_ProcessHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
55 | const uint8_t ReportID,
56 | const uint8_t ReportType,
57 | const void* ReportData,
58 | const uint16_t ReportSize);
59 | #endif
60 |
61 |
--------------------------------------------------------------------------------
/client/src/views/DeviceView/calibration/Calibration.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { animated, useSpring } from 'react-spring'
4 |
5 | import scale from '../../../utils/scale'
6 | import config from '../../../config'
7 | import CalibrationSlider from './CalibrationSlider'
8 | import CalibrationButton from './CalibrationButton'
9 | import { largeText } from '../../../components/Typography'
10 |
11 | const CALIBRATION_BACKDROP_ZINDEX = 10
12 |
13 | const CalibrationBackDrop = styled(animated.div)`
14 | position: fixed;
15 | top: 0;
16 | bottom: 0;
17 | left: 0;
18 | right: 0;
19 | z-index: ${CALIBRATION_BACKDROP_ZINDEX};
20 | background: linear-gradient(
21 | to bottom,
22 | rgba(0, 0, 0, 0.95),
23 | rgba(0, 0, 0, 0.3)
24 | );
25 | will-change: opacity, transform;
26 | `
27 |
28 | const CalibrationContainer = styled(animated.div)`
29 | left: 0;
30 | padding: ${scale(2)} ${scale(2)};
31 | pointer-events: none;
32 | position: fixed;
33 | right: 0;
34 | top: 0;
35 | will-change: opacity;
36 | z-index: ${CALIBRATION_BACKDROP_ZINDEX + 1};
37 | `
38 |
39 | const CalibrationButtons = styled.div`
40 | > * {
41 | margin-bottom: ${scale(1.5)};
42 | }
43 | margin-bottom: ${scale(4)};
44 | `
45 |
46 | const Header = styled.div`
47 | ${largeText};
48 | margin-bottom: ${scale(2)};
49 | `
50 |
51 | interface Props {
52 | isOpen: boolean
53 | onCancel: () => void
54 | onCalibrate: (calibrationBuffer: number) => void
55 | }
56 |
57 | const Calibration = React.memo(({ isOpen, onCalibrate, onCancel }) => {
58 | const calibrationBackdropStyle = useSpring({
59 | opacity: isOpen ? 1 : 0,
60 | pointerEvents: isOpen ? 'auto' : 'none',
61 | config: { mass: 1, tension: 400, friction: 30 }
62 | })
63 |
64 | const calibrationContainerStyle = useSpring({
65 | opacity: isOpen ? 1 : 0,
66 | transform: isOpen ? 'translateY(0%)' : 'translateY(-50%)',
67 | pointerEvents: isOpen ? 'auto' : 'none',
68 | config: { mass: 1, tension: 400, friction: 30 }
69 | })
70 |
71 | return (
72 | <>
73 |
77 |
78 | Calibrate all buttons to:
79 |
80 | {config.calibrationPresets.map((preset, i) => (
81 |
87 | ))}
88 |
89 | ...or set a custom value:
90 |
91 |
92 | >
93 | )
94 | })
95 |
96 | export default Calibration
97 |
--------------------------------------------------------------------------------
/client/src/stores/useServerStore.ts:
--------------------------------------------------------------------------------
1 | import create from 'zustand'
2 | import produce from 'immer'
3 |
4 | import ServerConnection from '../utils/ServerConnection'
5 | import { DeviceDescriptionMap } from '../../../common-types/device'
6 |
7 | export enum ServerConnectionStatus {
8 | Connected = 'connected',
9 | Disconnected = 'disconnected'
10 | }
11 |
12 | interface BaseServerState {
13 | address: string
14 | connectionStatus: ServerConnectionStatus
15 | }
16 |
17 | interface ConnectedServerState extends BaseServerState {
18 | connectionStatus: ServerConnectionStatus.Connected
19 | devices: DeviceDescriptionMap
20 | }
21 |
22 | interface DisconnectedServerState extends BaseServerState {
23 | connectionStatus: ServerConnectionStatus.Disconnected
24 | }
25 |
26 | export type ServerState = ConnectedServerState | DisconnectedServerState
27 |
28 | interface State {
29 | init: (serverAddresses: string[]) => void
30 | servers: { [serverAddress: string]: ServerState }
31 | serverConnections: { [serverAddress: string]: ServerConnection }
32 | }
33 |
34 | const [useServerStore] = create(set => {
35 | const setAndProduce = (fn: (draft: State) => void) => set(produce(fn))
36 |
37 | const connect = (address: string) =>
38 | setAndProduce(draft => {
39 | draft.servers[address] = {
40 | connectionStatus: ServerConnectionStatus.Connected,
41 | address: address,
42 | devices: {}
43 | }
44 | })
45 |
46 | const disconnect = (address: string) =>
47 | setAndProduce(draft => {
48 | draft.servers[address] = {
49 | connectionStatus: ServerConnectionStatus.Disconnected,
50 | address: address
51 | }
52 | })
53 |
54 | const devicesUpdated = (address: string, devices: DeviceDescriptionMap) =>
55 | setAndProduce(draft => {
56 | const server = draft.servers[address]
57 | if (server.connectionStatus === ServerConnectionStatus.Connected) {
58 | server.devices = devices
59 | }
60 | })
61 |
62 | const init = (serverAddresses: string[]) => {
63 | const serverConnections = serverAddresses.reduce(
64 | (acc, address) => ({
65 | ...acc,
66 | [address]: new ServerConnection({
67 | address,
68 | onConnect: () => connect(address),
69 | onDisconnect: () => disconnect(address),
70 | onDevicesUpdated: devices => devicesUpdated(address, devices)
71 | })
72 | }),
73 | {}
74 | )
75 |
76 | const servers = serverAddresses.reduce(
77 | (acc, address) => ({
78 | ...acc,
79 | [address]: {
80 | type: ServerConnectionStatus.Disconnected,
81 | address
82 | }
83 | }),
84 | {}
85 | )
86 |
87 | set({
88 | servers,
89 | serverConnections
90 | })
91 | }
92 |
93 | return {
94 | init,
95 | servers: {},
96 | serverConnections: {}
97 | }
98 | })
99 |
100 | export const serverByAddr = (addr: string) => (
101 | state: State
102 | ): ServerState | undefined => state.servers[addr]
103 |
104 | export const serverConnectionByAddr = (addr: string) => (
105 | state: State
106 | ): ServerConnection | undefined => state.serverConnections[addr]
107 |
108 | export default useServerStore
109 |
--------------------------------------------------------------------------------
/firmware/teensy2/Pad.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #include "Config/DancePadConfig.h"
5 | #include "ConfigStore.h"
6 | #include "Pad.h"
7 | #include "ADC.h"
8 |
9 | #define MIN(a,b) ((a) < (b) ? a : b)
10 |
11 | PadConfiguration PAD_CONF;
12 |
13 | PadState PAD_STATE = {
14 | .sensorValues = { [0 ... SENSOR_COUNT - 1] = 0 },
15 | .buttonsPressed = { [0 ... BUTTON_COUNT - 1] = false }
16 | };
17 |
18 | typedef struct {
19 | uint16_t sensorReleaseThresholds[SENSOR_COUNT];
20 | int8_t buttonToSensorMap[BUTTON_COUNT][SENSOR_COUNT + 1];
21 | } InternalPadConfiguration;
22 |
23 | InternalPadConfiguration INTERNAL_PAD_CONF;
24 |
25 | void Pad_UpdateInternalConfiguration(void) {
26 | for (int i = 0; i < SENSOR_COUNT; i++) {
27 | INTERNAL_PAD_CONF.sensorReleaseThresholds[i] = PAD_CONF.sensorThresholds[i] * PAD_CONF.releaseMultiplier;
28 | }
29 |
30 | // Precalculate array for mapping buttons to sensors.
31 | // For every button, there is an array of sensor indices. when there are no more buttons assigned to that sensor,
32 | // the value is -1.
33 | for (int buttonIndex = 0; buttonIndex < BUTTON_COUNT; buttonIndex++) {
34 | int mapIndex = 0;
35 |
36 | for (int sensorIndex = 0; sensorIndex < SENSOR_COUNT; sensorIndex++) {
37 | if (PAD_CONF.sensorToButtonMapping[sensorIndex] == buttonIndex) {
38 | INTERNAL_PAD_CONF.buttonToSensorMap[buttonIndex][mapIndex++] = sensorIndex;
39 | }
40 | }
41 |
42 | // mark -1 to end
43 | INTERNAL_PAD_CONF.buttonToSensorMap[buttonIndex][mapIndex] = -1;
44 | }
45 | }
46 |
47 | void Pad_Initialize(const PadConfiguration* padConfiguration) {
48 | ADC_Init();
49 | Pad_UpdateConfiguration(padConfiguration);
50 | }
51 |
52 | void Pad_UpdateConfiguration(const PadConfiguration* padConfiguration) {
53 | memcpy(&PAD_CONF, padConfiguration, sizeof (PadConfiguration));
54 | Pad_UpdateInternalConfiguration();
55 | }
56 |
57 | void Pad_UpdateState(void) {
58 | uint16_t newValues[SENSOR_COUNT];
59 |
60 | for (int i = 0; i < SENSOR_COUNT; i++) {
61 | newValues[i] = ADC_Read(i);
62 | }
63 |
64 | for (int i = 0; i < SENSOR_COUNT; i++) {
65 | // TODO: weight of old value and new value is not configurable for now
66 | // because division by unknown value means ass performance.
67 | PAD_STATE.sensorValues[i] = (PAD_STATE.sensorValues[i] + newValues[i]) / 2;
68 | }
69 |
70 | for (int i = 0; i < BUTTON_COUNT; i++) {
71 | bool newButtonPressedState = false;
72 |
73 | for (int j = 0; j < SENSOR_COUNT; j++) {
74 | int8_t sensor = INTERNAL_PAD_CONF.buttonToSensorMap[i][j];
75 |
76 | if (sensor == -1) {
77 | break;
78 | }
79 |
80 | if (sensor < 0 || sensor > SENSOR_COUNT) {
81 | break;
82 | }
83 |
84 | uint16_t sensorVal = PAD_STATE.sensorValues[sensor];
85 |
86 | if (PAD_STATE.buttonsPressed[i]) {
87 | if (sensorVal > INTERNAL_PAD_CONF.sensorReleaseThresholds[sensor]) {
88 | newButtonPressedState = true;
89 | break;
90 | }
91 | } else {
92 | if (sensorVal > PAD_CONF.sensorThresholds[sensor]) {
93 | newButtonPressedState = true;
94 | break;
95 | }
96 | }
97 | }
98 |
99 | PAD_STATE.buttonsPressed[i] = newButtonPressedState;
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/client/src/views/DeviceView/Device.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import TopBar from '../../components/topBar/TopBar'
3 | import TopBarTitle from '../../components/topBar/TopBarTitle'
4 | import DeviceConfigurationMenu from './deviceConfiguration/DeviceConfigurationMenu'
5 | import TopBarButton from '../../components/topBar/TopBarButton'
6 | import Calibration from './calibration/Calibration'
7 | import DeviceButtons from './deviceButtons/DeviceButtons'
8 | import {
9 | DeviceDescription,
10 | DeviceConfiguration
11 | } from '../../../../common-types/device'
12 | import { faBalanceScale, faCog } from '@fortawesome/free-solid-svg-icons'
13 | import useServerStore, {
14 | serverConnectionByAddr
15 | } from '../../stores/useServerStore'
16 | import TopBarSubtitle from '../../components/topBar/TopBarSubtitle'
17 |
18 | interface Props {
19 | serverAddress: string
20 | device: DeviceDescription
21 | }
22 |
23 | const Device = React.memo(({ serverAddress, device }) => {
24 | const serverConnection = useServerStore(serverConnectionByAddr(serverAddress))
25 |
26 | // configuration menu
27 |
28 | const [configurationMenuOpen, setConfigurationMenuOpen] = React.useState(
29 | false
30 | )
31 |
32 | const openConfigurationMenu = React.useCallback(() => {
33 | setConfigurationMenuOpen(true)
34 | }, [])
35 |
36 | const closeConfigurationMenu = React.useCallback(() => {
37 | setConfigurationMenuOpen(false)
38 | }, [])
39 |
40 | const handleSaveConfiguration = React.useCallback(
41 | (conf: Partial) => {
42 | if (!serverConnection) {
43 | return
44 | }
45 |
46 | serverConnection.updateConfiguration(device.id, conf, true)
47 | closeConfigurationMenu()
48 | },
49 | [closeConfigurationMenu, device.id, serverConnection]
50 | )
51 |
52 | // calibration
53 |
54 | const [calibrationMenuOpen, setCalibrationMenuOpen] = React.useState(false)
55 |
56 | const openCalibrationMenu = React.useCallback(() => {
57 | setCalibrationMenuOpen(true)
58 | }, [])
59 |
60 | const closeCalibrationMenu = React.useCallback(() => {
61 | setCalibrationMenuOpen(false)
62 | }, [])
63 |
64 | const handleCalibrate = React.useCallback(
65 | (calibrationBuffer: number) => {
66 | if (!serverConnection) {
67 | return
68 | }
69 |
70 | serverConnection.calibrate(device.id, calibrationBuffer)
71 | setTimeout(closeCalibrationMenu, 100)
72 | },
73 | [closeCalibrationMenu, device.id, serverConnection]
74 | )
75 |
76 | const eventRateFieldRef = React.useRef(null)
77 |
78 | const handleEventRateUpdate = React.useCallback((rate: number) => {
79 | if (!eventRateFieldRef.current) {
80 | return
81 | }
82 |
83 | eventRateFieldRef.current.innerText = rate + ' Hz'
84 | }, [])
85 |
86 | React.useEffect(() => {
87 | if (!serverConnection) {
88 | return
89 | }
90 |
91 | return serverConnection.subscribeToRateEvents(
92 | device.id,
93 | handleEventRateUpdate
94 | )
95 | }, [device.id, handleEventRateUpdate, serverConnection])
96 |
97 | return (
98 | <>
99 |
106 |
107 |
112 |
113 |
114 |
115 | {device.configuration.name}
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 | >
124 | )
125 | })
126 |
127 | export default Device
128 |
--------------------------------------------------------------------------------
/firmware/teensy2/Descriptors.h:
--------------------------------------------------------------------------------
1 | /*
2 | Based on LUFA Library example code (www.lufa-lib.org):
3 |
4 | Copyright 2017 Dean Camera (dean [at] fourwalledcubicle [dot] com)
5 |
6 | See other copyrights in LICENSE file on repository root.
7 |
8 | Permission to use, copy, modify, distribute, and sell this
9 | software and its documentation for any purpose is hereby granted
10 | without fee, provided that the above copyright notice appear in
11 | all copies and that both that the copyright notice and this
12 | permission notice and warranty disclaimer appear in supporting
13 | documentation, and that the name of the author not be used in
14 | advertising or publicity pertaining to distribution of the
15 | software without specific, written prior permission.
16 |
17 | The author disclaims all warranties with regard to this
18 | software, including all implied warranties of merchantability
19 | and fitness. In no event shall the author be liable for any
20 | special, indirect or consequential damages or any damages
21 | whatsoever resulting from loss of use, data or profits, whether
22 | in an action of contract, negligence or other tortious action,
23 | arising out of or in connection with the use or performance of
24 | this software.
25 | */
26 |
27 | #ifndef _DESCRIPTORS_H_
28 | #define _DESCRIPTORS_H_
29 |
30 | /* Includes: */
31 | #include
32 | #include
33 | #include
34 |
35 | /* Type Defines: */
36 | /** Type define for the device configuration descriptor structure. This must be defined in the
37 | * application code, as the configuration descriptor contains several sub-descriptors which
38 | * vary between devices, and which describe the device's usage to the host.
39 | */
40 | typedef struct
41 | {
42 | USB_Descriptor_Configuration_Header_t Config;
43 |
44 | // Generic HID Interface
45 | USB_Descriptor_Interface_t HID_Interface;
46 | USB_HID_Descriptor_HID_t HID_GenericHID;
47 | USB_Descriptor_Endpoint_t HID_ReportINEndpoint;
48 | } USB_Descriptor_Configuration_t;
49 |
50 | /** Enum for the device interface descriptor IDs within the device. Each interface descriptor
51 | * should have a unique ID index associated with it, which can be used to refer to the
52 | * interface from other descriptors.
53 | */
54 | enum InterfaceDescriptors_t
55 | {
56 | INTERFACE_ID_GenericHID = 0, /**< GenericHID interface descriptor ID */
57 | };
58 |
59 | /** Enum for the device string descriptor IDs within the device. Each string descriptor should
60 | * have a unique ID index associated with it, which can be used to refer to the string from
61 | * other descriptors.
62 | */
63 | enum StringDescriptors_t
64 | {
65 | STRING_ID_Language = 0, /**< Supported Languages string descriptor ID (must be zero) */
66 | STRING_ID_Manufacturer = 1, /**< Manufacturer string ID */
67 | STRING_ID_Product = 2, /**< Product string ID */
68 | };
69 |
70 |
71 | #define INPUT_REPORT_ID 0x01
72 | #define PAD_CONFIGURATION_REPORT_ID 0x02
73 | #define RESET_REPORT_ID 0x03
74 | #define SAVE_CONFIGURATION_REPORT_ID 0x04
75 | #define NAME_REPORT_ID 0x05
76 | #define UNUSED_ANALOG_JOYSTICK_REPORT_ID 0x06
77 |
78 | /* Macros: */
79 | /** Endpoint address of the Generic HID reporting IN endpoint. */
80 | #define GENERIC_IN_EPADDR (ENDPOINT_DIR_IN | 1)
81 |
82 | // Size in bytes of the Generic HID reporting endpoint.
83 | #define GENERIC_EPSIZE 64
84 |
85 | /* Function Prototypes: */
86 | uint16_t CALLBACK_USB_GetDescriptor(const uint16_t wValue,
87 | const uint16_t wIndex,
88 | const void** const DescriptorAddress)
89 | ATTR_WARN_UNUSED_RESULT ATTR_NON_NULL_PTR_ARG(3);
90 |
91 | #endif
92 |
93 |
--------------------------------------------------------------------------------
/client/src/views/DeviceView/deviceConfiguration/ConfigurationForm.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useFormik } from 'formik'
3 | import styled from 'styled-components'
4 |
5 | import scale from '../../../utils/scale'
6 | import { basicText, largeText } from '../../../components/Typography'
7 | import Range from '../../../components/Range'
8 | import { range } from 'lodash-es'
9 | import {
10 | DeviceDescription,
11 | DeviceConfiguration
12 | } from '../../../../../common-types/device'
13 | import SensorLabel from './SensorLabel'
14 |
15 | interface FormValues {
16 | name: string
17 | sensorToButtonMapping: number[]
18 | releaseThreshold: string
19 | }
20 |
21 | interface Props {
22 | device: DeviceDescription
23 | serverAddress: string
24 | onSubmit: (data: Partial) => void
25 | }
26 |
27 | const Header = styled.h3`
28 | ${largeText};
29 | margin-top: ${scale(3)};
30 | margin-bottom: ${scale(2)};
31 | `
32 |
33 | const Label = styled.label`
34 | display: block;
35 | ${basicText};
36 | margin-bottom: ${scale(0.5)};
37 | `
38 |
39 | const Form = styled.form`
40 | padding: ${scale(2)};
41 | `
42 |
43 | const FormItem = styled.div`
44 | margin-bottom: ${scale(2)};
45 | `
46 |
47 | const Input = styled.input`
48 | ${basicText};
49 | display: block;
50 | width: 100%;
51 | padding: ${scale(0.5)} ${scale(1)};
52 | `
53 |
54 | // TODO: extract button from elsewhere and use that.
55 | const SaveButton = styled.button`
56 | ${basicText};
57 | margin-top: ${scale(2)};
58 | `
59 |
60 | const SensorText = React.memo<{ buttonIndex: number }>(props => {
61 | if (props.buttonIndex < 0) {
62 | return <>Disabled>
63 | } else {
64 | return <>Button {props.buttonIndex + 1}>
65 | }
66 | })
67 |
68 | const ConfigurationForm = React.memo(
69 | ({ device, serverAddress, onSubmit }) => {
70 | const formik = useFormik({
71 | enableReinitialize: true,
72 |
73 | initialValues: {
74 | name: device.configuration.name,
75 | sensorToButtonMapping: device.configuration.sensorToButtonMapping,
76 | releaseThreshold: parseFloat(
77 | device.configuration.releaseThreshold.toFixed(4)
78 | ).toString()
79 | },
80 |
81 | onSubmit: (data: FormValues) =>
82 | onSubmit({
83 | name: data.name,
84 | sensorToButtonMapping: data.sensorToButtonMapping,
85 | releaseThreshold: parseFloat(data.releaseThreshold)
86 | })
87 | })
88 |
89 | return (
90 |
142 | )
143 | }
144 | )
145 |
146 | export default ConfigurationForm
147 |
--------------------------------------------------------------------------------
/client/src/views/DeviceView/deviceButtons/Button.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react'
2 | import styled from 'styled-components'
3 | import scale from '../../../utils/scale'
4 | import { colors } from '../../../utils/colors'
5 | import { ButtonType } from '../../../domain/Button'
6 | import { useSpring, animated } from 'react-spring'
7 | import Sensor from './Sensor'
8 | import {
9 | DeviceDescription,
10 | DeviceInputData
11 | } from '../../../../../common-types/device'
12 | import { faArrowCircleLeft } from '@fortawesome/free-solid-svg-icons'
13 | import IconButton from '../../../components/IconButton'
14 | import { largeText } from '../../../components/Typography'
15 | import { usePreventMobileSafariDrag } from '../../../utils/usePreventiOSDrag'
16 | import useServerStore, {
17 | serverConnectionByAddr
18 | } from '../../../stores/useServerStore'
19 |
20 | const NOT_PRESSED_BACKGROUND = `linear-gradient(to top, ${colors.buttonBottomColor} 0%, ${colors.buttonTopColor} 100%)`
21 | const PRESSED_BACKGROUND = `linear-gradient(to top, ${colors.pressedButtonBottomColor} 0%, ${colors.pressedBottomTopColor} 100%)`
22 |
23 | const Container = styled.div`
24 | position: relative;
25 | background: ${NOT_PRESSED_BACKGROUND};
26 | display: flex;
27 | white-space: nowrap;
28 | margin: 0 ${scale(1)};
29 | `
30 |
31 | const Header = styled(animated.div)`
32 | align-items: center;
33 | color: ${colors.text};
34 | display: flex;
35 | left: ${scale(2)};
36 | position: absolute;
37 | right: ${scale(2)};
38 | top: ${scale(2)};
39 | z-index: 2;
40 | `
41 |
42 | const ButtonName = styled.div`
43 | margin-left: ${scale(1)};
44 | ${largeText};
45 | `
46 |
47 | const Sensors = styled.div`
48 | position: absolute;
49 | display: flex;
50 | justify-content: center;
51 | align-items: flex-end;
52 | height: calc(100% - ${scale(10)});
53 | bottom: 0;
54 | left: 0;
55 | right: 0;
56 |
57 | > * {
58 | margin: 0 2%;
59 | width: 20%;
60 | }
61 | `
62 |
63 | interface Props {
64 | serverAddress: string
65 | device: DeviceDescription
66 | button: ButtonType
67 | selected?: boolean
68 | onSelect?: () => void
69 | onBack?: () => void
70 | }
71 |
72 | const Button = React.memo(
73 | ({ selected, button, device, serverAddress, onSelect, onBack }) => {
74 | const serverConnection = useServerStore(
75 | serverConnectionByAddr(serverAddress)
76 | )
77 |
78 | const headerStyle = useSpring({
79 | opacity: selected ? 0.75 : 0,
80 | config: { duration: 100 }
81 | })
82 |
83 | const buttonContainerRef = React.useRef(null)
84 | const currentlyPressedRef = React.useRef(false)
85 |
86 | // we need this so the sensor threshold drags won't be annoying.
87 | usePreventMobileSafariDrag(buttonContainerRef)
88 |
89 | const handleInputEvent = React.useCallback(
90 | (inputData: DeviceInputData) => {
91 | const isPressed = inputData.buttons[button.buttonIndex]
92 |
93 | if (
94 | currentlyPressedRef.current !== isPressed &&
95 | buttonContainerRef.current !== null
96 | ) {
97 | currentlyPressedRef.current = isPressed
98 | buttonContainerRef.current.style.background = isPressed
99 | ? PRESSED_BACKGROUND
100 | : NOT_PRESSED_BACKGROUND
101 | }
102 | },
103 | [button.buttonIndex]
104 | )
105 |
106 | useEffect(() => {
107 | if (!serverConnection) {
108 | return
109 | }
110 |
111 | return serverConnection.subscribeToInputEvents(
112 | device.id,
113 | handleInputEvent
114 | )
115 | }, [serverAddress, device, handleInputEvent, serverConnection])
116 |
117 | return (
118 |
122 |
123 |
128 | Button {button.buttonIndex + 1}
129 |
130 |
131 |
132 | {button.sensors.map(sensor => (
133 |
140 | ))}
141 |
142 |
143 | )
144 | }
145 | )
146 |
147 | export default Button
148 |
--------------------------------------------------------------------------------
/client/src/utils/ServerConnection.tsx:
--------------------------------------------------------------------------------
1 | import io from 'socket.io-client'
2 |
3 | import { ServerEvents, ClientEvents } from '../../../common-types/events'
4 |
5 | import {
6 | DeviceConfiguration,
7 | DeviceInputData,
8 | DeviceDescriptionMap
9 | } from '../../../common-types/device'
10 |
11 | import SubscriptionManager from './SubscriptionManager'
12 |
13 | interface ServerConnectionSettings {
14 | address: string
15 | onConnect: () => void
16 | onDisconnect: () => void
17 | onDevicesUpdated: (devices: DeviceDescriptionMap) => void
18 | }
19 |
20 | class ServerConnection {
21 | private ioSocket: SocketIOClient.Socket
22 | private inputEventSubscriptions: SubscriptionManager
23 | private rateEventSubscriptions: SubscriptionManager
24 |
25 | constructor(settings: ServerConnectionSettings) {
26 | this.inputEventSubscriptions = new SubscriptionManager()
27 | this.rateEventSubscriptions = new SubscriptionManager()
28 |
29 | this.ioSocket = io(settings.address, {
30 | transports: ['websocket'],
31 | reconnectionDelay: 250,
32 | reconnectionDelayMax: 1000
33 | })
34 | this.ioSocket.on('connect', settings.onConnect)
35 | this.ioSocket.on('disconnect', settings.onDisconnect)
36 | this.ioSocket.on('devicesUpdated', (event: ServerEvents.DevicesUpdated) =>
37 | settings.onDevicesUpdated(event.devices)
38 | )
39 | this.ioSocket.on('inputEvent', this.handleInputEvent)
40 | this.ioSocket.on('eventRate', this.handleRateEvent)
41 | }
42 |
43 | private handleInputEvent = (event: ServerEvents.InputEvent) => {
44 | this.inputEventSubscriptions.emit(event.deviceId, event.inputData)
45 | }
46 |
47 | private handleRateEvent = (event: ServerEvents.EventRate) => {
48 | this.rateEventSubscriptions.emit(event.deviceId, event.eventRate)
49 | }
50 |
51 | private subscribeToDevice = (deviceId: string) => {
52 | const event: ClientEvents.SubscribeToDevice = { deviceId }
53 | this.ioSocket.emit('subscribeToDevice', event)
54 | }
55 |
56 | private unsubscribeFromDevice = (deviceId: string) => {
57 | const event: ClientEvents.UnsubscribeFromDevice = {
58 | deviceId
59 | }
60 | this.ioSocket.emit('unsubscribeFromDevice', event)
61 | }
62 |
63 | private hasAnySubscriptionsForDevice = (deviceId: string) => {
64 | return (
65 | this.inputEventSubscriptions.hasSubscriptionsFor(deviceId) ||
66 | this.rateEventSubscriptions.hasSubscriptionsFor(deviceId)
67 | )
68 | }
69 |
70 | public subscribeToInputEvents = (
71 | deviceId: string,
72 | callback: (data: DeviceInputData) => void
73 | ) => {
74 | if (!this.hasAnySubscriptionsForDevice(deviceId)) {
75 | this.subscribeToDevice(deviceId)
76 | }
77 |
78 | this.inputEventSubscriptions.subscribe(deviceId, callback)
79 |
80 | return () => {
81 | this.inputEventSubscriptions.unsubscribe(deviceId, callback)
82 | if (!this.hasAnySubscriptionsForDevice(deviceId)) {
83 | this.unsubscribeFromDevice(deviceId)
84 | }
85 | }
86 | }
87 |
88 | public subscribeToRateEvents = (
89 | deviceId: string,
90 | callback: (rate: number) => void
91 | ) => {
92 | if (!this.hasAnySubscriptionsForDevice(deviceId)) {
93 | this.subscribeToDevice(deviceId)
94 | }
95 |
96 | this.rateEventSubscriptions.subscribe(deviceId, callback)
97 |
98 | return () => {
99 | this.rateEventSubscriptions.unsubscribe(deviceId, callback)
100 | if (!this.hasAnySubscriptionsForDevice(deviceId)) {
101 | this.unsubscribeFromDevice(deviceId)
102 | }
103 | }
104 | }
105 |
106 | public updateConfiguration = (
107 | deviceId: string,
108 | configuration: Partial,
109 | store: boolean
110 | ) => {
111 | const event: ClientEvents.UpdateConfiguration = {
112 | deviceId,
113 | configuration,
114 | store
115 | }
116 |
117 | this.ioSocket.emit('updateConfiguration', event)
118 | }
119 |
120 | public updateSensorThreshold = (
121 | deviceId: string,
122 | sensorIndex: number,
123 | newThreshold: number,
124 | store: boolean
125 | ) => {
126 | const event: ClientEvents.UpdateSensorThreshold = {
127 | deviceId,
128 | sensorIndex,
129 | newThreshold,
130 | store
131 | }
132 |
133 | this.ioSocket.emit('updateSensorThreshold', event)
134 | }
135 |
136 | public calibrate = (deviceId: string, calibrationBuffer: number) => {
137 | const event: ClientEvents.Calibrate = {
138 | deviceId,
139 | calibrationBuffer
140 | }
141 |
142 | this.ioSocket.emit('calibrate', event)
143 | }
144 | }
145 |
146 | export default ServerConnection
147 |
--------------------------------------------------------------------------------
/server/src/driver/teensy2/Teensy2Reports.ts:
--------------------------------------------------------------------------------
1 | import { Parser } from 'binary-parser'
2 |
3 | const MAX_NAME_SIZE = 50
4 |
5 | export enum ReportID {
6 | SENSOR_VALUES = 0x01,
7 | PAD_CONFIGURATION = 0x02,
8 | RESET = 0x03,
9 | SAVE_CONFIGURATION = 0x04,
10 | NAME = 0x05
11 | }
12 |
13 | export interface InputReport {
14 | buttons: boolean[]
15 | sensorValues: number[]
16 | }
17 |
18 | export interface ConfigurationReport {
19 | sensorThresholds: number[]
20 | releaseThreshold: number
21 | sensorToButtonMapping: number[]
22 | }
23 |
24 | export interface NameReport {
25 | name: string
26 | }
27 |
28 | export class ReportManager {
29 | private buttonCount: number
30 | private sensorCount: number
31 | private inputReportParser: Parser
32 | private configurationReportParser: Parser
33 | private nameReportParser: Parser
34 |
35 | constructor(settings: { buttonCount: number; sensorCount: number }) {
36 | this.buttonCount = settings.buttonCount
37 | this.sensorCount = settings.sensorCount
38 |
39 | this.inputReportParser = new Parser()
40 | .uint8('reportId', {
41 | assert: ReportID.SENSOR_VALUES
42 | })
43 | .uint16le('buttonBits')
44 | .array('sensorValues', {
45 | type: 'uint16le',
46 | length: settings.sensorCount
47 | })
48 |
49 | this.configurationReportParser = new Parser()
50 | .uint8('reportId', {
51 | assert: ReportID.PAD_CONFIGURATION
52 | })
53 | .array('sensorThresholds', {
54 | type: 'uint16le',
55 | length: this.sensorCount
56 | })
57 | .floatle('releaseThreshold')
58 | .array('sensorToButtonMapping', {
59 | type: 'int8',
60 | length: this.sensorCount
61 | })
62 |
63 | this.nameReportParser = new Parser()
64 | .uint8('reportId', {
65 | assert: ReportID.NAME
66 | })
67 | .uint8('size')
68 | .string('name', { length: 'size' })
69 | }
70 |
71 | private formatButtons = (data: number) => {
72 | const bitArray = new Array(this.buttonCount)
73 |
74 | for (let i = 0; i < this.buttonCount; i++) {
75 | bitArray[i] = (data >> i) % 2 != 0
76 | }
77 |
78 | return bitArray
79 | }
80 |
81 | parseInputReport(data: Buffer): InputReport {
82 | const parsed = this.inputReportParser.parse(data)
83 |
84 | return {
85 | buttons: this.formatButtons(parsed.buttonBits),
86 | sensorValues: parsed.sensorValues
87 | }
88 | }
89 |
90 | parseConfigurationReport(data: Buffer): ConfigurationReport {
91 | const parsed = this.configurationReportParser.parse(data)
92 |
93 | return {
94 | releaseThreshold: parsed.releaseThreshold,
95 | sensorThresholds: parsed.sensorThresholds,
96 | sensorToButtonMapping: parsed.sensorToButtonMapping
97 | }
98 | }
99 |
100 | parseNameReport(data: Buffer): NameReport {
101 | const parsed = this.nameReportParser.parse(data)
102 |
103 | return {
104 | name: parsed.name
105 | }
106 | }
107 |
108 | getConfigurationReportSize = () => {
109 | // size is as follows:
110 | // - 1 byte for report id
111 | // - 2 bytes for every sensor threshold (they're uint16)
112 | // - 4 bytes for request threshold (float)
113 | // - 1 byte for sensor to button mapping (int8)
114 | return 2 * this.sensorCount + 4 + this.sensorCount + 1
115 | }
116 |
117 | createConfigurationReport(conf: ConfigurationReport): number[] {
118 | const buffer = Buffer.alloc(this.getConfigurationReportSize())
119 | let pos = 0
120 |
121 | // report ID
122 | buffer.writeUInt8(ReportID.PAD_CONFIGURATION, pos)
123 | pos += 1
124 |
125 | // sensor thresholds
126 | for (let i = 0; i < this.sensorCount; i++) {
127 | buffer.writeUInt16LE(conf.sensorThresholds[i], pos)
128 | pos += 2
129 | }
130 |
131 | // release threshold
132 | buffer.writeFloatLE(conf.releaseThreshold, pos)
133 | pos += 4
134 |
135 | // sensor to button mapping
136 | for (let i = 0; i < this.sensorCount; i++) {
137 | buffer.writeInt8(conf.sensorToButtonMapping[i], pos)
138 | pos += 1
139 | }
140 |
141 | return [...buffer]
142 | }
143 |
144 | createSaveConfigurationReport(): number[] {
145 | return [ReportID.SAVE_CONFIGURATION, 0x00]
146 | }
147 |
148 | getNameReportSize(): number {
149 | // 1 for report id
150 | // 1 for size (uint8)
151 | return 1 + 1 + MAX_NAME_SIZE
152 | }
153 |
154 | createNameReport(report: NameReport): number[] {
155 | const buffer = Buffer.alloc(this.getNameReportSize())
156 | let pos = 0
157 |
158 | // report ID
159 | buffer.writeUInt8(ReportID.NAME, pos)
160 | pos += 1
161 |
162 | // name length
163 | buffer.writeUInt8(report.name.length, pos)
164 | pos += 1
165 |
166 | // name itself
167 | buffer.write(report.name, pos, 'utf8')
168 |
169 | return [...buffer]
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/firmware/teensy2/Config/LUFAConfig.h:
--------------------------------------------------------------------------------
1 | /*
2 | LUFA Library
3 | Copyright (C) Dean Camera, 2017.
4 |
5 | dean [at] fourwalledcubicle [dot] com
6 | www.lufa-lib.org
7 | */
8 |
9 | /*
10 | Copyright 2017 Dean Camera (dean [at] fourwalledcubicle [dot] com)
11 |
12 | Permission to use, copy, modify, distribute, and sell this
13 | software and its documentation for any purpose is hereby granted
14 | without fee, provided that the above copyright notice appear in
15 | all copies and that both that the copyright notice and this
16 | permission notice and warranty disclaimer appear in supporting
17 | documentation, and that the name of the author not be used in
18 | advertising or publicity pertaining to distribution of the
19 | software without specific, written prior permission.
20 |
21 | The author disclaims all warranties with regard to this
22 | software, including all implied warranties of merchantability
23 | and fitness. In no event shall the author be liable for any
24 | special, indirect or consequential damages or any damages
25 | whatsoever resulting from loss of use, data or profits, whether
26 | in an action of contract, negligence or other tortious action,
27 | arising out of or in connection with the use or performance of
28 | this software.
29 | */
30 |
31 | /** \file
32 | * \brief LUFA Library Configuration Header File
33 | *
34 | * This header file is used to configure LUFA's compile time options,
35 | * as an alternative to the compile time constants supplied through
36 | * a makefile.
37 | *
38 | * For information on what each token does, refer to the LUFA
39 | * manual section "Summary of Compile Tokens".
40 | */
41 |
42 | #ifndef _LUFA_CONFIG_H_
43 | #define _LUFA_CONFIG_H_
44 |
45 | #if (ARCH == ARCH_AVR8)
46 |
47 | /* Non-USB Related Configuration Tokens: */
48 | // #define DISABLE_TERMINAL_CODES
49 |
50 | /* USB Class Driver Related Tokens: */
51 | // #define HID_HOST_BOOT_PROTOCOL_ONLY
52 | // #define HID_STATETABLE_STACK_DEPTH {Insert Value Here}
53 | // #define HID_USAGE_STACK_DEPTH {Insert Value Here}
54 | // #define HID_MAX_COLLECTIONS {Insert Value Here}
55 | // #define HID_MAX_REPORTITEMS {Insert Value Here}
56 | // #define HID_MAX_REPORT_IDS {Insert Value Here}
57 | // #define NO_CLASS_DRIVER_AUTOFLUSH
58 |
59 | /* General USB Driver Related Tokens: */
60 | // #define ORDERED_EP_CONFIG
61 | #define USE_STATIC_OPTIONS (USB_DEVICE_OPT_FULLSPEED | USB_OPT_REG_ENABLED | USB_OPT_AUTO_PLL)
62 | #define USB_DEVICE_ONLY
63 | // #define USB_HOST_ONLY
64 | // #define USB_STREAM_TIMEOUT_MS {Insert Value Here}
65 | // #define NO_LIMITED_CONTROLLER_CONNECT
66 | // #define NO_SOF_EVENTS
67 |
68 | /* USB Device Mode Driver Related Tokens: */
69 | // #define USE_RAM_DESCRIPTORS
70 | #define USE_FLASH_DESCRIPTORS
71 | // #define USE_EEPROM_DESCRIPTORS
72 | // #define NO_INTERNAL_SERIAL
73 | #define FIXED_CONTROL_ENDPOINT_SIZE 8
74 | // #define DEVICE_STATE_AS_GPIOR {Insert Value Here}
75 | #define FIXED_NUM_CONFIGURATIONS 1
76 | // #define CONTROL_ONLY_DEVICE
77 | // #define INTERRUPT_CONTROL_ENDPOINT
78 | // #define NO_DEVICE_REMOTE_WAKEUP
79 | // #define NO_DEVICE_SELF_POWER
80 |
81 | /* USB Host Mode Driver Related Tokens: */
82 | // #define HOST_STATE_AS_GPIOR {Insert Value Here}
83 | // #define USB_HOST_TIMEOUT_MS {Insert Value Here}
84 | // #define HOST_DEVICE_SETTLE_DELAY_MS {Insert Value Here}
85 | // #define NO_AUTO_VBUS_MANAGEMENT
86 | // #define INVERTED_VBUS_ENABLE_LINE
87 |
88 | #elif (ARCH == ARCH_XMEGA)
89 |
90 | /* Non-USB Related Configuration Tokens: */
91 | // #define DISABLE_TERMINAL_CODES
92 |
93 | /* USB Class Driver Related Tokens: */
94 | // #define HID_HOST_BOOT_PROTOCOL_ONLY
95 | // #define HID_STATETABLE_STACK_DEPTH {Insert Value Here}
96 | // #define HID_USAGE_STACK_DEPTH {Insert Value Here}
97 | // #define HID_MAX_COLLECTIONS {Insert Value Here}
98 | // #define HID_MAX_REPORTITEMS {Insert Value Here}
99 | // #define HID_MAX_REPORT_IDS {Insert Value Here}
100 | // #define NO_CLASS_DRIVER_AUTOFLUSH
101 |
102 | /* General USB Driver Related Tokens: */
103 | #define USE_STATIC_OPTIONS (USB_DEVICE_OPT_FULLSPEED | USB_OPT_RC32MCLKSRC | USB_OPT_BUSEVENT_PRIHIGH)
104 | // #define USB_STREAM_TIMEOUT_MS {Insert Value Here}
105 | // #define NO_LIMITED_CONTROLLER_CONNECT
106 | // #define NO_SOF_EVENTS
107 |
108 | /* USB Device Mode Driver Related Tokens: */
109 | // #define USE_RAM_DESCRIPTORS
110 | #define USE_FLASH_DESCRIPTORS
111 | // #define USE_EEPROM_DESCRIPTORS
112 | // #define NO_INTERNAL_SERIAL
113 | #define FIXED_CONTROL_ENDPOINT_SIZE 8
114 | // #define DEVICE_STATE_AS_GPIOR {Insert Value Here}
115 | #define FIXED_NUM_CONFIGURATIONS 1
116 | // #define CONTROL_ONLY_DEVICE
117 | #define MAX_ENDPOINT_INDEX 1
118 | // #define NO_DEVICE_REMOTE_WAKEUP
119 | // #define NO_DEVICE_SELF_POWER
120 |
121 | #else
122 |
123 | #error Unsupported architecture for this LUFA configuration file.
124 |
125 | #endif
126 | #endif
127 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Analog Dance Pad
2 |
3 | *NOTE: this project contains submodules. You should clone with ` --recurse-submodules` flag.*
4 |
5 | For all your dance gaming needs that involve FSR (or perhaps other) sensors!
6 |
7 | - **Firmware for Teensy 2.0**
8 | - Works as a HID joystick.
9 | - Returns analog values alongside ordinary HID joystick data.
10 | - Remembers configuration after power loss.
11 |
12 | - **Socket.IO server**
13 | - Built with NodeJS/TypeScript.
14 | - Communicates with Teensy 2.0 devices via USB.
15 | - ...so the server needs to run on same computer your Teensy 2.0 devices are connected to.
16 | - Ability to communicate with multiple Teensy 2.0 devices.
17 | - Only running server instance required, even with multiple pads in one device!
18 | - Implements calibration logic and sensor value linearization.
19 | - Handles connecting and disconnecting devices on the fly.
20 |
21 | - **Web application**
22 | - Built with React/TypeScript.
23 | - Optimized for mobile use.
24 | - Ability to communicate to multiple servers at the same time.
25 | - Very useful for multi-cabinet environments!
26 |
27 | ## Getting Started
28 |
29 | *NOTE: Understand that everything is very much work in progress. Software is not in usable state for anyone else than the most adventurous!*
30 |
31 | ### Firmware
32 |
33 | You need **AVR GCC** and **Make** to build the project.
34 |
35 | ```bash
36 | cd firmware/teensy2/build
37 | make
38 | ```
39 |
40 | This results `AnalogDancePad.hex` in `build` folder that you can upload to Teensy 2.0 device using [Teensy Loader](https://www.pjrc.com/teensy/loader.html). If you have [Teensy Loader CLI](https://www.pjrc.com/teensy/loader_cli.html) in your PATH, you can also run `make install`.
41 |
42 | *NOTE: After uploading this firmware to your device, Teensy tools cannot reset it anymore due to USB Serial interface not being available. This means you need to reset it yourself. Pressing the reset button in firmware does still work. You can also run `npm run reset-teensy` in `server` directory in case it's not convenient to access your Teensy physically.*
43 |
44 | ### Server
45 |
46 | Server has been tested with NodeJS 12. You might need `libudev-dev` or similar package for your operating system in case `usb-detection` library doesn't have a prebuilt binary for you.
47 |
48 | For development, you can use:
49 |
50 | ```
51 | cd server
52 | npm install
53 | npm run start
54 | ```
55 |
56 | For production use:
57 |
58 | ```
59 | npm run build
60 | node dist/index.js
61 | ```
62 |
63 | You can use `PORT` and `HOST` environment variables. Default port is 3333. If you're running the server on a Linux machine, I recommend setting up a systemd unit file.
64 |
65 | ### Client
66 |
67 | In case of client, you need to build the common types first (server does it automatically). You also need to do this whenever you change these types.
68 |
69 | ```
70 | cd common-types
71 | npm install
72 | npm run build
73 | ```
74 |
75 | After that, you can run the client for development purposes like this:
76 |
77 | ```
78 | cd client
79 | npm install
80 | npm run start
81 | ```
82 |
83 | ...and building production-ready files to `build/` folder like this:
84 |
85 | ```
86 | npm run build
87 | ```
88 |
89 | You can then serve these files, for example, using [Surge](https://surge.sh/).
90 |
91 | #### Environment variables
92 |
93 | You probably need to set some environment variables for the client to be useful. This configuration needs to be done *on build* – so pass there environment variables to either `npm run start` or `npm run build`.
94 |
95 | - `REACT_APP_SERVER_ADDRESSES`
96 | - List of server addresses to connect separated by a comma.
97 | - Example: `196.168.1.10:3333,196.168.1.11:3333`
98 | - Default: `localhost:3333`
99 |
100 | - `REACT_APP_CALIBRATION_PRESETS`
101 | - List of presets in calibration UI.
102 | - Default: `Sensitive:0.05,Normal:0.1,Stiff:0.15`
103 |
104 | - `REACT_APP_FORCE_INSECURE`
105 | - Detect whether the client is accessed through HTTPS, and redirect to HTTP if it is.
106 | - Why? Well, sometimes you might want to deploy the client in Internet, because it's easy and you don't want to deal with your own HTTP server. However, many convenient website hosting services (such as [Surge](https://surge.sh/)) often automatically serve you HTTPS version of the site, or you just end up to the HTTPS version accidentally. Due to mixed-content security policies, you cannot access anything unsecured – such as servers in the local network – if you're accessing the client in HTTPS.
107 | - Disabled by default.
108 | - Set to `true` enable.
109 |
110 | ## Troubleshooting
111 |
112 | ### The UI shows more sensors than I have
113 |
114 | The firmware reads all 12 ADC pins on the Teensy. You may get fake readings from pins that have no sensors plugged in and are not grounded. If pressing a sensor causes more than one value to go up:
115 | - Determine which pins are unused. You can use this [pinout](https://www.pjrc.com/teensy/card2a.pdf). The sensors follow the pin order, e.g. ADC0 = 1, ADC1 = 2...
116 | - Press settings and disable the corresponding sensors in the UI, or
117 | - Ground the unused inputs on the board.
118 |
119 | ## License
120 |
121 | This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details.
122 |
123 | ## Acknowledgments
124 |
125 | - Thanks to [LUFA](http://www.fourwalledcubicle.com/LUFA.php) for making the Teensy 2 firmware possible!
126 | - Thanks to Finnish dance gaming community for creating a great opportunity to tinker endlessly with software.
127 |
--------------------------------------------------------------------------------
/client/src/serviceWorker.ts:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | )
22 |
23 | type Config = {
24 | onSuccess?: (registration: ServiceWorkerRegistration) => void
25 | onUpdate?: (registration: ServiceWorkerRegistration) => void
26 | }
27 |
28 | export function register(config?: Config) {
29 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
30 | // The URL constructor is available in all browsers that support SW.
31 | const publicUrl = new URL(
32 | (process as { env: { [key: string]: string } }).env.PUBLIC_URL,
33 | window.location.href
34 | )
35 | if (publicUrl.origin !== window.location.origin) {
36 | // Our service worker won't work if PUBLIC_URL is on a different origin
37 | // from what our page is served on. This might happen if a CDN is used to
38 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
39 | return
40 | }
41 |
42 | window.addEventListener('load', () => {
43 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
44 |
45 | if (isLocalhost) {
46 | // This is running on localhost. Let's check if a service worker still exists or not.
47 | checkValidServiceWorker(swUrl, config)
48 |
49 | // Add some additional logging to localhost, pointing developers to the
50 | // service worker/PWA documentation.
51 | navigator.serviceWorker.ready.then(() => {
52 | console.log(
53 | 'This web app is being served cache-first by a service ' +
54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
55 | )
56 | })
57 | } else {
58 | // Is not localhost. Just register service worker
59 | registerValidSW(swUrl, config)
60 | }
61 | })
62 | }
63 | }
64 |
65 | function registerValidSW(swUrl: string, config?: Config) {
66 | navigator.serviceWorker
67 | .register(swUrl)
68 | .then(registration => {
69 | registration.onupdatefound = () => {
70 | const installingWorker = registration.installing
71 | if (installingWorker == null) {
72 | return
73 | }
74 | installingWorker.onstatechange = () => {
75 | if (installingWorker.state === 'installed') {
76 | if (navigator.serviceWorker.controller) {
77 | // At this point, the updated precached content has been fetched,
78 | // but the previous service worker will still serve the older
79 | // content until all client tabs are closed.
80 | console.log(
81 | 'New content is available and will be used when all ' +
82 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
83 | )
84 |
85 | // Execute callback
86 | if (config && config.onUpdate) {
87 | config.onUpdate(registration)
88 | }
89 | } else {
90 | // At this point, everything has been precached.
91 | // It's the perfect time to display a
92 | // "Content is cached for offline use." message.
93 | console.log('Content is cached for offline use.')
94 |
95 | // Execute callback
96 | if (config && config.onSuccess) {
97 | config.onSuccess(registration)
98 | }
99 | }
100 | }
101 | }
102 | }
103 | })
104 | .catch(error => {
105 | console.error('Error during service worker registration:', error)
106 | })
107 | }
108 |
109 | function checkValidServiceWorker(swUrl: string, config?: Config) {
110 | // Check if the service worker can be found. If it can't reload the page.
111 | fetch(swUrl)
112 | .then(response => {
113 | // Ensure service worker exists, and that we really are getting a JS file.
114 | const contentType = response.headers.get('content-type')
115 | if (
116 | response.status === 404 ||
117 | (contentType != null && contentType.indexOf('javascript') === -1)
118 | ) {
119 | // No service worker found. Probably a different app. Reload the page.
120 | navigator.serviceWorker.ready.then(registration => {
121 | registration.unregister().then(() => {
122 | window.location.reload()
123 | })
124 | })
125 | } else {
126 | // Service worker found. Proceed as normal.
127 | registerValidSW(swUrl, config)
128 | }
129 | })
130 | .catch(() => {
131 | console.log(
132 | 'No internet connection found. App is running in offline mode.'
133 | )
134 | })
135 | }
136 |
137 | export function unregister() {
138 | if ('serviceWorker' in navigator) {
139 | navigator.serviceWorker.ready.then(registration => {
140 | registration.unregister()
141 | })
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/client/src/views/DeviceView/deviceButtons/Sensor.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { useDebouncedCallback } from 'use-debounce'
4 | import { animated, useSpring, interpolate } from 'react-spring'
5 | import { useDrag } from 'react-use-gesture'
6 | import { clamp } from 'lodash-es'
7 |
8 | import { colors } from '../../../utils/colors'
9 | import { SensorType } from '../../../domain/Button'
10 | import toPercentage from '../../../utils/toPercentage'
11 | import scale from '../../../utils/scale'
12 | import useSensorValueSpring from '../../../utils/useSensorValueSpring'
13 | import useServerStore, {
14 | serverConnectionByAddr
15 | } from '../../../stores/useServerStore'
16 | import { DeviceDescription } from '../../../../../common-types/device'
17 |
18 | const Container = styled.div`
19 | height: 100%;
20 | position: relative;
21 | `
22 |
23 | const ThresholdBar = styled(animated.div)`
24 | background-color: ${colors.thresholdBar};
25 | bottom: 0;
26 | left: 0;
27 | position: absolute;
28 | right: 0;
29 | top: 0;
30 | transform-origin: 50% 100%;
31 | will-change: transform;
32 | `
33 |
34 | const OverThresholdBar = styled(animated.div)`
35 | background-color: ${colors.overThresholdBar};
36 | left: 0;
37 | position: absolute;
38 | right: 0;
39 | bottom: 0;
40 | top: 0;
41 | transform-origin: 50% 100%;
42 | will-change: transform;
43 | `
44 |
45 | const Bar = styled(animated.div)`
46 | background-color: ${colors.sensorBarColor};
47 | bottom: 0;
48 | left: 0;
49 | position: absolute;
50 | right: 0;
51 | top: 0;
52 | transform-origin: 50% 100%;
53 | will-change: transform;
54 | `
55 |
56 | const ThumbContainer = styled(animated.div)`
57 | display: flex;
58 | justify-content: center;
59 | left: 50%;
60 | padding-bottom: ${scale(6)};
61 | padding-top: ${scale(6)};
62 | position: absolute;
63 | transform: translate(-50%, 50%);
64 | user-select: none;
65 | width: 100%;
66 | `
67 |
68 | const Thumb = styled(animated.div)`
69 | background-color: white;
70 | border-radius: 9999px;
71 | color: black;
72 | height: 100%;
73 | line-height: 1;
74 | padding: ${scale(1)} ${scale(1)};
75 | text-align: center;
76 | user-select: none;
77 | `
78 |
79 | interface Props {
80 | serverAddress: string
81 | device: DeviceDescription
82 | sensor: SensorType
83 | enableThresholdChange: boolean
84 | }
85 |
86 | const Sensor = React.memo(
87 | ({ serverAddress, device, sensor, enableThresholdChange }) => {
88 | const serverConnection = useServerStore(
89 | serverConnectionByAddr(serverAddress)
90 | )
91 |
92 | const containerRef = React.useRef(null)
93 | const currentlyDownRef = React.useRef(false)
94 |
95 | const sensorValue = useSensorValueSpring({
96 | serverConnection,
97 | deviceId: device.id,
98 | sensorIndex: sensor.sensorIndex
99 | })
100 |
101 | const [{ value: thresholdValue }, setThresholdValue] = useSpring(() => ({
102 | value: sensor.threshold
103 | }))
104 |
105 | // update threshold whenever it updates on state
106 | React.useEffect(() => {
107 | if (!currentlyDownRef.current) {
108 | setThresholdValue({ value: sensor.threshold, immediate: true })
109 | }
110 | }, [sensor.threshold, setThresholdValue])
111 |
112 | const thumbEnabledSpring = useSpring({
113 | opacity: enableThresholdChange ? 1 : 0,
114 | config: { duration: 100 }
115 | })
116 |
117 | const handleSensorThresholdUpdate = React.useCallback(
118 | (newThreshold: number, store: boolean) => {
119 | if (!serverConnection) {
120 | return
121 | }
122 |
123 | serverConnection.updateSensorThreshold(
124 | device.id,
125 | sensor.sensorIndex,
126 | newThreshold,
127 | store
128 | )
129 | },
130 | [device.id, sensor.sensorIndex, serverConnection]
131 | )
132 |
133 | const [
134 | throttledSensorUpdate,
135 | cancelThrottledSensorUpdate
136 | ] = useDebouncedCallback(handleSensorThresholdUpdate, 100, { maxWait: 250 })
137 |
138 | const bindThumb = useDrag(
139 | ({ down, xy }) => {
140 | if (!containerRef.current || !enableThresholdChange) {
141 | return
142 | }
143 |
144 | const boundingRect = containerRef.current.getBoundingClientRect()
145 | const newValue = clamp(
146 | (boundingRect.height + boundingRect.top - xy[1]) /
147 | boundingRect.height,
148 | 0,
149 | 1
150 | )
151 |
152 | if (down) {
153 | setThresholdValue({ value: newValue, immediate: true })
154 | throttledSensorUpdate(newValue, false)
155 | currentlyDownRef.current = true
156 | } else {
157 | cancelThrottledSensorUpdate()
158 | setThresholdValue({ value: newValue, immediate: true })
159 | handleSensorThresholdUpdate(newValue, true)
160 | currentlyDownRef.current = false
161 | }
162 | },
163 | { dragDelay: true } // try to prevent accidental drags
164 | )
165 |
166 | return (
167 |
168 | `scaleY(${value})`)
171 | }}
172 | />
173 |
174 | `scaleY(${value})`)
177 | }}
178 | />
179 |
180 | {
185 | const translateY = toPercentage(-thresholdValue)
186 | const scaleY = Math.max(sensorValue - thresholdValue, 0)
187 | return `translateY(${translateY}) scaleY(${scaleY})`
188 | }
189 | )
190 | }}
191 | />
192 |
193 |
200 |
201 | {thresholdValue.interpolate(threshold =>
202 | (threshold * 100).toFixed(1)
203 | )}
204 |
205 |
206 |
207 | )
208 | }
209 | )
210 |
211 | export default Sensor
212 |
--------------------------------------------------------------------------------
/firmware/teensy2/AnalogDancePad.c:
--------------------------------------------------------------------------------
1 | /*
2 | Based on LUFA Library example code (www.lufa-lib.org):
3 |
4 | Copyright 2017 Dean Camera (dean [at] fourwalledcubicle [dot] com)
5 |
6 | See other copyrights in LICENSE file on repository root.
7 |
8 | Permission to use, copy, modify, distribute, and sell this
9 | software and its documentation for any purpose is hereby granted
10 | without fee, provided that the above copyright notice appear in
11 | all copies and that both that the copyright notice and this
12 | permission notice and warranty disclaimer appear in supporting
13 | documentation, and that the name of the author not be used in
14 | advertising or publicity pertaining to distribution of the
15 | software without specific, written prior permission.
16 |
17 | The author disclaims all warranties with regard to this
18 | software, including all implied warranties of merchantability
19 | and fitness. In no event shall the author be liable for any
20 | special, indirect or consequential damages or any damages
21 | whatsoever resulting from loss of use, data or profits, whether
22 | in an action of contract, negligence or other tortious action,
23 | arising out of or in connection with the use or performance of
24 | this software.
25 | */
26 |
27 | #include
28 | #include
29 |
30 | #include "Config/DancePadConfig.h"
31 | #include "AnalogDancePad.h"
32 | #include "Communication.h"
33 | #include "Descriptors.h"
34 | #include "Pad.h"
35 | #include "Reset.h"
36 | #include "ConfigStore.h"
37 |
38 | /** Buffer to hold the previously generated HID report, for comparison purposes inside the HID class driver. */
39 | static uint8_t PrevHIDReportBuffer[GENERIC_EPSIZE];
40 |
41 | /** LUFA HID Class driver interface configuration and state information. This structure is
42 | * passed to all HID Class driver functions, so that multiple instances of the same class
43 | * within a device can be differentiated from one another.
44 | */
45 | USB_ClassInfo_HID_Device_t Generic_HID_Interface =
46 | {
47 | .Config =
48 | {
49 | .InterfaceNumber = INTERFACE_ID_GenericHID,
50 | .ReportINEndpoint =
51 | {
52 | .Address = GENERIC_IN_EPADDR,
53 | .Size = GENERIC_EPSIZE,
54 | .Banks = 1,
55 | },
56 | .PrevReportINBuffer = PrevHIDReportBuffer,
57 | .PrevReportINBufferSize = sizeof(PrevHIDReportBuffer),
58 | },
59 | };
60 |
61 | static Configuration configuration;
62 |
63 | /** Main program entry point. This routine contains the overall program flow, including initial
64 | * setup of all components and the main program loop.
65 | */
66 | int main(void)
67 | {
68 | SetupHardware();
69 | GlobalInterruptEnable();
70 | ConfigStore_LoadConfiguration(&configuration);
71 | Pad_Initialize(&configuration.padConfiguration);
72 |
73 | for (;;)
74 | {
75 | HID_Device_USBTask(&Generic_HID_Interface);
76 | USB_USBTask();
77 | }
78 | }
79 |
80 | /** Configures the board hardware and chip peripherals for the demo's functionality. */
81 | void SetupHardware(void)
82 | {
83 | #if (ARCH == ARCH_AVR8)
84 | /* Disable watchdog if enabled by bootloader/fuses */
85 | MCUSR &= ~(1 << WDRF);
86 | wdt_disable();
87 |
88 | /* Disable clock division */
89 | clock_prescale_set(clock_div_1);
90 | #elif (ARCH == ARCH_XMEGA)
91 | /* Start the PLL to multiply the 2MHz RC oscillator to 32MHz and switch the CPU core to run from it */
92 | XMEGACLK_StartPLL(CLOCK_SRC_INT_RC2MHZ, 2000000, F_CPU);
93 | XMEGACLK_SetCPUClockSource(CLOCK_SRC_PLL);
94 |
95 | /* Start the 32MHz internal RC oscillator and start the DFLL to increase it to 48MHz using the USB SOF as a reference */
96 | XMEGACLK_StartInternalOscillator(CLOCK_SRC_INT_RC32MHZ);
97 | XMEGACLK_StartDFLL(CLOCK_SRC_INT_RC32MHZ, DFLL_REF_INT_USBSOF, F_USB);
98 |
99 | PMIC.CTRL = PMIC_LOLVLEN_bm | PMIC_MEDLVLEN_bm | PMIC_HILVLEN_bm;
100 | #endif
101 |
102 | /* Hardware Initialization */
103 | USB_Init();
104 | }
105 |
106 | /** Event handler for the library USB Configuration Changed event. */
107 | void EVENT_USB_Device_ConfigurationChanged(void)
108 | {
109 | HID_Device_ConfigureEndpoints(&Generic_HID_Interface);
110 | USB_Device_EnableSOFEvents();
111 | }
112 |
113 | /** Event handler for the library USB Control Request reception event. */
114 | void EVENT_USB_Device_ControlRequest(void)
115 | {
116 | HID_Device_ProcessControlRequest(&Generic_HID_Interface);
117 | }
118 |
119 | /** Event handler for the USB device Start Of Frame event. */
120 | void EVENT_USB_Device_StartOfFrame(void)
121 | {
122 | HID_Device_MillisecondElapsed(&Generic_HID_Interface);
123 | }
124 |
125 | /** HID class driver callback function for the creation of HID reports to the host.
126 | *
127 | * \param[in] HIDInterfaceInfo Pointer to the HID class interface configuration structure being referenced
128 | * \param[in,out] ReportID Report ID requested by the host if non-zero, otherwise callback should set to the generated report ID
129 | * \param[in] ReportType Type of the report to create, either HID_REPORT_ITEM_In or HID_REPORT_ITEM_Feature
130 | * \param[out] ReportData Pointer to a buffer where the created report should be stored
131 | * \param[out] ReportSize Number of bytes written in the report (or zero if no report is to be sent)
132 | *
133 | * \return Boolean \c true to force the sending of the report, \c false to let the library determine if it needs to be sent
134 | */
135 | bool CALLBACK_HID_Device_CreateHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
136 | uint8_t* const ReportID,
137 | const uint8_t ReportType,
138 | void* ReportData,
139 | uint16_t* const ReportSize)
140 | {
141 | if (*ReportID == 0) {
142 | // no report id requested - write button and sensor data
143 | Communication_WriteInputHIDReport(ReportData);
144 | *ReportID = INPUT_REPORT_ID;
145 | *ReportSize = sizeof (InputHIDReport);
146 | } else if (*ReportID == PAD_CONFIGURATION_REPORT_ID) {
147 | PadConfigurationFeatureHIDReport* configurationHidReport = ReportData;
148 | configurationHidReport->configuration = PAD_CONF;
149 | *ReportSize = sizeof (PadConfigurationFeatureHIDReport);
150 | } else if (*ReportID == NAME_REPORT_ID) {
151 | NameFeatureHIDReport* nameHidReport = ReportData;
152 | memcpy(&nameHidReport->nameAndSize, &configuration.nameAndSize, sizeof (nameHidReport->nameAndSize));
153 | *ReportSize = sizeof (NameFeatureHIDReport);
154 | }
155 |
156 | return true;
157 | }
158 |
159 | /** HID class driver callback function for the processing of HID reports from the host.
160 | *
161 | * \param[in] HIDInterfaceInfo Pointer to the HID class interface configuration structure being referenced
162 | * \param[in] ReportID Report ID of the received report from the host
163 | * \param[in] ReportType The type of report that the host has sent, either HID_REPORT_ITEM_Out or HID_REPORT_ITEM_Feature
164 | * \param[in] ReportData Pointer to a buffer where the received report has been stored
165 | * \param[in] ReportSize Size in bytes of the received HID report
166 | */
167 | void CALLBACK_HID_Device_ProcessHIDReport(USB_ClassInfo_HID_Device_t* const HIDInterfaceInfo,
168 | const uint8_t ReportID,
169 | const uint8_t ReportType,
170 | const void* ReportData,
171 | const uint16_t ReportSize)
172 | {
173 | if (ReportID == PAD_CONFIGURATION_REPORT_ID && ReportSize == sizeof (PadConfigurationFeatureHIDReport)) {
174 | const PadConfigurationFeatureHIDReport* configurationHidReport = ReportData;
175 | memcpy(&configuration.padConfiguration, &configurationHidReport->configuration, sizeof (configuration.padConfiguration));
176 | Pad_UpdateConfiguration(&configurationHidReport->configuration);
177 | } else if (ReportID == RESET_REPORT_ID) {
178 | Reset_JumpToBootloader();
179 | } else if (ReportID == SAVE_CONFIGURATION_REPORT_ID) {
180 | ConfigStore_StoreConfiguration(&configuration);
181 | } else if (ReportID == NAME_REPORT_ID && ReportSize == sizeof (NameFeatureHIDReport)) {
182 | const NameFeatureHIDReport* nameHidReport = ReportData;
183 | memcpy(&configuration.nameAndSize, &nameHidReport->nameAndSize, sizeof (configuration.nameAndSize));
184 | }
185 | }
186 |
--------------------------------------------------------------------------------
/server/src/server.ts:
--------------------------------------------------------------------------------
1 | import consola from 'consola'
2 |
3 | import { ServerEvents, ClientEvents } from '../../common-types/events'
4 | import { Device } from './driver/Device'
5 | import { DeviceDriver } from './driver/Driver'
6 | import { DeviceInputData } from '../../common-types/device'
7 | import { clamp, mapValues } from 'lodash'
8 |
9 | const SECOND_AS_NS = BigInt(1e9)
10 | const INPUT_EVENT_SEND_NS = SECOND_AS_NS / BigInt(20) // 20hz
11 | const INPUT_EVENTS_REQUIRED_FOR_CALIBRATION = 250
12 |
13 | interface Params {
14 | expressApplication: Express.Application
15 | socketIOServer: SocketIO.Server
16 | deviceDrivers: DeviceDriver[]
17 | }
18 |
19 | type DeviceData = {
20 | id: string
21 | device: Device
22 | inputEventTracker: {
23 | lastSent: bigint
24 | accumulatedInputData: DeviceInputData | null
25 | }
26 | calibration: CalibrationStatus
27 | }
28 |
29 | // null = not calibrating
30 | type CalibrationStatus = {
31 | calibrationBuffer: number
32 | inputEventsCalculated: number
33 | currentSensorValueAverage: number[] | null // null = no data yet
34 | } | null
35 |
36 | const createServer = (params: Params) => {
37 | const deviceDataById: {
38 | [deviceId: string]: DeviceData
39 | } = {}
40 |
41 | /* Handlers */
42 |
43 | const getDevicesUpdatedEvent = (): ServerEvents.DevicesUpdated => ({
44 | devices: mapValues(deviceDataById, deviceData => ({
45 | id: deviceData.id,
46 | configuration: deviceData.device.configuration,
47 | properties: deviceData.device.properties,
48 | calibration: deviceData.calibration
49 | ? {
50 | calibrationBuffer: deviceData.calibration.calibrationBuffer
51 | }
52 | : null
53 | }))
54 | })
55 |
56 | const broadcastDevicesUpdated = () => {
57 | params.socketIOServer.emit('devicesUpdated', getDevicesUpdatedEvent())
58 | }
59 |
60 | const handleNewDevice = (device: Device) => {
61 | deviceDataById[device.id] = {
62 | id: device.id,
63 | device: device,
64 | inputEventTracker: {
65 | lastSent: BigInt(0),
66 | accumulatedInputData: null
67 | },
68 | calibration: null
69 | }
70 |
71 | device.on('disconnect', () => handleDisconnectDevice(device.id))
72 | device.on('inputData', data => handleInputData(device.id, data))
73 | device.on('eventRate', number => handleEventRate(device.id, number))
74 |
75 | broadcastDevicesUpdated()
76 |
77 | consola.info(`Connected to a new device id "${device.id}"`, {
78 | properties: device.properties,
79 | configuration: device.configuration
80 | })
81 | }
82 |
83 | const handleDisconnectDevice = (deviceId: string) => {
84 | delete deviceDataById[deviceId]
85 | broadcastDevicesUpdated()
86 | consola.info(`Disconnected from device id "${deviceId}"`)
87 | }
88 |
89 | const doCalibrationTick = async (deviceData: DeviceData, inputData: DeviceInputData) => {
90 | const { device, calibration } = deviceData
91 |
92 | // Calibration not active, do nothing.
93 | if (calibration === null) {
94 | return
95 | }
96 |
97 | if (calibration.currentSensorValueAverage === null) {
98 | calibration.currentSensorValueAverage = [...inputData.sensors]
99 | } else {
100 | // Update cumulative moving average. If not familiar:
101 | // https://en.wikipedia.org/wiki/Moving_average#Cumulative_moving_average
102 | for (let sensorIndex = 0; sensorIndex < device.properties.sensorCount; sensorIndex++) {
103 | const oldAverage = calibration.currentSensorValueAverage[sensorIndex]
104 | const newValue = inputData.sensors[sensorIndex]
105 | const newAverage =
106 | oldAverage + (newValue - oldAverage) / (calibration.inputEventsCalculated + 1)
107 | calibration.currentSensorValueAverage[sensorIndex] = newAverage
108 | }
109 | }
110 |
111 | calibration.inputEventsCalculated++
112 |
113 | // Have we enough data to update?
114 | if (calibration.inputEventsCalculated > INPUT_EVENTS_REQUIRED_FOR_CALIBRATION) {
115 | const sensorThresholds = calibration.currentSensorValueAverage.map(value =>
116 | clamp(value + calibration.calibrationBuffer, 0, 1)
117 | )
118 |
119 | // Remove calibration and update new values.
120 | deviceData.calibration = null
121 | await device.updateConfiguration({ sensorThresholds })
122 | await device.saveConfiguration()
123 | broadcastDevicesUpdated()
124 | }
125 | }
126 |
127 | const doSendInputEventToClient = (data: DeviceData, inputData: DeviceInputData) => {
128 | const inputEventTracker = data.inputEventTracker
129 | const now = process.hrtime.bigint()
130 |
131 | if (inputEventTracker.accumulatedInputData === null) {
132 | // first time receiving sensor values since sending an input event?
133 | inputEventTracker.accumulatedInputData = {
134 | sensors: [...inputData.sensors],
135 | buttons: [...inputData.buttons]
136 | }
137 | } else {
138 | // merge input data to accumulated input data so far.
139 | const inputData = inputEventTracker.accumulatedInputData
140 |
141 | // during accumulation, get the maximum sensor values of all input events received.
142 | for (let sensorIndex = 0; sensorIndex < data.device.properties.sensorCount; sensorIndex++) {
143 | inputEventTracker.accumulatedInputData.sensors[sensorIndex] = Math.max(
144 | inputData.sensors[sensorIndex],
145 | inputEventTracker.accumulatedInputData.sensors[sensorIndex]
146 | )
147 | }
148 |
149 | // during accumulation, show button as pressed if it was pressed at any time during input
150 | // events.
151 | for (let buttonIndex = 0; buttonIndex < data.device.properties.buttonCount; buttonIndex++) {
152 | inputEventTracker.accumulatedInputData.buttons[buttonIndex] =
153 | inputData.buttons[buttonIndex] ||
154 | inputEventTracker.accumulatedInputData.buttons[buttonIndex]
155 | }
156 | }
157 |
158 | // if we need to still to wait before sending an input event, do nothing.
159 | if (inputEventTracker.lastSent + INPUT_EVENT_SEND_NS > now) {
160 | return
161 | }
162 |
163 | const event: ServerEvents.InputEvent = {
164 | deviceId: data.id,
165 | inputData: inputEventTracker.accumulatedInputData
166 | }
167 |
168 | params.socketIOServer.volatile.to(data.id).emit('inputEvent', event)
169 |
170 | inputEventTracker.lastSent = now
171 | inputEventTracker.accumulatedInputData = null
172 | }
173 |
174 | const handleInputData = async (deviceId: string, inputData: DeviceInputData) => {
175 | const deviceData = deviceDataById[deviceId]
176 | doSendInputEventToClient(deviceData, inputData)
177 | await doCalibrationTick(deviceData, inputData)
178 | }
179 |
180 | const handleEventRate = (deviceId: string, rate: number) => {
181 | const event: ServerEvents.EventRate = { deviceId: deviceId, eventRate: rate }
182 | params.socketIOServer.to(deviceId).emit('eventRate', event)
183 | }
184 |
185 | /* Start server. */
186 |
187 | params.deviceDrivers.forEach(dd => {
188 | dd.on('newDevice', handleNewDevice)
189 | dd.start()
190 | })
191 |
192 | params.socketIOServer.on('connection', socket => {
193 | consola.info('New SocketIO connection from', socket.handshake.address)
194 | socket.emit('devicesUpdated', getDevicesUpdatedEvent())
195 |
196 | socket.on('subscribeToDevice', (data: ClientEvents.SubscribeToDevice) => {
197 | consola.info(`Socket "${socket.handshake.address}" subscribed to device "${data.deviceId}"`)
198 | socket.join(data.deviceId)
199 | })
200 |
201 | socket.on('unsubscribeFromDevice', (data: ClientEvents.UnsubscribeFromDevice) => {
202 | consola.info(
203 | `Socket "${socket.handshake.address}" unsubscribed from device "${data.deviceId}"`
204 | )
205 | socket.leave(data.deviceId)
206 | })
207 |
208 | socket.on('updateConfiguration', async (data: ClientEvents.UpdateConfiguration) => {
209 | const device = deviceDataById[data.deviceId].device
210 |
211 | await device.updateConfiguration(data.configuration)
212 | if (data.store) {
213 | await device.saveConfiguration()
214 | }
215 | broadcastDevicesUpdated()
216 |
217 | consola.info(`Device id "${data.deviceId}" configuration updated`, data.configuration)
218 | })
219 |
220 | socket.on('saveConfiguration', async (data: ClientEvents.SaveConfiguration) => {
221 | await deviceDataById[data.deviceId].device.saveConfiguration()
222 | })
223 |
224 | socket.on('updateSensorThreshold', async (data: ClientEvents.UpdateSensorThreshold) => {
225 | const { device } = deviceDataById[data.deviceId]
226 | const sensorThresholds = [...device.configuration.sensorThresholds]
227 | sensorThresholds[data.sensorIndex] = data.newThreshold
228 |
229 | await device.updateConfiguration({ sensorThresholds })
230 | if (data.store) {
231 | await device.saveConfiguration()
232 | }
233 | broadcastDevicesUpdated()
234 |
235 | consola.info(
236 | `Device id "${data.deviceId}" had sensor ${data.sensorIndex} threshold changed to ${data.newThreshold}`
237 | )
238 | })
239 |
240 | socket.on('calibrate', async (data: ClientEvents.Calibrate) => {
241 | const deviceData = deviceDataById[data.deviceId]
242 |
243 | deviceData.calibration = {
244 | calibrationBuffer: data.calibrationBuffer,
245 | currentSensorValueAverage: null,
246 | inputEventsCalculated: 0
247 | }
248 |
249 | broadcastDevicesUpdated()
250 | })
251 |
252 | socket.on('disconnect', (reason: string) => {
253 | consola.info(`Disconnected SocketIO from "${socket.handshake.address}", reason: "${reason}"`)
254 | })
255 | })
256 |
257 | /* Return function to close server resources. */
258 |
259 | return () => {
260 | params.deviceDrivers.forEach(dd => dd.close())
261 | }
262 | }
263 |
264 | export default createServer
265 |
--------------------------------------------------------------------------------
/server/src/driver/teensy2/Teensy2DeviceDriver.ts:
--------------------------------------------------------------------------------
1 | import * as HID from 'node-hid'
2 | import usbDetection from 'usb-detection'
3 | import consola from 'consola'
4 | import PQueue from 'p-queue'
5 |
6 | import { DeviceProperties, DeviceConfiguration } from '../../../../common-types/device'
7 | import { DeviceDriver, DeviceDriverEvents } from '../Driver'
8 | import { DeviceEvents, Device } from '../Device'
9 | import { ReportManager, ReportID } from './Teensy2Reports'
10 | import { ExtendableEmitter } from '../../util/ExtendableStrictEmitter'
11 | import delay from '../../util/delay'
12 | import { clamp } from 'lodash'
13 |
14 | export const VENDOR_ID = 0x03eb
15 | export const PRODUCT_ID = 0x204f
16 |
17 | // in future version, I'd like to device to tell this information
18 | const SENSOR_COUNT = 12
19 | const BUTTON_COUNT = 16
20 |
21 | const reportManager = new ReportManager({ buttonCount: BUTTON_COUNT, sensorCount: SENSOR_COUNT })
22 |
23 | const MAX_SENSOR_VALUE = 850 // Maximum value for a sensor reading. Depends on the used resistors in the setup.
24 | const NTH_DEGREE_COEFFICIENT = 0.9 // Magic number
25 | const FIRST_DEGREE_COEFFICIENT = 0.1 // Magic number jr.
26 | const LINEARIZATION_POWER = 4 // The linearization function degree / power
27 |
28 | const LINEARIZATION_MAX_VALUE = Math.pow(MAX_SENSOR_VALUE, LINEARIZATION_POWER) / MAX_SENSOR_VALUE
29 |
30 | const calculateLinearizationValue = (value: number): number => {
31 | const linearizedValue = Math.pow(value, LINEARIZATION_POWER) / LINEARIZATION_MAX_VALUE
32 | return linearizedValue * NTH_DEGREE_COEFFICIENT + value * FIRST_DEGREE_COEFFICIENT
33 | }
34 |
35 | interface LinearizationValue {
36 | [key: string]: number
37 | }
38 |
39 | const LINEARIZATION_LOOKUP_TABLE: LinearizationValue = {}
40 | const DELINEARIZATION_LOOKUP_TABLE: LinearizationValue = {}
41 | const DELINEARIZATION_LOOKUP_DIGITS = 12
42 |
43 | for (let i = MAX_SENSOR_VALUE; i >= 0; i--) {
44 | const linearizedValue = calculateLinearizationValue(i)
45 | LINEARIZATION_LOOKUP_TABLE[i] = linearizedValue
46 | DELINEARIZATION_LOOKUP_TABLE[Math.floor(linearizedValue)] = i
47 | DELINEARIZATION_LOOKUP_TABLE[linearizedValue.toFixed(DELINEARIZATION_LOOKUP_DIGITS)] = i
48 | }
49 |
50 | const linearizeValue = (value: number) => {
51 | value = clamp(value, 0, MAX_SENSOR_VALUE)
52 |
53 | if (typeof LINEARIZATION_LOOKUP_TABLE[value] === 'undefined') {
54 | return calculateLinearizationValue(value)
55 | }
56 |
57 | return LINEARIZATION_LOOKUP_TABLE[value]
58 | }
59 |
60 | const delinearizeValue = (value: number) => {
61 | value = clamp(value, 0, MAX_SENSOR_VALUE)
62 | const valueStr: string = value.toFixed(DELINEARIZATION_LOOKUP_DIGITS)
63 |
64 | if (typeof DELINEARIZATION_LOOKUP_TABLE[valueStr] === 'undefined') {
65 | value = Math.floor(value)
66 | while (value > 0) {
67 | const lookupValue = DELINEARIZATION_LOOKUP_TABLE[value]
68 | if (typeof lookupValue !== 'undefined') {
69 | return lookupValue
70 | }
71 | value--
72 | }
73 |
74 | return 0
75 | }
76 |
77 | return DELINEARIZATION_LOOKUP_TABLE[valueStr]
78 | }
79 |
80 | const linearizeSensorValues = (numbers: number[]) => numbers.map(linearizeValue)
81 | const delinearizeSensorValues = (numbers: number[]) => numbers.map(delinearizeValue)
82 | const normalizeSensorValues = (numbers: number[]) => numbers.map(n => n / MAX_SENSOR_VALUE)
83 | const denormalizeSensorValues = (numbers: number[]) =>
84 | numbers.map(n => Math.floor(n * MAX_SENSOR_VALUE))
85 |
86 | export class Teensy2Device extends ExtendableEmitter() implements Device {
87 | private device: HID.HID
88 | private path: string
89 | private onClose: () => void
90 | private eventsSinceLastUpdate: number
91 | private eventRateInterval: NodeJS.Timeout
92 | private sendQueue: PQueue
93 |
94 | id: string
95 |
96 | properties: DeviceProperties = {
97 | buttonCount: BUTTON_COUNT,
98 | sensorCount: SENSOR_COUNT
99 | }
100 |
101 | configuration: DeviceConfiguration
102 |
103 | static async fromDevicePath(devicePath: string, onClose: () => void): Promise {
104 | const hidDevice = new HID.HID(devicePath)
105 |
106 | try {
107 | const padConfigurationData = hidDevice.getFeatureReport(
108 | ReportID.PAD_CONFIGURATION,
109 | reportManager.getConfigurationReportSize()
110 | )
111 | const padConfigurationReport = reportManager.parseConfigurationReport(
112 | Buffer.from(padConfigurationData)
113 | )
114 |
115 | const nameData = hidDevice.getFeatureReport(ReportID.NAME, reportManager.getNameReportSize())
116 | const nameReport = reportManager.parseNameReport(Buffer.from(nameData))
117 |
118 | const configuration: DeviceConfiguration = {
119 | name: nameReport.name,
120 | sensorThresholds: normalizeSensorValues(
121 | linearizeSensorValues(padConfigurationReport.sensorThresholds)
122 | ),
123 | releaseThreshold: padConfigurationReport.releaseThreshold,
124 | sensorToButtonMapping: padConfigurationReport.sensorToButtonMapping
125 | }
126 | return new Teensy2Device(devicePath, configuration, hidDevice, onClose)
127 | } catch (e) {
128 | hidDevice.close()
129 | throw e
130 | }
131 | }
132 |
133 | private constructor(
134 | path: string,
135 | configuration: DeviceConfiguration,
136 | device: HID.HID,
137 | onClose: () => void
138 | ) {
139 | super()
140 | this.path = path
141 | this.id = 'teensy-2-device-' + path
142 | this.configuration = configuration
143 | this.device = device
144 | this.onClose = onClose
145 | this.device.on('error', this.handleError)
146 | this.device.on('data', this.handleData)
147 |
148 | // initialize event rate tracking
149 | this.eventRateInterval = setInterval(this.handleEventRateMeasurement, 1000)
150 | this.eventsSinceLastUpdate = 0
151 |
152 | // initialize send queue
153 | this.sendQueue = new PQueue({ concurrency: 1 })
154 | }
155 |
156 | private handleError = (e: Error) => {
157 | consola.error(`Error received from HID device in path "${this.path}":`, e)
158 | this.close()
159 | }
160 |
161 | private handleData = (data: Buffer) => {
162 | this.eventsSinceLastUpdate++
163 | const inputReport = reportManager.parseInputReport(data)
164 |
165 | this.emit('inputData', {
166 | buttons: inputReport.buttons,
167 | sensors: normalizeSensorValues(linearizeSensorValues(inputReport.sensorValues))
168 | })
169 | }
170 |
171 | private handleEventRateMeasurement = () => {
172 | this.emit('eventRate', this.eventsSinceLastUpdate)
173 | this.eventsSinceLastUpdate = 0
174 | }
175 |
176 | // What's the idea here? Well - node-hid doesn't like if we do multiple
177 | // different things (send feature reports, write data) to the same device
178 | // within same millisecond, because USB spec doesn't allow that. So we battle
179 | // this by putting all writes and feature report requests to a queue where
180 | // there will always be at least some milliseconds between events.
181 | private sendEventToQueue = async (event: () => Promise): Promise => {
182 | const promise = this.sendQueue.add(() => event())
183 | this.sendQueue.add(() => delay(2))
184 | return await promise
185 | }
186 |
187 | public async updateConfiguration(updates: Partial) {
188 | const newConfiguration = { ...this.configuration, ...updates }
189 |
190 | // TODO: only send configuration reports that are necessary
191 |
192 | await this.sendEventToQueue(async () => {
193 | const report = reportManager.createConfigurationReport({
194 | releaseThreshold: newConfiguration.releaseThreshold,
195 | sensorThresholds: delinearizeSensorValues(
196 | denormalizeSensorValues(newConfiguration.sensorThresholds)
197 | ),
198 | sensorToButtonMapping: newConfiguration.sensorToButtonMapping
199 | })
200 | this.device.sendFeatureReport(report)
201 | })
202 |
203 | await this.sendEventToQueue(async () => {
204 | const report = reportManager.createNameReport({ name: newConfiguration.name })
205 | this.device.sendFeatureReport(report)
206 | })
207 |
208 | this.configuration = newConfiguration
209 | }
210 |
211 | public async saveConfiguration() {
212 | await this.sendEventToQueue(async () => {
213 | this.device.write(reportManager.createSaveConfigurationReport())
214 | })
215 | }
216 |
217 | close() {
218 | clearInterval(this.eventRateInterval)
219 | this.sendQueue.pause()
220 | this.sendQueue.clear()
221 | this.device.close()
222 | this.onClose()
223 | this.emit('disconnect')
224 | }
225 | }
226 |
227 | export class Teensy2DeviceDriver extends ExtendableEmitter()
228 | implements DeviceDriver {
229 | private knownDevicePaths = new Set()
230 |
231 | private connectDevice = async (devicePath: string) => {
232 | this.knownDevicePaths.add(devicePath)
233 |
234 | try {
235 | const handleClose = () => this.knownDevicePaths.delete(devicePath)
236 | const newDevice = await Teensy2Device.fromDevicePath(devicePath, handleClose)
237 | this.emit('newDevice', newDevice)
238 | } catch (e) {
239 | this.knownDevicePaths.delete(devicePath)
240 | consola.error('Could not connect to a new device:', e)
241 | }
242 | }
243 |
244 | private connectToNewDevices() {
245 | HID.devices().forEach(device => {
246 | // only known devices
247 | if (device.productId !== PRODUCT_ID || device.vendorId !== VENDOR_ID) {
248 | return
249 | }
250 |
251 | const devicePath = device.path
252 |
253 | // this device doesn't have path, so we cannot know whether it's a new one or not. bail out.
254 | if (!devicePath) {
255 | consola.error('New device was detected, but no device path was returned')
256 | return
257 | }
258 |
259 | // this device we know of already. bail out.
260 | if (this.knownDevicePaths.has(devicePath)) {
261 | return
262 | }
263 |
264 | // Linux needs a while from plugging the device in to be able to use it with hidraw. Thus,
265 | // let's wait for a while! Windows doesn't seem to have the same problem, but certainly no
266 | // one is in such a hurry they can't wait a second, right?
267 | setTimeout(() => this.connectDevice(devicePath), 1000)
268 | })
269 | }
270 |
271 | start() {
272 | consola.info('Started Teensy2DeviceDriver, listening for new devices...')
273 | this.knownDevicePaths.clear()
274 |
275 | // first connect to whatever devices are connected to computer now
276 | this.connectToNewDevices()
277 |
278 | // ...and then start monitoring for future changes
279 | usbDetection.on('add', (device: { vendorId: number; productId: number }) => {
280 | if (device.vendorId === VENDOR_ID && device.productId === PRODUCT_ID) {
281 | consola.info('New Teensy2Driver devices detected, connecting...')
282 |
283 | // OSX seems to want to wait a while until it can find the new HID
284 | // device, so let's wait a while.
285 | setTimeout(() => this.connectToNewDevices(), 1000)
286 | }
287 | })
288 | usbDetection.startMonitoring()
289 | }
290 |
291 | close() {
292 | consola.info('Stopped Teensy2DeviceDriver')
293 | usbDetection.stopMonitoring()
294 | }
295 | }
296 |
--------------------------------------------------------------------------------
/firmware/teensy2/Descriptors.c:
--------------------------------------------------------------------------------
1 | /*
2 | Based on LUFA Library example code (www.lufa-lib.org):
3 |
4 | Copyright 2017 Dean Camera (dean [at] fourwalledcubicle [dot] com)
5 |
6 | See other copyrights in LICENSE file on repository root.
7 |
8 | Permission to use, copy, modify, distribute, and sell this
9 | software and its documentation for any purpose is hereby granted
10 | without fee, provided that the above copyright notice appear in
11 | all copies and that both that the copyright notice and this
12 | permission notice and warranty disclaimer appear in supporting
13 | documentation, and that the name of the author not be used in
14 | advertising or publicity pertaining to distribution of the
15 | software without specific, written prior permission.
16 |
17 | The author disclaims all warranties with regard to this
18 | software, including all implied warranties of merchantability
19 | and fitness. In no event shall the author be liable for any
20 | special, indirect or consequential damages or any damages
21 | whatsoever resulting from loss of use, data or profits, whether
22 | in an action of contract, negligence or other tortious action,
23 | arising out of or in connection with the use or performance of
24 | this software.
25 | */
26 |
27 | #include "Descriptors.h"
28 | #include "Communication.h"
29 |
30 | /** HID class report descriptor. This is a special descriptor constructed with values from the
31 | * USBIF HID class specification to describe the reports and capabilities of the HID device. This
32 | * descriptor is parsed by the host and its contents used to determine what data (and in what encoding)
33 | * the device will send, and what it may be sent back from the host. Refer to the HID specification for
34 | * more details on HID report descriptors.
35 | */
36 |
37 | const USB_Descriptor_HIDReport_Datatype_t PROGMEM GenericReport[] =
38 | {
39 | HID_RI_USAGE_PAGE(8, 0x01),
40 | HID_RI_USAGE(8, 0x04),
41 | HID_RI_COLLECTION(8, 0x01),
42 | HID_RI_REPORT_ID(8, INPUT_REPORT_ID),
43 | HID_RI_USAGE_PAGE(8, 0x09),
44 | HID_RI_USAGE_MINIMUM(8, 0x01),
45 | HID_RI_USAGE_MAXIMUM(8, BUTTON_COUNT),
46 | HID_RI_LOGICAL_MINIMUM(8, 0x00),
47 | HID_RI_LOGICAL_MAXIMUM(8, 0x01),
48 | HID_RI_REPORT_SIZE(8, 0x01),
49 | HID_RI_REPORT_COUNT(8, BUTTON_COUNT),
50 | HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
51 | // TODO: padding here if BUTTON_COUNT not divisible by 8
52 | HID_RI_USAGE_PAGE(16, 0xFF00), // vendor usage page
53 | HID_RI_USAGE(8, 0x01),
54 | HID_RI_COLLECTION(8, 0x00),
55 | HID_RI_USAGE(8, 0x01),
56 | HID_RI_LOGICAL_MINIMUM(8, 0x00),
57 | HID_RI_LOGICAL_MAXIMUM(8, 0xFF),
58 | HID_RI_REPORT_SIZE(8, 0x08),
59 | HID_RI_REPORT_COUNT(8, SENSOR_COUNT * 2),
60 | HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
61 | HID_RI_END_COLLECTION(0),
62 |
63 | HID_RI_REPORT_ID(8, PAD_CONFIGURATION_REPORT_ID),
64 | HID_RI_USAGE_PAGE(16, 0xFF00), // vendor usage page
65 | HID_RI_USAGE(8, 0x02),
66 | HID_RI_COLLECTION(8, 0x00),
67 | HID_RI_USAGE(8, 0x02),
68 | HID_RI_LOGICAL_MINIMUM(8, 0x00),
69 | HID_RI_LOGICAL_MAXIMUM(8, 0xFF),
70 | HID_RI_REPORT_SIZE(8, 0x08),
71 | HID_RI_REPORT_COUNT(8, sizeof (PadConfigurationFeatureHIDReport)),
72 | HID_RI_FEATURE(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE | HID_IOF_NON_VOLATILE),
73 | HID_RI_END_COLLECTION(0),
74 |
75 | // how this should be defined exactly?
76 | HID_RI_REPORT_ID(8, RESET_REPORT_ID),
77 | HID_RI_USAGE_PAGE(16, 0xFF00), // vendor usage page
78 | HID_RI_USAGE(8, 0x02),
79 | HID_RI_OUTPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE | HID_IOF_NON_VOLATILE),
80 |
81 | // how this should be defined exactly?
82 | HID_RI_REPORT_ID(8, SAVE_CONFIGURATION_REPORT_ID),
83 | HID_RI_USAGE_PAGE(16, 0xFF00), // vendor usage page
84 | HID_RI_USAGE(8, 0x02),
85 | HID_RI_OUTPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE | HID_IOF_NON_VOLATILE),
86 |
87 | HID_RI_REPORT_ID(8, NAME_REPORT_ID),
88 | HID_RI_USAGE_PAGE(16, 0xFF00), // vendor usage page
89 | HID_RI_USAGE(8, 0x02),
90 | HID_RI_COLLECTION(8, 0x00),
91 | HID_RI_USAGE(8, 0x02),
92 | HID_RI_LOGICAL_MINIMUM(8, 0x00),
93 | HID_RI_LOGICAL_MAXIMUM(8, 0xFF),
94 | HID_RI_REPORT_SIZE(8, 0x08),
95 | HID_RI_REPORT_COUNT(8, sizeof (NameFeatureHIDReport)),
96 | HID_RI_FEATURE(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE | HID_IOF_NON_VOLATILE),
97 | HID_RI_END_COLLECTION(0),
98 |
99 | // unused joystick report. we only report this because stepmania uses
100 | // old joystick interface on linux if device doesn't have any analog
101 | // axis.
102 | HID_RI_REPORT_ID(8, UNUSED_ANALOG_JOYSTICK_REPORT_ID),
103 | HID_RI_USAGE_PAGE(8, 0x01),
104 | HID_RI_USAGE(8, 0x04),
105 | HID_RI_COLLECTION(8, 0x00),
106 | HID_RI_USAGE(8, 0x30), // X axis
107 | HID_RI_LOGICAL_MINIMUM(16, 0),
108 | HID_RI_LOGICAL_MAXIMUM(16, 127),
109 | HID_RI_PHYSICAL_MINIMUM(16, 0),
110 | HID_RI_PHYSICAL_MAXIMUM(16, 127),
111 | HID_RI_REPORT_COUNT(8, 1),
112 | HID_RI_REPORT_SIZE(8, 8),
113 | HID_RI_INPUT(8, HID_IOF_DATA | HID_IOF_VARIABLE | HID_IOF_ABSOLUTE),
114 | HID_RI_END_COLLECTION(0),
115 | HID_RI_END_COLLECTION(0)
116 | };
117 |
118 | /** Device descriptor structure. This descriptor, located in FLASH memory, describes the overall
119 | * device characteristics, including the supported USB version, control endpoint size and the
120 | * number of device configurations. The descriptor is read out by the USB host when the enumeration
121 | * process begins.
122 | */
123 | const USB_Descriptor_Device_t PROGMEM DeviceDescriptor =
124 | {
125 | .Header = {.Size = sizeof(USB_Descriptor_Device_t), .Type = DTYPE_Device},
126 |
127 | .USBSpecification = VERSION_BCD(1,1,0),
128 | .Class = USB_CSCP_NoDeviceClass,
129 | .SubClass = USB_CSCP_NoDeviceSubclass,
130 | .Protocol = USB_CSCP_NoDeviceProtocol,
131 |
132 | .Endpoint0Size = FIXED_CONTROL_ENDPOINT_SIZE,
133 |
134 | .VendorID = 0x03EB,
135 | .ProductID = 0x204F,
136 | .ReleaseNumber = VERSION_BCD(0,0,1),
137 |
138 | .ManufacturerStrIndex = STRING_ID_Manufacturer,
139 | .ProductStrIndex = STRING_ID_Product,
140 | .SerialNumStrIndex = NO_DESCRIPTOR,
141 |
142 | .NumberOfConfigurations = FIXED_NUM_CONFIGURATIONS
143 | };
144 |
145 | /** Configuration descriptor structure. This descriptor, located in FLASH memory, describes the usage
146 | * of the device in one of its supported configurations, including information about any device interfaces
147 | * and endpoints. The descriptor is read out by the USB host during the enumeration process when selecting
148 | * a configuration so that the host may correctly communicate with the USB device.
149 | */
150 | const USB_Descriptor_Configuration_t PROGMEM ConfigurationDescriptor =
151 | {
152 | .Config =
153 | {
154 | .Header = {.Size = sizeof(USB_Descriptor_Configuration_Header_t), .Type = DTYPE_Configuration},
155 |
156 | .TotalConfigurationSize = sizeof(USB_Descriptor_Configuration_t),
157 | .TotalInterfaces = 1,
158 |
159 | .ConfigurationNumber = 1,
160 | .ConfigurationStrIndex = NO_DESCRIPTOR,
161 |
162 | .ConfigAttributes = (USB_CONFIG_ATTR_RESERVED | USB_CONFIG_ATTR_SELFPOWERED),
163 |
164 | .MaxPowerConsumption = USB_CONFIG_POWER_MA(100)
165 | },
166 |
167 | .HID_Interface =
168 | {
169 | .Header = {.Size = sizeof(USB_Descriptor_Interface_t), .Type = DTYPE_Interface},
170 |
171 | .InterfaceNumber = INTERFACE_ID_GenericHID,
172 | .AlternateSetting = 0x00,
173 |
174 | .TotalEndpoints = 1,
175 |
176 | .Class = HID_CSCP_HIDClass,
177 | .SubClass = HID_CSCP_NonBootSubclass,
178 | .Protocol = HID_CSCP_NonBootProtocol,
179 |
180 | .InterfaceStrIndex = NO_DESCRIPTOR
181 | },
182 |
183 | .HID_GenericHID =
184 | {
185 | .Header = {.Size = sizeof(USB_HID_Descriptor_HID_t), .Type = HID_DTYPE_HID},
186 |
187 | .HIDSpec = VERSION_BCD(1,1,1),
188 | .CountryCode = 0x00,
189 | .TotalReportDescriptors = 1,
190 | .HIDReportType = HID_DTYPE_Report,
191 | .HIDReportLength = sizeof(GenericReport)
192 | },
193 |
194 | .HID_ReportINEndpoint =
195 | {
196 | .Header = {.Size = sizeof(USB_Descriptor_Endpoint_t), .Type = DTYPE_Endpoint},
197 |
198 | .EndpointAddress = GENERIC_IN_EPADDR,
199 | .Attributes = (EP_TYPE_INTERRUPT | ENDPOINT_ATTR_NO_SYNC | ENDPOINT_USAGE_DATA),
200 | .EndpointSize = GENERIC_EPSIZE,
201 | .PollingIntervalMS = 0x01 // = 1000ms, important!
202 | },
203 | };
204 |
205 | /** Language descriptor structure. This descriptor, located in FLASH memory, is returned when the host requests
206 | * the string descriptor with index 0 (the first index). It is actually an array of 16-bit integers, which indicate
207 | * via the language ID table available at USB.org what languages the device supports for its string descriptors.
208 | */
209 | const USB_Descriptor_String_t PROGMEM LanguageString = USB_STRING_DESCRIPTOR_ARRAY(LANGUAGE_ID_ENG);
210 |
211 | /** Manufacturer descriptor string. This is a Unicode string containing the manufacturer's details in human readable
212 | * form, and is read out upon request by the host when the appropriate string ID is requested, listed in the Device
213 | * Descriptor.
214 | */
215 | const USB_Descriptor_String_t PROGMEM ManufacturerString = USB_STRING_DESCRIPTOR(L"Kauhsan koodipaja");
216 |
217 | /** Product descriptor string. This is a Unicode string containing the product's details in human readable form,
218 | * and is read out upon request by the host when the appropriate string ID is requested, listed in the Device
219 | * Descriptor.
220 | */
221 | const USB_Descriptor_String_t PROGMEM ProductString = USB_STRING_DESCRIPTOR(L"Hieno tanssi padi softa juttu");
222 |
223 | /** This function is called by the library when in device mode, and must be overridden (see library "USB Descriptors"
224 | * documentation) by the application code so that the address and size of a requested descriptor can be given
225 | * to the USB library. When the device receives a Get Descriptor request on the control endpoint, this function
226 | * is called so that the descriptor details can be passed back and the appropriate descriptor sent back to the
227 | * USB host.
228 | */
229 | uint16_t CALLBACK_USB_GetDescriptor(const uint16_t wValue,
230 | const uint16_t wIndex,
231 | const void** const DescriptorAddress)
232 | {
233 | const uint8_t DescriptorType = (wValue >> 8);
234 | const uint8_t DescriptorNumber = (wValue & 0xFF);
235 |
236 | const void* Address = NULL;
237 | uint16_t Size = NO_DESCRIPTOR;
238 |
239 | switch (DescriptorType)
240 | {
241 | case DTYPE_Device:
242 | Address = &DeviceDescriptor;
243 | Size = sizeof(USB_Descriptor_Device_t);
244 | break;
245 | case DTYPE_Configuration:
246 | Address = &ConfigurationDescriptor;
247 | Size = sizeof(USB_Descriptor_Configuration_t);
248 | break;
249 | case DTYPE_String:
250 | switch (DescriptorNumber)
251 | {
252 | case STRING_ID_Language:
253 | Address = &LanguageString;
254 | Size = pgm_read_byte(&LanguageString.Header.Size);
255 | break;
256 | case STRING_ID_Manufacturer:
257 | Address = &ManufacturerString;
258 | Size = pgm_read_byte(&ManufacturerString.Header.Size);
259 | break;
260 | case STRING_ID_Product:
261 | Address = &ProductString;
262 | Size = pgm_read_byte(&ProductString.Header.Size);
263 | break;
264 | }
265 |
266 | break;
267 | case HID_DTYPE_HID:
268 | Address = &ConfigurationDescriptor.HID_GenericHID;
269 | Size = sizeof(USB_HID_Descriptor_HID_t);
270 | break;
271 | case HID_DTYPE_Report:
272 | Address = &GenericReport;
273 | Size = sizeof(GenericReport);
274 | break;
275 | }
276 |
277 | *DescriptorAddress = Address;
278 | return Size;
279 | }
280 |
281 |
--------------------------------------------------------------------------------