Skip to content

Commit

Permalink
Merge pull request #26 from PepperDash/feature/documentation
Browse files Browse the repository at this point in the history
Feature/documentation
  • Loading branch information
ndorin authored Jul 23, 2024
2 parents 8b9045b + dcb5503 commit 5197adc
Show file tree
Hide file tree
Showing 43 changed files with 262 additions and 95 deletions.
53 changes: 31 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
# React + TypeScript + Vite
# Mobile Control React App Core Library

This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
This library uses vite for React.

Currently, two official plugins are available:
The purpose of this library is to provide the necessary building blocks to make developing React apps for Mobile Control easier and more efficient.

- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
Primarily, it provides a series of React hooks that handle API interaction with corresponding messengers in the [Essentials Mobile Control Plugin](https://github.com/PepperDash/epi-essentials-mobile-control) (or other Essentials plugins) as well as typescript interfaces for the device state objects that each messenger uses.

## Expanding the ESLint configuration
In addition, it also provides some core React components that can be consumed by a React App and deal with the particulars of running an HTML5 app on a touch device and allow interaction between button events to achieve press/hold/release functionality commonly necessary for things like ramping volume controls or sending IR commands.

If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
At the core of this library is a React Context that is intended to generate a websocket client that handles all the messaging to and from the Mobile Control Plugin. Along with that context, Redux is used to handle maintining the state for the room and all devices relevant to the current session. Redux selectors are provided for accessing common data in the store by room or device.

- Configure the top-level `parserOptions` property like this:
## How Mobile Control Works

```js
export default {
// other rules...
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: ['./tsconfig.json', './tsconfig.node.json'],
tsconfigRootDir: __dirname,
},
}
```
Mobile control uses a websocket to allow JSON formatted messages to be passed between the client app, running in React, and the Essentials Control System application.

- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked`
- Optionally add `plugin:@typescript-eslint/stylistic-type-checked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list
When the client initially connects to the control system, it makes an HTTP call to join a particular room. The response to that call provides the React app with the necessary information to connect to the websocket server. The server can either be running directly on the Crestron control system processor or it can also be running on the Mobile Control Edge Server, which then acts as a message relay to the control system.

Once a websocket connection is established, the React app must then request the room state and configuration which it will subsequently use to determine which device states it needs to request from the control system.

Supplied in this library is a hook (useGetAllDeviceStateFromRoomConfiguration) that takes a RoomConfiguration object and will then iterate the devices for that room and request their current full status. This hook should be called at the root level of the React App.

From that point forward, room and device state messages from the control system will be partial objects and will get overlayed onto the room/device state object in the store.

## How this Library is Intended to be Used

The intent is to use the various hooks provided in this library to easily link up to buttons and other elements on an HTML5 UI in a React App to handle the heavy lifting of integrating with the Mobile Control API. This library will provide hooks for the messengers in the Mobile Control plugin, but you will also be able to write hooks in the React App for custom API that can integrate directly with a messenger defined in any Essentials room or device plugin. This allows consistency with the core communication that most systems will rely on, while also providing easy customization and extensibility for more esoteric or application specific needs that may not see wide use and justify inclusion in this library.

# How to Set up Your Development Environment

Run `npm install` to install the required dependencies.

Load an instance of an Essentials v2.x program as well as the Essentials Mobile Control plugin v4.x to a Crestron program slot. Consult the Mobile Control plugin for direct server plugin configuration instructions.

Copy the `/public/_local-config/_config.default.json` to a file called `/public/_local-config/_config.local.json` and modify the `apiPath` value to point to the IP address of your test processor as well as the correct port. This file should *not* be added to the git repository because it applies only to your local test environment.

Run the app with `npm run dev` which will start the development server. The vite development server will then print it's local URL (eg: http://localhost:5173/mc/app). Open this URL in a browser. Initially you will just get a message saying that the client is disconnected. You will need to supply a token in the url as a search param that will associate your client with a particular client instance on the Mobile Control server. To get a token, run the `mobileinfo:[programSlot]` console command on the Crestron processor and copy the token value associated with the client instance you want your development client to connect to and paste it into the url as the token value (eg: http://localhost:5173/mc/app?token=[some-token-value]).

Your development client instance should now connect to the websocket server running on the Crestron program.
8 changes: 5 additions & 3 deletions src/lib/shared/hooks/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
export * from "./interfaceNames";
export * from "./useAvrControl";
export * from "./useDevicePresetsModel";
export * from "./useEndpoint";
export * from "./useIBasicVolumeWithFeedback";
export * from "./useIChannelMessenger";
export * from "./useIChannel";
export * from "./useIColor";
export * from "./useICommunicationMonitor";
export * from "./useIDPad";
Expand All @@ -19,15 +20,16 @@ export * from "./useIMcCiscoCodecUserInterfaceAppControl";
export * from "./useINumeric";
export * from "./useIProjectorScreenLiftControl";
export * from "./useIRoomEventSchedule";
export * from "./useIRunDefaultPresentRoute";
export * from "./useIRunDirectRouteAction";
export * from "./useIRunRouteAction";
export * from "./useISetTopBoxcontrols";
export * from "./useIShadesOpenCloseStop";
export * from "./useIShutdownPromptTimer";
export * from "./useISwitchedOutput";
export * from "./useITechPassword";
export * from "./useITheme";
export * from "./useITransport";
export * from "./useMobileControlTouchpanelController";
export * from "./useTwoWayDisplayBase";
export * from "./useIRunDefaultPresentRoute";
export * from "./useITheme";

6 changes: 6 additions & 0 deletions src/lib/shared/hooks/interfaces/useAvrControl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import { IHasPowerWithFeedbackProps, useIHasPowerControl } from './useIHasPowerC
import { IHasSelectableItemsReturn, useIHasSelectableItems } from './useIHasSelectableItems';
import { IHasSurroundChannelsReturn, useIHasSurroundChannels } from './useIHasSurroundChannels';


/**
* Provides a set of hooks to control an AVR device
* @param key key of the device
* @returns
*/
export function useAvrControl(key: string): AvrReturn | undefined {
const avrState = useGetDevice<PowerState>(key);
const powerControl = useIHasPowerControl(key);
Expand Down
5 changes: 4 additions & 1 deletion src/lib/shared/hooks/interfaces/useCameraBase.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { CameraState, PressHoldReleaseReturn, useGetDevice, useWebsocketContext } from 'src/lib';
import { useButtonHeldHeartbeat } from '../useHeldButtonAction';


/**
* Provides a set of hooks to control a device that extends the CameraBase class
* @param key key of the device
*/
export function useCameraBase(key: string): CameraBaseProps | undefined{
const { sendMessage } = useWebsocketContext();
const path = `/device/${key}`;
Expand Down
29 changes: 29 additions & 0 deletions src/lib/shared/hooks/interfaces/useDevicePresetsModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DevicePresetsState, PresetChannel, useGetDevice } from 'src/lib';
import { useWebsocketContext } from 'src/lib/utils';


export function useIDevicePresetsModel(key: string): IDevicePresetsModelProps | undefined {
const { sendMessage } = useWebsocketContext();
const state = useGetDevice<DevicePresetsState>(key);
const path = `/device/${key}`;

if (!state) return undefined;

const recallPreset = (deviceKey: string, preset: PresetChannel) => {
sendMessage(`${path}/recall`, {deviceKey, preset});
}

const savePresets = (presets: PresetChannel[]) => {
sendMessage(`${path}/save`, presets);
}


return { state, recallPreset, savePresets };
}

export interface IDevicePresetsModelProps {
state: DevicePresetsState;
recallPreset: (deviceKey: string, preset: PresetChannel) => void;
savePresets: (presets: PresetChannel[]) => void;
}

6 changes: 5 additions & 1 deletion src/lib/shared/hooks/interfaces/useEndpoint.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { useGetDevice } from 'src/lib';
import { EndpointState } from 'src/lib/types/state/state/endpointState/endpointState';


/**
* A hook that provides access to the endpoint state
* @param key the key of the endpoint
* @returns
*/
export function useEndpoint(key: string): IEndpointReturn | undefined {

const endpointState = useGetDevice<EndpointState>(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { useWebsocketContext } from "src/lib/utils/useWebsocketContext";
import { useButtonHeldHeartbeat } from "../useHeldButtonAction";
import { PressHoldReleaseReturn } from "../usePressHoldRelease";

/**
* hook to control a volume device that implements the IBasicVolumeWithFeedback interface
* @param path path prefix to for the device. i.e. /device/{key} or /room/{key}
* @param volumeState
* @returns
*/
export function useIBasicVolumeWithFeedback(
path: string, volumeState: Volume | undefined
): IBasicVolumeWithFeedbackReturn | undefined {
Expand Down Expand Up @@ -43,6 +49,7 @@ export interface IBasicVolumeWithFeedbackReturn {
muteOff: () => void;
}


export function useGetIBasicVolumeWithFeedback(
path: string, volumeState: Volume | undefined
): IBasicVolumeWithFeedbackReturn | undefined {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { useButtonHeldHeartbeat } from '../useHeldButtonAction';
import { PressHoldReleaseReturn } from '../usePressHoldRelease';

export function useIChannelMessenger(key: string): IChannelMessengerProps | undefined {
/**
* hook to control a channel messenger device that implements the IChannelMessenger interface
* @param key the key of the device
* @returns
*/
export function useIChannel(key: string): IChannelMessengerProps | undefined {

const path = `/device/${key}`;

Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useIColor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useButtonHeldHeartbeat } from '../useHeldButtonAction';
import { PressHoldReleaseReturn } from '../usePressHoldRelease';

/**
* hook to control a device that implements the IColor interface
* @param key
* @returns
*/
export function useIColor(key: string): IColorProps | undefined {
const path = `/device/${key}`;

Expand Down
6 changes: 6 additions & 0 deletions src/lib/shared/hooks/interfaces/useICommunicationMonitor.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { CommunicationMonitorState, useGetDevice } from 'src/lib';


/**
* hook to control a device that implements the ICommunicationMonitor interface
* @param key key of the device
* @returns
*/
export function useICommunicationMonitor(key: string): ICommunicationMonitorReturn | undefined {
const device = useGetDevice<CommunicationMonitorState>(key);

Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useIDPad.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useButtonHeldHeartbeat } from "../useHeldButtonAction";
import { PressHoldReleaseReturn } from "../usePressHoldRelease";

/**
* hook to control a device that implements the IDPad interface
* @param key key of the device
* @returns
*/
export function useIDPad(key: string): IDPadProps | undefined {
const path = `/device/${key}`;

Expand Down
6 changes: 5 additions & 1 deletion src/lib/shared/hooks/interfaces/useIDeviceInfoMessenger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { DeviceInfo, DeviceInfoState, useGetDevice } from 'src/lib';


/**
* hook that returns the info for a device that implements the IDeviceInfo interface
* @param key key of the device
* @returns
*/
export function useIDeviceInfoMessenger(key: string): DeviceInfo | undefined {
const device = useGetDevice<DeviceInfoState>(key);

Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useIDspPresets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { IKeyName, useWebsocketContext } from 'src/lib';

/**
* hook to control a device that implements the IDspPresets interface
* @param key key of the device
* @returns
*/
export function useIDspPresets(key: string) {
const { sendMessage } = useWebsocketContext();

Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useIDvr.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { useButtonHeldHeartbeat } from '../useHeldButtonAction';
import { PressHoldReleaseReturn } from '../usePressHoldRelease';

/**
* hook to control a device that implements the IDvr interface
* @param key key of the device
* @returns
*/
export function useIDvr(key: string): IDvrProps | undefined {
const path = `/device/${key}`;

Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useIEssentialsRoomCombiner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { useGetDevice } from 'src/lib/store';
import { IEssentialsRoomCombinerState } from 'src/lib/types/state/state/IEssentialsRoomCombinerState';
import { useWebsocketContext } from 'src/lib/utils';

/**
* hook to control a device that implements the IEssentialsRoomCombiner interface
* @param key key of the device
* @returns
*/
export function useIEssentialsRoomCombiner(key: string): IEssentialsRoomCombinerReturn | undefined {
const { sendMessage } = useWebsocketContext();
const roomCombinerState = useGetDevice(key) as IEssentialsRoomCombinerState | undefined;
Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useIHasPowerControl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useWebsocketContext } from 'src/lib/utils/useWebsocketContext';

/**
* hook to control a device that implements the IHasPowerControl interface
* @param key key of the device
* @returns
*/
export function useIHasPowerControl(key: string): IHasPowerWithFeedbackProps {
const { sendMessage } = useWebsocketContext();

Expand Down
3 changes: 3 additions & 0 deletions src/lib/shared/hooks/interfaces/useIHasSelectableItems.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { useGetDevice } from 'src/lib/store';
import { useWebsocketContext } from 'src/lib/utils/useWebsocketContext';


/**
* Hook for devices that have selectable items
* TState is the type of the expected state of the device
* @param key key of the device
* @returns
*/
export function useIHasSelectableItems<TState>(key: string): IHasSelectableItemsReturn<TState> | undefined {
const { sendMessage } = useWebsocketContext();
Expand Down
6 changes: 5 additions & 1 deletion src/lib/shared/hooks/interfaces/useIHasSurroundChannels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { LevelControlsState, Volume, useGetDevice } from 'src/lib';
import { useWebsocketContext } from 'src/lib/utils/useWebsocketContext';


/**
* hook to control a device that implements the IHasSurroundChannels interface
* @param key key of the device
* @returns
*/
export function useIHasSurroundChannels(key: string): IHasSurroundChannelsReturn | undefined {
const { sendMessage } = useWebsocketContext();

Expand Down
6 changes: 5 additions & 1 deletion src/lib/shared/hooks/interfaces/useILevelControls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { useGetDevice, useRoomLevelControls } from 'src/lib/store';
import { LevelControlsState } from 'src/lib/types/state/state/LevelControlsState';
import { useWebsocketContext } from 'src/lib/utils/useWebsocketContext';


/**
* hook that controls a device that implements the ILevelControls interface
* @param key key of the device
* @returns
*/
export function useILevelControls(key: string): ILevelControlsReturn | undefined {
const { sendMessage, sendSimpleMessage } = useWebsocketContext();
const device = useGetDevice<LevelControlsState>(key);
Expand Down
5 changes: 5 additions & 0 deletions src/lib/shared/hooks/interfaces/useILightingScenes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ import { useGetDevice } from 'src/lib/store';
import { LightingScene, LightingState } from 'src/lib/types';
import { useWebsocketContext } from 'src/lib/utils/useWebsocketContext';

/**
* hook to control a device that implements the ILightingScenes interface
* @param key key of the device
* @returns
*/
export function useILightingScenes(key: string): ILightingScenesReturn | undefined {
const { sendMessage } = useWebsocketContext();
const state = useGetDevice<LightingState>(key);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { useWebsocketContext } from 'src/lib/utils';

/**
* hook to control a device that implements the IMcCiscoCodecUserInterfaceAppControl interface
* @param key key of the device
* @returns
*/
export function useIMcCiscoCodecUserInterfaceAppControl(key: string) {
const { sendMessage } = useWebsocketContext();

Expand Down
2 changes: 1 addition & 1 deletion src/lib/shared/hooks/interfaces/useINumeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useButtonHeldHeartbeat } from '../useHeldButtonAction';
import { PressHoldReleaseReturn } from '../usePressHoldRelease';

/**
* hook to return the functions to trigger for a numeric keypad
* hook to return the functions to trigger for a numeric keypad for a device that implements the INumeric interface
* @param key device key
* @returns
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { IProjectorScreenLiftControlState, useGetDevice, useWebsocketContext } from 'src/lib';


/**
* hook to control a device that implements the IProjectorScreenLiftControl interface
* @param key key of the device
* @returns
*/
export function useIProjectorScreenLiftControl(key: string): IProjectorScreenLiftControlReturn | undefined {
const { sendMessage } = useWebsocketContext();
const projectorScreenLiftControlState = useGetDevice(key) as IProjectorScreenLiftControlState | undefined;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/shared/hooks/interfaces/useIRoomEventSchedule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ScheduleEvent, ScheduleState, useRoomState } from 'src/lib';
import { useWebsocketContext } from 'src/lib/utils/useWebsocketContext';


/**
* hook to control a device that implements the IRoomEventSchedule interface
* @param key key of the device
* @returns
*/
export function useIRoomEventSchedule(key: string): IRoomEventScheduleReturn | undefined {
const { sendMessage } = useWebsocketContext();
const roomEventScheduleState = useRoomState(key) as ScheduleState | undefined;
Expand Down
Loading

0 comments on commit 5197adc

Please sign in to comment.