├── .env.development
├── .env.production
├── .gitignore
├── README.md
├── config
└── contract.cse2j
├── contracts
├── ContractExample.cce
└── output
│ └── ContractExample
│ ├── interface
│ ├── HTML
│ │ └── HomePage.html
│ └── mapping
│ │ └── ContractExample.cse2j
│ └── programming
│ ├── SIMPL
│ └── ContractExample.chd
│ └── SIMPLSharp
│ └── ContractExample
│ ├── ComponentMediator.g.cs
│ ├── Contract.g.cs
│ ├── HomePage.g.cs
│ └── UIEventArgs.g.cs
├── eslint.config.js
├── index.html
├── package-lock.json
├── package.json
├── src
├── App.tsx
├── assets
│ ├── css
│ │ └── App.css
│ └── images
│ │ ├── favicon.ico
│ │ ├── screenshot_narrow.jpg
│ │ ├── screenshot_wide.jpg
│ │ └── vite.png
├── globals.d.ts
├── hooks
│ └── useWebXPanel.ts
├── main.tsx
└── vite-env.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_APP_ENV=development
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_APP_ENV=production
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | archive
13 | dist-ssr
14 | *.local
15 |
16 | # Editor directories and files
17 | .vscode/*
18 | !.vscode/extensions.json
19 | .idea
20 | .DS_Store
21 | *.suo
22 | *.ntvs*
23 | *.njsproj
24 | *.sln
25 | *.sw?
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CH5 Vite+React+TypeScript SPA project demo
2 |
3 | This project is a minimal demonstration of turning a Vite project with React + TypeScript acting as a single page application into a CH5 project.
4 |
5 | ## Usage
6 | Run `npm i` to install all dependencies.
7 |
8 | Run the `build` script to build the project.
9 |
10 | Run the `archive` script to package the contents of the /dist/ directory into a .ch5z that can be loaded onto a control system or panel.
11 |
12 | Run the `deploy:xpanel` script to upload the .ch5z to the control system as a WebXPanel. Adjust the IP address to match your control system.
13 |
14 | Run the `deploy:panel` script to upload the .ch5z to a touch panel as local project. Adjust the IP address to match your panel.
15 |
16 | ## Requirements
17 | - You must have Node.js 20.04.0 or higher and NPM 9.7.2 or higher. For more information see [System Requirements](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/QS-System-Requirements.htm)
18 | - The control system must have SSL and authentication enabled. For more information see [Control System Configuration](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/Platforms/X-CS-Settings.htm)
19 | - At the time of writing CH5 projects are only supported on 3 and 4-series processors (including VC-4), TST-1080, X60, and X70 panels, and the Crestron One app. For more information see [System Requirements](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/QS-System-Requirements.htm)
20 |
21 | ## Authentication
22 | Historically authenticating a CH5 session is handled by a redirect initiated by the WebXPanel library to the processor/server authentication service. However since CH5 2.8.0 an authentication token can be created on the processor/server instead of requiring manual user input for authentication. For processors (4-series only) this is handled via the ```websockettoken generate``` command. On VirtualControl servers the token is generated in the [web interface](https://docs.crestron.com/en-us/8912/content/topics/configuration/Web-Configuration.htm?#Tokens)
23 |
24 | ## The entry point
25 | The entry point is where the Crestron libraries (UMD) will be loaded into the application. In this demo index.html is treated as the entry point for the Crestron libraries.
26 |
27 | ## Contracts
28 | A [contract](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/CE-Overview.htm) is a document that defines how the elements in a UI will interact with a control system program. The Contract Editor outputs programming files for SIMPL Windows and C#, as well as an interface (.cse2j) file for a UI project to reference to interact with said programming files. This allows a UI and program to use descriptive names in a program; so rather than "digital join 1" a UI can reference "HomePage.Lighting.AllOff", which is much easier to read and understand at a glance.
29 |
30 | A contract interface (.cse2j) file can be used at both build and run time. At build time the contract file is referenced by the CH5 `archive` script (see [package.json](package.json)), while at run time it must be placed in `/config/contract.cse2j` (named exactly) at the root level of the served files.
31 |
32 | ## CH5 Web Components
33 | The Crestron HTML5 library (CH5, CrComLib) includes [purpose built HTML5 tags](https://sdkcon78221.crestron.com/sdk/Crestron_HTML5UI/Content/Topics/UI-QS-Common-Attribute-Property.htm) (web components) that can be included in HTML markup. These web components include things like `ch5-button` and `ch5-dpad`. In an HTML5 project that uses a standard framework such as React these components are not generally necessary and aren't as flexible as JSX. However, there are some components that cannot be reproduced in React such as the `ch5-video` for displaying RTSP streams on Crestron touchpanels and the Crestron One app. For that reason this project does support the use of CH5 components. The [globals.d.ts](/src/globals.d.ts) file contains bindings to map the CH5 web components to the JSX interface so they can be used without error and maintain type safety.
34 |
35 | ## Progressive Web App (PWA)
36 | Progressive web apps are HTML5 apps that can be "installed" on devices from browsers onto PC/Mac/iOS/Android devices. Browsers require strict security to perform this - so the server must serve the project via HTTPS and the server certificate must be signed by a CA that is trusted by the client device. See the wiki for this demo which details [how to create a certificate chain](https://github.com/jphillipsCrestron/ch5-react-ts-template/wiki/Creating-a-certificate-chain-(for-PWA)). PWA's are defined via a manifest JSON file which describes the PWA, and a service worker js file that is responsible for caching files for later retrieval in the event that the device loses network connectivity to the server so the user will still see the project.
37 |
38 | This demo uses the [vite-plugin-pwa](https://github.com/vite-pwa/vite-plugin-pwa) package to generate a manifest and service worker with the parameters defined in the [vite.config.ts](vite.config.ts) file, which goes into more detail about each parameter and requirements. For more information on PWA's in general, refer to [MDN](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps).
39 |
40 | ### Initialize the WebXPanel library if running in a browser:
41 | ```ts
42 | const webXPanelConfig = useMemo(() => ({
43 | ipId: '0x03',
44 | host: '0.0.0.0',
45 | roomId: '',
46 | authToken: ''
47 | }), []);
48 |
49 | useWebXPanel(webXPanelConfig);
50 | ```
51 |
52 | ### Receive data via joins from the control system:
53 | ```ts
54 | useEffect(() => {
55 | const d1Id = window.CrComLib.subscribeState('b', '1', (value: boolean) => setDigitalState(value));
56 | const a1Id = window.CrComLib.subscribeState('n', '1', (value: number) => setAnalogState(value));
57 | const s1Id = window.CrComLib.subscribeState('s', '1', (value: string) => setSerialState(value));
58 |
59 | // Contracts
60 | const dc1Id = window.CrComLib.subscribeState('b', 'HomePage.DigitalState', (value: boolean) => setDigitalContractState(value));
61 | const ac1Id = window.CrComLib.subscribeState('n', 'HomePage.AnalogState', (value: number) => setAnalogContractState(value));
62 | const sc1Id = window.CrComLib.subscribeState('s', 'HomePage.StringState', (value: string) => setSerial
63 |
64 | return () => {
65 | // Unsubscribe from digital, analog, and serial joins 1 when component unmounts
66 | window.CrComLib.unsubscribeState('b', '1', d1Id);
67 | window.CrComLib.unsubscribeState('n', '1', a1Id);
68 | window.CrComLib.unsubscribeState('s', '1', s1Id);
69 |
70 | // Contracts
71 | window.CrComLib.unsubscribeState('b', 'HomePage.DigitalState', dc1Id);
72 | window.CrComLib.unsubscribeState('n', 'HomePage.AnalogState', ac1Id);
73 | window.CrComLib.unsubscribeState('s', 'HomePage.StringState', sc1Id);
74 | }
75 | }, []);
76 | ```
77 |
78 | ### Send data via joins to the control system:
79 | ```ts
80 | const sendDigital = (value: boolean) => window.CrComLib.publishEvent('b', '1', value);
81 | const sendAnalog = (value: number) => window.CrComLib.publishEvent('n', '1', value);
82 | const sendSerial = (value: string) => window.CrComLib.publishEvent('s', '1', value);
83 |
84 | // Contracts
85 | const sendDigitalContract = (value: boolean) => window.CrComLib.publishEvent('b', 'HomePage.DigitalEvent', value);
86 | const sendAnalogContract = (value: number) => window.CrComLib.publishEvent('n', 'HomePage.AnalogEvent', value);
87 | const sendSerialContract = (value: string) => window.CrComLib.publishEvent('s', 'HomePage.StringEvent', value);
88 | ```
89 |
90 | ### Using CH5 components
91 | ```tsx
92 | // Import a CH5 theme (coming from the @crestron/ch5-theme package) for CH5 CSS (required when a CH5 component is in use)
93 | // If not using a CH5 component in the project then do not import the CSS as that adds ~2mb to the bundle size.
94 | import '@crestron/ch5-theme/output/themes/light-theme.css'
95 |
96 | function App() {
97 | return (
98 |
99 |
I'm regular JSX!
100 |
101 |
106 |
107 |
108 | )
109 | }
110 | ```
--------------------------------------------------------------------------------
/config/contract.cse2j:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ContractExample",
3 | "timestamp": "2025-02-08 18:53:21.937",
4 | "version": "1.0.0.0",
5 | "schema_version": 1,
6 | "extra_value": "",
7 | "signals": {
8 | "states": {
9 | "boolean": {
10 | "1": {
11 | "1": "HomePage.DigitalState"
12 | }
13 | },
14 | "numeric": {
15 | "1": {
16 | "1": "HomePage.AnalogState"
17 | }
18 | },
19 | "string": {
20 | "1": {
21 | "1": "HomePage.StringState"
22 | }
23 | }
24 | },
25 | "events": {
26 | "boolean": {
27 | "HomePage.DigitalEvent": {
28 | "joinId": 1,
29 | "smartObjectId": 1
30 | }
31 | },
32 | "numeric": {
33 | "HomePage.AnalogEvent": {
34 | "joinId": 1,
35 | "smartObjectId": 1
36 | }
37 | },
38 | "string": {
39 | "HomePage.StringEvent": {
40 | "joinId": 1,
41 | "smartObjectId": 1
42 | }
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/contracts/ContractExample.cce:
--------------------------------------------------------------------------------
1 | {
2 | "Errors": [],
3 | "id": "_75toa934f",
4 | "name": "ContractExample",
5 | "description": "",
6 | "company": "",
7 | "client": "",
8 | "author": "",
9 | "version": "1.0.0.0",
10 | "schemaVersion": 1,
11 | "subContractLinks": [],
12 | "subContracts": [],
13 | "specifications": [
14 | {
15 | "Errors": [],
16 | "parentId": "_75toa934f",
17 | "id": "_8to979t1b",
18 | "componentId": "_o3mh37scx",
19 | "instanceName": "HomePage",
20 | "numberOfInstances": 1
21 | }
22 | ],
23 | "components": [
24 | {
25 | "Errors": [],
26 | "parentId": "_75toa934f",
27 | "id": "_o3mh37scx",
28 | "name": "HomePage",
29 | "commands": [
30 | {
31 | "Errors": [],
32 | "name": "DigitalState",
33 | "siblingId": "_gtyas22m4",
34 | "dataType": 1,
35 | "notes": "",
36 | "id": "_wo3oj1nlo",
37 | "parentId": "_o3mh37scx",
38 | "attributeType": 0
39 | },
40 | {
41 | "Errors": [],
42 | "name": "AnalogState",
43 | "siblingId": "_o9mi9nv5f",
44 | "dataType": 2,
45 | "notes": "",
46 | "id": "_eb33pqpd8",
47 | "parentId": "_o3mh37scx",
48 | "attributeType": 0
49 | },
50 | {
51 | "Errors": [],
52 | "name": "StringState",
53 | "siblingId": "_ac100j0e2",
54 | "dataType": 3,
55 | "notes": "",
56 | "id": "_x62f89age",
57 | "parentId": "_o3mh37scx",
58 | "attributeType": 0
59 | }
60 | ],
61 | "feedbacks": [
62 | {
63 | "Errors": [],
64 | "name": "DigitalEvent",
65 | "siblingId": "_wo3oj1nlo",
66 | "dataType": 1,
67 | "notes": "",
68 | "id": "_gtyas22m4",
69 | "parentId": "_o3mh37scx",
70 | "attributeType": 1
71 | },
72 | {
73 | "Errors": [],
74 | "name": "AnalogEvent",
75 | "siblingId": "_eb33pqpd8",
76 | "dataType": 2,
77 | "notes": "",
78 | "id": "_o9mi9nv5f",
79 | "parentId": "_o3mh37scx",
80 | "attributeType": 1
81 | },
82 | {
83 | "Errors": [],
84 | "name": "StringEvent",
85 | "siblingId": "_x62f89age",
86 | "dataType": 3,
87 | "notes": "",
88 | "id": "_ac100j0e2",
89 | "parentId": "_o3mh37scx",
90 | "attributeType": 1
91 | }
92 | ],
93 | "specifications": []
94 | }
95 | ],
96 | "allComponentsForAllContracts": []
97 | }
--------------------------------------------------------------------------------
/contracts/output/ContractExample/interface/HTML/HomePage.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/contracts/output/ContractExample/interface/mapping/ContractExample.cse2j:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ContractExample",
3 | "timestamp": "2025-02-08 18:53:21.937",
4 | "version": "1.0.0.0",
5 | "schema_version": 1,
6 | "extra_value": "",
7 | "signals": {
8 | "states": {
9 | "boolean": {
10 | "1": {
11 | "1": "HomePage.DigitalState"
12 | }
13 | },
14 | "numeric": {
15 | "1": {
16 | "1": "HomePage.AnalogState"
17 | }
18 | },
19 | "string": {
20 | "1": {
21 | "1": "HomePage.StringState"
22 | }
23 | }
24 | },
25 | "events": {
26 | "boolean": {
27 | "HomePage.DigitalEvent": {
28 | "joinId": 1,
29 | "smartObjectId": 1
30 | }
31 | },
32 | "numeric": {
33 | "HomePage.AnalogEvent": {
34 | "joinId": 1,
35 | "smartObjectId": 1
36 | }
37 | },
38 | "string": {
39 | "HomePage.StringEvent": {
40 | "joinId": 1,
41 | "smartObjectId": 1
42 | }
43 | }
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/contracts/output/ContractExample/programming/SIMPL/ContractExample.chd:
--------------------------------------------------------------------------------
1 | [
2 | ObjTp=FSgntr
3 | Sgntr=CHD
4 | RelVrs=1
5 | Schema=1
6 | ContractEditor=1.0.0.0
7 | ]
8 | [
9 | ObjTp=Hd
10 | Schema=1
11 | ProjectFile=ContractExample
12 | ContractID=_75toa934f
13 | CEProjectVer=1.0.0.0
14 | DateTimeUTC=2025-02-08 18:53:21.946
15 | ]
16 | [
17 | ObjTp=Symbol
18 | Name=HomePage
19 | SmplCName=_8to979t1b1
20 | Hint=HomePage (Control Join Id 1)
21 | Code=1
22 | SMWRev=4.13.00
23 | Expand=expand_randomly
24 | MinVariableInputs=1
25 | MaxVariableInputs=1
26 | MinVariableOutputs=1
27 | MaxVariableOutputs=1
28 | MinVariableInputsList2=1
29 | MaxVariableInputsList2=1
30 | MinVariableOutputsList2=1
31 | MaxVariableOutputsList2=1
32 | MinVariableInputsList3=1
33 | MaxVariableInputsList3=1
34 | MinVariableOutputsList3=1
35 | MaxVariableOutputsList3=1
36 | NumFixedParams=1
37 | ParamCue1=ControlJoinId
38 | ParamSigType1=UI_RO_String
39 | ControlJoinId=1d
40 | MPp=1
41 | Pp1=1
42 | ChdH=1
43 | Render=8
44 | InputCue1=DigitalState
45 | InputSigType1=Digital
46 | SmplCInputCue1=_wo3oj1nlo
47 | InputList2Cue1=AnalogState
48 | InputList2SigType1=Analog
49 | SmplCInputList2Cue1=_eb33pqpd8
50 | InputList3Cue1=StringState
51 | InputList3SigType1=Serial
52 | SmplCInputList3Cue1=_x62f89age
53 | OutputCue1=DigitalEvent
54 | OutputSigType1=Digital
55 | SmplCOutputCue1=_gtyas22m4
56 | OutputList2Cue1=AnalogEvent
57 | OutputList2SigType1=Analog
58 | SmplCOutputList2Cue1=_o9mi9nv5f
59 | OutputList3Cue1=StringEvent
60 | OutputList3SigType1=Serial
61 | SmplCOutputList3Cue1=_ac100j0e2
62 | ]
63 | [
64 | ObjTp=Dp
65 | Tp=1
66 | HD=TRUE
67 | NF=1
68 | DNF=1
69 | EncFmt=0
70 | DVLF=1
71 | Sgn=0
72 | H=1
73 | DV=1d
74 | ]
75 | [
76 | ObjTp=CHD
77 | H=1
78 | ChdCode=1
79 | ParentChdFolder=0
80 | ]
81 |
--------------------------------------------------------------------------------
/contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/ComponentMediator.g.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Globalization;
4 | using Crestron.SimplSharpPro;
5 |
6 | namespace ContractExample
7 | {
8 | internal class ComponentMediator : IDisposable
9 | {
10 | #region Members
11 |
12 | private readonly IList
_smartObjects;
13 | private IList SmartObjects { get { return _smartObjects; } }
14 |
15 | private readonly Dictionary> _booleanOutputs;
16 | private Dictionary> BooleanOutputs { get { return _booleanOutputs; } }
17 |
18 | private readonly Dictionary> _numericOutputs;
19 | private Dictionary> NumericOutputs { get { return _numericOutputs; } }
20 |
21 | private readonly Dictionary> _stringOutputs;
22 | private Dictionary> StringOutputs { get { return _stringOutputs; } }
23 |
24 | #endregion
25 |
26 | #region Construction & Initialization
27 |
28 | public ComponentMediator()
29 | {
30 | _smartObjects = new List();
31 |
32 | _booleanOutputs = new Dictionary>();
33 | _numericOutputs = new Dictionary>();
34 | _stringOutputs = new Dictionary>();
35 | }
36 |
37 | public void HookSmartObjectEvents(SmartObject smartObject)
38 | {
39 | SmartObjects.Add(smartObject);
40 | smartObject.SigChange += SmartObject_SigChange;
41 | }
42 | public void UnHookSmartObjectEvents(SmartObject smartObject)
43 | {
44 | SmartObjects.Remove(smartObject);
45 | smartObject.SigChange -= SmartObject_SigChange;
46 | }
47 |
48 | #endregion
49 |
50 | #region Smart Object Event Handler
51 |
52 | private string GetKey(uint smartObjectId, uint join)
53 | {
54 | return smartObjectId.ToString(CultureInfo.InvariantCulture) + "." + join.ToString(CultureInfo.InvariantCulture);
55 | }
56 |
57 | internal void ConfigureBooleanEvent(uint controlJoinId, uint join, Action action)
58 | {
59 | string key = GetKey(controlJoinId, join);
60 | if (BooleanOutputs.ContainsKey(key))
61 | BooleanOutputs[key] = action;
62 | else
63 | BooleanOutputs.Add(key, action);
64 | }
65 | internal void ConfigureNumericEvent(uint controlJoinId, uint join, Action action)
66 | {
67 | string key = GetKey(controlJoinId, join);
68 | if (NumericOutputs.ContainsKey(key))
69 | NumericOutputs[key] = action;
70 | else
71 | NumericOutputs.Add(key, action);
72 |
73 | }
74 | internal void ConfigureStringEvent(uint controlJoinId, uint join, Action action)
75 | {
76 | string key = GetKey(controlJoinId, join);
77 | if (StringOutputs.ContainsKey(key))
78 | StringOutputs[key] = action;
79 | else
80 | StringOutputs.Add(key, action);
81 | }
82 |
83 | private void SmartObject_SigChange(GenericBase currentDevice, SmartObjectEventArgs args)
84 | {
85 | try
86 | {
87 | Dictionary> signals = null;
88 | switch (args.Sig.Type)
89 | {
90 | case eSigType.Bool:
91 | signals = BooleanOutputs;
92 | break;
93 | case eSigType.UShort:
94 | signals = NumericOutputs;
95 | break;
96 | case eSigType.String:
97 | signals = StringOutputs;
98 | break;
99 | }
100 |
101 | //Resolve and invoke the corresponding method
102 | Action action;
103 | string key = GetKey(args.SmartObjectArgs.ID, args.Sig.Number);
104 | if (signals != null &&
105 | signals.TryGetValue(key, out action) &&
106 | action != null)
107 | action.Invoke(args);
108 | }
109 | catch
110 | {
111 | }
112 | }
113 |
114 | #endregion
115 |
116 | #region IDisposable
117 |
118 | private bool IsDisposed { get; set; }
119 |
120 | public void Dispose()
121 | {
122 | if (IsDisposed)
123 | return;
124 |
125 | IsDisposed = true;
126 |
127 | for (int i = 0; i < SmartObjects.Count; i++)
128 | {
129 | SmartObjects[i].SigChange -= SmartObject_SigChange;
130 | }
131 | }
132 |
133 | #endregion
134 | }
135 | }
--------------------------------------------------------------------------------
/contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/Contract.g.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Crestron.SimplSharpPro.DeviceSupport;
5 | using Crestron.SimplSharpPro;
6 |
7 | namespace ContractExample
8 | {
9 | ///
10 | /// Common Interface for Root Contracts.
11 | ///
12 | public interface IContract
13 | {
14 | object UserObject { get; set; }
15 | void AddDevice(BasicTriListWithSmartObject device);
16 | void RemoveDevice(BasicTriListWithSmartObject device);
17 | }
18 |
19 | public class Contract : IContract, IDisposable
20 | {
21 | #region Components
22 |
23 | private ComponentMediator ComponentMediator { get; set; }
24 |
25 | public ContractExample.IHomePage HomePage { get { return (ContractExample.IHomePage)InternalHomePage; } }
26 | private ContractExample.HomePage InternalHomePage { get; set; }
27 |
28 | #endregion
29 |
30 | #region Construction and Initialization
31 |
32 | public Contract()
33 | : this(new List().ToArray())
34 | {
35 | }
36 |
37 | public Contract(BasicTriListWithSmartObject device)
38 | : this(new [] { device })
39 | {
40 | }
41 |
42 | public Contract(BasicTriListWithSmartObject[] devices)
43 | {
44 | if (devices == null)
45 | throw new ArgumentNullException("Devices is null");
46 |
47 | ComponentMediator = new ComponentMediator();
48 |
49 | InternalHomePage = new ContractExample.HomePage(ComponentMediator, 1);
50 |
51 | for (int index = 0; index < devices.Length; index++)
52 | {
53 | AddDevice(devices[index]);
54 | }
55 | }
56 |
57 | #endregion
58 |
59 | #region Standard Contract Members
60 |
61 | public object UserObject { get; set; }
62 |
63 | public void AddDevice(BasicTriListWithSmartObject device)
64 | {
65 | InternalHomePage.AddDevice(device);
66 | }
67 |
68 | public void RemoveDevice(BasicTriListWithSmartObject device)
69 | {
70 | InternalHomePage.RemoveDevice(device);
71 | }
72 |
73 | #endregion
74 |
75 | #region IDisposable
76 |
77 | public bool IsDisposed { get; set; }
78 |
79 | public void Dispose()
80 | {
81 | if (IsDisposed)
82 | return;
83 |
84 | IsDisposed = true;
85 |
86 | InternalHomePage.Dispose();
87 | ComponentMediator.Dispose();
88 | }
89 |
90 | #endregion
91 |
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/HomePage.g.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Crestron.SimplSharpPro.DeviceSupport;
5 | using Crestron.SimplSharpPro;
6 |
7 | namespace ContractExample
8 | {
9 | public interface IHomePage
10 | {
11 | object UserObject { get; set; }
12 |
13 | event EventHandler DigitalEvent;
14 | event EventHandler AnalogEvent;
15 | event EventHandler StringEvent;
16 |
17 | void DigitalState(HomePageBoolInputSigDelegate callback);
18 | void AnalogState(HomePageUShortInputSigDelegate callback);
19 | void StringState(HomePageStringInputSigDelegate callback);
20 |
21 | }
22 |
23 | public delegate void HomePageBoolInputSigDelegate(BoolInputSig boolInputSig, IHomePage homePage);
24 | public delegate void HomePageUShortInputSigDelegate(UShortInputSig uShortInputSig, IHomePage homePage);
25 | public delegate void HomePageStringInputSigDelegate(StringInputSig stringInputSig, IHomePage homePage);
26 |
27 | internal class HomePage : IHomePage, IDisposable
28 | {
29 | #region Standard CH5 Component members
30 |
31 | private ComponentMediator ComponentMediator { get; set; }
32 |
33 | public object UserObject { get; set; }
34 |
35 | public uint ControlJoinId { get; private set; }
36 |
37 | private IList _devices;
38 | public IList Devices { get { return _devices; } }
39 |
40 | #endregion
41 |
42 | #region Joins
43 |
44 | private static class Joins
45 | {
46 | internal static class Booleans
47 | {
48 | public const uint DigitalEvent = 1;
49 |
50 | public const uint DigitalState = 1;
51 | }
52 | internal static class Numerics
53 | {
54 | public const uint AnalogEvent = 1;
55 |
56 | public const uint AnalogState = 1;
57 | }
58 | internal static class Strings
59 | {
60 | public const uint StringEvent = 1;
61 |
62 | public const uint StringState = 1;
63 | }
64 | }
65 |
66 | #endregion
67 |
68 | #region Construction and Initialization
69 |
70 | internal HomePage(ComponentMediator componentMediator, uint controlJoinId)
71 | {
72 | ComponentMediator = componentMediator;
73 | Initialize(controlJoinId);
74 | }
75 |
76 | private void Initialize(uint controlJoinId)
77 | {
78 | ControlJoinId = controlJoinId;
79 |
80 | _devices = new List();
81 |
82 | ComponentMediator.ConfigureBooleanEvent(controlJoinId, Joins.Booleans.DigitalEvent, onDigitalEvent);
83 | ComponentMediator.ConfigureNumericEvent(controlJoinId, Joins.Numerics.AnalogEvent, onAnalogEvent);
84 | ComponentMediator.ConfigureStringEvent(controlJoinId, Joins.Strings.StringEvent, onStringEvent);
85 |
86 | }
87 |
88 | public void AddDevice(BasicTriListWithSmartObject device)
89 | {
90 | Devices.Add(device);
91 | ComponentMediator.HookSmartObjectEvents(device.SmartObjects[ControlJoinId]);
92 | }
93 |
94 | public void RemoveDevice(BasicTriListWithSmartObject device)
95 | {
96 | Devices.Remove(device);
97 | ComponentMediator.UnHookSmartObjectEvents(device.SmartObjects[ControlJoinId]);
98 | }
99 |
100 | #endregion
101 |
102 | #region CH5 Contract
103 |
104 | public event EventHandler DigitalEvent;
105 | private void onDigitalEvent(SmartObjectEventArgs eventArgs)
106 | {
107 | EventHandler handler = DigitalEvent;
108 | if (handler != null)
109 | handler(this, UIEventArgs.CreateEventArgs(eventArgs));
110 | }
111 |
112 |
113 | public void DigitalState(HomePageBoolInputSigDelegate callback)
114 | {
115 | for (int index = 0; index < Devices.Count; index++)
116 | {
117 | callback(Devices[index].SmartObjects[ControlJoinId].BooleanInput[Joins.Booleans.DigitalState], this);
118 | }
119 | }
120 |
121 | public event EventHandler AnalogEvent;
122 | private void onAnalogEvent(SmartObjectEventArgs eventArgs)
123 | {
124 | EventHandler handler = AnalogEvent;
125 | if (handler != null)
126 | handler(this, UIEventArgs.CreateEventArgs(eventArgs));
127 | }
128 |
129 |
130 | public void AnalogState(HomePageUShortInputSigDelegate callback)
131 | {
132 | for (int index = 0; index < Devices.Count; index++)
133 | {
134 | callback(Devices[index].SmartObjects[ControlJoinId].UShortInput[Joins.Numerics.AnalogState], this);
135 | }
136 | }
137 |
138 | public event EventHandler StringEvent;
139 | private void onStringEvent(SmartObjectEventArgs eventArgs)
140 | {
141 | EventHandler handler = StringEvent;
142 | if (handler != null)
143 | handler(this, UIEventArgs.CreateEventArgs(eventArgs));
144 | }
145 |
146 |
147 | public void StringState(HomePageStringInputSigDelegate callback)
148 | {
149 | for (int index = 0; index < Devices.Count; index++)
150 | {
151 | callback(Devices[index].SmartObjects[ControlJoinId].StringInput[Joins.Strings.StringState], this);
152 | }
153 | }
154 |
155 | #endregion
156 |
157 | #region Overrides
158 |
159 | public override int GetHashCode()
160 | {
161 | return (int)ControlJoinId;
162 | }
163 |
164 | public override string ToString()
165 | {
166 | return string.Format("Contract: {0} Component: {1} HashCode: {2} {3}", "HomePage", GetType().Name, GetHashCode(), UserObject != null ? "UserObject: " + UserObject : null);
167 | }
168 |
169 | #endregion
170 |
171 | #region IDisposable
172 |
173 | public bool IsDisposed { get; set; }
174 |
175 | public void Dispose()
176 | {
177 | if (IsDisposed)
178 | return;
179 |
180 | IsDisposed = true;
181 |
182 | DigitalEvent = null;
183 | AnalogEvent = null;
184 | StringEvent = null;
185 | }
186 |
187 | #endregion
188 |
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/contracts/output/ContractExample/programming/SIMPLSharp/ContractExample/UIEventArgs.g.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Crestron.SimplSharpPro;
3 | using Crestron.SimplSharpPro.DeviceSupport;
4 |
5 | namespace ContractExample
6 | {
7 | public class UIEventArgs : EventArgs
8 | {
9 | public BasicTriListWithSmartObject Device { get; internal set; }
10 | public SigEventArgs SigArgs { get; internal set; }
11 |
12 | internal static UIEventArgs CreateEventArgs(SmartObjectEventArgs eventArgs)
13 | {
14 | return new UIEventArgs
15 | {
16 | Device = (BasicTriListWithSmartObject)eventArgs.SmartObjectArgs.Device,
17 | SigArgs = eventArgs
18 | };
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [js.configs.recommended, ...tseslint.configs.recommended],
11 | files: ['**/*.{ts,tsx}'],
12 | languageOptions: {
13 | ecmaVersion: 2020,
14 | globals: globals.browser,
15 | },
16 | plugins: {
17 | 'react-hooks': reactHooks,
18 | 'react-refresh': reactRefresh,
19 | },
20 | rules: {
21 | ...reactHooks.configs.recommended.rules,
22 | 'react-refresh/only-export-components': [
23 | 'warn',
24 | { allowConstantExport: true },
25 | ],
26 | },
27 | },
28 | )
29 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | Vite + React + TS
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ch5-react-ts-template",
3 | "private": true,
4 | "version": "4.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc && vite build",
9 | "build:dev": "tsc && vite build --mode development",
10 | "build:prod": "tsc && vite build --mode production",
11 | "archive": "ch5-cli archive -p ch5-react-ts-template -d dist -o archive -c ./contracts/output/ContractExample/interface/mapping/ContractExample.cse2j",
12 | "deploy:mobile": "ch5-cli deploy -p -H 0.0.0.0 -t mobile archive/ch5-react-ts-template.ch5z",
13 | "deploy:xpanel": "ch5-cli deploy -p -H 0.0.0.0 -t web archive/ch5-react-ts-template.ch5z",
14 | "deploy:panel": "ch5-cli deploy -p -H 0.0.0.0 -t touchscreen archive/ch5-react-ts-template.ch5z --slow-mode",
15 | "lint": "eslint .",
16 | "preview": "vite preview"
17 | },
18 | "dependencies": {
19 | "@crestron/ch5-crcomlib": "^2.11.2",
20 | "@crestron/ch5-theme": "^2.11.2",
21 | "@crestron/ch5-webxpanel": "^2.8.0",
22 | "react": "^19.0.0",
23 | "react-dom": "^19.0.0"
24 | },
25 | "devDependencies": {
26 | "@crestron/ch5-shell-utilities-cli": "^2.11.2",
27 | "@crestron/ch5-utilities-cli": "^2.0.0",
28 | "@eslint/js": "^9.19.0",
29 | "@types/node": "^22.13.1",
30 | "@types/react": "^19.0.8",
31 | "@types/react-dom": "^19.0.3",
32 | "@vitejs/plugin-react": "^4.3.4",
33 | "eruda": "^3.4.1",
34 | "eslint": "^9.19.0",
35 | "eslint-plugin-react-hooks": "^5.0.0",
36 | "eslint-plugin-react-refresh": "^0.4.18",
37 | "globals": "^15.14.0",
38 | "typescript": "~5.7.2",
39 | "typescript-eslint": "^8.22.0",
40 | "vite": "^6.1.0",
41 | "vite-plugin-pwa": "^0.21.1",
42 | "vite-plugin-singlefile": "^2.1.0",
43 | "vite-plugin-static-copy": "^2.2.0"
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | // Uncomment the below line if you are using CH5 components.
2 | // import '@crestron/ch5-theme/output/themes/light-theme.css' // Crestron CSS. @crestron/ch5-theme/output/themes shows the other themes that can be used.
3 | import './assets/css/App.css' // Your CSS
4 | import { useState, useEffect, useMemo } from 'react';
5 | import useWebXPanel from './hooks/useWebXPanel';
6 |
7 | // Initialize eruda for panel/app debugging capabilities (in dev mode only)
8 | if (import.meta.env.VITE_APP_ENV === 'development') {
9 | import('eruda').then(({ default: eruda }) => {
10 | eruda.init();
11 | });
12 | }
13 |
14 | function App() {
15 | const [digitalState, setDigitalState] = useState(false);
16 | const [analogState, setAnalogState] = useState(0);
17 | const [serialState, setSerialState] = useState("");
18 | const [digitalContractState, setDigitalContractState] = useState(false);
19 | const [analogContractState, setAnalogContractState] = useState(0);
20 | const [serialContractState, setSerialContractState] = useState("");
21 |
22 | const webXPanelConfig = useMemo(() => ({
23 | ipId: '0x03',
24 | host: '0.0.0.0',
25 | roomId: '',
26 | authToken: ''
27 | }), []); // Dependencies array is empty, so this object is created only once
28 |
29 | useWebXPanel(webXPanelConfig);
30 |
31 | useEffect(() => {
32 | // Listen for digital, analog, and serial joins 1 from the control system.
33 | // d1Id, a1Id, and s1Id are the subscription IDs for each join, they are
34 | // only used to unsubscribe from the join when the component unmounts
35 | const d1Id = window.CrComLib.subscribeState('b', '1', (value: boolean) => setDigitalState(value));
36 | const a1Id = window.CrComLib.subscribeState('n', '1', (value: number) => setAnalogState(value));
37 | const s1Id = window.CrComLib.subscribeState('s', '1', (value: string) => setSerialState(value));
38 |
39 | // Contracts
40 | const dc1Id = window.CrComLib.subscribeState('b', 'HomePage.DigitalState', (value: boolean) => setDigitalContractState(value));
41 | const ac1Id = window.CrComLib.subscribeState('n', 'HomePage.AnalogState', (value: number) => setAnalogContractState(value));
42 | const sc1Id = window.CrComLib.subscribeState('s', 'HomePage.StringState', (value: string) => setSerialContractState(value));
43 |
44 | return () => {
45 | // Unsubscribe from digital, analog, and serial joins 1 when component unmounts
46 | window.CrComLib.unsubscribeState('b', '1', d1Id);
47 | window.CrComLib.unsubscribeState('n', '1', a1Id);
48 | window.CrComLib.unsubscribeState('s', '1', s1Id);
49 |
50 | // Contracts
51 | window.CrComLib.unsubscribeState('b', 'HomePage.DigitalState', dc1Id);
52 | window.CrComLib.unsubscribeState('n', 'HomePage.AnalogState', ac1Id);
53 | window.CrComLib.unsubscribeState('s', 'HomePage.StringState', sc1Id);
54 | }
55 | }, []);
56 |
57 | // Send digital, analog, and serial 1 joins to the control system
58 | const sendDigital = (value: boolean) => window.CrComLib.publishEvent('b', '1', value);
59 | const sendAnalog = (value: number) => window.CrComLib.publishEvent('n', '1', value);
60 | const sendSerial = (value: string) => window.CrComLib.publishEvent('s', '1', value);
61 |
62 | // Contracts
63 | const sendDigitalContract = (value: boolean) => window.CrComLib.publishEvent('b', 'HomePage.DigitalEvent', value);
64 | const sendAnalogContract = (value: number) => window.CrComLib.publishEvent('n', 'HomePage.AnalogEvent', value);
65 | const sendSerialContract = (value: string) => window.CrComLib.publishEvent('s', 'HomePage.StringEvent', value);
66 |
67 | return (
68 | <>
69 | {/* Joins */}
70 | Joins
71 |
72 |
73 |
sendDigital(!digitalState)}>Toggle Digital
74 |
{digitalState.toString()}
75 |
76 |
77 |
{analogState}
78 |
sendAnalog(Number(e.target.value))} />
79 |
80 |
81 | sendSerial(e.target.value)} />
82 |
83 |
84 | {/* Contracts */}
85 | Contracts
86 |
87 |
88 |
sendDigitalContract(!digitalContractState)}>Toggle Digital
89 |
{digitalContractState.toString()}
90 |
91 |
92 |
{analogContractState}
93 |
sendAnalogContract(Number(e.target.value))} />
94 |
95 |
96 | sendSerialContract(e.target.value)} />
97 |
98 |
99 | >
100 | )
101 | }
102 |
103 | export default App
104 |
--------------------------------------------------------------------------------
/src/assets/css/App.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300&display=swap');
2 |
3 | body {
4 | background-color: #121212;
5 | font-family: 'Roboto', sans-serif;
6 | display: flex;
7 | flex-direction: row;
8 | justify-content: center;
9 | overflow: hidden;
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | .controlGroupWrapper {
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | }
19 |
20 | .controlGroup {
21 | width: 25%;
22 | height: 80%;
23 | padding: 1rem;
24 | background-color: #696969;
25 | border-radius: 15px;
26 | padding: 1rem;
27 | display: flex;
28 | flex-direction: column;
29 | align-items: center;
30 | justify-content: center;
31 | margin-top: 1rem;
32 | margin-bottom: 1rem;
33 | }
34 |
35 | .controlGroup .btn {
36 | /* margin: 1rem; */
37 | outline: none;
38 | background-color: #0895e7;
39 | color: #ffffff;
40 | border: 0;
41 | border-radius: 10px;
42 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 6px 6px rgba(0, 0, 0, 0.1);
43 | padding: 14px 40px;
44 | font-size: 16px;
45 | }
46 |
47 | .controlGroup .btn:active {
48 | background-color: #71a0bb;
49 | }
50 |
51 | .controlGroup p {
52 | color: #ffffff;
53 | font-size: 2rem;
54 | }
55 |
56 | .controlgroup #analogSlider {
57 | margin: 1rem;
58 | appearance: none;
59 | width: 95%;
60 | height: 1.5rem;
61 | background-color: #d3d3d3;
62 | border-radius: 15px;
63 | outline: none;
64 | }
65 |
66 | #analogSlider::-webkit-slider-thumb {
67 | -webkit-appearance: none;
68 | appearance: none;
69 | width: 2rem;
70 | height: 2rem;
71 | background: #0895e7;
72 | border-radius: 50%;
73 | cursor: pointer;
74 | }
75 |
76 | #analogSlider::-moz-range-thumb {
77 | width: 2rem;
78 | height: 2rem;
79 | background: #0895e7;
80 | border-radius: 50%;
81 | cursor: pointer;
82 | }
83 |
84 | .controlGroup #currentSerialValue {
85 | background-color: #d3d3d3;
86 | border: 3px solid #d3d3d3;
87 | padding: 0.1rem;
88 | border-radius: 15px;
89 | margin: 1rem;
90 | width: 95%;
91 | height: 3rem;
92 | line-height: 2.5rem;
93 | outline: none;
94 | font-size: 2rem;
95 | }
96 |
97 | @media (max-width: 1200px) {
98 | #controlGroupWrapper {
99 | flex-direction: column;
100 | align-items: center;
101 | justify-content: space-evenly;
102 | }
103 |
104 | .controlGroup {
105 | width: 80%;
106 | height: 25%;
107 | }
108 | }
109 |
110 | @media (min-width: 1201px) {
111 | #controlGroupWrapper {
112 | flex-direction: row;
113 | align-items: center;
114 | justify-content: center;
115 | }
116 |
117 | .controlGroup {
118 | width: 25%;
119 | height: 80%;
120 | margin: 1rem;
121 | }
122 |
123 | body {
124 | height: 800px;
125 | }
126 | }
--------------------------------------------------------------------------------
/src/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jphillipsCrestron/ch5-react-ts-template/a0f3be7ed5fcaecba6761fc5a0f72e69c11fe45d/src/assets/images/favicon.ico
--------------------------------------------------------------------------------
/src/assets/images/screenshot_narrow.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jphillipsCrestron/ch5-react-ts-template/a0f3be7ed5fcaecba6761fc5a0f72e69c11fe45d/src/assets/images/screenshot_narrow.jpg
--------------------------------------------------------------------------------
/src/assets/images/screenshot_wide.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jphillipsCrestron/ch5-react-ts-template/a0f3be7ed5fcaecba6761fc5a0f72e69c11fe45d/src/assets/images/screenshot_wide.jpg
--------------------------------------------------------------------------------
/src/assets/images/vite.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jphillipsCrestron/ch5-react-ts-template/a0f3be7ed5fcaecba6761fc5a0f72e69c11fe45d/src/assets/images/vite.png
--------------------------------------------------------------------------------
/src/globals.d.ts:
--------------------------------------------------------------------------------
1 | import { Ch5Animation } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-animation";
2 | import { ICh5AnimationAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-animation/interfaces/i-ch5-animation-attributes";
3 | import { Ch5Background } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-background";
4 | import { ICh5BackgroundAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-background/interfaces";
5 | import { Ch5Button } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-button";
6 | import { ICh5ButtonAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-button/interfaces";
7 | import { Ch5ButtonList } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-button-list";
8 | import { ICh5ButtonListAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-button-list/interfaces/i-ch5-button-list-attributes";
9 | import { Ch5ColorChip } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-color-chip";
10 | import { ICh5ColorChipAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-color-chip/interfaces/i-ch5-color-chip-attributes";
11 | import { Ch5ColorPicker } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-color-picker";
12 | import { ICh5ColorPickerAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-color-picker/interfaces/i-ch5-color-picker-attributes";
13 | import { Ch5Dpad } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-dpad";
14 | import { ICh5DpadAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-dpad/interfaces/i-ch5-dpad-attributes";
15 | import { Ch5DateTime } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-datetime";
16 | import { ICh5DateTimeAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-datetime/interfaces/i-ch5-datetime-attributes";
17 | import { Ch5Form } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-form";
18 | import { ICh5FormAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-form/interfaces/i-ch5-form-attributes";
19 | import { Ch5Image } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-image";
20 | import { ICh5ImageAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-image/interfaces/i-ch5-image-attributes";
21 | import { Ch5ImportHtmlSnippet } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-import-htmlsnippet";
22 | import { ICh5ImportHtmlSnippetAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-import-htmlsnippet/interfaces/i-ch5-import-htmlsnippet-attributes";
23 | import { Ch5Keypad } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-keypad";
24 | import { ICh5KeypadAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-keypad/interfaces/i-ch5-keypad-attributes";
25 | import { Ch5JoinToTextBoolean } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-boolean";
26 | import { ICh5JoinToTextBooleanAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-boolean/interfaces";
27 | import { Ch5JoinToTextNumeric } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-numeric";
28 | import { ICh5JoinToTextNumericAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-numeric/interfaces/i-ch5-jointotext-numeric-attributes";
29 | import { Ch5JoinToTextString } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-string";
30 | import { ICh5JoinToTextStringAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-jointotext-string/interfaces/i-ch5-jointotext-string-attributes";
31 | import { Ch5List } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-list";
32 | import { ICh5ListAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-list/interfaces";
33 | import { Ch5ModalDialog } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-modal-dialog";
34 | import { ICh5ModalDialogAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-modal-dialog/interfaces/i-ch5-modal-dialog-attributes";
35 | import { Ch5OverlayPanel } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-overlay-panel";
36 | import { ICh5OverlayPanelAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-overlay-panel/interfaces";
37 | import { Ch5QrCode } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-qrcode";
38 | import { ICh5QrCodeAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-qrcode/interfaces/i-ch5-qrcode-attributes";
39 | import { Ch5SegmentedGauge } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-segmented-gauge";
40 | import { ICh5SegmentedGaugeAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-segmented-gauge/interfaces/i-ch5-segmented-gauge-attributes";
41 | import { Ch5Select } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-select";
42 | import { ICh5SelectAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-select/interfaces";
43 | import { Ch5SelectOption } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-select-option";
44 | import { ICh5SelectOptionAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-select-option/interfaces/i-ch5-select-option-attributes";
45 | import { Ch5SignalLevelGauge } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-signal-level-gauge";
46 | import { ICh5SignalLevelGaugeAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-signal-level-gauge/interfaces/i-ch5-signal-level-gauge-attributes";
47 | import { Ch5Slider } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-slider";
48 | import { ICh5SliderAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-slider/interfaces";
49 | import { Ch5Spinner } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-spinner";
50 | import { ICh5SpinnerAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-spinner/interfaces";
51 | import { Ch5SubpageReferenceList } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-subpage-reference-list";
52 | import { ICh5SubpageReferenceListAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-subpage-reference-list/interfaces/i-ch5-subpage-reference-list-attributes";
53 | import { Ch5TabButton } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-tab-button";
54 | import { ICh5TabButtonAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-tab-button/interfaces/i-ch5-tab-button-attributes";
55 | import { Ch5Template } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-template";
56 | import { ICh5TemplateAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-template/interfaces/i-ch5-template-attributes";
57 | import { Ch5Text } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-text";
58 | import { ICh5TextAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-text/interfaces/i-ch5-text-attributes";
59 | import { Ch5TextInput } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-textinput";
60 | import { ICh5TextInputAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-textinput/interfaces";
61 | import { Ch5Toggle } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-toggle";
62 | import { ICh5ToggleAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-toggle/interfaces/i-ch5-toggle-attributes";
63 | import { Ch5TriggerView } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-triggerview";
64 | import { ICh5TriggerViewAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-triggerview/interfaces/i-ch5-triggerview-attributes";
65 | import { Ch5Video } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-video";
66 | import { ICh5VideoAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-video/interfaces";
67 | import { Ch5VideoSwitcher } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-video-switcher";
68 | import { ICh5VideoSwitcherAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-video-switcher/interfaces";
69 | import { Ch5WifiSignalLevelGauge } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-wifi-signal-level-gauge";
70 | import { ICh5WifiSignalLevelGaugeAttributes } from "@crestron/ch5-crcomlib/build_bundles/umd/@types/ch5-wifi-signal-level-gauge/interfaces/i-ch5-wifi-signal-level-gauge-attributes";
71 |
72 | declare global {
73 | interface Window {
74 | CrComLib: typeof import("@crestron/ch5-crcomlib/build_bundles/umd/@types/index");
75 | WebXPanel: typeof import("@crestron/ch5-webxpanel/dist/types/index");
76 | }
77 | }
78 |
79 | declare module "react/jsx-runtime" {
80 | namespace JSX {
81 | interface IntrinsicElements {
82 | "ch5-animation": React.DetailedHTMLProps & Partial, Ch5Animation>;
83 | "ch5-background": React.DetailedHTMLProps & Partial, Ch5Background>;
84 | "ch5-button": React.DetailedHTMLProps & Partial, Ch5Button>;
85 | "ch5-button-list": React.DetailedHTMLProps & Partial, Ch5ButtonList>;
86 | "ch5-color-chip": React.DetailedHTMLProps & Partial, Ch5ColorChip>;
87 | "ch5-color-picker": React.DetailedHTMLProps & Partial, Ch5ColorPicker>;
88 | "ch5-dpad": React.DetailedHTMLProps & Partial, Ch5Dpad>;
89 | "ch5-datetime": React.DetailedHTMLProps & Partial, Ch5DateTime>;
90 | "ch5-form": React.DetailedHTMLProps & Partial, Ch5Form>;
91 | "ch5-image": React.DetailedHTMLProps & Partial, Ch5Image>;
92 | "ch5-import-htmlsnippet": React.DetailedHTMLProps & Partial, Ch5ImportHtmlSnippet>;
93 | "ch5-keypad": React.DetailedHTMLProps & Partial, Ch5Keypad>;
94 | "ch5-jointottext-boolean": React.DetailedHTMLProps & Partial, Ch5JoinToTextBoolean>;
95 | "ch5-jointottext-numeric": React.DetailedHTMLProps & Partial, Ch5JoinToTextNumeric>;
96 | "ch5-jointottext-string": React.DetailedHTMLProps & Partial, Ch5JoinToTextString>;
97 | "ch5-list": React.DetailedHTMLProps & Partial, Ch5List>;
98 | "ch5-modal-dialog": React.DetailedHTMLProps & Partial, Ch5ModalDialog>;
99 | "ch5-overlay-panel": React.DetailedHTMLProps & Partial, Ch5OverlayPanel>;
100 | "ch5-qrcode": React.DetailedHTMLProps & Partial, Ch5QrCode>;
101 | "ch5-segmented-gauge": React.DetailedHTMLProps & Partial, Ch5SegmentedGauge>;
102 | "ch5-select": React.DetailedHTMLProps & Partial, Ch5Select>;
103 | "ch5-select-option": React.DetailedHTMLProps & Partial, Ch5SelectOption>;
104 | "ch5-signal-level-gauge": React.DetailedHTMLProps & Partial, Ch5SignalLevelGauge>;
105 | "ch5-slider": React.DetailedHTMLProps & Partial, Ch5Slider>;
106 | "ch5-spinner": React.DetailedHTMLProps & Partial, Ch5Spinner>;
107 | "ch5-subpage-reference-list": React.DetailedHTMLProps & Partial, Ch5SubpageReferenceList>;
108 | "ch5-tab-button": React.DetailedHTMLProps & Partial, Ch5TabButton>;
109 | "ch5-template": React.DetailedHTMLProps & Partial, Ch5Template>;
110 | "ch5-text": React.DetailedHTMLProps & Partial, Ch5Text>;
111 | "ch5-textinput": React.DetailedHTMLProps & Partial, Ch5TextInput>;
112 | "ch5-toggle": React.DetailedHTMLProps & Partial, Ch5Toggle>;
113 | "ch5-triggerview": React.DetailedHTMLProps & Partial, Ch5TriggerView>;
114 | "ch5-video": React.DetailedHTMLProps & Partial, Ch5Video>;
115 | "ch5-video-switcher": React.DetailedHTMLProps & Partial, Ch5VideoSwitcher>;
116 | "ch5-wifi-signal-level-gauge": React.DetailedHTMLProps & Partial, Ch5WifiSignalLevelGauge>;
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/src/hooks/useWebXPanel.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { useState, useEffect } from 'react';
3 |
4 | type WebXPanelConfig = {
5 | host: string;
6 | ipId: string;
7 | roomId?: string;
8 | authToken?: string;
9 | };
10 |
11 | const useWebXPanel = (params: WebXPanelConfig) => {
12 | const [isActive, setIsActive] = useState(false);
13 |
14 | useEffect(() => {
15 | const { WebXPanel, isActive, WebXPanelEvents } = window.WebXPanel.getWebXPanel(!window.WebXPanel.runsInContainerApp());
16 |
17 | setIsActive(isActive);
18 |
19 | const config: Partial['WebXPanelConfigParams']> = params;
20 |
21 | if (isActive) {
22 | console.log("Initializing XPanel with config: " + JSON.stringify(config));
23 | WebXPanel.initialize(config);
24 |
25 | const connectWsListener = () => {
26 | console.log("WebXPanel websocket connection success");
27 | };
28 |
29 | const errorWsListener = ({ detail }: any) => {
30 | console.log(`WebXPanel websocket connection error: ${JSON.stringify(detail)}`);
31 | };
32 |
33 | const connectCipListener = () => {
34 | console.log("WebXPanel CIP connection success");
35 | };
36 |
37 | const authenticationFailedListener = ({ detail }: any) => {
38 | console.log(`WebXPanel authentication failed: ${JSON.stringify(detail)}`);
39 | };
40 |
41 | const notAuthorizedListener = ({ detail }: any) => {
42 | console.log(`WebXPanel not authorized: ${JSON.stringify(detail)}`);
43 | window.location = detail.redirectTo;
44 | };
45 |
46 | const disconnectWsListener = ({ detail }: any) => {
47 | console.log(`WebXPanel websocket connection lost: ${JSON.stringify(detail)}`);
48 | };
49 |
50 | const disconnectCipListener = ({ detail }: any) => {
51 | console.log(`WebXPanel CIP connection lost: ${JSON.stringify(detail)}`);
52 | };
53 |
54 | // Adding event listeners
55 | window.addEventListener(WebXPanelEvents.CONNECT_WS, connectWsListener);
56 | window.addEventListener(WebXPanelEvents.ERROR_WS, errorWsListener);
57 | window.addEventListener(WebXPanelEvents.CONNECT_CIP, connectCipListener);
58 | window.addEventListener(WebXPanelEvents.AUTHENTICATION_FAILED, authenticationFailedListener);
59 | window.addEventListener(WebXPanelEvents.NOT_AUTHORIZED, notAuthorizedListener);
60 | window.addEventListener(WebXPanelEvents.DISCONNECT_WS, disconnectWsListener);
61 | window.addEventListener(WebXPanelEvents.DISCONNECT_CIP, disconnectCipListener);
62 |
63 | // Cleanup function
64 | return () => {
65 | window.removeEventListener(WebXPanelEvents.CONNECT_WS, connectWsListener);
66 | window.removeEventListener(WebXPanelEvents.ERROR_WS, errorWsListener);
67 | window.removeEventListener(WebXPanelEvents.CONNECT_CIP, connectCipListener);
68 | window.removeEventListener(WebXPanelEvents.AUTHENTICATION_FAILED, authenticationFailedListener);
69 | window.removeEventListener(WebXPanelEvents.NOT_AUTHORIZED, notAuthorizedListener);
70 | window.removeEventListener(WebXPanelEvents.DISCONNECT_WS, disconnectWsListener);
71 | window.removeEventListener(WebXPanelEvents.DISCONNECT_CIP, disconnectCipListener);
72 | };
73 | }
74 | }, [params]);
75 |
76 | return { isActive };
77 | };
78 |
79 | export default useWebXPanel;
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import App from './App.tsx'
4 |
5 | createRoot(document.getElementById('root')!).render(
6 |
7 |
8 | ,
9 | )
10 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | "include": ["src"]
26 | }
27 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | "include": ["vite.config.ts"]
24 | }
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig, loadEnv } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 | import { viteSingleFile } from 'vite-plugin-singlefile';
4 | import { viteStaticCopy } from 'vite-plugin-static-copy';
5 | import { VitePWA } from 'vite-plugin-pwa';
6 |
7 | // https://vite.dev/config/
8 | export default (config: { mode: string; }) => {
9 | const env = loadEnv(config.mode, process.cwd());
10 | const isDevelopment = env.VITE_APP_ENV === 'development';
11 |
12 | return defineConfig({
13 | plugins: [
14 | react(),
15 | viteSingleFile(),
16 | viteStaticCopy({
17 | targets: [
18 | {
19 | src: 'node_modules/@crestron/ch5-crcomlib/build_bundles/umd/cr-com-lib.js',
20 | dest: ''
21 | },
22 | {
23 | src: 'node_modules/@crestron/ch5-webxpanel/dist/umd/index.js',
24 | dest: ''
25 | },
26 | {
27 | src: 'node_modules/@crestron/ch5-webxpanel/dist/umd/d4412f0cafef4f213591.worker.js',
28 | dest: ''
29 | },
30 | {
31 | src: 'src/assets/images/favicon.ico',
32 | dest: 'assets/'
33 | },
34 | {
35 | src: 'src/assets/images/screenshot_narrow.jpg',
36 | dest: 'assets/'
37 | },
38 | {
39 | src: 'src/assets/images/screenshot_wide.jpg',
40 | dest: 'assets/'
41 | },
42 | {
43 | src: 'src/assets/images/vite.png',
44 | dest: 'assets/'
45 | }
46 | ]
47 | }),
48 | // Note that browsers will only allow PWA service workers to work if the server
49 | // presents a certificate that is signed by a CA that is trusted by the client machine.
50 | VitePWA({
51 | base: '/ch5-react-ts-template/',
52 | registerType: 'autoUpdate',
53 | workbox: {
54 | globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg}'], // Pattern to match precached files to return to the client when offline
55 | runtimeCaching: [
56 | {
57 | urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i, // Example for caching Google Fonts (this demo uses Google Roboto in the App.css)
58 | handler: 'CacheFirst',
59 | options: {
60 | cacheName: 'google-fonts',
61 | expiration: {
62 | maxEntries: 10,
63 | maxAgeSeconds: 60 * 60 * 24 * 365, // Cache for a year
64 | },
65 | cacheableResponse: {
66 | statuses: [0, 200],
67 | },
68 | },
69 | },
70 | // If there are other resources in the project that may be pulled at runtime (and not stored in the project at build time) add caching for them here.
71 | // Refer to the above entry as an example.
72 | ],
73 | },
74 | manifest: {
75 | // The start_url is the URL to the app index.html on the server
76 | start_url: "https://0.0.0.0/ch5-react-ts-template/index.html",
77 | // The ID is a unique identifier for the PWA.
78 | // Common practice is to use the domain or subdomain of the app on its server.
79 | id: "/ch5-react-ts-template/",
80 | name: 'Crestron CH5 React PWA',
81 | // Keep the short name 12 characters or less for mobile devices
82 | short_name: 'CH5 React PWA',
83 | description: 'A Crestron CH5 project using React, Vite, and PWA features',
84 | theme_color: '#696969',
85 | background_color: '#121212',
86 | // The display/_override is to hide browser/OS controls, making the app full screen
87 | display: 'standalone',
88 | display_override: ["window-controls-overlay","standalone"],
89 | lang: 'en',
90 | // There should be at least one icon, at a minimum of 144x144px.
91 | // The src property is relative to the index.html path at runtime.
92 | // Since the VitePWA `base` property is set to /ch5-react-ts-template/
93 | // the path will resolve to https://ip/ch5-react-ts-template/assets/vite.png
94 | icons: [
95 | {
96 | src: './assets/vite.png',
97 | sizes: '800x800',
98 | type: 'image/png',
99 | }
100 | ],
101 | // There should be at least two screenshots - one wide (landscape) and one narrow (portrait). These cannot be SVG
102 | // The src property is relative to the index.html path at runtime.
103 | // Since the VitePWA `base` property is set to /ch5-react-ts-template/
104 | // the path will resolve to https://ip/ch5-react-ts-template/assets/screenshot_something.png
105 | screenshots: [
106 | {
107 | src: './assets/screenshot_wide.jpg',
108 | sizes: '1024x593',
109 | form_factor: 'wide',
110 | type: 'image/jpeg'
111 | },
112 | {
113 | src: './assets/screenshot_narrow.jpg',
114 | sizes: '540x720',
115 | form_factor: 'narrow',
116 | type: 'image/jpeg'
117 | }
118 | ]
119 | },
120 | }),
121 | ],
122 | base: './',
123 | build: {
124 | sourcemap: isDevelopment,
125 | },
126 | });
127 | };
128 |
--------------------------------------------------------------------------------